Spring Security 从零到生产级学习总览
Spring Security 不是一个“JWT 工具类”,也不是几条 requestMatchers 配置。它是一套围绕认证、授权、安全上下文、Web 攻击防护、会话管理和安全扩展建立的框架。对 Servlet 应用来说,请求通常在到达 Controller 之前就由安全过滤器链决定身份是否可信、访问是否允许以及失败如何响应。
本专栏以 JDK 8 + Spring Boot 2.7 + Spring Security 5.7 为存量基线,再说明 Boot 3/Security 6/Java 17 的差异。安全基础中的哈希、HMAC、数字签名、证书与 TLS 请先结合信息安全专栏学习。
一、真正学懂的验收标准
你需要能回答:
- 请求怎样从 Servlet 容器进入 Spring Security,又怎样回到业务 Filter 链?
DelegatingFilterProxy、FilterChainProxy、SecurityFilterChain分别是什么?- 多条 SecurityFilterChain 怎样选择,是否会把所有链串起来执行?
Authentication在认证前后分别保存什么?ProviderManager怎样选择AuthenticationProvider?- 用户名密码认证如何加载用户、校验密码并擦除凭证?
- SecurityContext 怎样创建、保存到 Session、恢复并在请求结束时清理?
- 401 与 403 分别在哪里产生,匿名用户为什么可能最终得到 401?
- JWT 为什么是签名载荷而不是加密载荷,泄露后如何撤销?
- CSRF 与 CORS 分别解决什么问题,为什么不能看到 Token 就一律关闭 CSRF?
- URL 授权、方法授权和数据权限如何分层?
- 线上出现错误 401/403、登录循环、上下文串号时如何获取证据?
二、正确学习路线
flowchart TD
A["HTTP、Cookie、Session 与密码哈希"] --> B["Servlet Filter 基础"]
B --> C["Security 过滤器链与上下文"]
C --> D["用户名密码认证与 Provider"]
D --> E["Session 登录与状态恢复"]
E --> F["JWT、刷新与撤销"]
F --> G["URL、方法与数据授权"]
G --> H["CSRF、CORS 与常见攻击"]
H --> I["商业权限模型与审计"]
I --> J["测试、监控和生产排查"]推荐顺序:
- 过滤器链全过程:所有 Web 安全能力的入口。
- 核心架构:Authentication、Manager、Provider 和 Context。
- 登录认证:用户名密码、Session 与认证事件。
- JWT 认证:签发、验证、刷新、撤销和密钥轮换。
- 权限控制:URL、方法和数据权限。
- CSRF 与 CORS:浏览器安全模型。
- 商业场景训练营:组合到医疗、后台与接口系统。
- 验收清单:逐项证明掌握程度。
- 面试题:标准回答跳回原理锚点。
三、认证与授权不能混淆
| 概念 | 回答的问题 | 例子 | 失败结果 |
|---|---|---|---|
| 认证 Authentication | 你是谁,凭证是否可信 | 密码、Session、JWT、证书 | 未认证,通常进入认证失败入口 |
| 授权 Authorization | 已知身份是否允许操作资源 | order:refund、管理员角色 | 已认证但无权限,通常 403 |
| 数据权限 | 对同类资源能看哪一行数据 | 只能看本医院、本人订单 | 需要业务查询条件和领域校验 |
| 审计 Audit | 谁在何时做了什么 | 导出患者数据、退款审批 | 记录证据,不替代授权 |
认证成功不代表拥有所有权限;有一个角色名称也不代表自动具备业务数据范围。
四、请求为什么在 Controller 前被拦截
flowchart TD
A["Tomcat 接收 HTTP 请求"] --> B["Servlet 容器 Filter 链"]
B --> C["DelegatingFilterProxy"]
C --> D["Spring Bean:FilterChainProxy"]
D --> E["选择第一条匹配的 SecurityFilterChain"]
E --> F["安全过滤器依次执行"]
F --> G["恢复身份、认证、异常翻译和授权"]
G --> H{"是否允许访问"}
H -- "否" --> I["写入 401 或 403 响应"]
H -- "是" --> J["继续进入 DispatcherServlet"]
J --> K["Controller 和 Service"]DelegatingFilterProxy 是 Servlet Filter,它把调用委托给 Spring 容器中的安全 Filter Bean;FilterChainProxy 管理多条 SecurityFilterChain;每条链包含一组有顺序的安全过滤器。
不是每个请求都执行所有 SecurityFilterChain。通常按配置顺序选择第一条 RequestMatcher 匹配的链,然后执行该链内部过滤器。具体过程见过滤器链全过程。
五、核心对象地图
| 对象 | 核心职责 | 常见误解 |
|---|---|---|
Authentication | 保存 principal、credentials、authorities 和认证状态 | 不只代表已登录对象,也可表示待认证请求 |
SecurityContext | 当前执行上下文中的 Authentication 容器 | 不是永久用户数据库 |
SecurityContextHolder | 提供上下文存取策略,Servlet 常见基于 ThreadLocal | 请求结束不清理会有线程复用风险 |
AuthenticationManager | 认证入口接口 | 不一定亲自查用户和校验密码 |
ProviderManager | 遍历支持当前 Token 类型的 Provider | 不是所有 Provider 都无条件执行 |
AuthenticationProvider | 执行一种认证机制 | 用户密码、短信、API Key 可各自实现 |
UserDetailsService | 按用户名加载用户安全信息 | 不负责比较提交密码 |
PasswordEncoder | 哈希密码并验证 | BCrypt 不是可逆加密 |
GrantedAuthority | 表示角色或细粒度权限字符串 | ROLE_ADMIN 与 admin 语义受表达式影响 |
SecurityContextRepository | 在请求之间加载和保存上下文 | JWT 无状态模式也要明确保存策略 |
六、用户名密码认证主链
flowchart TD
A["过滤器读取用户名和密码"] --> B["创建未认证 Authentication"]
B --> C["AuthenticationManager.authenticate"]
C --> D["ProviderManager 查找 supports=true 的 Provider"]
D --> E["DaoAuthenticationProvider"]
E --> F["UserDetailsService 查询用户"]
F --> G["检查锁定、禁用、过期状态"]
G --> H["PasswordEncoder.matches 校验密码"]
H --> I{"认证是否成功"}
I -- "否" --> J["抛出 AuthenticationException"]
I -- "是" --> K["返回已认证 Authentication"]
K --> L["写入 SecurityContext"]
L --> M["按策略保存到 Session 或仅当前请求"]密码比较不是“再次 encode 输入密码然后比较字符串”。BCrypt 每次编码会生成随机盐,正确方式是 matches(rawPassword, storedHash),编码器从已有哈希中读取算法参数和盐进行验证。
七、Session 与 JWT 不是新旧替代关系
| 对比 | Session | JWT Access Token |
|---|---|---|
| 状态主要保存 | 服务端 Session 存储 | Token 自身携带声明,服务端可保持较少状态 |
| 浏览器常见传输 | Cookie 自动携带 SessionId | Authorization Header 或 Cookie |
| 立即撤销 | 删除或失效 Session 较直接 | 需短有效期、黑名单、版本号等机制 |
| 多实例 | 共享 Session、粘性会话或集中存储 | 各实例能使用同一验签密钥或公钥 |
| 权限变更 | 下次加载或更新 Session | 长 Token 中权限快照可能滞后 |
| CSRF 风险 | Cookie 自动携带时重点防护 | 放 Cookie 仍有风险;Header 通常不被第三方页面自动附带 |
JWT payload 一般可被客户端解码看到;签名主要保证完整性和签发者真实性,不自动提供机密性。敏感数据不应放入 payload。
八、SecurityContext 的请求生命周期
flowchart TD
A["请求进入安全链"] --> B["创建空 SecurityContext"]
B --> C["从 Session 或 Repository 加载旧上下文"]
C --> D["认证过滤器可能更新 Authentication"]
D --> E["授权和业务代码读取当前身份"]
E --> F["按策略保存变更后的上下文"]
F --> G["finally 清理 SecurityContextHolder"]
G --> H["Tomcat 线程回到线程池"]最后的清理不能省。Servlet 工作线程会复用;ThreadLocal 身份残留可能让后续请求读取到错误用户。异步线程也不会天然安全继承上下文,需要显式传播策略,并防止在线程池中泄漏。
九、401 和 403 为什么容易看错
| 情况 | Security 视角 | 常见处理 |
|---|---|---|
| 没有凭证访问受保护资源 | 匿名、未完成认证 | AuthenticationEntryPoint,通常 401 |
| Token 无效或过期 | 认证失败 | 认证失败处理或 EntryPoint,通常 401 |
| 已认证但权限不足 | 授权拒绝 | AccessDeniedHandler,通常 403 |
| CSRF Token 缺失 | 访问拒绝异常 | 常见 403,但不是角色不足 |
| CORS 预检失败 | 可能在浏览器或 CORS Filter 阶段失败 | 不应简单归类为登录失败 |
ExceptionTranslationFilter 不是捕获所有 Java 异常的全局处理器。它重点翻译安全链下游的 AuthenticationException 和 AccessDeniedException。业务异常仍应由 MVC 异常体系处理。
十、密码为什么要用自适应哈希
密码存储目标不是未来解密出原文,而是验证提交值是否一致。推荐使用 BCrypt、Argon2、PBKDF2、SCrypt 等带盐且可调成本的密码哈希。
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}注册时:
String encoded = passwordEncoder.encode(rawPassword);登录时:
boolean matched = passwordEncoder.matches(rawPassword, encoded);不要使用明文、普通 MD5 或无盐 SHA-256 存密码。攻击者拿到数据库后可以高速离线尝试;自适应哈希通过随机盐抵抗彩虹表,并通过成本参数提高每次猜测代价。
十一、Boot 2 与 Boot 3 配置边界
| 基线 | 主要方式 | 注意事项 |
|---|---|---|
| Boot 2.6 及更早常见 | WebSecurityConfigurerAdapter | 已弃用,不建议新写 |
| Boot 2.7/Security 5.7/JDK 8 | SecurityFilterChain Bean | 本专栏存量基线 |
| Boot 3/Security 6/Java 17 | 组件式配置、requestMatchers | javax.servlet 迁移到 jakarta.servlet |
Boot 2.7/JDK 8 示例可使用组件式配置,但 DSL 细节与 Security 6 不完全相同。复制代码前先确认依赖版本;不要为了让新 DSL 编译,直接把存量系统一半依赖升级到 Boot 3。
十二、JDK 8 + Boot 2.7 最小配置
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.csrf().disable();
return http.build();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
UserDetails admin = User.withUsername("admin")
.password(encoder.encode("change-me"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(admin);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}这只是可运行的过滤器链验证配置,不是生产账号系统。生产用户应来自数据库或身份平台,密码不能写在源码,CSRF 是否关闭要根据凭证传输方式判断。
十三、商业系统安全边界
医疗数据平台至少要区分:
- 平台管理员、医院管理员、采集运维、审计人员和只读用户。
- 功能权限,例如启动采集、重跑批次、导出数据。
- 数据权限,例如只能访问所属医院和授权科室。
- 高风险操作二次确认、审批或双人复核。
- 登录失败、权限拒绝、导出和配置变更审计。
URL 白名单只解决入口是否公开,不能替代 Service 层的数据归属检查。一个拥有 patient:read 的用户也不应读取其他医院患者数据。
十四、生产排查入口
请求一直 401
检查凭证是否到达应用、请求匹配哪条 SecurityFilterChain、认证过滤器是否执行、Token/Session 是否有效、Provider 是否支持当前 Authentication 类型、上下文是否在授权前建立。
登录成功后下一请求仍未登录
Session 模式检查 Set-Cookie、Cookie Domain/Path/SameSite、代理 HTTPS、Session 存储和上下文保存;JWT 模式检查前端是否携带 Authorization、网关是否转发 Header、过滤器是否验证并写入上下文。
请求 403
先区分权限不足、CSRF、CORS 预检还是请求匹配了错误链;再检查 authority 字符串、ROLE_ 前缀、方法安全是否启用和数据权限。
Controller 根本没有日志
很可能在 Filter/Security/MVC 映射前被拦截。打开有范围的 Security DEBUG 日志,观察链选择和匿名/认证状态;生产环境不要长期全量 TRACE。
十五、面试标准回答
问:Spring Security 核心原理是什么?
Servlet 请求先经过 DelegatingFilterProxy,它把调用委托给 Spring 容器中的 FilterChainProxy。FilterChainProxy 按顺序选择第一条匹配的 SecurityFilterChain,再执行其中的上下文、认证、异常翻译和授权过滤器。认证过滤器把凭证封装成未认证 Authentication,交给 AuthenticationManager;常见 ProviderManager 选择支持该 Token 类型的 AuthenticationProvider,认证成功后返回带 principal 和 authorities 的已认证对象并写入 SecurityContext。授权阶段根据请求规则或方法规则判断权限;未认证由 AuthenticationEntryPoint 处理,已认证但权限不足由 AccessDeniedHandler 处理。请求结束后必须保存需要持久化的上下文并清理 ThreadLocal。
十六、问题到知识点跳转
| 问题 | 页面 |
|---|---|
| 请求到底经过哪些安全对象 | 过滤器链全过程 |
| Manager、Provider、Context 的关系 | 核心架构 |
| 用户名密码怎样认证 | 登录认证 |
| JWT 怎样签发、验证和撤销 | JWT 认证 |
| URL、方法和数据权限 | 权限控制 |
| CSRF 与 CORS 为什么不同 | CSRF 与 CORS |
| 怎样在商业项目落地 | 商业场景训练营 |
| 怎样准备面试 | 面试题 |
本章小结
Spring Security 的主线是:Servlet Filter 把请求交给 Spring 管理的安全代理,匹配一条安全链,加载或创建 SecurityContext,认证机制验证凭证并建立身份,授权机制判断是否允许访问,异常组件把安全失败转换成 401/403,最后保存需要持久化的身份并清理线程上下文。
只有能解释每一步的处理对象、状态变化、失败结果和排查证据,才算真正理解 Spring Security。
