Spring Security如何配置仅传JWT中username给loadUserByUsername
在JWT令牌中额外编码用户名、用户权限等业务数据时,出现Spring Security调用UserDetailsService.loadUserByUsername()方法时,传入参数为完整解码后的JWT载荷JSON字符串,而非单独username字段的问题,实际传入参数示例:{"username":"123456788","language":null,"deactivationReason":null,"terminationYear":null,"authorities":[{"authority":"ROLE_ADMIN_ROK"}],"active":false,"terminated":false}
待解决的两个核心问题:
- 如何修改配置,实现仅提取JWT中的username字段传递给
loadUserByUsername方法 AuthenticationProvider和UserDetailsService的调用顺序、执行优先级是怎样的
问题1:实现仅提取JWT中username字段传给loadUserByUsername方法
这个问题的根因在自定义的JwtRequestFilter中:Spring Security本身不会自动解析JWT内容,是过滤器在构造待认证的Authentication对象时,把完整解码后的JWT载荷JSON字符串设置为了principal身份信息,后续流程就会把这个完整字符串作为参数传给loadUserByUsername。
修复方案分两种,优先选第一种:
- 方案一(推荐):修改
JwtRequestFilter的解析逻辑
解析完JWT拿到载荷Claims后,不要传入整个载荷的JSON串,单独提取username字段值构造认证对象:// 解析JWT得到载荷后,单独提取username String username = claims.get("username", String.class); // 构造认证令牌时,principal参数传入单独提取的username,不要传整个JSON UsernamePasswordAuthenticationToken authReq = new UsernamePasswordAuthenticationToken(username, null, userAuthorities); SecurityContextHolder.getContext().setAuthentication(authReq); - 方案二(临时兜底):在
MyUserDetailsService中兼容解析入参
如果暂时不方便修改过滤器,可以在loadUserByUsername方法开头判断入参格式,遇到JSON串先解析出username再执行后续查库逻辑:@Override public UserDetails loadUserByUsername(String inputParam) throws UsernameNotFoundException { String actualUsername = inputParam; // 识别传入完整JWT载荷JSON的场景 if (inputParam != null && inputParam.trim().startsWith("{")) { try { ObjectMapper objectMapper = new ObjectMapper(); JsonNode payloadNode = objectMapper.readTree(inputParam); actualUsername = payloadNode.get("username").asText(); } catch (Exception e) { throw new UsernameNotFoundException("Invalid token content"); } } // 原有查库逻辑替换为使用actualUsername查询 Optional<Aektab> aekAztab = aektabRepository.findByGeNr(actualUsername); // 后续原有判断逻辑保持不变即可 }
问题2:AuthenticationProvider和UserDetailsService的调用顺序
二者执行顺序为AuthenticationProvider先触发,UserDetailsService由AuthenticationProvider在认证流程内部按需调用,完整执行链路如下:
- 请求经过安全过滤器链时,自定义过滤器解析请求携带的凭证,构造未认证的Authentication对象,传入AuthenticationManager
- AuthenticationManager遍历所有已注册的AuthenticationProvider,找到第一个支持当前Authentication类型的Provider,调用其
authenticate()方法执行认证逻辑 - 如果使用Spring Security默认的
DaoAuthenticationProvider,该Provider会在自身认证逻辑中调用注入的UserDetailsService的loadUserByUsername方法,查询用户信息、完成密码校验、权限填充 - 如果是自定义AuthenticationProvider(比如你当前写的
CustomAuthenticationProvider),只有你主动在authenticate()方法里调用UserDetailsService,它才会执行;你当前贴的Provider代码里没有任何调用UserDetailsService的逻辑,正常情况下你写的MyUserDetailsService根本不会被触发。
注意:你当前自定义的CustomAuthenticationProvider没有做任何凭证校验,只要传入UsernamePasswordAuthenticationToken就直接返回认证通过状态,存在严重安全风险,需要补充JWT签名合法性校验、用户状态校验、权限匹配逻辑后再上线使用。
内容的提问来源于stack exchange,提问作者Arjun

