怎么在 Spring Security 的过滤器链里加一个自定义过滤器?addFilterBefore/After 怎么用?
简化版
Spring Security 的核心是一条「过滤器链」——一系列 Filter 按顺序处理请求(认证、授权、CSRF、会话等各由一个过滤器负责)。要加自定义逻辑(如 JWT 校验、自定义鉴权),就往这条链里插一个自己的过滤器。插入方式:① addFilterBefore(myFilter, XxxFilter.class)——把 myFilter 插到某个已有过滤器之前;② addFilterAfter(myFilter, XxxFilter.class)——插到之后;③ addFilterAt(myFilter, XxxFilter.class)——插到和某过滤器同一位置(不推荐,顺序不确定)。最典型的场景是 JWT 校验过滤器:JWT 认证要在「用户名密码认证之前」校验 Token,所以 addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)——JWT 过滤器从请求头取 Token、校验、通过就把认证信息放进 SecurityContext,这样后续就是「已认证」状态。关键:自定义过滤器通常继承 OncePerRequestFilter(保证一次请求只过一次),在 doFilterInternal 里写逻辑,处理完调 filterChain.doFilter() 放行。插入位置很重要——要插在正确的过滤器前后(如认证过滤器要在授权过滤器之前)。
详细版
过滤器链的关键过滤器(顺序):
请求 → ... → SecurityContextPersistenceFilter(恢复 SecurityContext)
→ ...
→ UsernamePasswordAuthenticationFilter(表单登录认证)
→ ...
→ FilterSecurityInterceptor / AuthorizationFilter(授权)
→ ... → Controller
自定义 JWT 过滤器通常插在 UsernamePasswordAuthenticationFilter 之前
(先从 Token 认证,再走后续)
三种插入方式:
| 方法 | 作用 |
|---|---|
| addFilterBefore(f, X.class) | 把 f 插到 X 之前 |
| addFilterAfter(f, X.class) | 把 f 插到 X 之后 |
| addFilterAt(f, X.class) | 把 f 插到 X 的位置(顺序不定,慎用) |
// 自定义 JWT 过滤器
public class JwtAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse resp, FilterChain chain) throws ... {
String token = req.getHeader("Authorization");
if (token != null && jwtUtil.validate(token)) {
// Token 有效 → 构造认证信息放入 SecurityContext
UserDetails user = jwtUtil.getUser(token);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(req, resp); // 放行(继续过滤器链)
}
}
// 注册:插到用户名密码认证过滤器之前
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(new JwtAuthFilter(),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
⚠️ 自定义过滤器有几个必踩的点:① 继承
OncePerRequestFilter(保证一次请求只执行一次)、② 插入位置要对、③ 处理完一定要放行。OncePerRequestFilter是推荐的基类——它保证「同一个请求在一次处理中,这个过滤器只执行一次」(避免请求转发、异步等场景重复执行)。插入位置是核心:JWT 校验要在「认证」相关过滤器之前完成(把认证信息放进SecurityContext),这样后续的授权过滤器才能拿到认证信息判断权限——所以插在UsernamePasswordAuthenticationFilter之前。放行:doFilterInternal里处理完,必须调filterChain.doFilter(req, resp)让请求继续往下走,否则请求会卡在你的过滤器里、后续过滤器和 Controller 都执行不到。另一个易错点:Token 无效时通常也要放行(chain.doFilter),让后续的授权过滤器发现「没认证」而返回 401(而不是在过滤器里直接拦),除非你想在过滤器里直接返回错误。
完整版教学
一、过滤器链是 Spring Security 的核心
先理解 Spring Security 基于过滤器链的架构:
Spring Security 的核心:一条过滤器链(Filter Chain)
Spring Security 通过一系列 Filter 实现所有安全功能
每个 Filter 负责一个方面:
SecurityContextPersistenceFilter:加载/保存 SecurityContext
UsernamePasswordAuthenticationFilter:表单登录认证
BasicAuthenticationFilter:HTTP Basic 认证
CsrfFilter:CSRF 防护
LogoutFilter:登出
ExceptionTranslationFilter:异常处理(401/403)
AuthorizationFilter:授权
... 十几个过滤器
请求处理:
请求 → 依次经过这些 Filter → 到 Controller
→ 每个 Filter 做自己的安全检查
为什么基于过滤器:
Filter 在 Servlet 层,能在请求到达 Controller 前拦截处理
→ 安全检查放在过滤器,Controller 只管业务
→ 每个安全功能一个过滤器,职责清晰、可组合
所以加自定义安全逻辑 = 往过滤器链里加一个自定义 Filter
Spring Security 的核心是「一条过滤器链」——通过一系列 Filter 实现所有安全功能,每个 Filter 负责一方面(SecurityContextPersistenceFilter 加载 Context、UsernamePasswordAuthenticationFilter 表单认证、CsrfFilter CSRF、ExceptionTranslationFilter 异常、AuthorizationFilter 授权等十几个)。请求依次经过这些 Filter 到 Controller。基于过滤器是因为「Filter 在 Servlet 层能在到达 Controller 前拦截、每个安全功能一个过滤器职责清晰」。所以加自定义安全逻辑=往过滤器链加一个 Filter。理解「Spring Security 核心是过滤器链、每个 Filter 负责一方面(认证/授权/CSRF/异常)、请求依次经过到 Controller、加安全逻辑就是加自定义 Filter」,就理解了架构基础。
二、三种插入方式
理解 addFilterBefore/After/At 的区别:
往过滤器链插入自定义过滤器的三种方式:
addFilterBefore(myFilter, XxxFilter.class):
把 myFilter 插到 XxxFilter 之前
→ myFilter 先执行,再执行 XxxFilter
例:JWT 过滤器插在认证过滤器之前(先校验 Token)
addFilterAfter(myFilter, XxxFilter.class):
把 myFilter 插到 XxxFilter 之后
→ 先 XxxFilter,再 myFilter
例:在认证之后做点额外处理
addFilterAt(myFilter, XxxFilter.class):
把 myFilter 插到 XxxFilter 的位置(同一位置)
★ 但同一位置的多个过滤器顺序不确定 → 慎用
→ 一般用于"替换"某个过滤器(配合其他手段)
选择:
要在某过滤器"之前"做(如认证前校验 Token)→ addFilterBefore
要在某过滤器"之后"做 → addFilterAfter
别用 addFilterAt(顺序不定,除非明确要替换)
关键:选对"参照的过滤器"和"前/后"
→ 决定了你的过滤器在链中的执行时机
三种插入方式:addFilterBefore(myFilter, X.class)(插到 X 之前、myFilter 先执行,如 JWT 插在认证前)、addFilterAfter(myFilter, X.class)(插到 X 之后)、addFilterAt(myFilter, X.class)(插到 X 同一位置、顺序不确定慎用、一般用于替换)。选择:要在某过滤器之前做用 addFilterBefore、之后做用 addFilterAfter、别用 addFilterAt。关键是选对参照过滤器和前/后(决定执行时机)。理解「三种插入:addFilterBefore(之前,JWT 插认证前)/addFilterAfter(之后)/addFilterAt(同位置慎用顺序不定);选对参照过滤器和前后决定执行时机」,就掌握了插入方式。
三、JWT 过滤器:最典型的例子
以 JWT 校验过滤器为例,理解自定义过滤器的完整实现:
JWT 过滤器要做什么:
从请求头取 JWT → 校验 → 通过就把认证信息放进 SecurityContext
→ 这样后续的授权过滤器能拿到认证信息判断权限
为什么插在 UsernamePasswordAuthenticationFilter 之前:
① JWT 认证要在"授权判断之前"完成
(授权过滤器 AuthorizationFilter 在更后面,要有认证信息才能判断权限)
② 插在用户名密码认证过滤器之前,是约定的位置
(JWT 场景通常不用表单登录,但用这个位置作为参照点)
实现(OncePerRequestFilter):
public class JwtAuthFilter extends OncePerRequestFilter {
protected void doFilterInternal(req, resp, chain) {
String token = req.getHeader("Authorization");
if (token != null && valid(token)) {
// 校验通过,构造 Authentication 放入 SecurityContext
Authentication auth = buildAuth(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(req, resp); // ★ 放行(不管有没有 Token 都放行)
}
}
Token 无效/没有时的处理:
通常也放行(chain.doFilter)——不在这里直接拦
→ 让后续的授权过滤器发现"没认证" → 触发 ExceptionTranslationFilter
→ 返回 401(AuthenticationEntryPoint)
→ 职责分离:JWT 过滤器只管"校验并设置认证",拦截交给授权/异常过滤器
注册:
http.addFilterBefore(new JwtAuthFilter(),
UsernamePasswordAuthenticationFilter.class);
JWT 过滤器(最典型例子):从请求头取 JWT → 校验 → 通过就把认证信息放进 SecurityContext(后续授权过滤器能拿到判断权限)。为什么插在 UsernamePasswordAuthenticationFilter 之前:JWT 认证要在授权判断之前完成、这个位置是约定参照点。实现继承 OncePerRequestFilter、在 doFilterInternal 里校验并设置认证、放行 chain.doFilter(不管有没有 Token 都放行)。Token 无效/没有时通常也放行——让后续授权过滤器发现「没认证」触发 401(职责分离:JWT 过滤器只管校验设置认证、拦截交给授权/异常过滤器)。理解「JWT 过滤器:取 Token 校验通过放进 SecurityContext、插在认证过滤器之前(授权前完成);OncePerRequestFilter+doFilterInternal+chain.doFilter 放行;Token 无效也放行让授权过滤器触发 401(职责分离)」,就掌握了 JWT 过滤器这个典型例子。
四、OncePerRequestFilter 与放行
理解 OncePerRequestFilter 和「放行」的重要性:
OncePerRequestFilter(推荐的基类):
保证"一个请求在一次处理中,这个过滤器只执行一次"
→ 避免请求转发(forward)、异步(async)等场景重复执行
用法:继承它,重写 doFilterInternal(而非 doFilter)
为什么不直接实现 Filter:
直接实现 javax.servlet.Filter 的 doFilter,可能在
请求转发时被多次调用(每次转发都过一遍过滤器)
→ OncePerRequestFilter 帮你去重(一次请求只过一次)
放行(chain.doFilter)的重要性:
doFilterInternal 里处理完,★必须调 chain.doFilter(req, resp)
→ 让请求继续往下走(后续过滤器 + Controller)
→ 忘了调 → 请求卡在你的过滤器里,后续都执行不到(请求"消失")
放行的时机:
正常:处理完 → chain.doFilter(继续)
想拦截(直接返回错误):
→ 不调 chain.doFilter,直接写 response 返回
→ 但一般不这么做(拦截交给授权/异常过滤器,职责分离)
常见 bug:
✗ 忘了 chain.doFilter → 请求卡住
✗ 异常没处理 → 过滤器抛异常,请求失败
✗ 直接实现 Filter 没去重 → 转发时重复执行
OncePerRequestFilter(推荐基类) 保证「一个请求一次处理只执行一次」(避免请求转发/异步重复执行),继承它重写 doFilterInternal(而非 doFilter)。放行(chain.doFilter)的重要性:处理完必须调 chain.doFilter(req, resp) 让请求继续往下走(后续过滤器+Controller),忘了调请求会卡住(后续执行不到)。想拦截就不调 doFilter 直接写 response(但一般拦截交给授权/异常过滤器)。常见 bug:忘了 chain.doFilter 卡住、异常没处理、直接实现 Filter 没去重转发时重复执行。理解「OncePerRequestFilter 保证一请求一次(避免转发重复)、重写 doFilterInternal;放行 chain.doFilter 必须调(否则请求卡住)、想拦截不调直接写 response;bug:忘了 doFilter/异常没处理/没去重」,就掌握了 OncePerRequestFilter 和放行。
五、插入位置的选择
「插在哪」很关键——理解怎么选位置:
选插入位置的原则:
想清楚"你的过滤器要在什么时机执行"
→ 相对于哪个已有过滤器的前/后
常见位置选择:
① JWT/Token 认证过滤器:
插在 UsernamePasswordAuthenticationFilter 之前
→ 认证要在授权之前完成(把认证信息放进 SecurityContext)
② 请求日志/追踪过滤器:
插在很靠前(如 SecurityContextPersistenceFilter 之前/后)
→ 尽早记录请求
③ 自定义授权过滤器:
插在 AuthorizationFilter 附近
④ 限流/黑名单过滤器:
插在靠前(认证之前就拦掉,省得走后续)
关键参照点:
UsernamePasswordAuthenticationFilter:认证的参照点
→ 认证相关的插它之前
AuthorizationFilter:授权的参照点
→ 授权相关的插它附近
SecurityContextPersistenceFilter(新版 SecurityContextHolderFilter):
→ SecurityContext 加载,很靠前
错误的位置 → 问题:
JWT 过滤器插在授权过滤器之后 → 授权时还没认证信息 → 判断错
→ 所以认证类过滤器一定在授权之前
原则:
认证 → 在授权之前(要先知道你是谁,才能判断你能不能)
日志/限流 → 靠前(尽早处理)
按"执行时机"选参照点和前/后
插入位置的选择原则:想清楚「你的过滤器要在什么时机执行」相对于哪个已有过滤器前/后。常见选择:JWT/Token 认证插在 UsernamePasswordAuthenticationFilter 之前(认证要在授权之前完成)、日志/追踪插很靠前、自定义授权插 AuthorizationFilter 附近、限流/黑名单插靠前(认证前就拦掉)。关键原则:认证在授权之前(要先知道你是谁才能判断能不能)——JWT 过滤器插在授权之后会导致授权时还没认证信息判断错。理解「插入位置选择:想清楚执行时机相对哪个过滤器;认证类插 UsernamePasswordAuthenticationFilter 之前(认证在授权前)、日志限流靠前;认证一定在授权之前否则授权时没认证信息判断错」,就掌握了插入位置的选择。
六、实践与注意
总结自定义过滤器的实践和注意点:
实践步骤:
① 继承 OncePerRequestFilter,重写 doFilterInternal
② 写过滤逻辑(如校验 Token、设置 SecurityContext)
③ 处理完调 chain.doFilter 放行
④ 用 addFilterBefore/After 插到合适位置
注意点:
① 继承 OncePerRequestFilter(去重,避免转发重复执行)
② 一定要放行(chain.doFilter),否则请求卡住
③ 插入位置要对(认证类在授权之前)
④ 异常处理(过滤器里抛异常要考虑怎么处理,可能要 try-catch 返回错误)
⑤ 别在过滤器里做重活(阻塞、慢操作会拖慢所有请求)
⑥ SecurityContext 是线程绑定的(ThreadLocal),设置后同请求可用
Spring Security 6 的配置方式:
用 SecurityFilterChain Bean(WebSecurityConfigurerAdapter 已废弃)
http.addFilterBefore(...).build()
常见场景:
① JWT 校验过滤器(最常见)
② 请求日志/追踪
③ 自定义限流/黑名单
④ 自定义 header 校验(API Key)
⑤ 请求预处理(解密、验签)
核心总结:
过滤器链是 Spring Security 核心,加逻辑就是加过滤器
addFilterBefore/After 插到合适位置
继承 OncePerRequestFilter、一定要放行 chain.doFilter
认证类过滤器插在授权之前
自定义过滤器实践步骤:① 继承 OncePerRequestFilter 重写 doFilterInternal → ② 写逻辑(校验 Token/设置 SecurityContext)→ ③ 调 chain.doFilter 放行 → ④ addFilterBefore/After 插到合适位置。注意点:① 继承 OncePerRequestFilter 去重、② 一定放行否则卡住、③ 插入位置对(认证在授权前)、④ 异常处理、⑤ 别做重活、⑥ SecurityContext 线程绑定。Spring Security 6 用 SecurityFilterChain Bean。常见场景:JWT 校验、请求日志、限流/黑名单、API Key 校验、请求预处理。理解「实践:继承 OncePerRequestFilter+写逻辑+chain.doFilter 放行+addFilterBefore 插位置;注意:去重/放行/位置对/异常/别做重活/SecurityContext 线程绑定;场景 JWT/日志/限流/API Key」,就掌握了自定义过滤器的实践。
记忆钩子:「Spring Security 核心是过滤器链(每个 Filter 负责一方面:认证/授权/CSRF/异常),加自定义逻辑就是往链里插自定义 Filter;三种插入:addFilterBefore(f,X.class)插 X 之前/addFilterAfter 插之后/addFilterAt 同位置(顺序不定慎用);★最典型 JWT 校验过滤器:从请求头取 Token→校验→通过就把认证信息放进 SecurityContext,插在 UsernamePasswordAuthenticationFilter 之前(认证要在授权之前完成);实现继承 OncePerRequestFilter(保证一请求一次避免转发重复)、重写 doFilterInternal、★必须 chain.doFilter 放行(否则请求卡住)、Token 无效也放行让授权过滤器触发 401(职责分离);认证类过滤器一定在授权之前」。
七、常见误区与追问
- 误区:自定义过滤器直接实现 javax.servlet.Filter 就行。 推荐继承 OncePerRequestFilter——它保证「一个请求一次处理只执行一次」,避免请求转发(forward)、异步等场景重复执行;直接实现 Filter 的 doFilter 可能在转发时被多次调用。
- 误区:过滤器处理完不用调 chain.doFilter。 必须调——doFilterInternal 里处理完一定要调 filterChain.doFilter(req, resp) 让请求继续往下走(后续过滤器 + Controller);忘了调请求会卡在你的过滤器里、后续都执行不到(请求「消失」);这是最常见的 bug。
- 误区:JWT 过滤器插在哪都行。 插入位置很关键——JWT 认证要在授权判断之前完成(把认证信息放进 SecurityContext,后续授权过滤器才能判断权限),所以要插在 UsernamePasswordAuthenticationFilter 之前(用 addFilterBefore);插在授权过滤器之后会导致授权时还没认证信息、判断错误。
- 误区:Token 无效时要在过滤器里直接返回 401。 通常不这么做——Token 无效/没有时也放行(chain.doFilter),让后续的授权过滤器发现「没认证」、触发 ExceptionTranslationFilter 返回 401(AuthenticationEntryPoint);这样职责分离:JWT 过滤器只管「校验并设置认证」,拦截和返回错误交给授权/异常过滤器(当然也可以在过滤器里直接返回,看设计)。
- 追问:怎么在 Spring Security 里加一个 JWT 校验过滤器? ① 写一个过滤器继承 OncePerRequestFilter,在 doFilterInternal 里从请求头取 JWT、校验,通过就构造 Authentication 放进 SecurityContextHolder,然后调 chain.doFilter 放行;② 用 http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) 把它插到用户名密码认证过滤器之前(因为 JWT 认证要在授权之前完成);这样每个请求先过 JWT 过滤器校验、设置认证信息,后续授权过滤器就能判断权限。
- 追问:addFilterBefore、addFilterAfter、addFilterAt 有什么区别? addFilterBefore(f, X.class) 把 f 插到 X 过滤器之前(f 先执行);addFilterAfter(f, X.class) 插到 X 之后(f 后执行);addFilterAt(f, X.class) 插到 X 的同一位置——但同一位置多个过滤器的顺序不确定,一般慎用(除非明确要替换某个过滤器);根据你的过滤器要在什么时机执行(相对某个已有过滤器的前/后)选择。
- 追问:自定义过滤器为什么要继承 OncePerRequestFilter? 因为它保证「同一个请求在一次处理中,这个过滤器只执行一次」——在请求转发(forward)、include、异步(async)等场景下,一个请求可能多次经过过滤器链,OncePerRequestFilter 内部用请求属性标记,确保你的过滤逻辑只执行一次,避免重复(比如 JWT 校验重复、日志重复记录);直接实现 Filter 没有这个去重保证。
八、加强记忆
Spring Security 的核心是一条「过滤器链」——一系列 Filter 按顺序处理请求(认证、授权、CSRF、异常各由一个过滤器负责),加自定义安全逻辑就是往链里插一个自己的过滤器。三种插入方式:addFilterBefore(f, X.class)(插到 X 之前、f 先执行)、addFilterAfter(f, X.class)(插到 X 之后)、addFilterAt(f, X.class)(插到 X 同一位置、顺序不确定慎用)。最典型的是 JWT 校验过滤器:从请求头取 Token → 校验 → 通过就把认证信息放进 SecurityContext,插在 UsernamePasswordAuthenticationFilter 之前(认证要在授权之前完成,后续授权过滤器才能拿到认证信息判断权限)。实现要点:① 继承 OncePerRequestFilter(保证一个请求只执行一次、避免转发重复)、重写 doFilterInternal;② 处理完必须调 chain.doFilter(req, resp) 放行(忘了请求会卡住);③ Token 无效时通常也放行(让后续授权过滤器触发 401,职责分离)。插入位置原则:认证类过滤器在授权之前(要先知道你是谁才能判断能不能)。Spring Security 6 用 SecurityFilterChain Bean 配置。一句话「Spring Security 过滤器链加自定义 Filter:addFilterBefore/After 插到合适位置;典型 JWT 过滤器(取 Token 校验放进 SecurityContext)插在 UsernamePasswordAuthenticationFilter 之前(认证在授权前);继承 OncePerRequestFilter(一请求一次)、必须 chain.doFilter 放行(否则卡住)、Token 无效也放行让授权触发 401」。