在微服务架构和前后端分离成为主流的今天,传统的Session-based认证方案在水平扩展、跨域访问等方面暴露出诸多局限。Spring Security 6 带来了全面的架构革新,不仅移除了对Spring Security 5的向后兼容支持,还在配置方式、安全过滤器链和OAuth2集成方面做了深度重构。本文将系统地讲解如何基于Spring Security 6构建一个生产级的JWT无状态认证授权体系,并集成OAuth2资源服务器,涵盖从核心概念到代码实现的完整链路。

一、Spring Security 6核心架构变化
Spring Security 6是自5.x以来最大的一次架构升级。理解这些变化是正确使用新版本的前提,尤其是当你从Spring Boot 2.x迁移到3.x时,很多旧配置将不再适用。
1.1 WebSecurityConfigurerAdapter的废弃
在Spring Security 5中,绝大多数项目通过继承
1 | WebSecurityConfigurerAdapter |
来配置安全策略。6.x彻底移除了这个类,取而代之的是基于
1 | SecurityFilterChain |
Bean的声明式配置方式。这种变化并非形式上的调整——它反映了一种从继承走向组合的设计哲学。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 // Spring Security 5 (已废弃)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.anyRequest().authenticated();
}
}
// Spring Security 6 (推荐方式)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults()));
return http.build();
}
}
1.2 Lambda DSL与链式配置
Spring Security 6全面推广Lambda DSL配置风格。与旧版的链式调用不同,Lambda DSL利用类型安全的方法引用和组合函数,使得配置更加清晰,同时在IDE中也能获得更好的自动补全和编译期检查。
下面的表格对比了两种配置风格的关键差异:
| 特性 | 旧版链式DSL | Lambda DSL (6.x) |
|---|---|---|
| 配置方式 | 方法链式调用 | Lambda表达式/方法引用 |
| 类型安全 | 较弱,运行时才发现错误 | 较强,编译期可发现问题 |
| 可读性 | 配置复杂时难以阅读 | 层级清晰,易于扩展 |
| IDE支持 | 一般 | 优秀,自动补全完善 |
1.3 SecurityContextHolder策略调整
Spring Security 6默认使用
1 | InheritableThreadLocal |
策略来管理
1 | SecurityContext |
。这意味着在父子线程之间,安全上下文可以自动传递。但在虚拟线程(Virtual Thread)场景下,这个默认策略可能导致内存泄漏或上下文混乱。官方建议在高并发虚拟线程环境下显式设置策略:
1
2
3
4
5
6
7 @Bean
public SecurityContextHolderStrategy securityContextHolderStrategy() {
// 使用基本ThreadLocal,避免虚拟线程场景下的继承问题
SecurityContextHolderStrategy strategy = new ThreadLocalSecurityContextHolderStrategy();
SecurityContextHolder.setContextHolderStrategy(strategy);
return strategy;
}
二、JWT认证核心组件设计
JWT(JSON Web Token)是无状态认证的核心载体。一个完整的JWT认证体系需要解决三个问题:令牌如何生成、令牌如何验证、以及令牌如何与Spring Security的授权体系对接。
2.1 JWT的结构与安全考量
一个标准的JWT由三部分组成:Header、Payload和Signature,以
1 | . |
分隔。在设计JWT时,安全考量至关重要——很多人只关注功能实现,却忽视了令牌泄漏后的风险控制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 // Header
{
"alg": "HS256",
"typ": "JWT",
"kid": "key-2024-01" // 密钥标识,支持密钥轮换
}
// Payload (Claims)
{
"sub": "user-12345",
"iss": "https://auth.tbr8.org",
"aud": ["api.tbr8.org", "admin.tbr8.org"],
"exp": 1735689600,
"iat": 1704067200,
"jti": "550e8400-e29b-41d4-a716-446655440000",
"roles": ["ROLE_USER", "ROLE_EDITOR"],
"permissions": ["post:write", "post:delete"]
}
关键安全实践包括:
1 | jti |
(JWT ID)用于令牌唯一标识,可配合Redis实现令牌撤销;
1 | kid |
支持密钥轮换而无需停机;
1 | aud |
声明限定令牌受众,防止跨服务令牌复用攻击。
2.2 JWT生成器实现
以下是一个生产级的JWT生成器实现,支持Access Token和Refresh Token双令牌策略:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57 @Component
public class JwtTokenProvider {
private final SecretKey secretKey;
private final long accessTokenValidity = 3600_000L; // 1小时
private final long refreshTokenValidity = 604800_000L; // 7天
public JwtTokenProvider(@Value("${jwt.secret}") String secret) {
this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
}
public TokenResponse generateToken(Authentication authentication) {
String username = authentication.getName();
Date now = new Date();
Date accessExpiry = new Date(now.getTime() + accessTokenValidity);
Date refreshExpiry = new Date(now.getTime() + refreshTokenValidity);
String accessToken = Jwts.builder()
.subject(username)
.claim("roles", authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()))
.claim("type", "ACCESS")
.issuedAt(now)
.expiration(accessExpiry)
.signWith(secretKey)
.compact();
String refreshToken = Jwts.builder()
.subject(username)
.claim("type", "REFRESH")
.issuedAt(now)
.expiration(refreshExpiry)
.signWith(secretKey)
.compact();
return new TokenResponse(accessToken, refreshToken, accessExpiry.getTime());
}
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
}
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
log.warn("Invalid JWT token: {}", e.getMessage());
return false;
}
}
}
2.3 自定义JWT认证过滤器
JWT认证的核心在于将HTTP请求中的Bearer Token解析为Spring Security可识别的
1 | Authentication |
对象。这需要自定义一个过滤器并插入到Spring Security过滤器链的合适位置——通常在
1 | UsernamePasswordAuthenticationFilter |
之前。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 @Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) {
Claims claims = tokenProvider.parseToken(token);
// 只处理ACCESS类型令牌
if (!"ACCESS".equals(claims.get("type", String.class))) {
filterChain.doFilter(request, response);
return;
}
String username = claims.getSubject();
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}

