Spring Security 请求全过程解析:从 DelegatingFilterProxy 到 FilterSecurityInterceptor 的过滤器链源码追踪
【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter
Spring Security 是基于 Spring 的安全框架,核心能力是认证(Authentication)与授权(Authorization)两大模块。与 Apache Shiro 相比,它功能更强大,可轻松自定义扩展,且对常见 Web 安全攻击提供内置防护。本文以 Spring Boot 集成 Spring Security 为切入点,从源码层面完整追踪一次 HTTP 请求在安全过滤器链中的流转全过程——从DelegatingFilterProxy接收请求,到FilterChainProxy调度各过滤器,再到FilterSecurityInterceptor完成授权决策并抛出异常、最终重定向到登录页的完整链路。读完本文,你将掌握 Spring Security 过滤器链的核心成员职责、请求在链上的判定逻辑、匿名认证与访问决策机制,以及如何用 Debug 断点亲手验证这一过程。
本仓库对应的完整源码笔记为 SpringSecurity 请求全过程解析,并可与 SpringSecurity 自定义用户认证、SpringSecurity 流程补充 配合阅读。
环境准备:Spring Boot 集成 Spring Security
本文使用 Spring Boot 版本2.5.3,Spring Security 版本5.5.1。使用 IDEA 创建一个 Spring Boot 项目后,在构建脚本中引入spring-boot-starter-security:
dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.projectlombok:lombok:1.18.8' annotationProcessor 'org.projectlombok:lombok:1.18.8' providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' }接下来创建一个HelloController,对外提供一个/hello服务:
@RestController public class HelloController { @GetMapping("hello") public String hello() { return "hello world"; } }直接启动项目并访问http://localhost:8080/hello,会发现页面被跳转到了一个登录页面:
默认的用户名为user,密码由 Spring Security 自动生成。回到 IDEA 的控制台即可找到密码信息:
Using generated security password: 4f06ba04-37e9-4bdd-a085-3305260da0d6输入用户名user、密码4f06ba04-37e9-4bdd-a085-3305260da0d6后,即可成功访问/hello接口。
版本差异提示:在基于 spring-boot-dependencies 2.7.7 的新版本中,控制台日志会略有不同,输出为
Using generated springSecurity password: ...并附带 "This generated password is for development use only..." 的提示。该默认用户由UserDetailsServiceAutoConfiguration自动装配的InMemoryUserDetailsManager提供,属于非持久化的内存映射实现,主要用于测试和演示,生产环境必须替换为自定义的用户信息来源(详见 SpringSecurity 流程补充 与 SpringSecurity 自定义用户认证)。
基本原理:启动加载链路与请求执行链路
Spring Security 默认为我们开启了一套安全配置。要理解它,需要先弄清两条链路:启动时的加载链路与请求到达时的执行链路。
当 Spring Boot 项目配置了 Spring Security 后,其整个加载过程如下图所示:
而当我们访问http://localhost:8080/hello时,代码的整个执行过程如下图所示:
如上图所示,Spring Security 包含了众多的过滤器,这些过滤器形成了一条链(Filter Chain),所有请求都必须通过这些过滤器后才能成功访问到资源。一次典型的未认证访问会经历:
GET /hello:请求进入过滤器链,UsernamePasswordAuthenticationFilter判定不是登录提交(非 POST 的/login),DefaultLoginPageGeneratingFilter判定不是登录页请求,AnonymousAuthenticationFilter生成匿名认证 Token,最终FilterSecurityInterceptor判定匿名用户无权限,抛出AccessDeniedException,请求被重定向到/login;GET /login:DefaultLoginPageGeneratingFilter判定是登录页请求,直接返回登录页面;POST /login:UsernamePasswordAuthenticationFilter判定是登录提交,封装并校验用户名密码,认证通过后重定向回原请求路径/hello;GET /hello(认证后):过滤器链一路放行,请求最终到达HelloController,返回 "hello world"。
过滤器链的核心成员
请求执行链路中比较重要的几个过滤器(均位于FilterChainProxy的过滤器链内)有:
| 过滤器 | 核心职责 | 关键判定方法 |
|---|---|---|
UsernamePasswordAuthenticationFilter | 拦截登录提交,封装用户名密码并认证 | requiresAuthentication():是否匹配登录提交(POST 请求) |
DefaultLoginPageGeneratingFilter | 生成默认登录页面 | isLoginUrlRequest():请求路径是否为/login |
AnonymousAuthenticationFilter | 为未认证请求创建匿名身份 | 检查SecurityContextHolder中是否存在认证信息 |
ExceptionTranslationFilter | 捕获过滤器链中抛出的安全异常 | handleSpringSecurityException():区分认证异常与访问拒绝异常 |
FilterSecurityInterceptor | 对受保护资源做最终授权决策 | 委托AccessDecisionManager投票决定放行或拒绝 |
Debug 验证请求全过程:一行一行看过滤器链
下面我们通过 Debug 断点来逐步验证整个过程。
第一站:DelegatingFilterProxy 接收请求
当有请求来到时,最先由DelegatingFilterProxy负责接收,因此在其doFilter()的首行打上断点。启动项目并访问http://localhost:8080/hello,程序首先跳转到该断点上。此时delegate还是 null,继续执行可以看到它最终被赋值一个FilterChainProxy的实例。
DelegatingFilterProxy是 Spring 整合 Servlet Filter 的桥接器:它本身是一个 Servlet 过滤器,但真正的过滤逻辑在 Spring 容器中的 bean 里,它通过 bean 名(springSecurityFilterChain)懒加载找到目标过滤器再委派执行。
第二站:FilterChainProxy 与 VirtualFilterChain
DelegatingFilterProxy会将请求委派给FilterChainProxy进行处理,在FilterChainProxy.doFilter()的首行打上断点。FilterChainProxy会在doFilterInternal()中生成一个内部类VirtualFilterChain的实例,用其调用 Spring Security 的整条过滤器链,在VirtualFilterChain.doFilter()的首行打上断点。
VirtualFilterChain会通过内部游标currentPosition依次调用存放在additionalFilters列表中的过滤器。整个调用链为:DelegatingFilterProxy→FilterChainProxy→VirtualFilterChain→ 各安全过滤器。
第三站:UsernamePasswordAuthenticationFilter —— requiresAuthentication()
接着程序跳转到AbstractAuthenticationProcessingFilter(UsernamePasswordAuthenticationFilter的父类)的doFilter()中,通过requiresAuthentication()判定当前请求是否为登录提交。由于对/hello的首次访问是 GET 请求,判定结果为 false,过滤器直接放行,不执行认证逻辑。
第四站:DefaultLoginPageGeneratingFilter —— isLoginUrlRequest()
程序跳转到DefaultLoginPageGeneratingFilter.doFilter()中,通过isLoginUrlRequest()判断请求路径是否是/login。由于当前请求是/hello,判定结果为 false,过滤器放行。
第五站:AnonymousAuthenticationFilter —— 匿名 Token 的诞生
程序跳转到AnonymousAuthenticationFilter.doFilter()中。由于是首次请求,此时SecurityContextHolder.getContext().getAuthentication()为 null,因此过滤器会生成一个AnonymousAuthenticationToken的实例放入SecurityContextHolder,代表当前用户是一个匿名用户。
第六站:ExceptionTranslationFilter —— 异常捕获者
程序跳转到ExceptionTranslationFilter.doFilter()中。它的职责是捕获FilterSecurityInterceptor抛出的安全异常,我们在其 catch 代码块的首行打上断点,等待后续异常到来。
第七站:FilterSecurityInterceptor —— 授权决策
程序跳转到FilterSecurityInterceptor.doFilter()中,依次执行代码后,程序停留在其父类AbstractSecurityInterceptor的attemptAuthorization()中。这里的关键是accessDecisionManager——即AccessDecisionManager(访问决策器)的实例。
AccessDecisionManager主要有 3 个实现类:
| 实现类 | 策略 | 说明 |
|---|---|---|
AffirmativeBased | 一票通过 | 只要有一个投票器同意即放行,Spring Security 默认采用 |
ConsensusBased | 少数服从多数 | 依据投票结果取多数意见 |
UnanimousBased | 一票否决 | 所有投票器都同意才放行 |
此时AccessDecisionManager的实现类是AffirmativeBased,程序进入其decide()方法。决策的关键在voter.vote(authentication, object, configAttributes)这句代码上——由各个AccessDecisionVoter投票器对当前认证信息投票。
决策落定:AuthenticationTrustResolverImpl.isAnonymous()
通过跟踪调试,程序最终进入AuthenticationTrustResolverImpl.isAnonymous()中。isAssignableFrom()判断前者是否是后者的父类:anonymousClass被固定为AnonymousAuthenticationToken.class,而参数authentication由前面AnonymousAuthenticationFilter可知正是AnonymousAuthenticationToken的实例,因此isAnonymous()返回 true——即当前用户是匿名用户,没有访问/hello的权限。
异常抛出:AccessDeniedException
判定为匿名后,FilterSecurityInterceptor抛出AccessDeniedException异常,程序返回到ExceptionTranslationFilter的 catch 块中。
回到 ExceptionTranslationFilter:入口点的选择
在ExceptionTranslationFilter中,handleSpringSecurityException()会区分异常类型:AuthenticationException(认证异常)与AccessDeniedException(访问拒绝异常)。匿名用户访问受保护资源触发的是后者,但 Spring Security 的处理策略是:如果SecurityContextHolder中没有认证信息(或只有匿名认证信息),则按未认证处理,启动认证流程。
程序会依次进入DelegatingAuthenticationEntryPoint、LoginUrlAuthenticationEntryPoint中。DelegatingAuthenticationEntryPoint会按RequestMatcher逐一匹配已注册的入口点,没有匹配项时使用默认入口点。最后由LoginUrlAuthenticationEntryPoint.commence()决定重定向到/login,同时在重定向前,ExceptionTranslationFilter.sendStartAuthentication()会清空SecurityContextHolder并通过requestCache.saveRequest()保存原始请求,以便认证成功后回跳。
源码佐证:
ExceptionTranslationFilter的异常分流与sendStartAuthentication()的调用链,在 SpringSecurity 流程补充 中有完整的源码片段,包括DelegatingAuthenticationEntryPoint.commence()遍历entryPoints匹配并执行对应入口点的逻辑。
登录流程:从 login.html 到 /hello
后续对/login的请求同样会经过之前的执行流程。
GET /login:返回登录页面
在DefaultLoginPageGeneratingFilter.doFilter()中,通过isLoginUrlRequest()判定为 true(请求路径是/login),直接返回login.html——也就是我们在开头看到的登录页面。在默认配置下,该过滤器会在内存中动态生成一个包含 Username、Password 输入框和 Sign in 按钮的简单 HTML 登录表单。
POST /login:UsernamePasswordAuthenticationFilter 认证
当我们输入用户名和密码,点击Sign in,程序来到AbstractAuthenticationProcessingFilter.doFilter()中,通过requiresAuthentication()判定为 true(POST 请求且路径匹配登录提交路径),因此交给其子类UsernamePasswordAuthenticationFilter处理。
UsernamePasswordAuthenticationFilter会从请求参数中取出用户名和密码,封装成一个UsernamePasswordAuthenticationToken的实例,然后交给AuthenticationManager(实际由ProviderManager及其持有的AuthenticationProvider链,如DaoAuthenticationProvider)进行校验。校验通过后会将请求重定向到我们一开始请求的路径/hello(该路径正是此前被requestCache保存下来的原始请求)。
源码佐证:默认的认证入口与校验链路在 SpringSecurity 自定义用户认证 中有完整 Debug 追踪:
UsernamePasswordAuthenticationFilter→AuthenticationManager→ProviderManager.authenticate()→AbstractUserDetailsAuthenticationProvider.authenticate()→DaoAuthenticationProvider.retrieveUser()→ 最终调用UserDetailsService.loadUserByUsername()获取用户信息。这也解释了为什么实现自定义UserDetailsService即可接管用户信息来源。
认证成功后:回到 /hello
后续对/hello的请求经过过滤器链时就可以一路开绿灯——SecurityContextHolder中已存在认证信息,不再生成匿名 Token,FilterSecurityInterceptor的授权决策通过,请求最终交由HelloController返回 "Hello World"。
纵览:安全过滤器链在 Spring Boot 中是如何被装配起来的
请求全过程解析清楚了,还有一个关键问题:这条过滤器链到底是谁、在什么时候装配起来的?这部分结合仓库内的源码笔记做一次纵向串联。
三个自动配置类
引入spring-boot-starter-security后,Spring Boot 会通过自动装配(AutoConfiguration.imports)加载一组安全相关的配置类,核心是三个:
SecurityAutoConfiguration:Spring Security 的自动配置入口,通过@EnableConfigurationProperties(SecurityProperties.class)绑定security.user、security.filter等配置项,并注册DefaultAuthenticationEventPublisher(认证成功/失败事件的发布订阅器);UserDetailsServiceAutoConfiguration:在没有自定义UserDetailsService/AuthenticationProvider/AuthenticationManager时,自动装配内存版的InMemoryUserDetailsManager,并生成默认用户user与随机密码(即控制台打印的那条日志);SecurityFilterAutoConfiguration:注册DelegatingFilterProxyRegistrationBean,把名为springSecurityFilterChain的过滤器以Order = -100、DispatcherType覆盖ASYNC/ERROR/REQUEST的方式注册进 Servlet 容器。
@EnableWebSecurity 与 springSecurityFilterChain
WebSecurityEnablerConfiguration为容器添加了@EnableWebSecurity注解,这是 Spring Security 的核心注解。它@Import了WebSecurityConfiguration、HttpSecurityConfiguration等配置类:
WebSecurityConfiguration负责创建WebSecurity并聚合所有SecurityConfigurer(如WebSecurityConfigurerAdapter),其springSecurityFilterChain()方法最终通过webSecurity.build()构建出名为springSecurityFilterChain的FilterChainProxybean;HttpSecurityConfiguration创建原型作用域的HttpSecurity,默认装配CsrfConfigurer、ExceptionHandlingConfigurer、AnonymousConfigurer、DefaultLoginPageConfigurer、LogoutConfigurer等一系列SecurityConfigurer,并在doBuild()的init()/configure()阶段把它们转换成实际的过滤器加入链中。
DelegatingFilterProxyRegistrationBean → FilterChainProxy → SecurityFilterChain
整个装配的依赖关系可以浓缩为一条链:
DelegatingFilterProxyRegistrationBean → FilterChainProxy → SecurityFilterChainDelegatingFilterProxyRegistrationBean通过targetBeanName(默认值springSecurityFilterChain)从容器中找到FilterChainProxy并注册为 Servlet 过滤器,完成 Spring 过滤器与 Servlet 容器(如内嵌 Tomcat)的整合;FilterChainProxy持有若干SecurityFilterChain(默认只有一条),并根据请求选择匹配的链;SecurityFilterChain接口只有两个方法:boolean matches(HttpServletRequest request)判断该链是否匹配当前请求,List<Filter> getFilters()返回匹配后要执行的过滤器列表——这正是我们在 Debug 中看到的VirtualFilterChain.additionalFilters的来源。
当存在多个SecurityFilterChainbean 时(例如按@Order配置的多种安全规则),FilterChainProxy会按顺序用matches()匹配,命中即执行该链上的过滤器。
自定义安全配置的两种方式
如果你希望脱离默认配置,Spring Security 提供了两条路径:
- 声明
SecurityFilterChainBean(Spring Security 5.x 推荐,Spring Boot 2.7 后WebSecurityConfigurerAdapter已废弃):
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.formLogin() // 启用表单登录 .loginPage("/login.html") // 自定义登录页 .loginProcessingUrl("/login") // 登录认证路径(对应表单 action) .and() .authorizeRequests() .antMatchers("/login.html", "/css/**", "/error").permitAll() // 免认证路径 .anyRequest().authenticated() // 其余所有请求都需要认证 .and().csrf().disable(); return http.build(); }- 继承
WebSecurityConfigurerAdapter(旧版方式):通过configure(HttpSecurity http)做同样的配置,其原理在 SpringSecurity 自定义用户认证 中有从AbstractConfiguredSecurityBuilder.doBuild()→WebSecurityConfiguration.setFilterChainProxySecurityConfigurer()→AutowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()的完整 Debug 追踪。
自定义登录页、UserDetailsService、登录成功/失败处理等扩展点,均可在 SpringSecurity 自定义用户认证 中找到可运行的改造示例。
小结
一次普通 HTTP 请求经过 Spring Security 的完整生命周期可以概括为:
- 接收:
DelegatingFilterProxy作为桥接器接收请求,懒加载并委派给FilterChainProxy; - 调度:
FilterChainProxy通过内部类VirtualFilterChain按currentPosition游标依次执行过滤器链; - 过滤:
UsernamePasswordAuthenticationFilter(登录提交判定)→DefaultLoginPageGeneratingFilter(登录页判定)→AnonymousAuthenticationFilter(匿名身份)→ExceptionTranslationFilter(异常捕获)→FilterSecurityInterceptor(授权决策); - 决策:
FilterSecurityInterceptor委托AccessDecisionManager(默认AffirmativeBased一票通过)中的各投票器决策,匿名用户被AuthenticationTrustResolverImpl.isAnonymous()判定无权限; - 引导认证:
ExceptionTranslationFilter捕获AccessDeniedException,经DelegatingAuthenticationEntryPoint→LoginUrlAuthenticationEntryPoint重定向到/login,同时requestCache保存原始请求; - 认证:POST
/login由UsernamePasswordAuthenticationFilter封装UsernamePasswordAuthenticationToken并校验,成功后重定向回原始路径,后续请求在过滤器链中一路放行直达业务 Controller。
掌握这条链路,也就掌握了 Spring Security 的骨架:无论是自定义登录页、接入UserDetailsService、配置方法级授权(@EnableGlobalMethodSecurity),还是排查"为什么我的接口被拦截/放行"这类问题,都可以回到这条过滤器链上定位。更完整的配置加载原理可继续阅读本仓库的 SpringSecurity 流程补充 与 SpringSecurity 自定义用户认证。
【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考