资讯详情

Spring Security自定义认证过滤器:原理、注册与避坑指南

📅 2026/10/13 12:09:07 | 华诺云谱 👁 阅读
Spring Security自定义认证过滤器:原理、注册与避坑指南
简介这份资源是围绕 SpringSecurity 自定义认证过滤器编写的完整项目源码面向已掌握 Spring Boot 与 SpringSecurity 基础、希望深入理解认证流程扩展机制的 Java 后端开发者。项目以 JwtAuthenticateFilter 为核心示例演示如何将自定义过滤器接入过滤器链完成基于 JWT 的认证逻辑适合用于登录鉴权模块的学习与二次开发参考。压缩包共 78 个文件约 85KB包含 14 个 java 源文件与对应 class 文件、15 个 xml 配置、17 个 json 数据文件以及 yml、properties、http 请求脚本和 Maven 包装脚本等覆盖源码、配置、测试与构建环节目录结构清晰便于按模块阅读。目前已有 140 人学习下载。通过该源码可掌握自定义过滤器的注册方式、认证信息封装与校验流程并借助 http 请求文件快速验证接口行为是理解 SpringSecurity 认证扩展的实用参考。1. 自定义认证过滤器为什么你的 Spring Security 总是“差一点”很多开发者第一次接触 Spring Security 自定义认证过滤器都是在项目里遇到一个绕不过去的需求登录请求里带了加密的签名头或者 token 放在 cookie 的某个自定义字段里又或者认证前需要先校验设备指纹。默认的UsernamePasswordAuthenticationFilter只认表单里的 username 和 password完全对不上业务。这时候你搜到“自定义认证过滤器”照着几篇博客抄了一个OncePerRequestFilter注册进链里结果要么过滤器根本不执行要么执行了但SecurityContext里始终是匿名用户要么认证成功后下一个请求又变回 401。这个标题讲的就是怎么把这件事从头做对从过滤器在 Spring Security 过滤链中的位置、认证对象的构造、SecurityContext 的写入时机到注册方式和常见翻车点。适合已经能跑通默认表单登录、但需要接入自定义凭证体系的后端开发者也适合想读懂框架认证入口的进阶读者。下面按“先立住原理、再动手复现、最后排坑”的顺序展开。2. 认证过滤器在过滤链里到底站在哪先搞清 DelegatingFilterProxy 到 FilterChainProxy 的调用顺序2.1 从 Servlet 容器到 Security 过滤链的两次委托Servlet 容器启动时DelegatingFilterProxy作为一个标准的Filter注册进容器但它本身不干活而是把doFilter委托给 Spring 容器里名为springSecurityFilterChain的 Bean。这个 Bean 的真实类型是FilterChainProxy它内部维护一个SecurityFilterChain列表每个链匹配一组 URL链里按顺序排列着十几个 Security 过滤器。请求进来后FilterChainProxy根据RequestMatcher选中一条链然后依次调用链中的过滤器。理解这个双层委托很关键因为很多人把自定义过滤器注册成普通 Spring Bean 就以为生效了实际上它根本没被加进SecurityFilterChain自然不执行。默认的过滤器顺序里UsernamePasswordAuthenticationFilter大致排在LogoutFilter之后、DefaultLoginPageGeneratingFilter之前。如果你要替换表单登录的认证逻辑最稳的做法是继承UsernamePasswordAuthenticationFilter并覆盖attemptAuthentication然后通过HttpSecurity.addFilterAt把它放在原位置。如果你只是想在认证之前做一层预处理比如解析自定义头并构造一个未认证的Authentication那就用OncePerRequestFilter通过addFilterBefore放在UsernamePasswordAuthenticationFilter前面。2.2 认证对象、ProviderManager 与 SecurityContext 的三角关系过滤器本身不负责“认证”它只负责从请求里提取凭证组装成一个未认证的Authentication对象然后交给AuthenticationManager。AuthenticationManager的默认实现是ProviderManager它持有一组AuthenticationProvider逐个调用supports判断谁能处理这个类型的Authentication再调用authenticate做真正的校验。校验通过后返回一个已认证的Authentication过滤器再把它写入SecurityContextHolder。这里有个容易忽略的点SecurityContextHolder默认策略是MODE_THREADLOCAL也就是说认证结果只对当前线程可见。请求结束后SecurityContextPersistenceFilter新版本叫SecurityContextHolderFilter负责在请求开始时从HttpSession或自定义仓库恢复上下文在请求结束时清空。如果你的自定义过滤器在SecurityContextPersistenceFilter之前执行写入的上下文可能在后续被覆盖如果在它之后执行但没手动保存下一个请求就恢复不出来。这就是“认证成功但下一个请求又 401”的根因。2.3 一个最小可跑的自定义过滤器骨架下面这段代码演示一个从自定义请求头X-Auth-Token中读取凭证、构造Authentication并交给AuthenticationManager的过滤器。它继承OncePerRequestFilter保证一次请求只执行一次。import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class TokenAuthFilter extends OncePerRequestFilter { private final AuthenticationManager authenticationManager; public TokenAuthFilter(AuthenticationManager authenticationManager) { this.authenticationManager authenticationManager; } Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 1. 从自定义头提取凭证没有则直接放行交给后续过滤器处理 String token request.getHeader(X-Auth-Token); if (token null || token.isEmpty()) { chain.doFilter(request, response); return; } try { // 2. 构造未认证的 Authenticationtoken 作为 principal 或 credentials TokenAuthentication authRequest new TokenAuthentication(token); // 3. 交给 ProviderManager 认证 Authentication authResult authenticationManager.authenticate(authRequest); // 4. 认证成功写入 SecurityContext SecurityContextHolder.getContext().setAuthentication(authResult); } catch (AuthenticationException ex) { // 5. 认证失败清空上下文不抛出让后续入口处理 401 SecurityContextHolder.clearContext(); } chain.doFilter(request, response); } }逻辑说明第 1 步的“没有凭证就放行”是刻意为之因为过滤器链后面可能还有匿名认证或其他认证方式直接拒绝会破坏链的协作。第 2 步构造的TokenAuthentication需要实现Authentication接口并且isAuthenticated()返回 false。第 3 步的authenticate会触发ProviderManager找到支持TokenAuthentication的 Provider。第 4 步写入上下文后后续的FilterSecurityInterceptor才能看到已认证用户。第 5 步捕获异常后不抛出是为了让ExceptionTranslationFilter有机会把未认证请求重定向到登录入口或返回 401而不是直接 500。参数说明authenticationManager必须是从HttpSecurity共享出来的那个实例不能自己 new 一个ProviderManager否则你注册的 Provider 和框架用的不是同一套。OncePerRequestFilter的shouldNotFilter方法可以覆盖用来跳过静态资源或特定路径避免无谓的 token 解析。3. 把过滤器接进 HttpSecurity注册位置、Provider 绑定与 SecurityContext 保存3.1 用 addFilterBefore 还是 addFilterAt位置决定行为HttpSecurity提供了addFilterBefore、addFilterAfter、addFilterAt三个方法。addFilterAt是“替换”语义如果目标位置已有同类型过滤器会替换掉它addFilterBefore和addFilterAfter是“插入”语义。对于自定义 token 认证常见做法是addFilterBefore(tokenAuthFilter, UsernamePasswordAuthenticationFilter.class)让它在表单认证之前尝试处理。如果你要完全接管表单登录就继承UsernamePasswordAuthenticationFilter并用addFilterAt放在原位置。注册时还要注意过滤器的顺序号。Spring Security 内部给每个标准过滤器分配了Order常量比如UsernamePasswordAuthenticationFilter的 order 是 1900 左右。用addFilterBefore时框架会自动计算一个相对顺序但如果你同时插入多个自定义过滤器最好显式用addFilterBefore的链式调用明确先后避免顺序玄学。3.2 自定义 Authentication 与 AuthenticationProvider 的配套写法过滤器只负责搬运真正的校验逻辑在AuthenticationProvider里。下面是一个配套的 Provider 示例它校验 token 的合法性并返回已认证对象。import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.authentication.BadCredentialsException; public class TokenAuthenticationProvider implements AuthenticationProvider { private final TokenService tokenService; public TokenAuthenticationProvider(TokenService tokenService) { this.tokenService tokenService; } Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String token (String) authentication.getPrincipal(); // 1. 校验 token 是否有效无效直接抛异常 if (!tokenService.isValid(token)) { throw new BadCredentialsException(invalid token); } // 2. 根据 token 解析出用户身份和权限 UserDetails user tokenService.loadUser(token); // 3. 返回已认证的 Authenticationcredentials 通常置空 TokenAuthentication result new TokenAuthentication(user, user.getAuthorities()); result.setAuthenticated(true); return result; } Override public boolean supports(Class? authentication) { // 4. 只处理 TokenAuthentication 类型 return TokenAuthentication.class.isAssignableFrom(authentication); } }逻辑说明第 1 步的校验要区分“token 格式错误”和“token 过期”前者抛BadCredentialsException后者可以抛CredentialsExpiredException方便前端区分处理。第 2 步加载用户时建议走缓存否则每个请求都查库会成为瓶颈。第 3 步返回的已认证对象里credentials一般设为 null避免敏感信息留在内存。第 4 步的supports必须和过滤器构造的Authentication类型严格对应否则ProviderManager找不到 Provider会抛ProviderNotFoundException。参数说明TokenService是自定义的服务类负责 token 的签发、校验和用户加载。TokenAuthentication需要提供两个构造器一个用于未认证请求只传 token一个用于已认证结果传 UserDetails 和权限列表。getAuthorities在未认证时返回空列表已认证时返回真实权限。3.3 配置类里把过滤器、Provider、AuthenticationManager 串起来下面是一个基于 Java 配置的完整装配示例展示如何把自定义过滤器注册进链并把 Provider 绑定到AuthenticationManager。import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import java.util.Collections; Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http, TokenService tokenService) throws Exception { // 1. 手动构造 Provider 和 AuthenticationManager TokenAuthenticationProvider provider new TokenAuthenticationProvider(tokenService); AuthenticationManager authManager new ProviderManager(Collections.singletonList(provider)); // 2. 构造自定义过滤器注入 AuthenticationManager TokenAuthFilter tokenFilter new TokenAuthFilter(authManager); http .csrf().disable() .authorizeRequests() .antMatchers(/public/**).permitAll() .anyRequest().authenticated() .and() // 3. 把过滤器插到表单认证之前 .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }逻辑说明第 1 步手动构造ProviderManager是为了让自定义 Provider 生效。如果你用http.authenticationProvider(provider)也可以但那样AuthenticationManager由框架构建过滤器里注入的必须是同一个实例所以更推荐显式构造再共享。第 2 步把authManager传给过滤器保证过滤器和框架用同一套 Provider。第 3 步的addFilterBefore确保 token 认证先于表单认证执行如果 token 存在且有效后续表单认证不会覆盖已认证上下文。参数说明csrf().disable()在纯 token 认证场景下常见因为 token 本身防跨站但如果是 cookie 携带 token建议保留 CSRF 防护。antMatchers的放行路径要按实际业务调整避免把需要认证的接口误放行。ProviderManager的构造参数是ListAuthenticationProvider多个 Provider 时按顺序尝试前一个抛异常不会自动尝试下一个除非异常类型是AuthenticationException且下一个 Provider 的supports返回 true。4. 避坑与排查认证过滤器最常见的五类翻车现场4.1 过滤器不执行现象是断点不进、日志不打印现象自定义过滤器里的doFilterInternal完全不被调用日志里也看不到任何输出。原因通常是过滤器没有真正加入SecurityFilterChain只是被声明成了一个普通 Spring Bean。DelegatingFilterProxy只认springSecurityFilterChain这个 Bean其他Filter类型的 Bean 不会自动进入安全链。解决检查配置类里是否调用了addFilterBefore或addFilterAt而不是只写了Bean。另外如果用了WebSecurityCustomizer的ignoring方法忽略了某些路径那些路径上的所有安全过滤器都不会执行包括自定义的。4.2 认证成功但下一个请求又 401SecurityContext 没保存现象当前请求里SecurityContextHolder.getContext().getAuthentication()有值但下一个请求进来又变成匿名。原因是SecurityContext没有持久化到HttpSession或自定义仓库。默认情况下SecurityContextPersistenceFilter会在请求结束时把上下文保存到 session但前提是上下文在保存时仍然存在且未被清空。如果你的过滤器在SecurityContextPersistenceFilter之前执行写入的上下文可能在链的后续被覆盖如果用了SessionCreationPolicy.STATELESS框架根本不会保存上下文每个请求都需要重新认证。解决确认过滤器的位置在SecurityContextPersistenceFilter之后或者显式调用SecurityContextRepository.saveContext。无状态场景下每个请求都必须携带 token 并重新走认证流程这是设计预期不是 bug。4.3 ProviderNotFoundExceptionsupports 方法没对上类型现象过滤器调用authenticationManager.authenticate时抛出ProviderNotFoundException提示找不到支持某类型的 Provider。原因是自定义Authentication的类型和 Provider 的supports返回值不匹配或者 Provider 根本没注册进AuthenticationManager。解决在 Provider 的supports里打印一下传入的authentication.getClass()确认类型一致。如果用了http.authenticationProvider(provider)检查配置类是否被EnableWebSecurity注解扫描到。另外ProviderManager的parent属性如果指向了另一个AuthenticationManager而父管理器里也没有对应 Provider同样会抛这个异常。4.4 认证异常被吞掉ExceptionTranslationFilter 没机会处理现象token 无效时接口返回 500 而不是 401或者前端收到一个空白响应。原因是过滤器里捕获了AuthenticationException后没有重新抛出也没有调用AuthenticationEntryPoint导致异常被吞掉后续的ExceptionTranslationFilter看不到。解决在catch块里清空上下文后可以选择直接返回 401或者把异常重新抛给ExceptionTranslationFilter。更规范的做法是让过滤器只负责提取凭证认证失败时抛出自定义异常由全局异常处理器统一返回。注意不要在过滤器里写response.getWriter().write后不return否则会继续走链导致响应重复提交。4.5 多次执行与重复认证OncePerRequestFilter 不是万能的现象同一个请求里过滤器执行了两次日志出现两条认证记录。原因是OncePerRequestFilter只保证在同一个请求的同一个FilterChain中不重复执行但如果请求被forward或include或者被RequestDispatcher再次派发它可能重新进入过滤链。另外如果自定义过滤器同时注册成了普通 Bean 和 Security 链过滤器也会执行两次。解决覆盖shouldNotFilterAsyncDispatch和shouldNotFilterErrorDispatch返回 true避免异步和错误派发时重复执行。检查FilterRegistrationBean是否把自定义过滤器又注册了一遍如果是去掉那个注册。5. 进阶技巧用 AuthenticationEntryPoint 与自定义 SecurityContextRepository 收口认证边界5.1 认证失败时返回结构化 JSON 而不是重定向默认的LoginUrlAuthenticationEntryPoint会把未认证请求重定向到登录页这在前后端分离项目里很别扭。自定义AuthenticationEntryPoint可以让失败响应直接返回 JSON。下面是一个示例。import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class JsonAuthEntryPoint implements AuthenticationEntryPoint { Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { // 1. 设置状态码和内容类型 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(application/json;charsetUTF-8); // 2. 返回统一错误结构不暴露内部异常细节 response.getWriter().write({\code\:401,\message\:\unauthorized\}); } }逻辑说明第 1 步的状态码必须是 401不要用 200 包一层业务错误码否则前端拦截器无法统一处理。第 2 步的响应体结构要和全局异常处理器保持一致避免前端写两套解析逻辑。这个 EntryPoint 需要在HttpSecurity里通过exceptionHandling().authenticationEntryPoint(...)注册否则不生效。参数说明commence方法里不要抛异常直接写响应并返回。如果用了HttpServletResponse.SC_UNAUTHORIZED注意某些网关会把它改写成 200需要在网关层排查。5.2 自定义 SecurityContextRepository 实现无状态与有状态混合有些场景下部分接口需要 session 保持登录部分接口用 token 无状态认证。这时候可以自定义SecurityContextRepository在loadContext里优先从 token 解析失败再回退到 session。下面是一个简化示例。import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class HybridContextRepository implements SecurityContextRepository { private final TokenService tokenService; public HybridContextRepository(TokenService tokenService) { this.tokenService tokenService; } Override public SecurityContext loadContext(HttpRequestResponseHolder holder) { // 1. 先尝试从 token 恢复 String token holder.getRequest().getHeader(X-Auth-Token); if (token ! null tokenService.isValid(token)) { SecurityContext context SecurityContextHolder.createEmptyContext(); context.setAuthentication(tokenService.buildAuth(token)); return context; } // 2. 回退到 session return SecurityContextHolder.createEmptyContext(); } Override public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { // 3. 无状态场景不保存有状态场景可写入 session } Override public boolean containsContext(HttpServletRequest request) { return request.getHeader(X-Auth-Token) ! null; } }逻辑说明第 1 步的 token 恢复逻辑要和过滤器里的认证逻辑保持一致避免两处解析规则不同导致行为分裂。第 2 步的回退到 session 需要注入HttpSessionSecurityContextRepository并委托给它这里为了简洁省略。第 3 步的saveContext如果留空等于完全无状态适合纯 token 场景如果需要混合就在 token 不存在时委托给 session 仓库。参数说明HttpRequestResponseHolder是框架传入的请求响应包装不要缓存它每次loadContext都重新获取。containsContext的返回值影响框架是否调用loadContext返回 false 时框架会直接创建空上下文。5.3 一个验证过滤器是否生效的排查习惯我一般会在过滤器里加一行logger.debug(token filter hit, uri{}, request.getRequestURI())然后在配置类里打开org.springframework.security的 DEBUG 日志。启动后访问一个受保护接口观察日志里过滤器的执行顺序和SecurityContext的变化。如果日志里看不到自定义过滤器的输出先检查addFilterBefore是否写在了正确的HttpSecurity链上如果看到了但认证没生效检查AuthenticationManager是不是同一个实例。这个习惯帮我省掉了大量“玄学”排查时间也避免了一上来就怀疑框架的弯路。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