文章
Spring Security 学习笔记:认证与授权
牛耕田
暂存笔记,持续补充中。后端系统只要涉及「登录 + 权限」,就绕不开 Spring Security。
一、本质:一条过滤器链
Spring Security 的一切都发生在 Servlet Filter Chain 上。核心是一个名为 springSecurityFilterChain 的 FilterChainProxy,内部又串联了十几个过滤器:
请求 → SecurityContextPersistenceFilter
→ UsernamePasswordAuthenticationFilter(表单登录)
→ BearerTokenAuthenticationFilter(JWT)
→ FilterSecurityInterceptor / AuthorizationFilter(鉴权)
→ 到达 Controller
每个过滤器只做一件事,按顺序执行、可插拔。
二、两个核心概念
| 概念 | 含义 | 存放位置 |
|---|---|---|
| Authentication(认证) | 「你是谁」 | SecurityContextHolder |
| Authorization(授权) | 「你能干什么」 | 由 AuthorizationFilter 校验 |
SecurityContextHolder默认用ThreadLocal存储,因此请求结束后必须清理(框架已自动处理)。这也是为什么异步线程里拿不到登录用户——ThreadLocal不跨线程传递。
三、Spring Security 6 配置方式
Spring Security 6 起全面推荐基于组件的 Lambda 配置,WebSecurityConfigurerAdapter 已废弃。
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 开启 @PreAuthorize
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable) // 前后端分离可关
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(e -> e
.authenticationEntryPoint(restAuthEntryPoint) // 未登录 401
.accessDeniedHandler(restAccessDeniedHandler)); // 无权限 403
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 不要用 MD5/SHA1
}
}
注意:hasRole("ADMIN") 会自动补 ROLE_ 前缀,即要求权限为 ROLE_ADMIN;而 hasAuthority("user:add") 不补前缀,做细粒度权限时更灵活。
四、认证流程(表单登录版)
UsernamePasswordAuthenticationFilter拦截/login请求。- 封装成
UsernamePasswordAuthenticationToken,交给AuthenticationManager。 AuthenticationManager委派给DaoAuthenticationProvider。UserDetailsService.loadUserByUsername()查库,返回UserDetails。PasswordEncoder.matches()比对密码。- 成功后将
Authentication写入SecurityContextHolder。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) {
User u = userMapper.findByUsername(username);
if (u == null) throw new UsernameNotFoundException("用户不存在");
return org.springframework.security.core.userdetails.User
.withUsername(u.getUsername())
.password(u.getPassword()) // 存的是 BCrypt 密文
.authorities(u.getRoles().toArray(new String[0]))
.build();
}
}
五、JWT 无状态登录(前后端分离主流方案)
思路:服务端不存 Session,登录成功后签发一个自包含的 Token,客户端每次请求带上,服务端验签即可。
@Component
public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse resp,
FilterChain chain) throws ServletException, IOException {
String header = req.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
try {
Claims claims = jwtUtil.parse(token); // 验签 + 过期校验
String username = claims.getSubject();
var auth = new UsernamePasswordAuthenticationToken(
username, null,
jwtUtil.getAuthorities(claims));
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (JwtException e) {
// 令牌无效:不设置认证信息,后续会被 401 拦截
}
}
chain.doFilter(req, resp);
}
}
JWT 三部分:Header.Payload.Signature
- Header:算法类型(HS256/RS256)。
- Payload:claims(
sub、exp、自定义字段),Base64 可解,绝不能放密码等敏感信息。 - Signature:用密钥对前两部分签名,防篡改。
实践要点:
- 密钥(secret)必须足够长(HS256 建议 ≥ 256 bit)且存环境变量,不硬编码。
- 设置合理
exp(如 2 小时),配合 Refresh Token 做续期。 - 退出登录无法真正让 JWT 失效(服务端无状态),需依赖 Redis 黑名单或短过期时间。
六、方法级授权
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal")
public Order getOrder(Long userId, Long orderId) { ... }
@PreAuthorize("@ss.hasPermi('order:delete')") // 自定义 Bean 做细粒度校验
public void delete(Long orderId) { ... }
@PostAuthorize("returnObject.ownerId == authentication.principal")
public Order load(Long id) { ... } // 返回后校验
}
越权防护重点:水平越权(改别人的 id)必须靠业务层校验资源归属,光靠
@PreAuthorize的角色判断不够。
七、常见攻击与防护
| 攻击 | 原理 | 防护 |
|---|---|---|
| CSRF | 借用户 Cookie 发伪造请求 | 开 CSRF Token;前后端分离用 JWT 天然免疫 |
| XSS | 注入脚本窃取 Token | 输出转义、CSP;Token 别存 localStorage,优先 HttpOnly Cookie |
| 暴力破解 | 撞库 | 登录失败次数限制 + 验证码 + 账号锁定 |
| 会话固定 | 登录后不换 SessionId | 框架默认已处理 |
| 密码泄露 | 存明文 | BCrypt / Argon2 加盐哈希 |
八、小结
- Spring Security = 过滤器链 + 认证/授权两件事。
- 前后端分离推荐:JWT 无状态 + BCrypt 加密 +
@PreAuthorize方法级鉴权。 - 三条红线:密钥不入代码库、密码必须加盐哈希、越权校验必须落到业务层。