如何在Spring Boot的Spring Security中自定义JWT Token?
问题
在Spring Boot的Spring Security中自定义包含额外信息的JWT Token时,遇到响应格式和Token Claims不符合预期的问题:
当前响应格式
{"message": "success","username": "username12","accessToken": "Bearer accesstoken_string","refreshToken": "refreshToken_string","expireDate": 1680095658406,"roles": ["ROLE_ADMIN"]}
当前响应构建代码
return TokenDTO.builder() .username(userDetails.getUsername()) .message("success") .accessToken("Bearer " + accessToken) .roles(roles) .refreshToken(refreshToken.getToken()) .expireDate(expiryDate.getTime()) .build();
期望响应格式
{"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjIsInVzZXJuYW1lIjoiYWRtaW4tdXNlciIsInJvbGVzIjpbIkFETUlOIiwiU1VQRVJfQURNSU4iXX0.HRUslkLQPb3ujTCu_a4vs1TF1kidjziQsi9V369Edq0","expireDate": 1220227200,"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjIsInVzZXJuYW1lIjoiYWRtaW4tdXNlciIsInJvbGVzIjpbIkFETUlOIiwiU1VQRVJfQURNSU4iXX0.HRUslkLQPb3ujTCu_a4vs1TF1kidjziQsi9V369Edq0"}
期望Token解析后的Claims
{"sub": "1234567890","iat": 1516239022,"exp": 1516239022,"username": "username12","roles": ["ADMIN","SUPER_ADMIN"]}
现有相关代码
CustomJwtUserDetails类
@RequiredArgsConstructor public class CustomJwtUserDetails implements UserDetails { private final User user; @Override public Collection<? extends GrantedAuthority> getAuthorities() { UserRole roles = user.getUserRole(); List<SimpleGrantedAuthority> authories = new ArrayList<>(); authories.add(new SimpleGrantedAuthority(roles.name())); return authories; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } public Long getId() { return user.getId(); } public String getEmail() { return user.getEmail(); } }
TokenProvider类
@Component public class JwtTokenProvider { @Value("${token.secret}") private String APP_SECRET; @Value("${token.expires-in}") private long EXPIRES_IN; public String generateJwtToken(Authentication auth) { UserDetails userDetails = (UserDetails) auth.getPrincipal(); Date now = new Date(); Date expiryDate = new Date(now.getTime() + EXPIRES_IN); String token = Jwts.builder() .setSubject(userDetails.getUsername()) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, APP_SECRET) .compact(); return token; } public String getUserNameFromJwtToken(String token) { return Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token).getBody().getSubject(); } public Long getUserIdFromJwt(String token) { Claims claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token).getBody(); return Long.parseLong(claims.getSubject()); } boolean validateToken(String token) { try { Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token); return !isTokenExpired(token); } catch (MalformedJwtException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException e) { return false; } } private boolean isTokenExpired(String token) { Date expiration = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token).getBody().getExpiration(); return expiration.before(new Date()); } }
解决方案
1. 修改generateJwtToken方法,添加自定义Claims
将UserDetails强转为自定义实现类,补充username、roles等自定义Claims,同时把sub字段改为用户ID(匹配期望格式):
import java.util.stream.Collectors; public String generateJwtToken(Authentication auth) { // 强转为自定义UserDetails实现 CustomJwtUserDetails userDetails = (CustomJwtUserDetails) auth.getPrincipal(); Date now = new Date(); Date expiryDate = new Date(now.getTime() + EXPIRES_IN); // 提取角色并移除ROLE_前缀,匹配期望的ADMIN格式 List<String> roles = userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .map(role -> role.replace("ROLE_", "")) .collect(Collectors.toList()); return Jwts.builder() .setSubject(userDetails.getId().toString()) // sub字段存储用户ID .setIssuedAt(now) .setExpiration(expiryDate) // 添加自定义Claims .claim("username", userDetails.getUsername()) .claim("roles", roles) .signWith(SignatureAlgorithm.HS512, APP_SECRET) .compact(); }
2. 调整响应构建代码
按照期望格式移除多余字段,同时去掉accessToken的Bearer 前缀:
return TokenDTO.builder() .accessToken(accessToken) // 移除Bearer前缀 .refreshToken(refreshToken.getToken()) .expireDate(expiryDate.getTime()) .build();
同时需要确保TokenDTO类仅保留accessToken、expireDate、refreshToken三个字段,删除message、username、roles属性。
3. 修正getUserIdFromJwt方法
现在sub字段存储的是用户ID,直接解析即可:
public Long getUserIdFromJwt(String token) { Claims claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token).getBody(); return Long.parseLong(claims.getSubject()); }
4. 额外注意事项
- 若
UserRole枚举值本身就是ADMIN而非ROLE_ADMIN,可删除replace("ROLE_", "")代码; - 生成refreshToken时,需同步应用上述自定义Claims逻辑,保证格式一致;
- 确保
TokenDTO的序列化配置不会输出多余字段。
内容的提问来源于stack exchange,提问作者Sercan Noyan Germiyanoğlu
相关产品推荐
相关产品推荐