三、OAuth2资源服务器集成
在微服务架构中,认证服务器和资源服务器通常是分离的。Spring Security 6提供了开箱即用的OAuth2资源服务器支持,可以无缝集成外部JWT签发方(如Keycloak、Auth0、Okta等)。这与自建JWT方案并不矛盾——你可以在同一系统中同时使用自签发JWT和外部OAuth2 Provider签发的令牌。
3.1 OAuth2资源服务器配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51 @Configuration
@EnableWebSecurity
public class OAuth2ResourceServerConfig {
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/user/**").hasAuthority("SCOPE_read")
.requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter()))
.authenticationEntryPoint(new CustomAuthenticationEntryPoint()));
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
// 从授权服务器获取JWK Set
NimbusJwtDecoder decoder = NimbusJwtDecoder
.withJwkSetUri("https://auth.tbr8.org/.well-known/jwks.json")
.cache(Duration.ofMinutes(10)) // 缓存JWK Set避免频繁请求
.build();
// 配置时钟偏移容忍度(防止服务器间时间不同步)
OAuth2TokenValidator withClockSkew = new DelegatingOAuth2TokenValidator(
new JwtTimestampValidator(Duration.ofSeconds(30)),
new JwtIssuerValidator("https://auth.tbr8.org"));
decoder.setJwtValidator(withClockSkew);
return decoder;
}
private Converter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter =
new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthoritiesClaimName("roles");
authoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
}
3.2 多授权服务器支持
当系统需要同时接受来自多个授权服务器的JWT令牌时,可以通过
1 | JwtIssuerAuthenticationManagerResolver |
实现动态路由。这在企业级集成场景中非常常见,比如同时对接内部Keycloak和外部合作伙伴的Auth0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 @Bean
public AuthenticationManagerResolver authenticationManagerResolver() {
Map managers = new HashMap();
// 内部授权服务器
managers.put("https://auth.tbr8.org",
jwtDecoderManager("https://auth.tbr8.org/.well-known/jwks.json"));
// 合作伙伴授权服务器
managers.put("https://partner.auth0.com",
jwtDecoderManager("https://partner.auth0.com/.well-known/jwks.json"));
return new JwtIssuerAuthenticationManagerResolver(managers::get);
}
四、方法级安全与方法授权
认证只是安全体系的第一步,更关键的是细粒度的授权控制。Spring Security 6引入了新的方法级安全注解,支持SpEL表达式,可以实现基于业务规则的动态权限判断。
4.1 启用方法级安全
1
2
3
4
5
6
7
8
9
10
11
12
13 @Configuration
@EnableWebSecurity
@EnableMethodSecurity // Spring Security 6新注解,替代@EnableGlobalMethodSecurity
public class MethodSecurityConfig {
@Bean
public MethodSecurityExpressionHandler expressionHandler() {
DefaultMethodSecurityExpressionHandler handler =
new DefaultMethodSecurityExpressionHandler();
handler.setDefaultRolePrefix(""); // 去掉ROLE_前缀
return handler;
}
}
4.2 实战授权注解用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 @Service
public class ArticleService {
@PreAuthorize("hasAuthority('post:write')")
public Article createArticle(ArticleDTO dto) {
return articleRepository.save(dto.toEntity());
}
// 只有文章作者或管理员可以删除
@PreAuthorize("#article.author == authentication.name or hasRole('ADMIN')")
public void deleteArticle(Article article) {
articleRepository.delete(article);
}
// 根据返回值过滤——用户只能查看自己有权访问的数据
@PostAuthorize("returnObject.status == 'PUBLISHED' or hasRole('EDITOR')")
public Article getArticle(Long id) {
return articleRepository.findById(id).orElseThrow();
}
// 过滤集合参数,只保留用户有权限的元素
@PreFilter("filterObject.author == authentication.name")
public void batchDeleteArticles(List<Article> articles) {
articleRepository.deleteAll(articles);
}
// 过滤返回值集合
@PostFilter("filterObject.isPublic or hasRole('ADMIN')")
public List<Article> listArticles() {
return articleRepository.findAll();
}
}
五、安全事件审计与异常处理
一个完善的安全体系必须包含审计日志和统一的异常处理机制。Spring Security 6提供了
1 | SecurityEventListener |
机制,可以监听认证成功/失败、授权拒绝等事件。
5.1 安全事件监听
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42 @Component
public class SecurityEventListener {
private final AuditLogRepository auditLogRepository;
@EventListener
public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
AuditLog log = AuditLog.builder()
.eventType("AUTH_SUCCESS")
.username(event.getAuthentication().getName())
.ipAddress(extractIp(event))
.timestamp(Instant.now())
.build();
auditLogRepository.save(log);
}
@EventListener
public void onAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
AuditLog log = AuditLog.builder()
.eventType("AUTH_FAILURE")
.username(event.getAuthentication().getName())
.reason(event.getException().getMessage())
.ipAddress(extractIp(event))
.timestamp(Instant.now())
.build();
auditLogRepository.save(log);
// 失败次数过多自动锁定账户
if (auditLogRepository.countFailuresInLastHour(event.getAuthentication().getName()) > 5) {
log.warn("Account locked due to too many failed attempts: {}",
event.getAuthentication().getName());
// 触发账户锁定逻辑...
}
}
@EventListener
public void onAuthorizationDenied(AuthorizationDeniedEvent event) {
log.warn("Authorization denied for user {} on resource {}",
event.getAuthentication().getName(),
event.getSource());
}
}
5.2 统一异常处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 @RestControllerAdvice
public class SecurityExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity handleAccessDenied(AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("ACCESS_DENIED",
"您没有权限执行此操作"));
}
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity handleAuthentication(AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("AUTHENTICATION_FAILED",
"认证失败,请检查令牌是否有效"));
}
@ExceptionHandler(ExpiredJwtException.class)
public ResponseEntity handleExpiredJwt(ExpiredJwtException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("TOKEN_EXPIRED",
"令牌已过期,请使用Refresh Token重新获取"));
}
}
六、性能优化与最佳实践
在认证授权体系中,性能问题往往出现在令牌解析、JWK Set获取和数据库查询等环节。以下是一些经过生产验证的优化建议:
- JWK Set缓存:配置
1NimbusJwtDecoder
的缓存时间,避免每次验证都向授权服务器发起HTTP请求。推荐缓存5-10分钟,并在密钥轮换时通过缓存失效机制主动刷新。
- 用户信息缓存:
1UserDetailsService
的
1loadUserByUsername调用通常涉及数据库查询,应使用Caffeine或Redis缓存用户权限信息,TTL设置建议与Access Token有效期一致。
- 避免SecurityContext线程继承:在虚拟线程场景下,
1InheritableThreadLocal
会导致上下文在虚拟线程之间意外共享,应显式使用
1ThreadLocalSecurityContextHolderStrategy。
- 令牌精简:JWT的体积直接影响每个请求的带宽开销。只放入必要声明,避免将大量用户信息塞入Payload。
- 密钥轮换:使用
1kid
声明支持多密钥并存,实现无停机密钥轮换。旧令牌在新密钥生效后自然过期,不影响在线用户。
6.1 JWK Set缓存优化示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 @Bean
public JwtDecoder jwtDecoder() {
CacheJWKSet(jwkSetUri, Duration.ofMinutes(5), jwk -> {
// 监听JWK Set变化,触发密钥轮换逻辑
log.info("JWK Set refreshed, new keys: {}", jwk.size());
});
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
.cache(Duration.ofMinutes(5))
.build();
}
// 用户权限缓存
@Bean
public UserDetailsService cachedUserDetailsService(UserRepository repo) {
return new CachingUserDetailsService(
username -> repo.findByUsername(username)
.map(this::toUserDetails)
.orElseThrow(() -> new UsernameNotFoundException(username)),
Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(30))
.build());
}
七、完整架构总结
综合上述内容,一个生产级的Spring Security 6 JWT认证授权体系包含以下关键组件:
| 组件 | 职责 | 技术选型 |
|---|---|---|
| JWT生成器 | 签发Access/Refresh Token | jjwt库 + HMAC-SHA256 |
| JWT认证过滤器 | 解析请求中的令牌并设置安全上下文 | OncePerRequestFilter |
| OAuth2资源服务器 | 接受外部授权服务器签发的JWT | Spring Security OAuth2 |
| 方法级安全 | 细粒度的业务权限控制 | @EnableMethodSecurity + SpEL |
| 安全事件审计 | 记录认证授权事件 | Spring Event机制 |
| 异常处理 | 统一的错误响应 | @RestControllerAdvice |
| 令牌撤销 | 主动使令牌失效 | Redis + jti黑名单 |
从架构层面看,这套体系遵循了关注点分离原则:认证逻辑封装在过滤器层,授权逻辑通过方法级注解声明,安全事件通过事件总线解耦,异常处理集中在切面层。这种分层设计使得每一层都可以独立测试和演进,也方便在需要时替换具体实现——比如从自签JWT切换到外部OAuth2 Provider。
在实际项目中,建议将上述组件封装为独立的Spring Boot Starter,以便在多个微服务中复用。同时务必关注Spring Security的版本更新日志,6.x仍在持续迭代中,每个小版本都可能带来安全修复和性能改进。
汤不热吧