HttpSecurity 是如何组装过滤器链的
有小伙伴们问到这个问题,简单写篇文章和大伙聊一下。
一 SecurityFilterChain
首先大伙都知道,Spring Security 里边的一堆功能都是通过 Filter 来实现的,无论是认证、RememberMe Login、会话管理、CSRF 处理等等,各种功能都是通过 Filter 来实现的。
所以,我们配置 Spring Security,说白了其实就是配置这些 Filter。
以前旧版继承自 WebSecurityConfigurerAdapter 类,然后重写 configure 方法,利用 HttpSecurity 去配置过滤器链,这种写法其实不太好理解,特别对于新手来说,可能半天整不明白到底配置了啥。
现在新版写法我觉得更加合理,因为直接就是让开发者自己去配置过滤器链,类似下面这样:
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {return http.build();
}
这样开发者更容易理解,自己是在配置配置 SecurityFilter 过滤器链,因为就是要配置这样一个 Bean。
SecurityFilterChain 是一个接口,这个接口只有一个实现类 DefaultSecurityFilterChain。
DefaultSecurityFilterChain 中有一个 requestMatcher,通过 requestMatcher 可以识别出来哪些请求需要拦截,拦截下来之后,经由该类的另外一个属性 filters 进行处理,这个 filters 中保存了我们配置的所有 Filter。
public final class DefaultSecurityFilterChain implements SecurityFilterChain {private final RequestMatcher requestMatcher;private final List<Filter> filters;}
因此,在配置 SecurityFilterChain 这个 Bean 的时候,我们甚至可以按照如下方式来写:
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {return new DefaultSecurityFilterChain(new AntPathRequestMatcher("/**"));
}
这个配置就表示拦截所有请求,但是拦截下来之后,这些请求所经过的过滤器为空,即拦截所有请求但是不做任何处理。
我们也可以为 DefaultSecurityFilterChain 对象去配置过滤器链,大家看到,它的构造器可以传入 Filter:
但是,Spring Security 过滤器数量有 30+,我们平日开发使用的也在 15 个左右,这样一个一个去配置太麻烦了,每个过滤器并非 new 出来就能用了,还要配置各种属性,所以我们一般不会自己一个一个去配置这些过滤器。
二 SecurityConfigurer
为了简化 Spring Security 中各个组件的配置,官方推出了 SecurityConfigurer,SecurityConfigurer 在 Spring Security 中扮演着非常重要的角色,其主要作用可以归纳为以下几点:
- 配置过滤器:在 Spring Security 中,过滤器链中的每一个过滤器都是通过 xxxConfigurer 来进行配置的,而这些 xxxConfigurer 实际上都是 SecurityConfigurer 的实现。这意味着 SecurityConfigurer 负责配置和管理安全过滤器链中的各个组件。
- 初始化和配置安全构建器:SecurityConfigurer 接口中定义了两个主要方法:init 和 configure。init 方法用于初始化安全构建器(SecurityBuilder),而 configure 方法则用于配置这个构建器。这两个方法共同确保了安全组件的正确设置和初始化。
- 提供扩展点:SecurityConfigurer 的三个主要实现类(SecurityConfigurerAdapter、GlobalAuthenticationConfigurerAdapter、WebSecurityConfigurer)为开发者提供了扩展 Spring Security 功能的点。特别是,大部分的 xxxConfigurer 都是 SecurityConfigurerAdapter 的子类,这使得开发者能够轻松地定制和扩展安全配置。
- 构建安全上下文:通过配置和组合不同的 SecurityConfigurer 实现,可以构建一个完整的安全上下文,包括身份验证、授权、加密等各个方面,从而确保应用程序的安全性。
换句话说,Spring Security 中的各种 Filter,其实都有各自对应的 xxxConfigurer,通过这些 xxxConfigurer 完成了对 Filter 的配置。
举几个简答的例子,如:
- CorsConfigurer 负责配置 CorsFilter
- RememberMeConfigurer 负责配置 RememberMeAuthenticationFilter
- FormLoginConfigurer 负责配置 UsernamePasswordAuthenticationFilter
以上是前置知识。
三 SecurityBuilder
SecurityBuilder
是 Spring Security 框架中的一个核心接口,它体现了建造者设计模式,用于构建和配置安全组件。这个接口并不直接与安全功能如认证或授权的具体实现绑定,而是提供了一种灵活的方式来组织和装配这些功能组件,特别是在构建安全上下文和过滤器链过程中。
核心特点
- 灵活性与模块化:通过
SecurityBuilder
,开发者可以以模块化的方式添加、移除或替换安全配置中的各个部分,而不需要了解整个安全架构的细节。这促进了代码的复用性和可维护性。 - 层次结构:在 Spring Security 中,
SecurityBuilder
及其子类形成了一个层次结构,允许配置像嵌套娃娃一样层层深入,每一个层级都可以贡献自己的配置逻辑,最终形成一个完整的安全配置。 - 安全上下文构建:在认证过程中,
SecurityBuilder
用于构造SecurityContext
,该上下文中包含了当前认证主体(通常是用户)的详细信息,以及主体所关联的权限和角色。 - 过滤器链构建:在 Web 应用中,
SecurityBuilder
还用于构建FilterChainProxy
,这是一个关键组件,负责组织和执行一系列的过滤器,这些过滤器负责处理 HTTP 请求的安全性,比如检查用户是否已经登录、是否有权限访问某个资源等。
简而言之,在 SecurityBuilder 的子类 AbstractConfiguredSecurityBuilder 中,有一个名为 configurers 的集合,这个集合中保存的就是我们前面所说的各种 xxxConfigure 对象。
AbstractConfiguredSecurityBuilder 收集到所有的 xxxConfigure 之后,将来会调用每个 xxxConfigure 的 configure 方法完成过滤器的构建。
另外很重要的一点,HttpSecurity 也是一个 SecurityBuilder。因此,HttpSecurity 其实就是帮我们收集各种各样的 xxxConfigure,并存入到 configurers 集合中,以备将来构建过滤器使用。
四 HttpSecurity
根据前面的介绍,我们知道,无论我们怎么配置,最终拿到手的一定是一个 DefaultSecurityFilterChain 对象,因为这是 SecurityFilterChain 的唯一实现类。
所以,HttpSecurity 其实就是在帮我们配置 DefaultSecurityFilterChain,我们看到 HttpSecurity 里边就有两个非常关键的属性:
private List<OrderedFilter> filters = new ArrayList<>();
private RequestMatcher requestMatcher = AnyRequestMatcher.INSTANCE;
这两个恰恰就是构建 DefaultSecurityFilterChain 所需的关键参数。
事实确实如此,我们看到,在 HttpSecurity#performBuild 方法中,就是利用这两个参数构建出来了 DefaultSecurityFilterChain:
@Override
protected DefaultSecurityFilterChain performBuild() {ExpressionUrlAuthorizationConfigurer<?> expressionConfigurer = getConfigurer(ExpressionUrlAuthorizationConfigurer.class);AuthorizeHttpRequestsConfigurer<?> httpConfigurer = getConfigurer(AuthorizeHttpRequestsConfigurer.class);boolean oneConfigurerPresent = expressionConfigurer == null ^ httpConfigurer == null;this.filters.sort(OrderComparator.INSTANCE);List<Filter> sortedFilters = new ArrayList<>(this.filters.size());for (Filter filter : this.filters) {sortedFilters.add(((OrderedFilter) filter).filter);}return new DefaultSecurityFilterChain(this.requestMatcher, sortedFilters);
}
这里先对 filters 进行排序,然后据此创建出来 DefaultSecurityFilterChain 对象。
那么问题来了,filters 中的过滤器从何而来呢?
前面我们提到,HttpSecurity 本质上也是一个 SecurityBuilder,我们平时在 HttpSecurity 配置的各种东西,本质上其实就是一个 xxxConfigure,这些 xxxConfigure 被 HttpSecurity 收集起来,最后会遍历收集起来的 xxxConfigure,调用其 configure 方法,最终获取过滤器,并将获取到的过滤器存入到 filters 集合中。
这里松哥以我们最为常见的登录配置为例来和大家捋一捋这个流程。
新版的登录配置我们一般按照如下方式来配置:
http.formLogin(f -> f.permitAll());
这个方法如下:
public HttpSecurity formLogin(Customizer<FormLoginConfigurer<HttpSecurity>> formLoginCustomizer) throws Exception {formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer<>()));return HttpSecurity.this;
}
从这段源码可以看到,我们配置里边写的 lambda,其实就是在配置 FormLoginConfigurer 对象。
getOrApply 方法主要主要有两方面的作用:
- 确保这个 Bean 不会重复配置。
- 如果该 Bean 是第一次配置,那么将该对象添加到 SecurityBuilder 中(其实就是 HttpSecurity 对象自身),这个 SecurityBuilder 里边保存了所有配置好的 xxxConfigure 对象,将来在过滤器链构建的时候,会去遍历这些 xxxConfigure 对象并调用其 configure 方法,完成过滤器的构建。
在 FormLoginConfigurer#init 方法中,完成了和登录相关过滤器的配置,如:
- 登录请求处理过滤器(构造方法中完成的)
- 注销过滤器的配置
- 登录失败端点配置
- 登录页面的配置
这里涉及到的属性都会在对应的 xxxConfigurer 中完成配置。
当过滤器链开始构建的时候,会调用到所有 xxxConfigurer 的 configure 方法,在这个方法中,最终完成相关过滤器的创建,并将之添加到 HttpSecurity 的 filters 属性这个集合中。
FormLoginConfigurer 的 configure 方法在其父类中,如下:
@Override
public void configure(B http) throws Exception {PortMapper portMapper = http.getSharedObject(PortMapper.class);if (portMapper != null) {this.authenticationEntryPoint.setPortMapper(portMapper);}RequestCache requestCache = http.getSharedObject(RequestCache.class);if (requestCache != null) {this.defaultSuccessHandler.setRequestCache(requestCache);}this.authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));this.authFilter.setAuthenticationSuccessHandler(this.successHandler);this.authFilter.setAuthenticationFailureHandler(this.failureHandler);if (this.authenticationDetailsSource != null) {this.authFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource);}SessionAuthenticationStrategy sessionAuthenticationStrategy = http.getSharedObject(SessionAuthenticationStrategy.class);if (sessionAuthenticationStrategy != null) {this.authFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);}RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class);if (rememberMeServices != null) {this.authFilter.setRememberMeServices(rememberMeServices);}SecurityContextConfigurer securityContextConfigurer = http.getConfigurer(SecurityContextConfigurer.class);if (securityContextConfigurer != null && securityContextConfigurer.isRequireExplicitSave()) {SecurityContextRepository securityContextRepository = securityContextConfigurer.getSecurityContextRepository();this.authFilter.setSecurityContextRepository(securityContextRepository);}this.authFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());F filter = postProcess(this.authFilter);http.addFilter(filter);
}
关键在最后这一句 http.addFilter(filter);
,这句代码将配置好的过滤器放到了 HttpSecurity 的 filters 集合中。
其他的也都类似,例如配置 CorsFilter 的 CorsConfigurer#configure 方法,如下:
@Override
public void configure(H http) {ApplicationContext context = http.getSharedObject(ApplicationContext.class);CorsFilter corsFilter = getCorsFilter(context);http.addFilter(corsFilter);
}
这些被添加到 HttpSecurity 的 filters 属性上的 Filter,最终就成为了创建 DefaultSecurityFilterChain 的原材料。
类似的道理,我们也可以分析出 disable 方法的原理,例如我们要关闭 csrf,一般配置如下:
.csrf(c -> c.disable());
那么很明显,这段代码其实就是调用了 CsrfConfigurer 的 disable 方法:
public B disable() {getBuilder().removeConfigurer(getClass());return getBuilder();
}
该方法直接从 SecurityBuilder 的 configurers 集合中移除了 CsrfConfigurer,所以导致最终调用各个 xxxConfigure 的 configure 方法的时候,没有 CsrfConfigurer#configure 了,就导致 csrf 过滤器没有配置上,进而 CSRF filter 失效。
如果小伙伴们想要彻底学会 Spring Security,那么不妨看看我最近刚刚录完的 Spring Security+OAuth2 教程。
相关文章:

