You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 07:55:02