概述
企业级应用的安全认证从传统的 Session-Cookie 模式逐步演进到无状态 JWT + OAuth2 架构。Spring Security 6.x 基于 Jakarta EE namespace,全面支持 Spring Boot 3.x,内置对 OAuth2 Resource Server、Spring Authorization Server(取代已停止维护的 Spring Security OAuth)的一等支持。本文系统讲解 RBAC/ABAC 设计、Spring Security 过滤器链、OAuth2 四大授权模式与 PKCE、Spring Authorization Server 配置、JWT 签发与验证、资源服务器搭建、方法级安全、前后端分离 CORS 方案以及常见攻击防御和生产调优。
1. 访问控制模型
1.1 RBAC 与 ABAC
RBAC(Role-Based Access Control)基于静态角色进行权限映射,适合组织架构清晰的业务系统。ABAC(Attribute-Based Access Control)基于动态属性(时间、地点、资源标签、用户等级)做细粒度决策,适合零信任架构和复杂政策引擎场景。
| 特性 | RBAC | ABAC |
|---|---|---|
| 决策维度 | 角色-权限映射 | 用户/资源/环境属性组合 |
| 复杂度 | 低,易于维护 | 高,需政策引擎 |
| 扩展性 | 角色爆炸问题 | 灵活,属性组合即可 |
| 典型场景 | B 端后台管理系统 | 金融风控、零信任网关 |
在 Spring Security 中,RBAC 通过 authority 与 role 表达,前缀 ROLE_ 区分角色与普通权限。ABAC 可通过自定义 PermissionEvaluator 或 SpEL 方法安全实现。
2. Spring Security 6 过滤器链架构
2.1 SecurityFilterChain 的核心设计
Spring Security 的执行完全围绕 SecurityFilterChain 展开。DelegatingFilterProxy(注册到 Servlet 容器)将请求委托给 FilterChainProxy,后者匹配并执行一条或多条 SecurityFilterChain。
// SecurityConfig.java — 定义自定义过滤器链
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
// 匹配以 /api 开头的请求
.securityMatcher("/api/**")
// 禁用 Session,采用无状态 JWT 方案
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 配置授权规则
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
// 启用 JWT 资源服务器
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtDecoder(jwtDecoder())
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
)
// 禁用 CSRF(无状态 API 无需 CSRF Token)
.csrf(csrf -> csrf.disable())
// 配置异常处理
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler())
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
// 使用 JWK Set URI 动态获取公钥验证 JWT 签名
return ReactiveJwtDecoders.fromIssuerLocation("http://auth-server:9000");
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
// 将 JWT 中的 claims 映射为 Spring Security 的 GrantedAuthority
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthoritiesClaimName("roles");
authoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
}
2.2 关键内置过滤器顺序
| 过滤器 | 职责 |
|---|---|
OAuth2AuthorizationRequestRedirectFilter | 构造并重定向到授权服务器 |
AuthorizationFilter | Spring Security 6 替代 FilterSecurityInterceptor,执行 authorizeHttpRequests |
BearerTokenAuthenticationFilter | 从请求头解析 Bearer Token 并认证 |
ExceptionTranslationFilter | 捕获 AccessDeniedException / AuthenticationException 并处理 |
Spring Security 6 的重大变更是将方法级和请求级安全都统一到 AuthorizationFilter,基于 AuthorizationManager 接口执行决策。
3. OAuth2 授权模式与 PKCE
3.1 四大授权模式对比
| 模式 | 适用场景 | Refresh Token | 安全风险 |
|---|---|---|---|
| Authorization Code | 服务端 Web 应用 | 支持 | 低,配合 PKCE 可防御授权码拦截 |
| PKCE | 单页应用(SPA)、移动端 | 支持 | 极低,无 Client Secret 也能安全 |
| Client Credentials | 服务间调用(M2M) | 不支持 | 低,依赖 Client Secret 安全存储 |
| Device Code | 输入受限设备(智能电视、IoT) | 支持 | 中,需轮询并限制用户码有效期 |
3.2 Authorization Code + PKCE 完整流程
// application.yml — 客户端配置示例(注册到授权服务器)
spring:
security:
oauth2:
client:
registration:
my-oidc-client:
provider: my-provider
client-id: demo-client
client-secret: "{noop}secret" # 生产环境使用加密方式
client-authentication-method: client_secret_basic
authorization-grant-type: authorization_code
redirect-uri: "http://localhost:8080/login/oauth2/code/my-oidc-client"
scope:
- openid
- profile
- email
- read
- write
provider:
my-provider:
issuer-uri: http://auth-server:9000 # 自动发现 .well-known/openid-configuration
3.3 自定义授权服务器端点与 PKCE 校验
Spring Authorization Server 内置支持 PKCE。当客户端注册为 public client(无 secret)时必须使用 PKCE。code_challenge 采用 S256 变换,授权服务器在校换 Token 时验证 code_verifier。
// OAuth2AuthorizationServerConfig.java — 授权服务器 Bean 配置
@Configuration
public class AuthorizationServerConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
// 自定义授权确认页面端点
.authorizationEndpoint(authorizationEndpoint ->
authorizationEndpoint.consentPage("/oauth2/consent")
)
// 启用 OIDC 1.0
.oidc(Customizer.withDefaults());
// 配置 CORS 与 CSRF(授权服务器端点通常需要特定处理)
http.cors(Customizer.withDefaults());
http.csrf(csrf -> csrf.ignoringRequestMatchers("/oauth2/**", "/oidc/**"));
return http.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
// 使用数据库存储已注册客户端,支持动态注册
JdbcRegisteredClientRepository repository = new JdbcRegisteredClientRepository(jdbcTemplate);
// 示例:SPA 应用,public client,强制 PKCE
RegisteredClient spaClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("spa-client")
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE) // 无 secret
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://localhost:3000/callback")
.scope(OidcScopes.OPENID)
.scope("read")
.scope("write")
.clientSettings(ClientSettings.builder()
.requireProofKey(true) // 强制 PKCE
.requireAuthorizationConsent(true) // 要求用户显式确认授权
.build())
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(15))
.refreshTokenTimeToLive(Duration.ofDays(7))
.build())
.build();
repository.save(spaClient);
return repository;
}
}
4. Spring Authorization Server 完整配置
4.1 JWK 与 Token 签发器
// JwtConfig.java — 配置非对称密钥对用于 JWT 签名
@Configuration
public class JwtConfig {
@Bean
public JWKSource<SecurityContext> jwkSource() {
// 生成 RSA 密钥对(实际生产环境应从 HSM 或密钥管理服务加载)
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
private static RSAKey generateRsa() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString()) // kid 用于匹配密钥
.build();
}
private static KeyPair generateRsaKey() {
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048); // RSA-2048 是安全与性能的平衡点
return keyGen.generateKeyPair();
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException(ex);
}
}
@Bean
public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
return new NimbusJwtEncoder(jwkSource);
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
// 使用 JWKSource 创建解码器,支持密钥轮换
Set<JWSAlgorithm> jwsAlgs = new HashSet<>();
jwsAlgs.addAll(JWSAlgorithm.Family.RSA);
jwsAlgs.addAll(JWSAlgorithm.Family.EC);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer("http://auth-server:9000");
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri("http://auth-server:9000/.well-known/jwks.json")
.jwsAlgorithms(algs -> algs.addAll(jwsAlgs))
.build();
decoder.setJwtValidator(withIssuer);
return decoder;
}
}
4.2 授权服务信息端点与 Token 自定义
// OAuth2TokenCustomizerConfig.java — 向 JWT 中注入自定义声明
@Component
public class CustomOAuth2TokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {
@Override
public void customize(JwtEncodingContext context) {
// 根据 Token 类型区分处理
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
Authentication principal = context.getPrincipal();
if (principal instanceof UsernamePasswordAuthenticationToken auth) {
// 注入自定义用户属性到 Access Token
UserDetails user = (UserDetails) auth.getPrincipal();
context.getClaims().claim("username", user.getUsername());
context.getClaims().claim("roles", user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList());
// 注入租户信息(多租户场景)
context.getClaims().claim("tenant_id", resolveTenant(user));
}
}
// Refresh Token 可注入设备指纹等元数据
if (OAuth2TokenType.REFRESH_TOKEN.equals(context.getTokenType())) {
HttpServletRequest request = getCurrentRequest();
if (request != null) {
context.getClaims().claim("client_ip", request.getRemoteAddr());
}
}
}
private String resolveTenant(UserDetails user) {
// 从用户扩展属性解析租户
return "T001";
}
private HttpServletRequest getCurrentRequest() {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
if (attrs instanceof ServletRequestAttributes servletAttrs) {
return servletAttrs.getRequest();
}
return null;
}
}
5. JWT 实战:JJWT 与 Spring Security 集成
5.1 JJWT 库签发与验证 Access/Refresh Token
// JwtTokenProvider.java — 使用 JJWT 0.12.x 版本 API
@Component
public class JwtTokenProvider {
private final SecretKey accessKey;
private final SecretKey refreshKey;
private final long accessExpirationMs = 15 * 60 * 1000; // 15 分钟
private final long refreshExpirationMs = 7 * 24 * 60 * 60 * 1000; // 7 天
public JwtTokenProvider(@Value("${jwt.access.secret}") String accessSecret,
@Value("${jwt.refresh.secret}") String refreshSecret) {
// 使用 HMAC-SHA 算法生成安全密钥
this.accessKey = Keys.hmacShaKeyFor(accessSecret.getBytes(StandardCharsets.UTF_8));
this.refreshKey = Keys.hmacShaKeyFor(refreshSecret.getBytes(StandardCharsets.UTF_8));
}
public String generateAccessToken(String userId, String username, List<String> roles) {
Date now = new Date();
Date expiry = new Date(now.getTime() + accessExpirationMs);
return Jwts.builder()
.subject(userId)
.claim("username", username)
.claim("roles", roles)
.claim("type", "access")
.issuedAt(now)
.expiration(expiry)
.id(UUID.randomUUID().toString()) // jti 用于 Token 黑名单
.signWith(accessKey, Jwts.SIG.HS256)
.compact();
}
public String generateRefreshToken(String userId) {
Date now = new Date();
Date expiry = new Date(now.getTime() + refreshExpirationMs);
return Jwts.builder()
.subject(userId)
.claim("type", "refresh")
.issuedAt(now)
.expiration(expiry)
.signWith(refreshKey, Jwts.SIG.HS512) // Refresh Token 使用更强的算法
.compact();
}
public Claims parseAccessToken(String token) {
return Jwts.parser()
.verifyWith(accessKey)
.build()
.parseSignedClaims(token)
.getPayload();
}
public boolean validateAccessToken(String token) {
try {
Claims claims = parseAccessToken(token);
return "access".equals(claims.get("type", String.class));
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SecurityException ex) {
return false;
}
}
}
5.2 JWE(加密 Token)示例
在敏感场景中,需要对 JWT payload 加密以防止 claims 被读取。JWE 使用公钥加密,私钥解密。
// JweTokenProvider.java — JWT 加密实现
@Component
public class JweTokenProvider {
private final RSAKey rsaKey;
public JweTokenProvider() throws Exception {
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
gen.initialize(2048);
KeyPair pair = gen.generateKeyPair();
this.rsaKey = new RSAKey.Builder((RSAPublicKey) pair.getPublic())
.privateKey((RSAPrivateKey) pair.getPrivate())
.build();
}
public String generateJweToken(String subject, Map<String, Object> claims) throws Exception {
JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder()
.subject(subject)
.issueTime(new Date())
.expirationTime(new Date(System.currentTimeMillis() + 900_000))
.jwtID(UUID.randomUUID().toString())
.build();
// 将明文 JWT 作为 JWE payload
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), jwtClaims);
signedJWT.sign(new RSASSASigner(rsaKey.toRSAPrivateKey()));
JWEObject jweObject = new JWEObject(
new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
.contentType("JWT")
.build(),
new Payload(signedJWT)
);
jweObject.encrypt(new RSAEncrypter(rsaKey.toRSAPublicKey()));
return jweObject.serialize();
}
}
6. 资源服务器搭建
6.1 纯资源服务器配置
微服务架构中,独立的资源服务器负责校验 Access Token 并将 JWT claims 转换为本地认证上下文。
// ResourceServerConfig.java — 资源服务器核心配置
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuerUri;
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/health", "/api/actuator/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/orders/**").hasAuthority("SCOPE_read")
.requestMatchers(HttpMethod.POST, "/api/orders/**").hasAuthority("SCOPE_write")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtDecoder(jwtDecoder())
.jwtAuthenticationConverter(grantedAuthoritiesExtractor())
)
)
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()));
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
// 默认解码器通过 issuer-uri 自动发现公钥
return JwtDecoders.fromIssuerLocation(issuerUri);
}
@Bean
public JwtAuthenticationConverter grantedAuthoritiesExtractor() {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
// 从 "scope" 或 "scp" claim 提取权限(默认行为)
authoritiesConverter.setAuthorityPrefix("SCOPE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
// 将 sub claim 映射为 Principal 名称
jwtAuthenticationConverter.setPrincipalClaimName("sub");
return jwtAuthenticationConverter;
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000", "https://app.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Request-ID"));
config.setExposedHeaders(List.of("X-Total-Count"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
7. 方法级安全(@PreAuthorize)
7.1 启用方法安全与 SpEL 表达式
// MethodSecurityConfig.java — 启用全局方法安全
@Configuration
@EnableMethodSecurity(
prePostEnabled = true, // 启用 @PreAuthorize / @PostAuthorize
securedEnabled = true, // 启用 @Secured
jsr250Enabled = true // 启用 @RolesAllowed
)
public class MethodSecurityConfig {
// 注册自定义 PermissionEvaluator 支持 ABAC 模型
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(new DomainPermissionEvaluator());
return handler;
}
}
// PermissionEvaluator 实现
public class DomainPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication authentication, Object targetDomain, Object permission) {
if (authentication == null || !authentication.isAuthenticated()) {
return false;
}
// 示例:判断用户是否拥有目标对象的权限
String targetType = targetDomain.getClass().getSimpleName().toUpperCase();
return checkPrivilege(authentication, targetType, permission.toString().toUpperCase());
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
// 基于 ID 和类型的权限校验
return false; // 实际项目中从数据库查询 ACL
}
private boolean checkPrivilege(Authentication auth, String target, String perm) {
return auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.anyMatch(a -> a.equals(target + ":" + perm));
}
}
7.2 Service 层使用示例
@Service
public class OrderService {
// 要求用户拥有 ORDER:READ 权限或 ADMIN 角色
@PreAuthorize("hasAuthority('ORDER:READ') or hasRole('ADMIN')")
public Order getOrderById(Long id) {
return orderRepository.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id));
}
// 使用 SpEL 表达式:只能查看自己的订单,管理员可查看全部
@PostAuthorize("returnObject.customerId == authentication.name or hasRole('ADMIN')")
public Order getOrderSecure(Long id) {
return orderRepository.findById(id).orElseThrow();
}
// 检查用户名是否与路径变量一致(ABAC 属性校验)
@PreAuthorize("#username == authentication.name")
public List<Order> getUserOrders(String username) {
return orderRepository.findByCustomerUsername(username);
}
// 基于自定义 PermissionEvaluator 的域对象权限校验
@PreAuthorize("hasPermission(#orderId, 'Order', 'DELETE')")
public void deleteOrder(Long orderId) {
orderRepository.deleteById(orderId);
}
// 过滤返回集合:只返回用户有权限查看的数据
@PostFilter("hasPermission(filterObject, 'READ')")
public List<Order> getAllVisibleOrders() {
return orderRepository.findAll();
}
}
8. 前后端分离与 Stateless JWT
8.1 无状态认证架构
// StatelessAuthFilter.java — 自定义无状态认证过滤器(用于非标准 JWT 场景)
@Component
public class StatelessAuthFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
public StatelessAuthFilter(JwtTokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = resolveToken(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateAccessToken(jwt)) {
Claims claims = tokenProvider.parseAccessToken(jwt);
// 构建 Spring Security 上下文
List<SimpleGrantedAuthority> authorities =
((List<?>) claims.get("roles", List.class)).stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.toList();
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
new UserPrincipal(claims.getSubject(), claims.get("username", String.class)),
null,
authorities
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("无法设置用户认证上下文", ex);
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
8.2 安全上下文持有策略
// SecurityContextConfig.java — 配置异步场景下的 SecurityContext 传播
@Configuration
public class SecurityContextConfig implements AsyncConfigurer {
@Override
@Bean(name = "taskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(500);
// 关键:将当前 SecurityContext 传播到子线程
executor.setTaskDecorator(new SecurityContextTaskDecorator());
executor.initialize();
return executor;
}
public static class SecurityContextTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
SecurityContext context = SecurityContextHolder.getContext();
return () -> {
try {
SecurityContextHolder.setContext(context);
runnable.run();
} finally {
SecurityContextHolder.clearContext();
}
};
}
}
}
9. 攻击防护:CSRF、XSS 与重放
9.1 CSRF 与 XSS 综合防御
// SecurityHeadersConfig.java — 安全响应头配置
@Configuration
public class SecurityHeadersConfig {
@Bean
public SecurityFilterChain securityHeadersChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
// HSTS 强制 HTTPS
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
)
// XSS 过滤器(现代浏览器已弃用,建议配合 CSP)
.xssProtection(xss -> xss.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
// Content Security Policy 防止 XSS
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;")
)
// 防止站点被嵌入 iframe(点击劫持)
.frameOptions(frame -> frame.deny())
// 防止 MIME 嗅探
.contentTypeOptions(Customizer.withDefaults())
// Referrer Policy
.referrerPolicy(referrer -> referrer
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
)
);
return http.build();
}
}
9.2 Token 黑名单与重放防护
// TokenBlacklistService.java — Redis 实现 Token 黑名单(登出/刷新场景)
@Service
public class TokenBlacklistService {
private final StringRedisTemplate redisTemplate;
private static final String BLACKLIST_PREFIX = "jwt:blacklist:";
public TokenBlacklistService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void blacklistToken(String jti, long expirationMillis) {
if (jti == null) return;
// 黑名单有效期与 Token 剩余有效期一致
redisTemplate.opsForValue().set(
BLACKLIST_PREFIX + jti,
"1",
expirationMillis,
TimeUnit.MILLISECONDS
);
}
public boolean isBlacklisted(String jti) {
return Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX + jti));
}
// 在过滤器中校验 Token 是否已被吊销
public void validateTokenNotBlacklisted(String token) {
Claims claims = tokenProvider.parseAccessToken(token);
String jti = claims.getId();
if (jti != null && isBlacklisted(jti)) {
throw new TokenBlacklistedException("Token 已被吊销");
}
}
}
10. 生产环境实战建议
10.1 密钥与算法安全
生产环境绝不可使用对称密钥(如 HS256)通过客户端分发,应使用非对称密钥(RS256/ES256)并定期轮换。Spring Authorization Server 支持通过 jwk-set-uri 暴露公钥集,实现热轮换。
# application-prod.yml
spring:
security:
oauth2:
authorizationserver:
issuer: https://auth.example.com
resourceserver:
jwt:
issuer-uri: https://auth.example.com
# 启用允许时钟偏差的 NTP 容差
jws-algorithms: RS256,ES256
server:
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-type: PKCS12
10.2 监控与日志脱敏
// AuthEventListener.java — 审计日志(脱敏处理)
@Component
public class AuthEventListener {
private static final Logger auditLog = LoggerFactory.getLogger("AUTH_AUDIT");
@EventListener
public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
Authentication auth = event.getAuthentication();
auditLog.info("认证成功 | 用户:{} | 类型:{} | 时间:{}",
maskUsername(auth.getName()),
auth.getClass().getSimpleName(),
Instant.now());
}
@EventListener
public void onAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
auditLog.warn("认证失败 | 用户:{} | 原因:{} | 时间:{}",
maskUsername(event.getAuthentication().getName()),
event.getException().getMessage(),
Instant.now());
}
private String maskUsername(String username) {
if (username == null || username.length() <= 2) return "***";
return username.charAt(0) + "***" + username.charAt(username.length() - 1);
}
}
10.3 Spring Security 6 迁移要点
WebSecurityConfigurerAdapter已移除,全面使用SecurityFilterChainBean。authorizeRequests()->authorizeHttpRequests()(lambda DSL)。antMatchers()->requestMatchers()。- 默认启用 Spring One Login Page,需注意 SPA 场景的
formLogin配置。
FAQ
Q1: Access Token 与 Refresh Token 应该如何存储在 SPA 中?
Access Token 可存放在 memory 中以减少 XSS 风险。Refresh Token 必须使用 HttpOnly, Secure, SameSite=Strict Cookie 存储,并在授权服务器端绑定 client_id 与设备指纹,防止 Token 旋转攻击。
Q2: JWT 真的无状态吗?如何实现强制登出?
纯 JWT 本身是无状态的,强制登出需要引入状态层。推荐方案:通过 Redis 维护 jti 黑名单(有效期与 Token 相同),在网关或资源服务器校验黑名单。对于高安全场景,缩短 Access Token 有效期至 5-15 分钟。
Q3: Spring Security OAuth 项目与 Spring Authorization Server 的关系是什么?
Spring Security OAuth(spring-security-oauth2)已于 2020 年停止维护。Spring Authorization Server(spring-authorization-server)是官方继任项目,基于 Spring Security 6 构建,完全支持 OAuth2.1 与 OIDC 1.0。
Q4: Resource Server 如何校验 Token 而不引入巨大的授权服务器依赖?
资源服务器仅需引入 spring-boot-starter-oauth2-resource-server,配置 issuer-uri 即可通过 .well-known 端点自动拉取 JWKS 公钥完成自包含 JWT 校验。无需连接授权服务器数据库或 HTTP 内省端点。
Q5: 如何应对 JWT 密钥泄露或轮换场景?
- 使用
kid(Key ID)标识当前密钥,JWT Header 中包含kid。 - 授权服务器支持同时暴露新旧两组公钥在 JWKS 端点。
- 缩短 Token 有效期,确保旧密钥签发的 Token 尽快失效。
- 通过配置中心推送新密钥,无需重启资源服务器。
总结
Spring Security 6.x 配合 Spring Authorization Server 提供了企业级 OAuth2/JWT 方案的完整技术栈。核心要点包括:使用非对称密钥(RS256/ES256)签发 JWT,采用 PKCE 保护 SPA 和移动端授权码流程,资源服务器通过 JWKS 实现无状态自校验,方法级安全通过 PermissionEvaluator 实现 ABAC,生产环境务必配置 Token 黑名单、CSP 安全头和 HTTPS 强制策略。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。