如何限制JWT Refresh Token仅可用于刷新端点?
解决方案:限制Refresh Token仅用于刷新端点并实现一次性使用
一、修复JWT权限解析,让Refresh Token权限生效
问题根源是你的JwtAuthenticationConverter仅配置从roles字段提取权限,但Refresh Token的权限存在scope字段中,导致Spring Security无法识别Refresh Token的专属权限。调整转换器,让它同时支持从两个字段提取权限:
修改WebSecurityConfig中的jwtAuthenticationConverter相关代码:
@Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { // 处理Refresh Token的scope字段权限 JwtGrantedAuthoritiesConverter scopeConverter = new JwtGrantedAuthoritiesConverter(); scopeConverter.setAuthoritiesClaimName("scope"); scopeConverter.setAuthorityPrefix(""); // 你的scope已经自带ROLE_前缀,无需重复添加 // 处理AccessToken的roles字段权限 JwtGrantedAuthoritiesConverter rolesConverter = new JwtGrantedAuthoritiesConverter(); rolesConverter.setAuthoritiesClaimName("roles"); rolesConverter.setAuthorityPrefix("ROLE_"); // 组合两个转换器,同时解析两种权限来源 DelegatingJwtGrantedAuthoritiesConverter delegatingConverter = new DelegatingJwtGrantedAuthoritiesConverter(); delegatingConverter.setConverters(List.of(rolesConverter, scopeConverter)); var jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(delegatingConverter); return jwtAuthenticationConverter; }
然后刷新端点的权限校验调整为:
@PreAuthorize("hasAuthority('ROLE_REFRESH_TOKEN')") @GetMapping("/token/refresh") public TokensResponse refreshToken(HttpServletRequest request) { // 原有逻辑 }
此时AccessToken因无ROLE_REFRESH_TOKEN权限,将无法访问刷新端点。
二、实现Refresh Token一次性使用(需存储令牌)
要实现一次性使用,需记录有效Refresh Token,每次刷新时验证有效性,使用后标记失效,同时生成新令牌。以下用Redis作为存储示例:
1. 定义Refresh Token存储组件
@Component public class RefreshTokenStore { private final StringRedisTemplate redisTemplate; // 与Refresh Token生成时的过期时间保持一致(10分钟) private static final long TOKEN_EXPIRE_SECONDS = 10 * 60; public RefreshTokenStore(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } // 存储有效令牌 public void storeValidToken(String username, String refreshToken) { String key = "refresh_token:" + username + ":" + refreshToken; redisTemplate.opsForValue().set(key, "valid", TOKEN_EXPIRE_SECONDS, TimeUnit.SECONDS); } // 验证令牌是否有效 public boolean isTokenValid(String username, String refreshToken) { String key = "refresh_token:" + username + ":" + refreshToken; Boolean exists = redisTemplate.hasKey(key); return Boolean.TRUE.equals(exists); } // 标记令牌失效 public void invalidateToken(String username, String refreshToken) { String key = "refresh_token:" + username + ":" + refreshToken; redisTemplate.delete(key); } // 清除用户所有旧令牌(登录时调用) public void invalidateAllTokens(String username) { Set<String> keys = redisTemplate.keys("refresh_token:" + username + ":*"); if (keys != null && !keys.isEmpty()) { redisTemplate.delete(keys); } } }
2. 修改令牌生成与刷新逻辑
- 在
JwtTokenServiceImpl中注入存储组件,生成Refresh Token后存储:
@Service @RequiredArgsConstructor public class JwtTokenServiceImpl implements JwtTokenService { private final JwtEncoder jwtEncoder; private final RefreshTokenStore refreshTokenStore; @Override public String generateRefreshToken(User user) { Instant now = Instant.now(); String scope = "ROLE_REFRESH_TOKEN"; JwtClaimsSet claims = JwtClaimsSet.builder() .issuer("self") .issuedAt(now) .expiresAt(now.plus(10, ChronoUnit.MINUTES)) .subject(user.getUsername()) .claim("scope", scope) .build(); String refreshToken = this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue(); // 存储有效令牌 refreshTokenStore.storeValidToken(user.getUsername(), refreshToken); return refreshToken; } // 其他方法不变 }
- 修改
AuthController的刷新端点,添加验证与失效逻辑:
@PreAuthorize("hasAuthority('ROLE_REFRESH_TOKEN')") @GetMapping("/token/refresh") public TokensResponse refreshToken(HttpServletRequest request) { String headerAuth = request.getHeader("Authorization"); String previousRefreshToken = headerAuth.substring(7); String username = tokenService.parseToken(previousRefreshToken); // 验证令牌是否有效 if (!refreshTokenStore.isTokenValid(username, previousRefreshToken)) { throw new SecurityException("无效或已过期的Refresh Token"); } var user = (User) usrDetailsService.loadUserByUsername(username); // 失效旧令牌 refreshTokenStore.invalidateToken(username, previousRefreshToken); // 生成新令牌 String accessToken = tokenService.generateAccessToken(user); String refreshToken = tokenService.generateRefreshToken(user); return new TokensResponse(accessToken, refreshToken); }
- 登录时清除用户所有旧Refresh Token:
@PostMapping("/sign-in") public TokensResponse login(@RequestBody LoginRequest request) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(request.username, request.password); authManager.authenticate(authenticationToken); var user = (User) usrDetailsService.loadUserByUsername(request.username); // 清除旧令牌 refreshTokenStore.invalidateAllTokens(user.getUsername()); String accessToken = tokenService.generateAccessToken(user); String refreshToken = tokenService.generateRefreshToken(user); return new TokensResponse(accessToken, refreshToken); }
三、验证效果
- 使用AccessToken调用刷新端点:返回403权限拒绝
- 使用有效Refresh Token调用:成功获取新令牌,旧令牌再次调用会提示无效
- 用户重新登录后,之前的Refresh Token全部失效
内容的提问来源于stack exchange,提问作者eternal
相关产品推荐
相关产品推荐