HttpSecurity 是如何组装过滤器链的
有小伙伴们问到这个问题,简单写篇文章和大伙聊一下。 一 SecurityFilterChain 首先大伙都知道,Spring Security 里边的一堆功能都是通过 Filter 来实现的,无论是认证、RememberMe Login、会话管理、CSRF 处理等等,各种功能都是通…...

STM32 入门教程(江科大教材)#笔记2
3-4按键控制LED /** LED.c**/ #include "stm32f10x.h" // Device headervoid LED_Init(void) {/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_I…...

python zip()函数(将多个可迭代对象的元素配对,创建一个元组的迭代器)zip_longest()
文章目录 Python zip() 函数深入解析基本用法函数原型基础示例 处理不同长度的迭代器高级用法多个迭代器使用 zip() 与 dict()解压序列 注意事项内存效率:zip() 返回的是一个迭代器,这意味着直到迭代发生前,元素不会被消耗。这使得 zip() 特别…...
React.forwardRef 使用
React.forwardRef 是一个React提供的高阶组件函数,用于向函数组件传递ref。在函数组件中无法直接访问ref,如果需要在函数组件中操作子组件的DOM元素或组件实例,就可以使用React.forwardRef来转发ref给子组件。 当使用React.forwardRef包裹一…...
C# 中的值类型与引用类型:内存大小解析
在 C# 中,类型可以被归类为值类型或引用类型,它们在内存中的存储和管理方式不同。了解这些差异对于优化程序性能和资源管理至关重要。 值类型 (Value Types) 值类型包括所有内置的数值类型(如 int, double 等)、char 类型、bool…...
object对象列表使用sorted函数按照对象的某个字段排序
在Python中,如果你想要根据列表中对象的某个属性(比如create_time)来进行逆序排序,你可以使用sorted()函数并指定一个key参数。key参数应该是一个函数,该函数接受一个列表元素并返回一个用于排序的值。 假设你的objec…...

