Spring Security 深入原理
Spring Security 是 Spring 生态中的认证授权框架,核心是过滤器链(Filter Chain)。所有请求先经过过滤器链处理,认证通过后才到达业务代码。
1. 核心概念
1.1 认证(Authentication)vs 授权(Authorization)
| 概念 | 说明 | 问题 |
|---|---|---|
| 认证 | 验证"你是谁" | 登录时验证用户名密码 |
| 授权 | 验证"你能做什么" | RBAC:判断用户有没有访问接口的权限 |
1.2 核心组件
| 组件 | 职责 |
|---|---|
SecurityFilterChain |
过滤器链,Spring Security 的核心 |
Authentication |
认证信息,存放用户身份 |
AuthenticationManager |
认证管理器,校验认证信息 |
UserDetailsService |
加载用户数据(从数据库/内存) |
PasswordEncoder |
密码加密与验证 |
SecurityContext |
持有当前 Authentication 的上下文 |
AccessDecisionManager |
授权决策,决定能否访问资源 |
2. 过滤器链原理
2.1 过滤器链结构
Spring Security 的请求处理流程:
HTTP 请求
↓
Filter Chain(过滤器链)
↓
① WebAsyncManagerIntegrationFilter
② SecurityContextPersistenceFilter → 从 Session 加载 SecurityContext
③ HeaderWriterFilter → 添加安全响应头
④ CsrfFilter → CSRF 防护
⑤ LogoutFilter → 处理登出
⑥ UsernamePasswordAuthenticationFilter → 用户名密码认证
⑦ DefaultLoginPageGeneratingFilter → 生成登录页
⑧ BasicAuthenticationFilter → HTTP Basic 认证
⑨ RequestCacheAwareFilter → 请求缓存
⑩ SecurityContextHolderAwareAwareFilter
⑪ AnonymousAuthenticationFilter → 未认证用户设为匿名
⑫ SessionManagementFilter → Session 管理
⑬ ExceptionTranslationFilter → 包装异常为 HTTP 响应
⑭ FilterSecurityInterceptor → 最后一关:权限校验
↓
通过 → 业务代码
不通过 → 403 Forbidden / 302 重定向到登录页
过滤器按顺序执行,前面的过滤器处理完,后续才有机会。
2.2 一个请求经过过滤器链的全流程
请求 GET /admin/users
↓
FilterSecurityInterceptor 检查:需要 ADMIN 角色
↓
SecurityContext 中没有 Authentication
↓
AnonymousAuthenticationFilter 塞入匿名 Authentication
↓
AccessDecisionManager 投票:匿名用户没有 ADMIN 角色
↓
投票结果:拒绝访问
↓
ExceptionTranslationFilter 捕获
↓
判断是否已认证:
未认证 → 重定向到 /login 页面
已认证但权限不足 → 403 Forbidden
2.3 自定义过滤器
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(myCustomFilter(), UsernamePasswordAuthenticationFilter.class)
// 在 UsernamePasswordAuthenticationFilter 之前添加自定义过滤器
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/home")
);
return http.build();
}
}
3. 认证流程
3.1 UsernamePasswordAuthenticationFilter
// 登录请求 POST /login,携带 username & password
// UsernamePasswordAuthenticationFilter 拦截
UsernamePasswordAuthenticationFilter 内部:
↓
attemptAuthentication(request, response)
↓
从 request 获取 username 和 password
↓
创建 UsernamePasswordAuthenticationToken(未认证)
↓
交给 AuthenticationManager.authenticate(token)
↓
认证成功:
1. SecurityContextHolder.getContext().setAuthentication(Authentication)
2. 调用 AuthenticationSuccessHandler
认证失败:
1. SecurityContext 清空
2. 调用 AuthenticationFailureHandler
3.2 AuthenticationManager 认证管理器
默认实现是 ProviderManager,内部维护多个 AuthenticationProvider:
AuthenticationManager(ProviderManager)
↓
provider1.canSupport(认证类型)? → DaoAuthenticationProvider → 查数据库
provider2.canSupport(认证类型)? → JwtAuthenticationProvider → JWT 验证
provider3.canSupport(认证类型)? → OAuth2AuthenticationProvider → OAuth2
↓
遍历找到能处理的 Provider,执行认证
3.3 UserDetailsService
加载用户数据:
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// UserDetails 包含用户名、密码、角色列表
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}
3.4 PasswordEncoder 密码加密
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// 推荐:BCrypt(带盐,强度自适应)
return new BCryptPasswordEncoder();
}
}
// 使用
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
return username -> {
// 注册时:加密存储
String encoded = encoder.encode("plainPassword");
// 登录时:Spring Security 自动比对
};
}
不要用 MD5/SHA-1:这些没盐且可被彩虹表破解。BCrypt 是业界标准。
4. 授权流程
4.1 基于配置文件的授权
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/login").permitAll() // 公开
.requestMatchers("/admin/**").hasRole("ADMIN") // 需要 ADMIN 角色
.requestMatchers("/user/**").hasAnyRole("ADMIN", "USER") // ADMIN 或 USER
.anyRequest().authenticated() // 其他需登录
);
return http.build();
}
}
4.2 基于注解的方法级授权
@RestController
public class AdminController {
// 只有 ADMIN 角色才能访问
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/users")
public List<User> getUsers() { ... }
// 表达式:用户名是当前用户本人或者是 ADMIN
@PreAuthorize("#username == authentication.name or hasRole('ADMIN')")
@GetMapping("/user/{username}")
public User getUser(@PathVariable String username) { ... }
// 按钮级别权限:数据库中定义了按钮权限的用户才能访问
@PreAuthorize("@permissionService.hasPermission('user:export')")
@GetMapping("/user/export")
public void exportUsers() { ... }
}
4.3 权限投票机制 AccessDecisionManager
// 决策方式:多数票胜出
public class AffirmativeBased extends AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
int grant = 0;
for (AccessDecisionVoter voter : decisionVoters) {
int result = voter.vote(authentication, object, attributes);
if (result == ACCESS_GRANTED) return; // 有票同意就放行
}
throw new AccessDeniedException("无权限访问");
}
}
Voter 的实现:
RoleHierarchyVoter:角色层级(ADMIN 包含 USER 权限)AuthenticatedVoter:判断IS_AUTHENTICATED_FULLY等CustomVoter:自定义投票逻辑
5. Session 管理
5.1 Session 并发控制
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.maximumSessions(1) // 同一个用户最多一个会话
.maxSessionsPreventsLogin(false) // false=踢掉前一个用户,true=禁止新登录
);
}
maximumSessions(1) 效果:第二个设备登录后,第一个设备被踢下线。
5.2 Session 并发问题
用户 A 登录(Session1 在服务器)
↓
Session1 被别人异地登录,A 被踢下线
↓
A 再次登录(Session2)
↓
Session1 还存活(A 的旧会话)
↓
如果 maximumSessions 设置为 1,Session1 会被标记为"已过期"但未销毁
↓
下次 A 访问时,过期 Session 被检测到,触发 Session 并发逻辑
5.3 Redis Session 共享
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
@Configuration
@EnableRedisHttpSession // Session 存 Redis
public class SessionConfig {}
多节点部署时,用户请求可能打到不同服务器,Session 共享保证用户体验一致。
6. 登录后的用户信息获取
6.1 在 Controller 中获取当前用户
@RestController
public class UserController {
// 方式1:通过 SecurityContext
@GetMapping("/me")
public User me() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName(); // 用户名
return userService.findByUsername(username);
}
// 方式2:通过 @AuthenticationPrincipal 注解(推荐)
@GetMapping("/me2")
public UserDetails me2(@AuthenticationPrincipal UserDetails user) {
return user;
}
// 方式3:Controller 方法参数注入
@GetMapping("/me3")
public User me3(Authentication auth) {
return userService.findByUsername(auth.getName());
}
}
6.2 在 Service 中获取
@Service
public class OrderService {
public Order createOrder() {
// 从 SecurityContext 获取当前用户名
String username = SecurityContextHolder.getContext()
.getAuthentication().getName();
User user = userMapper.findByUsername(username);
// ...
}
}
7. Spring Security 与微服务
7.1 微服务认证的两种模式
| 模式 | 原理 | 适用场景 |
|---|---|---|
| 网关鉴权 | 网关统一验证 Token,其他服务信任网关 | 内部微服务 |
| JWT 无状态 | 每个服务独立验证 JWT | 跨团队、开放 API |
| OAuth2 + JWT | OAuth2 发放 JWT,服务独立验证 | 第三方接入 |
7.2 JWT 认证流程
用户登录 → 验证用户名密码 → 颁发 JWT Token
↓
后续请求:Authorization: Bearer <token>
↓
过滤器链中:JwtAuthenticationFilter 拦截
↓
验证 JWT 签名 + 过期时间
↓
提取用户信息 → 创建 Authentication
↓
SecurityContext 设置 Authentication
↓
请求继续到业务代码
7.3 Spring Security OAuth2 Resource Server
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
);
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthorityConverter grantedAuthoritiesConverter = new JwtGrantedAuthorityConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
8. 常见问题
8.1 @PreAuthorize 不生效
检查是否开启了全局方法安全注解:
@Configuration
@EnableMethodSecurity // 必须加!否则 @PreAuthorize 不生效
public class SecurityConfig {}
8.2 静态资源被拦截
.authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
.anyRequest().authenticated()
)
静态资源放在 src/main/resources/static/ 下。
8.3 跨域配置与 Security 冲突
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowCredentials(true);
}
}
// 同时在 Security 中配置
http.cors(cors -> {}); // 启用 CORS
8.4 密码加密后登录仍然失败
常见原因:
- 注册时加密,登录时却用
NoOpPasswordEncoder(不加密)比对 - 前后端密码传输编码不一致
// 确保 PasswordEncoder 注册正确
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
9. Spring Security 与 Shiro 对比
| 维度 | Spring Security | Apache Shiro |
|---|---|---|
| 与 Spring 集成 | 原生集成 | 需要集成 |
| 过滤器链 | 完整过滤器链 | Filter 链 |
| 权限粒度 | 方法级(@PreAuthorize) | 方法级 + 注解 |
| OAuth2 | 原生支持 | 无,需扩展 |
| JWT | 需配合 Resource Server | 需扩展 |
| 学习曲线 | 陡峭 | 平缓 |
| 复杂度 | 高 | 低 |
结论:
- Spring 全家桶项目 → Spring Security(生态最契合)
- 非 Spring 项目或追求简单 → Shiro