题目
实现JWT令牌吊销与自定义权限控制的Spring Security OAuth2资源服务器
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
OAuth2资源服务器配置,JWT验证机制,自定义权限控制,动态令牌吊销,Spring Security扩展
快速回答
实现要点:
- 配置
JwtDecoder并添加自定义验证器 - 通过
@PreAuthorize或自定义AccessDecisionVoter实现权限控制 - 使用Redis存储吊销令牌实现实时失效
- 自定义
JwtAuthenticationConverter转换权限 - 实现令牌吊销检查的
OAuth2TokenValidator
场景需求
在分布式系统中,需要确保已吊销的JWT令牌不能访问受保护资源,同时实现基于业务逻辑的细粒度权限控制(如:要求用户必须拥有REPORT_GENERATE权限且所属部门匹配)。
实现步骤
1. 基础资源服务器配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(jwtDecoder())
.jwtAuthenticationConverter(customJwtAuthConverter())
)
);
return http.build();
}
}2. 自定义权限转换器
public class CustomJwtAuthConverter implements Converter<Jwt, AbstractAuthenticationToken> {
@Override
public AbstractAuthenticationToken convert(Jwt jwt) {
// 从JWT claims提取自定义权限
Set<String> permissions = extractPermissions(jwt);
return new JwtAuthenticationToken(jwt, permissions);
}
private Set<String> extractPermissions(Jwt jwt) {
// 示例:从自定义claim 'user_authorities' 获取权限
return jwt.getClaimAsStringList("user_authorities")
.stream()
.map(perm -> "PERM_" + perm)
.collect(Collectors.toSet());
}
}3. 实现令牌吊销验证
Redis存储吊销令牌:
@Component
public class TokenRevocationService {
private final RedisTemplate<String, String> redisTemplate;
public void revokeToken(String jti, Duration ttl) {
redisTemplate.opsForValue().set("revoked:" + jti, "true", ttl);
}
public boolean isTokenRevoked(String jti) {
return Boolean.TRUE.equals(redisTemplate.hasKey("revoked:" + jti));
}
}自定义JWT验证器:
public class RevokedTokenValidator implements OAuth2TokenValidator<Jwt> {
private final TokenRevocationService revocationService;
@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
String jti = jwt.getId();
if (revocationService.isTokenRevoked(jti)) {
return OAuth2TokenValidatorResult.failure(
new OAuth2Error("invalid_token", "Token revoked", null)
);
}
return OAuth2TokenValidatorResult.success();
}
}配置JwtDecoder:
@Bean
public JwtDecoder jwtDecoder(TokenRevocationService revocationService) {
NimbusJwtDecoder decoder = JwtDecoders.fromIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(
new JwtTimestampValidator(),
new JwtIssuerValidator(issuerUri),
new RevokedTokenValidator(revocationService) // 添加自定义验证
);
decoder.setJwtValidator(validator);
return decoder;
}4. 自定义权限控制
方法级权限控制:
@RestController
public class ReportController {
@PreAuthorize("hasAuthority('REPORT_GENERATE') and @departmentChecker.hasSameDepartment(authentication)")
@GetMapping("/reports")
public ResponseEntity<?> generateReport() {
// 业务逻辑
}
}
@Component("departmentChecker")
public class DepartmentChecker {
public boolean hasSameDepartment(Authentication auth) {
Jwt jwt = ((JwtAuthenticationToken) auth).getToken();
String tokenDept = jwt.getClaimAsString("department");
String userDept = getCurrentUserDept(); // 从数据库获取
return tokenDept.equals(userDept);
}
}关键原理
- JWT验证流程:资源服务器使用
JwtDecoder解析和验证JWT签名/有效期,自定义验证器在链式验证中执行 - 权限映射:
JwtAuthenticationConverter将JWT claims转换为GrantedAuthority集合 - 吊销机制:基于JTI(JWT ID)在Redis存储吊销状态,验证时实时检查
- SpEL扩展:
@PreAuthorize支持调用Spring Bean实现复杂业务逻辑
最佳实践
- 使用短期令牌配合刷新令牌,减少吊销列表压力
- 为吊销令牌设置TTL(略大于JWT有效期)
- 在网关层或资源服务器添加速率限制防止吊销检查被滥用
- 敏感操作使用二次认证(如Step-up认证)
常见错误
- 未验证JWT签名导致伪造令牌攻击
- 权限映射时未添加前缀(Spring Security默认要求
ROLE_前缀) - 吊销检查未考虑时钟偏移造成提前失效
- 直接使用JWT中的权限数据而未二次验证业务状态
扩展知识
- OIDC用户信息端点:当需要实时用户状态时,可调用
/userinfo - Introspection端点:RFC 7662定义的令牌主动验证标准
- 分布式吊销:通过发布/订阅模式同步吊销事件
- JWT密钥轮换:使用
JwkSetUri自动获取新密钥