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,然后执行以下验证链:
- 签名验证:使用授权服务器提供的公钥(JWK Set URI或本地配置)验证JWT签名,防止令牌被篡改。
- 有效期检查:校验
exp(过期时间)和nbf(生效时间)声明,拒绝过期或未生效的令牌。 - 颁发者校验:验证
iss声明是否与授权服务器匹配。 - 受众校验(可选):验证
aud声明是否包含当前资源服务器的标识。 - 自定义声明校验:根据业务需求校验
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令牌从请求到达资源服务器到完成解析的完整流程:
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图展示了鉴权异常处理的分发路径:
实现自定义异常处理需要实现 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机制实现安全的令牌续期。