侧边栏壁纸
博主头像
colo

欲买桂花同载酒

  • 累计撰写 1823 篇文章
  • 累计收到 0 条评论

实现JWT令牌吊销与自定义权限控制的Spring Security OAuth2资源服务器

2025-12-13 / 0 评论 / 4 阅读

题目

实现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自动获取新密钥