【再探】设计模式—中介者模式、观察者模式及模板方法模式
中介者模式让多对多的复杂引用关系变成一对多,同时能通过中间类来封装多个类中的行为,观察者模式在目标状态更新时能自动通知给订阅者,模版方法模式则是控制方法的执行顺序,子类在不改变算法的结构基础上可以扩展功能实现。 1 中…...

vue中使用svg图像
一 、svg图像是什么 SVG(可缩放矢量图形)是一种图像格式,它以XML文档的形式存在,用以描述图像中的形状、线条、文本和颜色等元素。由于其基于矢量的特性,SVG图像在放大或改变尺寸时能够保持图形质量不受影响。这种格式…...

Deconfounding Duration Bias in Watch-time Prediction for Video Recommendation
Abstract 观看时间预测仍然是通过视频推荐加强用户粘性的关键因素。然而,观看时间的预测不仅取决于用户与视频的匹配,而且经常被视频本身的持续时间所误导。为了提高观看时间,推荐总是偏向于长时间的视频。在这种不平衡的数据上训练的模型面…...
python多进程
python多进程的使用有两种方式: multiprocessingconcurrent的使用方式 multiprocessing的使用方式 定义线程池的数量开始处理,结果回调 下面以多进程下载图像为例: import multiprocessing import requests from io import BytesIO from…...
springboot 的yaml配置文件加密
springboot 的yaml配置文件加密 一、采用yaml 插件加密添加依赖创建启动类配置加密密钥加密需要加密的内容用过测试类编写加密的YAML配置解密配置可选:自定义配置扩展:修改ENC() 一、采用yaml 插件加密 使用Jasypt对Spring Boot的YAML配置文件进行加密是…...

