Spring Boot 安全防线:OAuth2 资源服务器与细粒度鉴权实战
Spring Boot 安全防线:OAuth2 资源服务器与细粒度鉴权实战
一、认证不等于鉴权:微服务场景下的权限空洞
很多 Spring Boot 项目在安全上只做了"认证"——用户登录后拿到 JWT,后续请求携带 Token 即可访问所有接口。这在单体应用中勉强可行,但在微服务架构下会暴露严重的权限空洞:所有已认证用户拥有相同的访问范围,服务间调用缺乏身份传递,接口级别的操作权限完全缺失。
更常见的问题是 JWT 中塞入过多权限信息,导致 Token 体积膨胀到数 KB,每次请求都携带巨大的 Header。一旦权限变更,已签发的 Token 无法即时失效,只能等待过期。OAuth2 资源服务器模式提供了一种更合理的拆分——认证由授权服务器负责,资源服务器只做 Token 验证和鉴权,职责清晰且可独立演进。
二、OAuth2 资源服务器架构:Token 验证与权限加载
Spring Security 的 OAuth2 Resource Server 模块支持 JWT 和 Introspection 两种 Token 验证方式。JWT 方式本地验证签名,无需回调授权服务器,性能更优;Introspection 方式每次请求都向授权服务器校验 Token,支持即时撤销。
sequenceDiagram
participant C as 客户端
participant GW as API 网关
participant RS as 资源服务器
participant AS as 授权服务器
participant DB as 权限数据库
C->>AS: 登录获取 JWT
AS-->>C: JWT (含 userId, scope)
C->>GW: 携带 JWT 访问资源
GW->>RS: 转发请求 + Token
RS->>RS: 本地验证 JWT 签名
RS->>DB: 根据 userId 查询细粒度权限
DB-->>RS: 权限列表
RS->>RS: 鉴权判断
alt 权限不足
RS-->>GW: 403 Forbidden
GW-->>C: 权限不足
else 权限通过
RS-->>GW: 200 OK + 资源数据
GW-->>C: 响应数据
end
关键设计点在于"JWT 只携带身份标识,权限实时查询"。JWT 中只保留 sub(用户 ID)和 scope(OAuth2 标准范围),细粒度的接口权限由资源服务器从数据库或缓存中加载。这样既控制了 Token 体积,又支持权限的即时生效。
三、生产级代码实现:多层鉴权与自定义权限模型
3.1 资源服务器基础配置
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// 公开端点
.requestMatchers("/api/public/**").permitAll()
// 健康检查
.requestMatchers("/actuator/health").permitAll()
// 其他请求需认证
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(new CustomJwtAuthenticationConverter())
)
// Token 验证失败时的自定义响应
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler())
)
// 禁用 CSRF(无状态 API)
.csrf(CsrfConfigurer::disable)
// 无状态会话
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
}
3.2 自定义 JWT 转换器:从 Token 到权限
public class CustomJwtAuthenticationConverter
implements Converter<Jwt, AbstractAuthenticationToken> {
private final PermissionService permissionService;
@Override
public AbstractAuthenticationToken convert(Jwt jwt) {
// 从 JWT 中提取用户标识
String userId = jwt.getSubject();
String tenantId = jwt.getClaimAsString("tenant_id");
// 构建基础权限集合(来自 JWT 的 scope)
Collection<GrantedAuthority> authorities = extractScopes(jwt);
// 加载细粒度权限(从缓存或数据库)
Collection<GrantedAuthority> fineGrainedAuthorities =
permissionService.loadPermissions(userId, tenantId);
// 合并权限
List<GrantedAuthority> allAuthorities = new ArrayList<>(authorities);
allAuthorities.addAll(fineGrainedAuthorities);
return new JwtAuthenticationToken(jwt, allAuthorities, userId);
}
private Collection<GrantedAuthority> extractScopes(Jwt jwt) {
String scope = jwt.getClaimAsString("scope");
if (scope == null || scope.isEmpty()) {
return Collections.emptyList();
}
return Arrays.stream(scope.split(" "))
.map(s -> new SimpleGrantedAuthority("SCOPE_" + s))
.collect(Collectors.toList());
}
}
3.3 方法级鉴权:自定义权限注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasPermission(#resourceId, #resourceType, #action)")
public @interface RequirePermission {
String resourceType();
String action();
}
// 权限评估器实现
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
private final PermissionRepository permissionRepo;
@Override
public boolean hasPermission(Authentication authentication,
Object resourceId,
Object permission) {
if (authentication == null || !authentication.isAuthenticated()) {
return false;
}
String userId = authentication.getName();
// permission 格式: "resourceType:action"
String[] parts = permission.toString().split(":");
if (parts.length != 2) {
return false;
}
String resourceType = parts[0];
String action = parts[1];
// 检查用户是否拥有该资源的操作权限
return permissionRepo.hasPermission(userId,
resourceId.toString(), resourceType, action);
}
@Override
public boolean hasPermission(Authentication authentication,
Serializable targetId,
String targetType,
Object permission) {
return hasPermission(authentication, targetId,
targetType + ":" + permission);
}
}
3.4 服务间调用的身份传递
@Component
public class ServiceAuthenticationInterceptor implements ClientHttpRequestInterceptor {
private final TokenRelayService tokenRelay;
@Override
public ClientHttpResponse intercept(HttpRequest request,
byte[] body,
ClientHttpRequestExecution execution)
throws IOException {
// 从当前上下文获取用户身份
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof JwtAuthenticationToken jwtAuth) {
// 传递原始 JWT,下游服务可验证
request.getHeaders().set("Authorization",
"Bearer " + jwtAuth.getToken().getTokenValue());
// 附加服务间调用标识
request.getHeaders().set("X-Caller-Service", "order-service");
} else {
// 服务间内部调用,使用服务账号 Token
String serviceToken = tokenRelay.getServiceToken();
request.getHeaders().setBearerAuth(serviceToken);
}
return execution.execute(request, body);
}
}
四、安全架构的隐性代价:性能、复杂度与密钥轮换
权限实时查询的性能开销:每次请求都查询数据库获取权限,在高并发场景下会成为瓶颈。建议采用两级缓存——本地 Caffeine 缓存(TTL 30 秒)+ Redis 缓存(TTL 5 分钟),权限变更时主动失效缓存。缓存的代价是权限生效延迟,需根据业务安全等级权衡 TTL。
JWT 密钥轮换的窗口风险:RSA 密钥需要定期轮换,但轮换期间旧 Token 可能仍被使用。Spring Security 支持配置多个验证密钥(jwkSetUri 自动处理),但自签发场景需手动维护密钥列表。建议设置 15 分钟的密钥重叠期,新旧密钥同时有效。
细粒度鉴权的维护成本:每个接口都配置权限注解,随着业务增长,权限矩阵会变得极其复杂。建议按资源类型而非接口粒度配置权限,通过 RBAC + ABAC 混合模型降低复杂度。角色覆盖 80% 的通用场景,属性规则覆盖 20% 的特殊场景。
服务间 Token 传递的信任边界:下游服务收到上游传递的 JWT 后,无法区分请求是来自用户还是服务。建议在 JWT 中增加 azp(authorized party)声明,或在 Header 中增加 X-Caller-Service 标识,下游服务据此决定是否信任该请求的权限范围。
五、总结
Spring Boot 安全架构的核心原则是"认证集中、鉴权分散"——授权服务器负责签发 Token,资源服务器负责验证和鉴权。本文方案的关键实现为:JWT 轻量化(只携带身份标识)、权限实时加载(缓存 + 数据库)、方法级鉴权(自定义注解 + PermissionEvaluator)、服务间身份传递(Token Relay)。落地时需重点关注三个配置:JWT 签名算法(建议 RS256,避免 HS256 的密钥分发问题)、权限缓存 TTL(建议 30 秒本地 + 5 分钟 Redis)、密钥轮换周期(建议 90 天,重叠期 15 分钟)。上线前务必进行权限矩阵的全面审计,确保无越权接口。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)