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

如何限制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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 18:45:29