基于 Spring Security + JWT + OAuth 2.0 的完整架构与安全实践
文档版本 :v1.3
适用场景 :金融、政务、大型企业级微服务系统
技术栈 :Spring Boot 3.2+(Jakarta EE 10)、Spring Security 6.1+、Spring Authorization Server 1.1+、OAuth 2.1、JWT(RS256)
合规标准:等保2.0、GDPR、OWASP ASVS 4.0、NIST SP 800-63B
一、核心架构与认证流程
1.1 架构角色定义
| 组件 | 职责 | 是否执行认证 |
|---|---|---|
| 授权服务器(Authorization Server) | 用户身份核验、令牌签发、会话管理 | ✅ 唯一认证点 |
| 客户端应用(Client Application) | 发起 OAuth2 登录流程、处理回调、存储令牌 | ❌ |
| API 网关(Gateway) | 统一入口、JWT 验证、权限路由、安全头注入 | ❌(仅验证) |
| 资源服务器(Resource Server) | 业务逻辑、细粒度授权控制 | ❌(仅验证) |
🔑 关键结论:
- 认证(Authentication)只在授权服务器完成
- 网关和业务服务仅执行 JWT 验证(Verification)与授权(Authorization)
1.2 SSO 认证流程(OAuth 2.1 PKCE 模式)
业务服务 API 网关 授权服务器 客户端应用 用户浏览器 业务服务 API 网关 授权服务器 客户端应用 用户浏览器 访问受保护页面 重定向到 /oauth2/authorization/sso 请求登录 (含 PKCE code_challenge) 返回登录页 提交用户名/密码 验证凭证 + MFA(可选) 重定向回 Client (带 code + state) 访问 /login/oauth2/code/sso?code=xxx 用 code + code_verifier 换取 token 返回 Access Token (JWT) + ID Token 设置 Cookie/LocalStorage,跳转原页面 请求 /api/data (带 Bearer Token) 验证 JWT(签名/过期/issuer) 透传请求 + X-User-ID 头 返回业务数据
二、客户端应用配置(Spring Boot 3.x)
2.1 Maven 依赖(pom.xml)
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<dependencies>
<!-- Web 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 安全框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- JWT 支持(官方推荐) -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.37</version> <!-- 固定版本防漏洞 -->
</dependency>
<!-- 安全加固 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
</dependencies>
2.2 安全配置类(SecurityConfig.java)
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
/**
* OAuth2 客户端安全链(处理登录/回调)
*/
@Bean
@Order(1)
public SecurityFilterChain oauth2FilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/login/oauth2/**", "/oauth2/**", "/logout")
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2Login(oauth2 -> oauth2
.loginPage("/oauth2/authorization/sso")
.authorizationEndpoint(endpoint -> endpoint
.authorizationRequestRepository(cookieAuthorizationRequestRepository()) // PKCE 关键
)
.redirectionEndpoint(endpoint -> endpoint
.baseUri("/login/oauth2/code/*")
)
.userInfoEndpoint(endpoint -> endpoint
.oidcUserService(oidcUserService())
)
.successHandler(authenticationSuccessHandler())
.failureHandler(authenticationFailureHandler())
)
.logout(logout -> logout
.logoutUrl("/logout")
.deleteCookies("JSESSIONID", "SESSION")
.logoutSuccessHandler(logoutSuccessHandler())
)
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/login/oauth2/code/*") // OAuth2 回调放行
)
.sessionManagement(session -> session
.maximumSessions(1) // 防会话固定
.maxSessionsPreventsLogin(false)
);
return http.build();
}
/**
* 静态资源放行
*/
@Bean
@Order(2)
public SecurityFilterChain staticResourceFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/css/**", "/js/**", "/images/**", "/public/**")
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
/**
* API 资源服务器安全链(验证 JWT)
*/
@Bean
@Order(3)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
.decoder(jwtDecoder()) // JWK 动态解码
)
)
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives(
"default-src 'self'; script-src 'self' https://trusted.cdn.com; object-src 'none'"))
.xssProtection(xss -> xss.block(true))
.frameOptions(fo -> fo.deny())
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)) // HSTS 1年
);
return http.build();
}
// ========== 关键组件 ==========
@Bean
public AuthorizationRequestRepository<OAuth2AuthorizationRequest>
cookieAuthorizationRequestRepository() {
CookieAuthorizationRequestRepository repo = new CookieAuthorizationRequestRepository();
repo.setCookieHttpOnly(true); // 防 XSS
repo.setCookieSecure(true); // 仅 HTTPS
return repo;
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri("https://auth-server.com/.well-known/jwks.json")
.jwtProcessorCustomizer(processor -> {
processor.setJWSKeySelector(new JWSVerificationKeySelector<>(
JWSAlgorithm.RS256, // 强制 RS256
new JWKSource<SecurityContext>() { /* 实现略 */ }
));
})
.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
converter.setAuthoritiesClaimName("authorities");
converter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
return jwtConverter;
}
}
2.3 应用配置(application.yml)
yaml
spring:
security:
oauth2:
client:
provider:
sso:
issuer-uri: "https://auth-server.com" # 自动发现 endpoints
jwk-set-uri: "https://auth-server.com/.well-known/jwks.json"
registration:
sso:
client-id: "${CLIENT_ID}" # 从 Vault 注入
client-secret: "${CLIENT_SECRET}"
authorization-grant-type: "authorization_code"
redirect-uri: "{baseUrl}/login/oauth2/code/sso"
scope: "openid,profile,email,roles" # 最小权限
client-authentication-method: "client_secret_post"
# 安全头默认值(可被 SecurityConfig 覆盖)
server:
forward-headers-strategy: native # 信任代理头(X-Forwarded-Proto)
三、授权服务器关键实现(Spring Authorization Server)
3.1 JWT 生成与声明定制
java
@Configuration
public class AuthorizationServerConfig {
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer() {
return context -> {
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
// 注入企业级声明
context.getClaims()
.claim("client_id", context.getPrincipal().getName())
.claim("tenant_id", resolveTenantId(context))
.claim("authorities", extractAuthorities(context));
// 安全时效控制
String clientId = context.getRegisteredClientId();
if ("mobile-app".equals(clientId)) {
context.getClaims().expiresAt(Instant.now().plus(2, ChronoUnit.HOURS));
} else {
context.getClaims().expiresAt(Instant.now().plus(30, ChronoUnit.MINUTES));
}
}
};
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
// 从 HSM/Vault 加载密钥(示例)
RSAKey rsaKey = new RSAKey.Builder(loadPublicKey())
.privateKey(loadPrivateKeyFromHSM()) // 私钥绝不硬编码
.keyID("auth-key-2026")
.algorithm(JWSAlgorithm.RS256)
.build();
return new ImmutableJWKSet<>(new JWKSet(rsaKey));
}
}
3.2 为什么必须用非对称加密(RS256)?
| 对比项 | HS256(对称) | RS256(非对称) |
|---|---|---|
| 密钥管理 | 所有服务共享 secret | 仅授权服务器持有私钥 |
| 安全风险 | 任一服务泄露即全系统沦陷 | 业务服务无密钥,风险隔离 |
| 合规性 | 不符合等保要求 | 满足金融级安全标准 |
| 验证方式 | 需网络调用查密钥 | 本地公钥验证 |
✅ 企业强制要求:使用 RS256 或 ES256,禁用 HS256。
四、API 网关集成方案(Spring Cloud Gateway)
4.1 网关安全配置
java
@Configuration
public class GatewaySecurityConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder,
JwtDecoder jwtDecoder) {
return builder.routes()
.route("user-service", r -> r
.path("/api/users/**")
.filters(f -> f
.tokenRelay() // 透传令牌
.dedupeResponseHeader("Access-Control-Allow-Credentials", "RETAIN_FIRST")
)
.uri("lb://user-service")
)
.build();
}
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
ReactiveJwtDecoder jwtDecoder) {
http
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/api/public/**").permitAll()
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtDecoder(jwtDecoder))
)
.csrf(ServerHttpSecurity.CsrfSpec::disable) // Gateway 通常禁用 CSRF
.headers(headers -> headers
.hsts(hsts -> hsts.includeSubdomains(true).maxAge(Duration.ofDays(365)))
.frameOptions(frame -> frame.deny())
);
return http.build();
}
}
4.2 网关职责边界
| 功能 | 是否执行 | 说明 |
|---|---|---|
| JWT 签名验证 | ✅ | 使用 JWK 公钥 |
| 令牌过期检查 | ✅ | 检查 exp claim |
| 权限校验 | ⚠️ 可选 | 仅粗粒度(如 /admin/**) |
| 用户身份认证 | ❌ | 不查询用户数据库 |
| 敏感头透传 | ✅ | X-User-ID, X-Roles |
五、企业级安全加固清单
5.1 传输层安全
- 全站 HTTPS(TLS 1.2+)
- HSTS 头(
max-age=31536000; includeSubDomains) - 负载均衡器强制 HTTP→HTTPS 跳转
5.2 令牌安全
- Access Token ≤ 30 分钟
- Refresh Token ≤ 7 天 + 设备绑定
- JWK Set 动态密钥轮转
- Redis 黑名单支持紧急吊销
5.3 应用防护
| 风险 | 防护措施 |
|---|---|
| CSRF | PKCE + CookieAuthorizationRequestRepository |
| XSS | CSP 头 + Thymeleaf 自动转义 |
| 会话固定 | 登录后生成新 Session ID |
| 暴力破解 | 登录失败 5 次锁定 15 分钟(Redis 计数) |
| 信息泄露 | 日志脱敏(屏蔽 token/密码) |
5.4 运维安全
- 密钥管理:私钥存储于 HashiCorp Vault/AWS KMS
- 审计日志:记录所有登录事件(IP、设备、时间)
- 健康检查 :
/actuator/health暴露 JWT 验证状态 - 监控告警:令牌验证失败率突增告警
六、常见问题解答(FAQ)
Q1: 网关是否执行认证?
否。网关仅验证 JWT 有效性(签名/过期),不进行用户身份认证。认证只在授权服务器完成。
Q2: 为什么不用 Session 而用 JWT?
- 微服务友好:无状态,无需共享 Session 存储
- 性能优势:本地验证,无 Redis 查询开销
- 标准兼容:符合 OAuth 2.0 / OpenID Connect 规范
Q3: 如何实现登出?
- 客户端清除本地 Token
- 调用授权服务器
/connect/logout(OIDC 标准)- (可选)将 Token 加入 Redis 黑名单(应对 Access Token 未过期场景)
Q4: 多租户如何支持?
在 JWT 中注入
tenant_idclaim:
javacontext.getClaims().claim("tenant_id", resolveTenantId(context));网关/业务服务通过该 claim 路由或过滤数据。
七、总结与最佳实践
✅ 架构原则
- 认证与授权分离:授权服务器专职认证,业务系统专注授权
- 最小权限原则:OAuth2 Scope 精细化控制
- 纵深防御:传输层 + 应用层 + 运维层多层防护
- 标准优先:严格遵循 OAuth 2.1 / OIDC 1.0 规范
🚫 反模式警告
- 在业务服务中实现登录页
- 使用 HS256 对称加密
- 硬编码 client-secret
- 忽略 PKCE(公共客户端必须启用)
🔮 扩展建议
- MFA 集成:在授权服务器添加 TOTP/SMS 验证
- 设备管理:记录登录设备指纹,支持远程登出
- ABAC 支持 :在 JWT 中注入属性(如
department=finance) - 合规审计:对接 SIEM 系统(如 Splunk/ELK)
文档更新日期:2026年1月29日