文章

Spring Security 学习笔记:认证与授权

更新于 2026/10/05牛耕田

暂存笔记,持续补充中。后端系统只要涉及「登录 + 权限」,就绕不开 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") 不补前缀,做细粒度权限时更灵活。

四、认证流程(表单登录版)#

  1. UsernamePasswordAuthenticationFilter 拦截 /login 请求。
  2. 封装成 UsernamePasswordAuthenticationToken,交给 AuthenticationManager。
  3. AuthenticationManager 委派给 DaoAuthenticationProvider。
  4. UserDetailsService.loadUserByUsername() 查库,返回 UserDetails。
  5. PasswordEncoder.matches() 比对密码。
  6. 成功后将 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 方法级鉴权。
  • 三条红线:密钥不入代码库、密码必须加盐哈希、越权校验必须落到业务层。
返回主页