4. 资源服务器配置:JWT令牌解析、权限校验、自定义鉴权异常处理

在异步导出服务中,资源服务器是OAuth2.0架构的最后一环,负责验证访问令牌的有效性、解析用户身份与权限,并最终决定是否放行请求。本章聚焦于Spring Security Resource Server的实战配置,涵盖JWT令牌解析、细粒度权限校验以及自定义鉴权异常处理三个核心模块。

前置知识:确保已引入 spring-security-oauth2-resource-server 和 spring-security-oauth2-jose 依赖,并已配置授权服务器的JWK Set URI或本地公钥。

4.1 JWT令牌解析与验证流程

资源服务器收到请求后,首先从 Authorization: Bearer <token> 头中提取JWT,然后执行以下验证链:

  1. 签名验证:使用授权服务器提供的公钥(JWK Set URI或本地配置)验证JWT签名,防止令牌被篡改。
  2. 有效期检查:校验 exp(过期时间)和 nbf(生效时间)声明,拒绝过期或未生效的令牌。
  3. 颁发者校验:验证 iss 声明是否与授权服务器匹配。
  4. 受众校验(可选):验证 aud 声明是否包含当前资源服务器的标识。
  5. 自定义声明校验:根据业务需求校验 scope、role 等自定义声明。
配置示例:基于Spring Boot的Resource Server自动配置,只需在 application.yml 中指定 spring.security.oauth2.resourceserver.jwt.issuer-uri 或 jwk-set-uri,框架会自动完成签名验证和有效期检查。
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com
          # 或直接指定 jwk-set-uri: https://auth.example.com/.well-known/jwks.json

以下SVG图展示了JWT令牌从请求到达资源服务器到完成解析的完整流程:

JWT令牌解析与验证流程 JWT令牌解析与验证流程 客户端请求 提取 Authorization: Bearer 签名验证(JWK公钥) 声明校验(exp/nbf/iss/scope) 构建 JwtAuthenticationToken

4.2 权限校验:从JWT声明到方法级控制

JWT令牌解析完成后,资源服务器需要根据令牌中的权限声明进行访问控制。Spring Security支持两种主流权限校验方式:

校验方式 配置位置 典型用法 适用场景
HttpSecurity URL规则 SecurityFilterChain .antMatchers("/api/export/**").hasAuthority("SCOPE_export") 粗粒度接口级权限
方法级注解 Service/Controller方法 @PreAuthorize("hasAuthority('SCOPE_export')") 细粒度业务逻辑权限
自定义权限评估器 自定义PermissionEvaluator @PreAuthorize("hasPermission(#taskId, 'EXPORT')") 基于资源的动态权限
注意:JWT中的 scope 声明默认会被映射为 SCOPE_ 前缀的权限。例如,JWT中包含 "scope": "export read",则对应的权限为 SCOPE_export 和 SCOPE_read。如果使用自定义声明(如 roles),需要实现 Converter<Jwt, AbstractAuthenticationToken> 进行映射。

以下是一个自定义JWT权限转换器的实现示例:

@Component
public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {

    @Override
    public AbstractAuthenticationToken convert(Jwt jwt) {
        // 从JWT中提取自定义roles声明
        List<String> roles = jwt.getClaimAsStringList("roles");
        if (roles == null) {
            roles = Collections.emptyList();
        }
        // 将roles转换为GrantedAuthority,并保留scope权限
        Set<GrantedAuthority> authorities = new HashSet<>();
        roles.forEach(role -> authorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
        // 同时保留scope权限
        authorities.addAll(OAuth2TokenExtractor.extractAuthorities(jwt));
        return new JwtAuthenticationToken(jwt, authorities);
    }
}

在Security配置中注入该转换器:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .oauth2ResourceServer(oauth2 -> oauth2
            .jwt(jwt -> jwt
                .jwtAuthenticationConverter(customJwtAuthenticationConverter())
            )
        )
        .authorizeHttpRequests(authz -> authz
            .antMatchers("/api/export/**").hasAuthority("SCOPE_export")
            .antMatchers("/api/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
        );
    return http.build();
}

4.3 自定义鉴权异常处理

当JWT验证失败或权限不足时,Spring Security默认返回通用的401或403响应,缺乏业务可读性。通过自定义异常处理,可以返回结构化的错误信息,便于客户端定位问题。

以下SVG图展示了鉴权异常处理的分发路径:

自定义鉴权异常处理流程 自定义鉴权异常处理流程 请求进入资源服务器 BearerTokenAuthenticationFilter 鉴权成功 → 执行业务 鉴权失败 → 异常抛出 自定义异常处理器 返回结构化JSON错误

实现自定义异常处理需要实现 AuthenticationEntryPoint(处理401未认证)和 AccessDeniedHandler(处理403无权限):

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpStatus.UNAUTHORIZED.value());

        Map<String, Object> errorBody = new HashMap<>();
        errorBody.put("code", 401);
        errorBody.put("message", "认证失败:" + authException.getMessage());
        errorBody.put("path", request.getRequestURI());
        errorBody.put("timestamp", LocalDateTime.now().toString());

        response.getWriter().write(new ObjectMapper().writeValueAsString(errorBody));
    }
}

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpStatus.FORBIDDEN.value());

        Map<String, Object> errorBody = new HashMap<>();
        errorBody.put("code", 403);
        errorBody.put("message", "权限不足:" + accessDeniedException.getMessage());
        errorBody.put("path", request.getRequestURI());
        errorBody.put("timestamp", LocalDateTime.now().toString());

        response.getWriter().write(new ObjectMapper().writeValueAsString(errorBody));
    }
}

在Security配置中注册这两个处理器:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .exceptionHandling(exceptions -> exceptions
            .authenticationEntryPoint(customAuthenticationEntryPoint)
            .accessDeniedHandler(customAccessDeniedHandler)
        )
        .oauth2ResourceServer(oauth2 -> oauth2
            .jwt(jwt -> jwt
                .jwtAuthenticationConverter(customJwtAuthenticationConverter())
            )
        )
        .authorizeHttpRequests(authz -> authz
            .antMatchers("/api/export/**").hasAuthority("SCOPE_export")
            .anyRequest().authenticated()
        );
    return http.build();
}
最佳实践:自定义异常处理返回的错误信息应遵循统一的API规范,包含错误码、可读消息、请求路径和时间戳。避免在错误响应中泄露敏感信息(如堆栈跟踪、内部路径等)。

4.4 常见问题与避坑指南

问题 原因 解决方案
JWT签名验证失败 JWK Set URI不可达或公钥不匹配 检查网络连通性;确认授权服务器与资源服务器使用相同的签名算法和密钥对
权限始终为 SCOPE_ 前缀 默认的JWT权限映射只处理 scope 声明 实现自定义 JwtAuthenticationConverter 映射自定义声明
自定义异常处理器不生效 配置顺序错误或未注入Bean 确保 exceptionHandling() 配置在 oauth2ResourceServer() 之前;确认处理器被Spring管理
JWT过期后仍能访问 时钟偏差(clock skew)配置过大 在 NimbusJwtDecoder 中设置合理的 clockSkew(默认60秒)
安全提示:生产环境中务必启用HTTPS,防止JWT在传输过程中被截获。同时,建议设置较短的JWT有效期(如15分钟),并配合Refresh Token机制实现安全的令牌续期。