Spring Boot中JWT令牌携带用户ID实现及解析错误排查
嗨,我来帮你拆解并解决这三个Spring Boot + JWT场景下的常见问题:
问题1:纯JWT(无OAuth2)携带用户ID的实现方式
不用OAuth2完全可以实现JWT携带用户ID,核心是在生成JWT时把用户ID作为自定义Claim加入令牌,解析时再从中取出。具体步骤如下:
第一步:自定义UserDetails实现类,包含用户ID字段
创建CustomUserDetails类,实现UserDetails接口并添加userId属性:public class CustomUserDetails implements UserDetails { private Long userId; private String username; private String password; private Collection<? extends GrantedAuthority> authorities; // 构造方法、getter/setter,以及UserDetails接口要求的isAccountNonExpired等方法实现 }第二步:编写JWT生成工具类,加入用户ID到令牌
写一个JwtTokenProvider组件,负责JWT的生成与解析:@Component public class JwtTokenProvider { @Value("${jwt.secret}") private String secretKey; @Value("${jwt.expiration}") private long validityInMilliseconds; // 生成带用户ID的JWT令牌 public String createToken(CustomUserDetails userDetails) { Claims claims = Jwts.claims().setSubject(userDetails.getUsername()); claims.put("userId", userDetails.getUserId()); // 把用户ID加入自定义Claim claims.put("roles", userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList())); Date now = new Date(); Date validity = new Date(now.getTime() + validityInMilliseconds); return Jwts.builder() .setClaims(claims) .setIssuedAt(now) .setExpiration(validity) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); } // 从JWT中解析用户ID public Long getUserIdFromToken(String token) { Claims claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); return claims.get("userId", Long.class); } // 辅助方法:验证token有效性、获取用户名等 }第三步:登录接口中生成并返回带用户ID的令牌
在登录成功后,调用工具类生成令牌并返回给前端:@PostMapping("/login") public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) { // 用户名密码验证逻辑 CustomUserDetails userDetails = customUserDetailsService.loadUserByUsername(loginRequest.getUsername()); String jwt = jwtTokenProvider.createToken(userDetails); return ResponseEntity.ok(new JwtResponse(jwt, userDetails.getUserId(), userDetails.getUsername())); }
问题2:继承WebSecurityConfigurerAdapter时扩展JWT(类似TokenEnhancer)
OAuth2的TokenEnhancer是用来扩展OAuth2令牌的,但纯JWT场景下不需要依赖它——我们可以通过自定义JWT生成逻辑实现同样的扩展效果,本质就是在生成JWT时添加自定义Claims,就像上面JwtTokenProvider里做的那样。
在你的WebSecurityConfigurerAdapter配置类中,只需注入自定义的JwtTokenProvider,并在过滤器中使用它解析令牌即可:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtTokenProvider jwtTokenProvider; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated() .and() .addFilterBefore(new JWTAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); } // 其他配置:密码编码器、UserDetailsService实例化等 }
这种方式比OAuth2的TokenEnhancer更轻量,完全适配纯JWT场景。
问题3:JsonParseException错误的解决
你的错误是因为直接把整个JWT字符串传给objectMapper.readTree(jwt)——JWT是由.分隔的三段式字符串(Header.Payload.Signature),并不是纯JSON,所以直接解析会抛出异常。
正确的做法是先提取中间的Payload部分,Base64解码后再解析成JSON:
// 在JWTAuthenticationFilter中添加Payload提取方法 private String extractJwtPayload(String jwt) { String[] jwtParts = jwt.split("\\."); if (jwtParts.length != 3) { throw new IllegalArgumentException("无效的JWT令牌格式"); } // JWT使用URL安全的Base64编码,需要用对应解码器 return new String(Base64.getUrlDecoder().decode(jwtParts[1])); } // 修复解析逻辑 String payload = extractJwtPayload(jwt); JsonNode jsonJwt = objectMapper.readTree(payload); Long userId = jsonJwt.get("userId").asLong();
这样就能正确解析出JWT中的用户ID等自定义信息了。
内容的提问来源于stack exchange,提问作者dEs12ZER

