作为 Spring 生态的核心安全框架,Spring Security 始终是企业级应用认证与授权的首选方案。随着 Spring Boot 3.x的普及,Spring Security 6.x 带来了重大重构,摒弃了旧版冗余 API,采用函数式配置模型,强化了类型安全性与可扩展性。本文将从核心原理出发,结合实战案例,覆盖单体应用、前后端分离、微服务 OAuth2 等场景,同时拆解高频踩坑点,助你快速构建健壮的安全体系。
一、核心概念与架构原理
Spring Security 的核心目标是解决"认证(Authentication)"与"授权(Authorization)"两大问题,通过过滤器链机制拦截请求,实现无侵入式安全控制。理解以下核心组件是掌握框架的关键。
1.1 核心组件拆解
-
SecurityContextHolder:安全上下文持有器,默认基于 ThreadLocal 存储当前登录用户的认证信息(Authentication 对象),确保线程安全,可在应用任意层级获取用户信息。
-
Authentication:认证信息载体,包含用户身份(principal)、凭证(credentials,认证成功后清除)、权限集合(authorities)、认证状态(authenticated)四大核心属性。
-
AuthenticationManager:认证核心管理器,默认实现为 ProviderManager,通过委托多个 AuthenticationProvider 处理不同类型的认证(如用户名密码、JWT、OAuth2)。
-
AuthenticationProvider:具体认证逻辑执行者,常用实现包括 DaoAuthenticationProvider(数据库用户名密码认证)、JwtAuthenticationProvider(JWT 令牌认证)。
-
UserDetailsService:用户信息加载接口,核心方法为 loadUserByUsername,用于从数据库、缓存等数据源获取用户详情(用户名、加密密码、权限、账号状态等),返回 UserDetails 对象。
-
PasswordEncoder:密码加密与校验工具,推荐使用 BCryptPasswordEncoder(自动生成盐值,不可逆加密),严禁使用 NoOpPasswordEncoder(明文存储,仅测试可用)。
-
SecurityFilterChain:6.x 版本核心过滤器链,替代旧版 WebSecurityConfigurerAdapter,通过函数式配置定义拦截规则、认证方式、权限控制逻辑。
1.2 核心执行流程
否
是
否
是
否
是
客户端发起请求
SecurityFilterChain拦截
是否需要认证
直接放行至目标接口
引导认证/提取认证信息
AuthenticationManager处理认证
AuthenticationProvider执行校验
UserDetailsService加载用户信息
PasswordEncoder比对密码
认证成功?
返回认证失败响应
存储Authentication至SecurityContext
AccessDecisionManager授权判断
权限足够?
返回403禁止访问
二、实战篇:从基础认证到JWT无状态架构
以下案例基于 Spring Boot 3.2 + Spring Security 6.x 实现,覆盖单体应用、前后端分离两大主流场景,代码可直接复用。
2.1 环境准备:依赖导入
Maven 依赖配置(按需引入数据库、Redis、JWT 依赖):
xml
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 数据库相关(可选) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT 工具(可选) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
2.2 场景1:基于数据库的用户名密码认证(单体应用)
步骤1:自定义 UserDetailsService(加载数据库用户)
java
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository; // 自定义JPA仓库
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库查询用户
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
// 转换为Spring Security的UserDetails对象(可自定义扩展)
return User.withUsername(user.getUsername())
.password(user.getPassword()) // 数据库存储BCrypt加密后的密码
.roles(user.getRoles().toArray(new String[0])) // 角色集合
.accountExpired(!user.isAccountNonExpired())
.accountLocked(!user.isAccountNonLocked())
.credentialsExpired(!user.isCredentialsNonExpired())
.disabled(!user.isEnabled())
.build();
}
}
步骤2:配置 SecurityFilterChain(核心安全规则)
java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 启用方法级权限控制
public class SecurityConfig {
@Autowired
private CustomUserDetailsService userDetailsService;
// 密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 认证管理器
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
// 安全过滤器链
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 关闭CSRF(前后端分离场景需关闭,单体表单登录建议开启)
.csrf(csrf -> csrf.disable())
// 授权规则配置
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/auth/login").permitAll() // 公开接口
.requestMatchers("/admin/**").hasRole("ADMIN") // 管理员权限
.anyRequest().authenticated() // 其余接口需认证
)
// 表单登录配置(单体应用)
.formLogin(form -> form
.loginProcessingUrl("/auth/login") // 登录接口
.successHandler((request, response, auth) -> {
// 登录成功自定义响应(如返回用户信息)
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(Result.success("登录成功", auth.getPrincipal())));
})
.failureHandler((request, response, ex) -> {
// 登录失败自定义响应
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(Result.fail("登录失败:" + ex.getMessage())));
})
)
// 退出登录配置
.logout(logout -> logout
.logoutUrl("/auth/logout")
.logoutSuccessHandler((request, response, auth) -> {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(Result.success("退出成功")));
})
);
return http.build();
}
}
步骤3:方法级权限控制示例
java
@RestController
@RequestMapping("/admin")
public class AdminController {
// 仅ADMIN角色可访问
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/user/list")
public Result getUserList() {
// 业务逻辑
return Result.success("用户列表");
}
// 多条件权限控制(ADMIN或拥有USER_MANAGE权限)
@PreAuthorize("hasRole('ADMIN') or hasAuthority('USER_MANAGE')")
@PostMapping("/user/add")
public Result addUser(@RequestBody User user) {
// 业务逻辑
return Result.success("用户新增成功");
}
}
2.3 场景2:JWT无状态认证(前后端分离)
前后端分离场景下,采用 JWT 替代 Session 实现无状态认证,核心是通过自定义过滤器校验 Token。
步骤1:JWT 工具类
java
@Component
public class JwtUtils {
@Value("${jwt.secret}")
private String secret; // 签名密钥(生产环境需加密存储)
@Value("${jwt.expire}")
private long expire; // 访问令牌过期时间(毫秒)
@Value("${jwt.refresh-expire}")
private long refreshExpire; // 刷新令牌过期时间(毫秒)
// 生成访问令牌
public String generateAccessToken(String username, List<String> roles) {
return generateToken(username, roles, expire);
}
// 生成刷新令牌
public String generateRefreshToken(String username, List<String> roles) {
return generateToken(username, roles, refreshExpire);
}
// 核心生成方法
private String generateToken(String username, List<String> roles, long expire) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expire);
return Jwts.builder()
.setSubject(username)
.claim("roles", roles) // 存储角色信息
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS256, secret) // 签名算法
.compact();
}
// 解析Token获取用户名
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
// 验证Token有效性
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (Exception e) {
// Token过期、签名错误等均返回false
return false;
}
}
}
步骤2:自定义JWT认证过滤器
java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 提取Token(Authorization: Bearer <token>)
String header = request.getHeader("Authorization");
String token = null;
String username = null;
if (header != null && header.startsWith("Bearer ")) {
token = header.substring(7);
try {
username = jwtUtils.getUsernameFromToken(token);
} catch (Exception e) {
logger.error("Token解析失败:" + e.getMessage());
}
}
// Token有效且未认证
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 验证Token有效性
if (jwtUtils.validateToken(token)) {
// 构建认证信息并存入上下文
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
步骤3:更新 SecurityFilterChain 整合JWT
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态,不创建Session
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/auth/login", "/auth/refresh").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// 添加JWT过滤器(置于UsernamePasswordAuthenticationFilter之前)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
// 自定义异常处理器(401/403响应)
.exceptionHandling(ex -> ex
.authenticationEntryPoint((request, response, ex) -> {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(Result.fail(401, "未认证,请登录")));
})
.accessDeniedHandler((request, response, ex) -> {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(Result.fail(403, "权限不足")));
})
);
return http.build();
}
三、进阶篇:OAuth2.0授权服务器整合
微服务场景下,通常采用 OAuth2.0 + 授权服务器实现统一认证授权。Spring Security 6.x 推荐使用独立的 Spring Authorization Server 组件,以下为核心配置。
3.1 依赖导入
xml
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.2 授权服务器核心配置
java
@Configuration
public class AuthorizationServerConfig {
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// 应用授权服务器默认安全规则
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.build();
}
// 客户端配置(支持内存/数据库存储)
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
// 基于数据库存储客户端信息(生产环境推荐)
JdbcRegisteredClientRepository clientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
// 动态注册客户端(示例)
RegisteredClient webClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("web-client")
.clientSecret("{bcrypt}$2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") // BCrypt加密
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) // 授权码模式
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) // 刷新令牌
.redirectUri("https://web.app/callback") // 回调地址
.scope("read")
.scope("write")
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofHours(1)) // 访问令牌有效期
.refreshTokenTimeToLive(Duration.ofDays(1)) // 刷新令牌有效期
.build())
.build();
// 保存客户端(首次初始化时执行)
if (clientRepository.findByClientId("web-client") == null) {
clientRepository.save(webClient);
}
return clientRepository;
}
// JWT签名密钥(生产环境使用非对称密钥)
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAKey rsaKey = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
.privateKey((RSAPrivateKey) keyPair.getPrivate())
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
// 生成RSA密钥对
private static KeyPair generateRsaKey() {
KeyPairGenerator keyPairGenerator = null;
try {
keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
四、高频踩坑指南与最佳实践
4.1 常见陷阱与解决方案
-
陷阱1:配置类顺序错误导致规则失效
原因:多个 SecurityFilterChain 未指定 @Order 优先级,或优先级数字越大越先执行(数字越小优先级越高)。
解决方案:用 @Order 明确优先级,公开接口过滤器链优先级高于认证过滤器链。
-
陷阱2:密码编码器前缀缺失
原因:客户端密钥、数据库密码未添加算法前缀(如 {bcrypt}),导致 Spring Security 无法识别加密方式。
解决方案:存储密码时明确前缀,如 BCryptPasswordEncoder 加密后的密码自带 {bcrypt}。
-
陷阱3:内部方法调用权限注解失效
原因:同一类中方法调用未经过 Spring 代理,@PreAuthorize 注解无法触发。
解决方案:将方法拆分至不同类,或通过 AopContext.currentProxy() 获取代理对象调用。
-
陷阱4:JWT Token无法刷新/黑名单失效
原因:无状态架构下未维护 Token 状态,刷新令牌未持久化。
解决方案:用 Redis 存储刷新令牌与 Token 黑名单,设置过期时间与 JWT 一致。
4.2 最佳实践
-
密码存储:强制使用 BCrypt、Argon2 等强哈希算法,禁用明文/MD5 加密。
-
Token 安全:JWT 签名使用非对称密钥(RSA),访问令牌有效期不宜过长(1小时内),刷新令牌需严格校验。
-
权限设计:采用"RBAC 角色权限模型",细粒度控制资源访问,避免过度授权。
-
日志监控:记录认证失败、权限拒绝等行为,及时发现暴力破解、越权访问风险。
-
版本适配:Spring Boot 3.x 必须搭配 Spring Security 6.x,避免使用已废弃的 WebSecurityConfigurerAdapter。
五、总结与学习资源
Spring Security 6.x 凭借函数式配置、强大的扩展性,成为企业级安全框架的首选。掌握其核心原理后,可灵活适配单体、前后端分离、微服务等多种场景。核心要点在于理解"认证-授权"双流程,熟练运用 SecurityFilterChain、JWT、OAuth2 等组件,同时规避配置陷阱。
推荐学习资源:
- 官方文档:Spring Security 6.x 官方指南
- 组件文档:Spring Authorization Server
- 实战项目:Spring Security + Vue 前后端分离权限系统(GitHub 搜索关键词)
后续可深入研究 Spring Security 与 Spring Cloud Gateway 整合、多因素认证(MFA)、OAuth2.0 与 OpenID Connect 集成等高级场景,构建更全面的安全体系。