- 文档
- 教程
- 知识库
【免费下载链接】source-code-hunter
😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等
导读:Spring Security 是 Spring 生态中最常用的安全框架,覆盖认证(Authentication)与授权(Authorization)两大核心模块。本文基于 Spring Boot 2.5.3 + Spring Security 5.5.1,从一个最简单的
/hello接口出发,完整还原「未登录请求 → 默认登录页 → 提交登录表单 → 放行原请求」的整条调用链,并借助断点调试逐帧验证DelegatingFilterProxy、FilterChainProxy、VirtualFilterChain及 15 个安全过滤器的协作机制。读完本文,你将能够独立回答"Spring Security 的一次请求究竟经过了多少个过滤器、每层过滤器各自做了什么、为什么未登录会被重定向到/login"等面试与实战高频问题。
本仓库 docs/SpringSecurity/SpringSecurity请求全过程解析.md 对该主题做了体系化梳理,本文在其基础上结合仓库源码语境进一步展开,方便搜索引擎与开发者检索引用。
一、技术环境与工程搭建:用 Spring Boot 快速开启 Spring Security
Spring Security 与另一款流行的安全框架 Apache Shiro 相比,功能更为强大:它同时提供认证与授权两大安全模块,支持轻松的自定义扩展,并且对常见的 Web 安全攻击(如 CSRF、会话固定等)提供了内置防护。如果 Web 框架选择了 Spring,Spring Security 通常是最自然的组合。
本文演示环境如下:
| 组件 | 版本 |
|---|---|
| Spring Boot | 2.5.3 |
| Spring Security | 5.5.1 |
| 构建工具 | Gradle |
1. 引入依赖
使用 IDEA 创建一个 Spring Boot 项目,在build.gradle中引入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' }说明:
spring-boot-starter-security是安全能力的入口 starter,它借助 Spring Boot 的自动装配机制完成 Spring Security 的默认初始化(关于自动装配的整体原理,可参考本仓库 SpringBoot-自动装配);spring-boot-starter-web提供 Web 能力;spring-security-test用于编写安全的单元测试。
2. 编写一个测试接口
创建一个HelloController,对外提供一个/hello服务:
@RestController public class HelloController { @GetMapping("hello") public String hello() { return "hello world"; } }3. 启动后的默认安全行为
直接启动项目,访问http://localhost:8080/hello,可以看到页面跳转到一个登录页面,地址为http://localhost:8080/login:
这说明:仅仅引入了依赖、没有编写任何安全配置,Spring Security 就已默认接管了所有请求。默认的用户名为user,密码由 Spring Security 自动生成,回到 IDEA 的控制台可以找到密码信息:
Using generated security password: 4f06ba04-37e9-4bdd-a085-3305260da0d6输入用户名user和该随机密码后,便可以成功访问/hello接口。这段开箱即用的体验背后,正是 Spring Security 默认安全配置 + 过滤器链在起作用。
二、基本原理:Spring Security 是如何"接管"所有请求的
Spring Security 默认为我们开启了一套简单的安全配置。当 Spring Boot 项目配置了 Spring Security 后,其整个加载过程如下:
而当我们访问http://localhost:8080/hello时,代码的整个执行过程如下:
从时序图可以清晰看到,Spring Security 包含了众多的过滤器,这些过滤器形成了一条链,所有请求都必须通过这些过滤器后,才能成功访问到目标资源。
这条过滤器链的骨架由三个关键类组成:
DelegatingFilterProxy:它是一个桥接过滤器(Bridge Filter),由 Servlet 容器直接注册。它本身并不做安全处理,而是在 Spring 容器中查找名为springSecurityFilterChain的 Bean(即FilterChainProxy),把请求委派给它执行;FilterChainProxy:Spring Security 的门面,内部持有SecurityFilterChain的集合(filterChains),在doFilterInternal()中根据请求匹配对应的过滤器链,并生成内部类VirtualFilterChain实例;VirtualFilterChain:真正的链式执行器,通过游标currentPosition依次调用additionalFilters中注册的每一个过滤器。
整个框架在 web 容器(如内置 Tomcat)层面的注册,可以追溯到 Spring Boot 自动装配中注册的springSecurityFilterChainBean——DelegatingFilterProxy正是通过springSecurityFilterChain这个名字找到FilterChainProxy的。
三、断点实战:一次未认证请求的完整旅程
下面我们通过 IDEA 的 Debug 功能,逐帧验证上述执行过程。这一部分是对文档 SpringSecurity请求全过程解析 的完整还原。
第一步:在链的入口打上断点
通过前面的原理可知,当请求来到时,最先由DelegatingFilterProxy负责接收,因此在DelegatingFilterProxy的doFilter()首行打上断点:
接着,DelegatingFilterProxy会将请求委派给FilterChainProxy进行处理,在FilterChainProxy的doFilter()首行打上断点:
FilterChainProxy会在doFilterInternal()中生成一个内部类VirtualFilterChain的实例,以此来调用 Spring Security 的整条过滤器链,在VirtualFilterChain的doFilter()首行打上断点:
第二步:了解过滤器链的成员
VirtualFilterChain会通过currentPosition依次调用存在additionalFilters中的过滤器。从调试变量面板可以看到,additionalFilters是一个包含15 个元素的ArrayList,覆盖了WebAsyncManagerIntegrationFilter、SecurityContextPersistenceFilter、CsrfFilter、LogoutFilter等(从截图可见其下标 0、1、3、4 依次对应上述过滤器,下标 14 为FilterSecurityInterceptor)。
其中比较重要的几个过滤器有:
UsernamePasswordAuthenticationFilter:处理表单登录认证;DefaultLoginPageGeneratingFilter:生成/返回默认登录页面;AnonymousAuthenticationFilter:为未认证请求填充匿名身份AnonymousAuthenticationToken;ExceptionTranslationFilter:捕获FilterSecurityInterceptor抛出的异常并转化为重定向/响应;FilterSecurityInterceptor:位于链尾的授权决策过滤器。
我们依次在这些过滤器的doFilter()首行打上断点:
第三步:启动并观察断点命中过程
准备完毕后,启动项目,访问http://localhost:8080/hello,程序首先跳转到DelegatingFilterProxy的断点上:
此时delegate还是null的(说明尚未初始化),继续依次执行代码,可以看到delegate最终被赋值一个FilterChainProxy的实例(其beanName为springSecurityFilterChain):
接下来程序依次跳转到FilterChainProxy的doFilter()和VirtualFilterChain的doFilter()中:
从调试变量可以看到,此刻
currentPosition为 0、size为 15,VirtualFilterChain正要开始迭代 15 个过滤器。
第四步:逐过滤器观察未认证请求的处理
①AbstractAuthenticationProcessingFilter(UsernamePasswordAuthenticationFilter的父类)
程序跳转到AbstractAuthenticationProcessingFilter的doFilter()中,通过requiresAuthentication()判定为false——因为当前请求是GET /hello,而该过滤器只处理POST提交的登录请求,因此直接放行:
②DefaultLoginPageGeneratingFilter
接着程序跳转到DefaultLoginPageGeneratingFilter的doFilter()中,通过isLoginUrlRequest()判定为false——当前请求路径不是/login,因此不返回登录页,继续放行:
③AnonymousAuthenticationFilter
接着程序跳转到AnonymousAuthenticationFilter的doFilter()中。由于是首次请求,此时SecurityContextHolder.getContext().getAuthentication()为null,因此该过滤器会生成一个AnonymousAuthenticationToken的实例(标记为匿名身份),让后续的授权决策有"身份"可查:
④ExceptionTranslationFilter
程序跳转到ExceptionTranslationFilter的doFilter()中。它本身不执行安全判断,而是负责捕获FilterSecurityInterceptor抛出的异常,我们在其 catch 代码块的首行打上断点:
⑤FilterSecurityInterceptor(授权决策)
接着程序跳转到FilterSecurityInterceptor的doFilter()中,依次执行代码后程序停留在其父类(AbstractSecurityInterceptor)的attemptAuthorization()中:
其中accessDecisionManager是AccessDecisionManager(访问决策器)的实例。AccessDecisionManager主要有 3 个实现类:
| 实现类 | 决策策略 | 说明 |
|---|---|---|
AffirmativeBased | 一票通过 | 只要有投票器同意即放行 |
ConsensusBased | 少数服从多数 | 根据投票结果汇总决定 |
UnanimousBased | 一票否决 | 必须全部同意才放行 |
此时AccessDecisionManager的实现类是AffirmativeBased,可以看到程序进入AffirmativeBased的decide()中:
从上图可以看出,决策的关键在voter.vote(authentication, object, configAttributes)这句代码上。通过跟踪调试,程序最终进入AuthenticationTrustResolverImpl的isAnonymous()中:
isAssignableFrom()用于判断前者是否是后者的父类。这里anonymousClass被固定为AnonymousAuthenticationToken.class,而参数authentication由前面AnonymousAuthenticationFilter可以知道正是AnonymousAuthenticationToken的实例,因此isAnonymous()返回true——匿名身份被视为"未认证",FilterSecurityInterceptor随即抛出AccessDeniedException异常,程序返回到ExceptionTranslationFilter的 catch 块中:
⑥ 异常转化为重定向
接着程序会依次进入DelegatingAuthenticationEntryPoint、LoginUrlAuthenticationEntryPoint中,最后由LoginUrlAuthenticationEntryPoint的commence()决定重定向到/login:
第五步:登录页的返回
后续对/login的请求同样会经过之前的执行流程。区别在于:在DefaultLoginPageGeneratingFilter的doFilter()中,通过isLoginUrlRequest()判定为true(请求路径是/login),于是直接返回login.html——也就是我们开头看到的登录页面。
第六步:提交登录表单,重定向回原请求
当我们输入用户名和密码,点击Sign in,程序来到AbstractAuthenticationProcessingFilter的doFilter()中,通过requiresAuthentication()判定为true(这次是POST请求),因此交给其子类UsernamePasswordAuthenticationFilter进行处理。UsernamePasswordAuthenticationFilter会将用户名和密码封装成一个UsernamePasswordAuthenticationToken的实例并进行校验,当校验通过后会将请求重定向到我们一开始请求的路径:/hello。
后续对/hello的请求经过过滤器链时就可以一路开绿灯(此时SecurityContextHolder中已经是完整的认证身份,不再被判定为匿名),最终交由HelloController返回"Hello World"。
四、链路全景与关键组件速查
综合以上调试过程,一次请求在 Spring Security 中的完整链路可以概括为:
浏览器请求 /hello → DelegatingFilterProxy.doFilter() // 桥接:查找 springSecurityFilterChain → FilterChainProxy.doFilter() // 门面:匹配 SecurityFilterChain → VirtualFilterChain.doFilter() // 迭代 15 个过滤器 → UsernamePasswordAuthenticationFilter // GET 请求直接放行;POST 才做表单认证 → DefaultLoginPageGeneratingFilter // 请求 /login 时返回登录页 → AnonymousAuthenticationFilter // 未认证时填充匿名身份 → ExceptionTranslationFilter // 捕获授权异常 → FilterSecurityInterceptor // 授权决策,未认证抛 AccessDeniedException → AccessDecisionManager(AffirmativeBased).decide() → AuthenticationTrustResolverImpl.isAnonymous() == true ← 异常被 ExceptionTranslationFilter 捕获 ← LoginUrlAuthenticationEntryPoint.commence() → 重定向 /login各关键过滤器职责汇总:
| 过滤器 | 职责 |
|---|---|
DelegatingFilterProxy | Servlet 容器与 Spring 容器之间的桥接,委派给FilterChainProxy |
FilterChainProxy | Spring Security 入口门面,管理多条SecurityFilterChain |
VirtualFilterChain | 用currentPosition游标迭代执行过滤器链 |
UsernamePasswordAuthenticationFilter | 处理 POST 表单登录,封装UsernamePasswordAuthenticationToken |
DefaultLoginPageGeneratingFilter | 拦截/login请求并生成默认登录页 |
AnonymousAuthenticationFilter | 为未认证请求创建AnonymousAuthenticationToken |
ExceptionTranslationFilter | 捕获授权异常,交给AuthenticationEntryPoint处理 |
FilterSecurityInterceptor | 链尾授权决策,调用AccessDecisionManager |
AuthenticationTrustResolverImpl | 判定身份是否匿名(isAnonymous()) |
LoginUrlAuthenticationEntryPoint | 决定重定向到/login |
五、总结与进一步探索
本文以 Spring Boot 2.5.3 + Spring Security 5.5.1 为例,完整还原了一次未认证请求从进入DelegatingFilterProxy到被重定向至/login的全过程,并用断点验证了以下核心结论:
- Spring Security 通过过滤器链接管所有请求,链的入口是
DelegatingFilterProxy,核心执行者是FilterChainProxy内部的VirtualFilterChain; - 未认证请求会被
AnonymousAuthenticationFilter标记为匿名身份,最终在FilterSecurityInterceptor的授权决策环节被AccessDecisionManager(默认AffirmativeBased一票通过策略)判定为无权限并抛出AccessDeniedException; - 异常由
ExceptionTranslationFilter捕获,再经由LoginUrlAuthenticationEntryPoint完成到/login的重定向; - 登录成功后,
UsernamePasswordAuthenticationFilter将表单参数封装为UsernamePasswordAuthenticationToken完成认证,并将请求重定向回原始访问路径。
如果你想进一步深入,可以在本仓库的 README.md 的 SpringSecurity 章节查看该主题在项目中的完整文档索引,并结合 SpringSecurity请求全过程解析.md 原文中的多张调试截图逐帧比对。在此基础上,还可以继续研究如何自定义用户认证逻辑(如基于数据库的用户加载)、如何配置放行规则与自定义登录页等扩展主题。
适用前提说明:本文演示基于 Spring Boot 2.5.3 / Spring Security 5.5.1 版本,不同版本的过滤器链组成与内部类实现可能略有差异,但「委托代理 → 门面 → 虚拟过滤器链 → 认证/授权过滤器」的整体架构在 Spring Security 5.x 中保持一致。
- 文档
- 教程
- 知识库
【免费下载链接】source-code-hunter
😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等
相关推荐
Spring Security 请求全过程解析:从 DelegatingFilterProxy 到 FilterSecurityInterceptor 的过滤器链源码追踪
Spring Security 请求全过程解析:从 DelegatingFilterProxy 到 FilterSecurityInterceptor 的过滤器
文档教程技术博客知识库一文读懂Spring Security过滤器链:从配置到执行全解析
一文读懂Spring Security过滤器链:从配置到执行全解析 你还在为Spring Security的过滤器链配置感到困惑吗?作为Spring生态中最流行
后端认证鉴权应用安全解决登录重定向难题:Spring Security RequestCache深度解析
解决登录重定向难题:Spring Security RequestCache深度解析 你是否遇到过用户登录后无法返回原请求页面的问题?作为开发者,我们希望用户体
后端认证鉴权应用安全
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考