Spring Security 是 Java 生态中事实上的安全标准框架,但它的复杂度也是出了名的——根据 Snyk 2025 年 Java 生态报告,超过 65% 的 Spring 应用存在安全配置缺陷,其中最常见的问题就是认证授权配置不当。很多开发者在 Stack Overflow 上复制一段 SecurityFilterChain 配置就上线了,直到被安全审计打回来才发现 CSRF 没关对、CORS 没配好、JWT 过期策略根本没设。Spring Security 6 对配置方式做了颠覆性改造,废弃了继承 WebSecurityConfigurerAdapter 的老模式,全面转向 Lambda DSL——如果你还在用旧写法,是时候升级了。
本文不讲理论概念,只讲生产环境真正需要的东西:Filter Chain 怎么配?JWT 怎么签发和验证?RBAC 权限怎么从数据库动态加载?方法级权限怎么控制? 每个方案都有完整可运行的代码,每个坑都是真实项目踩过的。
📌 记住: 安全不是「加上 Spring Security 依赖就完事」。它需要理解 Filter Chain 的执行顺序、Token 的生命周期管理、权限模型的设计,以及各种边界情况的处理。
🔐 一、Spring Security 6 架构:Filter Chain 深度解析
1.1 SecurityFilterChain 工作原理
Spring Security 的核心是一条 Filter Chain(过滤器链)。每个 HTTP 请求都会经过这条链,链上的每个 Filter 负责一个安全关注点:认证、授权、CSRF 防护、Session 管理、CORS 等。
理解 Filter Chain 的执行顺序是掌握 Spring Security 的关键。以下是核心 Filter 的执行流程:
请求 → SecurityContextPersistenceFilter
→ UsernamePasswordAuthenticationFilter (表单登录)
→ BearerTokenAuthenticationFilter (JWT/OAuth2)
→ ExceptionTranslationFilter (异常处理)
→ FilterSecurityInterceptor (授权决策)
→ Controller
Spring Security 6 的一大变化是 AuthorizationFilter 替代了 FilterSecurityInterceptor,授权逻辑从 AOP 切面移入了 Filter 链,这让调试和理解执行流程更加直观。
1.2 从旧配置到 Lambda DSL 的迁移
Spring Security 6 彻底废弃了 WebSecurityConfigurerAdapter。对比新旧写法:
❌ 旧写法(已废弃):
// 继承 WebSecurityConfigurerAdapter —— Spring Security 6 已移除
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
}
✅ 新写法(Lambda DSL):
// Spring Security 6 推荐配置方式
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults())
);
return http.build();
}
}
⚠️ 警告: 不要在生产环境无脑
csrf().disable()。只有纯 API 服务(前后端分离、使用 JWT)才应该禁用 CSRF。如果你的服务有 Session 认证,禁用 CSRF 会导致严重的安全漏洞。
Lambda DSL 的优势在于:代码更清晰、IDE 补全更好、每个配置块都是独立的 Lambda,不会因为 .and() 链式调用出错。
🔑 二、JWT 认证实战:从 Token 签发到资源保护
2.1 认证方案选型对比
在动手写代码之前,先搞清楚该用什么认证方案。很多开发者一上来就用 JWT,但实际上并非所有场景都适合 JWT。
| 对比维度 | Session + Cookie | JWT (无状态) | OAuth2 + JWT |
|---|---|---|---|
| 状态管理 | 服务端有状态 | 客户端自包含 | 服务端有状态 (Token 存储) |
| 适用场景 | 传统 Web 应用 | SPA / 移动端 API | 第三方授权 / 微服务 |
| 扩展性 | ⚠️ 需 Session 共享 | ✅ 天然支持分布式 | ✅ 天然支持分布式 |
| Token 撤销 | ✅ 删除 Session 即可 | ❌ 需额外机制 | ✅ 撤销 Refresh Token |
| 安全性 | ✅ HttpOnly Cookie 防 XSS | ⚠️ 需防范 Token 泄露 | ✅ 短期 Token + 刷新 |
| 实现复杂度 | ⭐⭐ 低 | ⭐⭐⭐ 中 | ⭐⭐⭐⭐ 高 |
⚡ 关键结论: 如果你的应用是纯后端 API + 前端 SPA,选 JWT + OAuth2 Resource Server;如果是传统服务端渲染应用,Session + Cookie 可能更简单安全。
2.2 JWT Token 生成与验证完整实现
以下是一个生产级的 JWT 工具类,包含 Token 签发、验证和过期处理:
// JWT 工具类:签发、验证、解析 Token
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.access-token-expiration:3600000}") // 默认 1 小时
private long accessTokenExpiration;
@Value("${jwt.refresh-token-expiration:604800000}") // 默认 7 天
private long refreshTokenExpiration;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
}
// 签发 Access Token
public String generateAccessToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
claims.put("type", "access");
return Jwts.builder()
.claims(claims)
.subject(userDetails.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + accessTokenExpiration))
.signWith(getSigningKey())
.compact();
}
// 签发 Refresh Token
public String generateRefreshToken(UserDetails userDetails) {
return Jwts.builder()
.claim("type", "refresh")
.subject(userDetails.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + refreshTokenExpiration))
.signWith(getSigningKey())
.compact();
}
// 验证 Token 有效性
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token);
return true;
} catch (ExpiredJwtException e) {
log.warn("JWT Token 已过期: {}", e.getMessage());
return false;
} catch (JwtException e) {
log.warn("JWT Token 无效: {}", e.getMessage());
return false;
}
}
// 从 Token 中提取用户名
public String getUsernameFromToken(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
}
💡 提示: JWT Secret 必须至少 256 位(32 字节),建议使用 Base64 编码的随机字符串。永远不要把 Secret 硬编码在代码里,通过环境变量或配置中心注入。
2.3 双 Token 刷新流程
单 Token 方案有一个致命问题:Token 一旦签发就无法撤销。如果 Access Token 泄露,在过期之前攻击者可以一直使用。双 Token 方案(Access Token + Refresh Token)是业界标准解决方案:
// Token 刷新端点:用 Refresh Token 换取新的 Access Token
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final JwtTokenProvider jwtTokenProvider;
private final AuthenticationManager authenticationManager;
private final RefreshTokenRepository refreshTokenRepository;
@PostMapping("/refresh")
public ResponseEntity<TokenResponse> refreshToken(@RequestBody RefreshRequest request) {
// 1. 验证 Refresh Token 有效性
if (!jwtTokenProvider.validateToken(request.getRefreshToken())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
// 2. 检查 Refresh Token 是否在数据库中(防止已撤销的 Token 被使用)
RefreshToken storedToken = refreshTokenRepository
.findByToken(request.getRefreshToken())
.orElseThrow(() -> new RuntimeException("Refresh Token 已失效"));
// 3. 检查是否过期
if (storedToken.getExpiryDate().isBefore(Instant.now())) {
refreshTokenRepository.delete(storedToken);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
// 4. 签发新的 Access Token
String username = jwtTokenProvider.getUsernameFromToken(request.getRefreshToken());
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String newAccessToken = jwtTokenProvider.generateAccessToken(userDetails);
// 5. 可选:Refresh Token 轮转(每次刷新都换新的 Refresh Token,更安全)
// refreshTokenRepository.delete(storedToken);
// String newRefreshToken = jwtTokenProvider.generateRefreshToken(userDetails);
// saveRefreshToken(newRefreshToken, username);
return ResponseEntity.ok(new TokenResponse(newAccessToken, request.getRefreshToken()));
}
}
📌 记住: Refresh Token 轮转(Rotation)是指每次使用 Refresh Token 时,旧的立即失效,同时签发新的 Refresh Token。这样即使 Refresh Token 泄露,攻击者也只能使用一次。Spring Authorization Server 默认就启用了这个策略。
2.4 OAuth2 Resource Server 配置
在微服务架构中,通常有一个专门的 Authorization Server 签发 Token,其他服务作为 Resource Server 验证 Token。Spring Security 6 对 OAuth2 Resource Server 的支持非常完善:
// OAuth2 Resource Server 配置:验证 JWT Token
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") // 只对 /api/** 路径生效
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/public/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/articles/**").permitAll()
.requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
return http.build();
}
// 自定义 JWT -> Authentication 转换器
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
JwtAuthenticationConverter authenticationConverter = new JwtAuthenticationConverter();
authenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return authenticationConverter;
}
}
application.yml 配置:
# OAuth2 Resource Server JWT 配置
spring:
security:
oauth2:
resourceserver:
jwt:
# 方式一:直接配置 JWK Set URI(推荐)
jwk-set-uri: https://auth.example.com/.well-known/jwks.json
# 方式二:本地密钥验证
# issuer-uri: https://auth.example.com
👥 三、RBAC 权限控制:数据库驱动的动态授权
3.1 RBAC 权限模型设计
生产级的权限系统通常采用 RBAC(Role-Based Access Control)模型。以下是基于 MySQL 的核心表结构 DDL:
-- RBAC 权限模型:5 张核心表
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL, -- BCrypt 加密后固定 60 字符
enabled TINYINT DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_name VARCHAR(50) NOT NULL, -- 显示名:管理员
role_code VARCHAR(50) NOT NULL UNIQUE -- 角色码:ROLE_ADMIN
);
CREATE TABLE sys_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
permission_name VARCHAR(100) NOT NULL, -- 显示名:查看用户
permission_code VARCHAR(100) NOT NULL UNIQUE, -- 权限码:user:read
resource_url VARCHAR(200) -- 可选:对应的 URL 模式
);
CREATE TABLE sys_user_role (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id)
);
CREATE TABLE sys_role_permission (
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
PRIMARY KEY (role_id, permission_id)
);
💡 提示: 权限码(permission_code)的命名建议采用
资源:操作的格式,如user:read、order:export、report:delete。这种命名方式语义清晰,且方便在@PreAuthorize注解中使用通配符匹配,例如hasAuthority('user:*')匹配所有用户相关权限。
3.2 UserDetailsService 实现
Spring Security 通过 UserDetailsService 接口加载用户信息。以下是数据库驱动的实现:
// 基于数据库的 UserDetailsService 实现
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户基本信息
SysUser user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(
"用户不存在: " + username));
// 查询用户角色和权限
List<SysRole> roles = roleRepository.findByUserId(user.getId());
// 构建 GrantedAuthority 列表
List<GrantedAuthority> authorities = roles.stream()
.flatMap(role -> {
List<GrantedAuthority> roleAuthorities = new ArrayList<>();
// 添加角色(如 ROLE_ADMIN)
roleAuthorities.add(new SimpleGrantedAuthority(role.getRoleCode()));
// 添加权限(如 user:read, user:write)
role.getPermissions().forEach(perm ->
roleAuthorities.add(new SimpleGrantedAuthority(perm.getPermissionCode()))
);
return roleAuthorities.stream();
})
.distinct()
.collect(Collectors.toList());
return new CustomUserDetails(
user.getId(),
user.getUsername(),
user.getPassword(),
user.getEnabled(),
authorities
);
}
}
3.3 方法级权限控制
Spring Security 支持通过注解在方法级别进行权限控制,这比 URL 级别控制更精细:
// 启用方法级安全控制
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
// Spring Security 6 默认已启用 @PreAuthorize
// 无需额外配置即可使用
}
// 在 Service 层使用方法级权限控制
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
// 只有拥有 user:read 权限的用户才能调用
@PreAuthorize("hasAuthority('user:read')")
public UserDTO getUser(Long userId) {
SysUser user = userRepository.findById(userId)
.orElseThrow(() -> new EntityNotFoundException("用户不存在"));
return UserDTO.fromEntity(user);
}
// 只有 ADMIN 角色或用户本人可以修改
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public UserDTO updateUser(Long userId, UpdateUserRequest request) {
SysUser user = userRepository.findById(userId)
.orElseThrow(() -> new EntityNotFoundException("用户不存在"));
user.setEmail(request.getEmail());
userRepository.save(user);
return UserDTO.fromEntity(user);
}
// 需要同时满足多个权限
@PreAuthorize("hasAuthority('user:delete') and hasRole('ADMIN')")
public void deleteUser(Long userId) {
userRepository.deleteById(userId);
}
// 支持 SpEL 表达式的复杂权限逻辑
@PreAuthorize("@permissionService.hasProjectAccess(#projectId, authentication)")
public ProjectDTO getProject(Long projectId) {
// 项目级权限检查:自定义 PermissionService Bean
return projectService.findById(projectId);
}
}
⚠️ 警告:
@PreAuthorize只能保护 Spring 管理的 Bean 的方法。如果一个类内部调用自己的@PreAuthorize方法(this.method()),权限检查不会生效——因为 Spring AOP 代理不拦截自调用。这是最常见的坑之一。
⚡ 四、生产环境避坑指南
4.1 常见配置陷阱与解决方案
在实际项目中,以下问题出现频率极高:
坑点 1:CSRF 与前后端分离
前后端分离项目中,前端通常使用 localStorage 存储 JWT,此时不需要 CSRF 保护。但如果你错误地在禁用 CSRF 的同时保留了 Session 认证,就会暴露 CSRF 漏洞。
// ✅ 正确:前后端分离 API 服务
http.csrf(csrf -> csrf.disable()) // JWT 认证不需要 CSRF
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// ❌ 错误:有 Session 的 Web 应用禁用了 CSRF
http.csrf(csrf -> csrf.disable()) // 这会让表单提交暴露给 CSRF 攻击!
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
坑点 2:CORS 配置顺序
Spring Security 6 中,CORS 配置必须在 authorizeHttpRequests 之前,否则预检请求(OPTIONS)会被拦截:
// ✅ 正确的配置顺序
http
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS 在前
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authorize -> authorize // 授权在后
.anyRequest().authenticated()
);
坑点 3:Filter 优先级冲突
当项目中有多个 SecurityFilterChain Bean 时,Spring Security 按 @Order 注解排序,第一个匹配的 Filter Chain 生效。如果配置不当,可能所有请求都走了错误的 Chain。
// 多 Filter Chain 配置示例
@Configuration
@EnableWebSecurity
public class MultiSecurityConfig {
// API 路径走 JWT 认证
@Bean
@Order(1) // 优先级更高
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
// 其他路径走表单登录
@Bean
@Order(2)
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/login", "/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
}
4.2 安全加固 Checklist
部署到生产环境前,务必检查以下配置:
- ✅ JWT Secret 足够强:至少 256 位,通过环境变量注入
- ✅ Token 过期时间合理:Access Token 15-60 分钟,Refresh Token 7-30 天
- ✅ 启用 HTTPS:生产环境必须使用 TLS
- ✅ 密码使用 BCrypt:
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - ✅ 配置安全响应头:X-Content-Type-Options、X-Frame-Options、Strict-Transport-Security
- ✅ 日志不记录敏感信息:Token、密码、密钥不能出现在日志中
- ❌ 不要自己实现加密算法:使用 Spring Security 内置的密码编码器和 JWT 库
- ❌ 不要在 URL 中传递 Token:Token 应该放在 Authorization Header 中
⚠️ 警告: 如果你的应用需要支持 Token 撤销(如用户登出后立即失效),单纯使用无状态 JWT 是不够的。你需要引入 Redis 维护一个 Token 黑名单,或者使用短 Access Token(5-15 分钟)配合 Refresh Token 轮转策略。
4.3 安全测试:用 MockMvc 验证权限配置
很多开发者写完安全配置就直接上线了,从不写测试验证权限是否生效。Spring Security Test 提供了强大的测试支持:
// Spring Security 集成测试:验证权限配置是否正确
@SpringBootTest
@AutoConfigureMockMvc
class SecurityIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(username = "user1", roles = {"USER"})
void userCanAccessOwnProfile() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "user1", roles = {"USER"})
void userCannotAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void adminCanAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
@Test
void unauthenticatedRequestReturns401() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isUnauthorized());
}
}
📌 记住: 安全测试不是可选的。每个权限规则都应该有对应的测试用例。特别要测试「反向场景」——验证无权限用户确实被拒绝,比验证有权限用户能访问更重要。
📊 总结与最佳实践
Spring Security 6 的学习曲线确实陡峭,但一旦理解了 Filter Chain 的核心模型,其他功能都是在这个模型上的扩展。以下是我的实践建议:
架构选型建议:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 前后端分离 SPA | JWT + OAuth2 Resource Server | 无状态,易扩展 |
| 传统服务端渲染 | Session + Cookie + CSRF | 简单安全,成熟可靠 |
| 微服务架构 | OAuth2 Authorization Server + JWT | 统一认证,集中管理 |
| 移动端 API | JWT + Refresh Token | 适合移动端存储 |
核心原则:
- ✅ 认证方案选对,不要所有场景都用 JWT
- ✅ 使用 Lambda DSL 配置,不要再用废弃的继承方式
- ✅ 方法级权限用
@PreAuthorize,比 URL 级别更精细 - ✅ 生产环境做好安全加固 Checklist
- ❌ 不要无脑禁用 CSRF,搞清楚你的认证模式
- ❌ 不要在代码中硬编码密钥
相关工具推荐:
- Spring Authorization Server:Spring 官方的 OAuth2 授权服务器,替代过时的 Spring Security OAuth
- jjwt:最流行的 Java JWT 库,本文代码示例基于此库
- Keycloak:开源身份和访问管理(IAM)服务器,适合不想自建认证系统的团队
- Spring Security Test:
@WithMockUser、@WithJwt等测试注解,让安全测试更简单
Spring Security 的本质是 Filter Chain + Authentication + Authorization 三件套。把这三件搞清楚,你就能应对 90% 的安全需求。剩下的 10%——比如 OAuth2 授权码流程、PKCE、Passkey——那是另一篇文章的事了。