npm发布、更新、删除包
如何将自己开发的依赖包发布到npmjs上供别人使用?五个步骤搞定! 实现步骤: 创建自己的工具包项目,进行开发。注册npmjs账号。执行npm login在控制台登录,填写用户信息。执行npm publish发布包。更新及删除。 步骤一…...

【JavaEE进阶】——Mybatis操作数据库(使用注解和XML方式)
目录 🚩三层架构 🎈JDBC操作回顾 🚩什么是MyBatis 🚩MyBatis⼊⻔ 🎈准备工作 📝创建⼯程 📝数据准备 🎈配置数据库连接字符串 🎈写持久层代码 🎈单…...

【数据结构】六种排序实现方法及区分比较
文章目录 前言插入排序希尔排序选择排序堆排序快速排序冒泡排序总结 前言 众所周知,存在许多种排序方法,作为新手,最新接触到的就是冒泡排序,这种排序方法具有较好的教学意义,但是实用意义不高,原因就在于…...
QT之QTableWidget详细介绍
本文来自于学习QT时遇到QTableWidget类时进行总结的知识点,涵盖了QTableWidget主要函数。本人文笔有限,欢迎大家评论区讨论。 一、QTableWidget介绍 QTableWidget 类是 Qt 框架中的一个用于展示和编辑二维表格数据的控件。它是对 QTableView 和 QStand…...

