Spring Security 过滤链:认证、授权与上下文怎样穿过 Servlet 请求
沿 DelegatingFilterProxy、FilterChainProxy、SecurityFilterChain、SecurityContext、认证、授权与异常转换解释请求。
Spring Security 7 的 Servlet 架构仍以 FilterChainProxy 与 SecurityFilterChain 为核心;AuthorizationManager 是当前授权 API,旧 AccessDecisionManager 迁入兼容模块。
容器只看见一个安全 Filter 入口
DelegatingFilterProxy 把 Servlet Filter 生命周期桥接到 Spring Bean,通常委托名为 springSecurityFilterChain 的 FilterChainProxy。后者按 RequestMatcher 选择第一条匹配的 SecurityFilterChain,并执行其中有序过滤器。多链配置的风险不是“少一个注解”,而是路径误匹配后整条认证、CSRF、Header 与授权策略都不同。
链顺序从更具体到更一般,最后保留明确兜底;静态资源用 permitAll 通常比完全绕过安全链更容易保留响应头和上下文清理。启动测试要枚举关键 URL、HTTP 方法、DispatcherType 与命中的链。
Authentication 是输入凭证也是已认证主体
认证过滤器从请求生成未认证 Authentication,交给 AuthenticationManager/ProviderManager,再由匹配的 AuthenticationProvider 校验并返回包含 principal 与 authorities 的已认证对象。凭证在成功后应擦除;失败由 AuthenticationEntryPoint 建立稳定协议响应,不能让内部异常细节穿透。
SecurityContextHolder 默认以 ThreadLocal 暴露当前上下文。FilterChainProxy 在请求结束时清理它;线程池、异步 Servlet 和应用自建任务不会自动获得正确身份。需要显式捕获、传播和清理,后台任务更适合自己的服务主体而不是借用请求用户。
授权必须覆盖请求与业务对象
AuthorizationFilter 使用 AuthorizationManager 对请求做决策,方法安全补充领域操作和对象级规则。URL 角色不能证明用户拥有 orderId=42;控制器传入的 tenantId 也不能覆盖认证主体的租户。数据查询需带主体/租户条件,写入使用条件更新防止检查后对象变化。
拒绝时 AccessDeniedException 与未认证异常语义不同,ExceptionTranslationFilter 分别转换为 403 或认证入口。自定义过滤器放在 AuthorizationFilter 前后会改变是否受授权保护,必须用失败路径和 ERROR/FORWARD dispatch 测试证明。
过滤器顺序决定异常由谁接住
认证过滤器通常要在授权前建立 Authentication,自定义租户或签名过滤器若抛异常的位置不受 ExceptionTranslationFilter 覆盖,响应可能变成 500 或泄漏堆栈。将自定义过滤器放在某个标准过滤器前后时,要写清依赖的上下文、是否需要授权、异常转换者和重复 dispatch 行为。
CSRF 与 CORS 不是“前后端分离就关闭”。浏览器自动携带 Cookie 时仍需 CSRF 防护;Bearer token 放 Authorization Header 且不由浏览器自动附加时模型不同。CORS 只控制浏览器读取跨源响应,不是服务端授权,预检放行也不能放行业务请求。
方法安全与事务边界要一起验证
方法授权代理存在 self-invocation 边界,final/不可代理调用和非 Spring 对象可能绕开拦截;对象授权若在事务外先读后写,会被并发修改。更稳的做法是将授权需要的租户/owner 条件带入同一查询或更新,在受保护服务入口启用方法安全,并用集成测试从真实代理调用。
升级到新 AuthorizationManager API 时,先枚举旧 voter 的 abstain/deny/grant 组合和角色前缀,再写等价测试;不能只把类名替换。多链、ERROR/FORWARD、异步恢复和线程复用都要验证 SecurityContext 在出口为空。
一条请求可能经历多次 dispatch
Servlet 的 REQUEST、ASYNC、ERROR、FORWARD 与 INCLUDE 可能再次进入过滤链。OncePerRequestFilter 是否过滤异步或错误 dispatch 由实现选择,自定义过滤器若假定只运行一次,可能重复读 body、重复认证或漏掉错误页面。测试应让 Controller 抛错、启动异步并转发,检查授权规则、上下文和审计各执行几次。
请求缓存和登录重定向适合浏览器页面,不适合纯 API;API 认证失败应返回稳定 401 与 WWW-Authenticate,权限不足返回 403。错误处理器不得把认证异常再次转给会产生 HTML 的默认入口,也不能把内部策略原因完整返回给调用方。
PasswordEncoder 与用户缓存也有版本边界
DelegatingPasswordEncoder 用 id 前缀选择编码器,可在成功登录时升级旧哈希;没有前缀或未知 id 不能静默按弱算法解释。UserDetails 缓存需要随密码、状态、权限和 securityVersion 变化失效,高风险授权不能无限依赖陈旧 GrantedAuthority。
资源服务器验证 JWT 时,JwtDecoder 负责密码学与声明验证,权限转换器只把可信 claim 映射为 GrantedAuthority。把客户端可控字段直接映射 ROLE_ADMIN,或在签名验证前解析租户,都会把框架正确链拆断。自定义 converter 的输入、issuer 与 audience 必须写集成测试。
配置审查以实际 FilterChainProxy 输出和 MockMvc/真实容器测试为证据,不以 DSL 看起来合理为证据。关键断言包括默认 deny、管理路径匹配、CSRF/CORS 语义、Session 创建策略、响应安全头、方法授权和线程复用后上下文为空。
最小集成实验必须穿过真实链
准备公开、普通用户、管理员、跨租户对象、文件下载、异步和错误页面七类端点,以匿名、错误凭证、过期 token、普通用户和管理员组合请求。断言命中的 SecurityFilterChain、HTTP 状态、WWW-Authenticate、Session 是否创建、CSRF/CORS Header、业务表副作用和审计事件;单元测试直接调用 Controller 不能证明过滤链。
再让同一个容器线程先处理 alice 请求、随后处理匿名请求,验证第二次 SecurityContext 为空;启动异步任务时验证只有显式包装的任务获得预期上下文,完成后 worker 线程也为空。异常路径覆盖 AuthenticationException、AccessDeniedException、Controller 业务异常和 ERROR dispatch,避免某个出口跳过清理。
性能测试不能只测无安全链基线。分别观察密码登录、JWT 本地验签、opaque token 内省、方法授权、Session 远程读取和审计写入的 p95/p99;为远程依赖设置连接池与 deadline。若授权缓存用于降本,缓存键必须包含主体、对象范围与策略版本,并有安全变更失效路径。
安全状态机必须让失败停在安全位置
一次安全决策沿“解析—认证—授权—执行—审计”推进。任何阶段超时或证据不足都不能伪装成允许;同时要区分未认证、无权限、输入拒绝、依赖不可用和结果未知,避免客户端盲目重试。
反向验证要证明旁路也被关闭
测试不止提交非法字符串,还要更换租户、对象、HTTP 方法、内容类型、转发头、异步线程、重定向目标和旧版本凭证;在认证服务、审计存储、DNS、密钥源和策略缓存超时时观察默认行为。拒绝必须无业务副作用,错误响应不泄露资源存在性或内部实现。
容量同样属于安全边界。密码派生、签名验证、文件扫描、令牌内省和审计写入都可能被放大;为主体、租户、IP、操作和下游分别设置预算,并记录拒绝原因。限流不能只按可伪造 Header,也不能让攻击流量占满合法用户恢复通道。
生产证据与治理
上线门禁要求:威胁场景有 owner;允许和拒绝路径都有测试;默认拒绝与应急旁路已演练;秘密可轮换;审计可查询且防越权;依赖与构件来源可证明;旧版本兼容窗口和撤销路径明确。安全控制只有在故障、升级和应急状态仍成立时才算完成。
用两个 Java 17 模型固定安全不变量
模型不尝试实现密码算法或攻击载荷,只把控制输入、判定顺序和确定输出固化,真实系统再替换为框架、密钥服务与持久化策略。
javac --release 17 -Xlint:all -Werror examples/backend-development/security/spring-security-chain/SecurityChainDemo.java examples/backend-development/security/spring-security-chain/ContextCleanupDemo.java
java -cp examples/backend-development/security/spring-security-chain SecurityChainDemo
java -cp examples/backend-development/security/spring-security-chain ContextCleanupDemorequest=/admin chain=admin authentication=alice authorities=[ADMIN] decision=GRANTED
requestThread=worker-7 contextBefore=alice contextAfter=EMPTY asyncPropagated=false leak=false输出变化必须对应一条明确策略或迁移决定;禁止为了让测试通过而放宽 issuer、audience、tenant、路径、算法或来源校验。
把控制成本写进容量与事故恢复
“请求必须命中预期过滤链,安全上下文只能在受控范围建立并在所有出口清理”不是免费承诺。认证与密码派生消耗 CPU,远程策略和 Session 占用连接,签名与哈希增加计算,扫描和隔离消耗磁盘与队列,审计增加写放大。容量模型按峰值合法流量叠加恶意放大:每请求验证次数、下游往返、最大输入、并发隔离任务、失败重试、日志体积和安全状态保留期。平均延迟正常而拒绝队列持续增长,仍然意味着控制即将失效。
事故恢复按“阻止继续扩大—保存证据—撤销资格—修复权威状态—重建派生状态—验证不变量”推进。只修代码却不撤销 token、secret 或错误策略缓存,攻击窗口仍然存在;只轮换凭证却不检查已发生业务写,也无法证明资产恢复。对 SecurityChainDemo 与 ContextCleanupDemo 对应的决策输出建立线上指标或审计查询,才能把模型与生产证据连接起来。
升级时先部署严格兼容的读取端,再切换签发者或写入端,观察旧版本使用归零后删除兼容分支。任何临时兼容都记录 owner、截止、拒绝指标和回滚条件;为了兼容而跳过验证不属于迁移策略,而是新增旁路。