mac电脑安卓设备文件传输助手:MacDroid pro 中文激活版
MacDroid Pro是一款专为Mac电脑和Android设备设计的软件,旨在简化两者之间的文件传输和数据管理,双向文件传输:支持从Mac电脑向Android设备传输文件,也可以将Android设备上的文件轻松传输到Mac电脑上。完整的文件访问和管理&#…...

车流量监控系统
1.项目介绍 本文档是对于“车流量检测平台”的应用技术进行汇总,适用于此系统所有开发,测试以及使用人员,其中包括设计背景,应用场景,系统架构,技术分析,系统调度,环境依赖…...

LAMP集群分布式实验报告
前景: 1.技术成熟度和稳定性: LAMP架构(Linux、Apache、MySQL、PHP)自1998年提出以来,经过长时间的发展和完善,已经成为非常成熟和稳定的Web开发平台。其中,Linux操作系统因其高度的灵活性和稳…...
vue3中函数必须有返回值么?
在 Vue 3 中,特别是涉及到Composition API的使用时,setup() 函数确实必须有返回值。setup() 函数是组件的入口点,它的返回值会被用来决定哪些数据和方法是可被模板访问的。返回的对象中的属性和方法可以直接在模板中使用。如果setup()没有返回…...
经常用到的函数
创建文件夹和删除文件夹的函数 def make_dirs(*dirs):for new_dir in dirs:if not os.path.exists(new_dir):try:os.makedirs(new_dir)except RuntimeError:return Falsereturn Truedef remove_files(file_path_list):""" 删除列表中指定路径文件Args:file_pat…...

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》
在注意力分散、内容高度同质化的时代,情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现,消费者对内容的“有感”程度,正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中࿰…...
【学习笔记】深入理解Java虚拟机学习笔记——第4章 虚拟机性能监控,故障处理工具
第2章 虚拟机性能监控,故障处理工具 4.1 概述 略 4.2 基础故障处理工具 4.2.1 jps:虚拟机进程状况工具 命令:jps [options] [hostid] 功能:本地虚拟机进程显示进程ID(与ps相同),可同时显示主类&#x…...
【Go语言基础【12】】指针:声明、取地址、解引用
文章目录 零、概述:指针 vs. 引用(类比其他语言)一、指针基础概念二、指针声明与初始化三、指针操作符1. &:取地址(拿到内存地址)2. *:解引用(拿到值) 四、空指针&am…...

使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...

Python Ovito统计金刚石结构数量
大家好,我是小马老师。 本文介绍python ovito方法统计金刚石结构的方法。 Ovito Identify diamond structure命令可以识别和统计金刚石结构,但是无法直接输出结构的变化情况。 本文使用python调用ovito包的方法,可以持续统计各步的金刚石结构,具体代码如下: from ovito…...

STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...
Vite中定义@软链接
在webpack中可以直接通过符号表示src路径,但是vite中默认不可以。 如何实现: vite中提供了resolve.alias:通过别名在指向一个具体的路径 在vite.config.js中 import { join } from pathexport default defineConfig({plugins: [vue()],//…...
【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案
目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后,迭代器会失效,因为顺序迭代器在内存中是连续存储的,元素删除后,后续元素会前移。 但一些场景中,我们又需要在执行删除操作…...

Linux部署私有文件管理系统MinIO
最近需要用到一个文件管理服务,但是又不想花钱,所以就想着自己搭建一个,刚好我们用的一个开源框架已经集成了MinIO,所以就选了这个 我这边对文件服务性能要求不是太高,单机版就可以 安装非常简单,几个命令就…...
HTML前端开发:JavaScript 获取元素方法详解
作为前端开发者,高效获取 DOM 元素是必备技能。以下是 JS 中核心的获取元素方法,分为两大系列: 一、getElementBy... 系列 传统方法,直接通过 DOM 接口访问,返回动态集合(元素变化会实时更新)。…...