HttpSecurity 是如何组装过滤器链的

有小伙伴们问到这个问题,简单写篇文章和大伙聊一下。

一 SecurityFilterChain

首先大伙都知道,Spring Security 里边的一堆功能都是通过 Filter 来实现的,无论是认证、RememberMe Login、会话管理、CSRF 处理等等,各种功能都是通过 Filter 来实现的。

所以,我们配置 Spring Security,说白了其实就是配置这些 Filter。

以前旧版继承自 WebSecurityConfigurerAdapter 类,然后重写 configure 方法,利用 HttpSecurity 去配置过滤器链,这种写法其实不太好理解,特别对于新手来说,可能半天整不明白到底配置了啥。

现在新版写法我觉得更加合理,因为直接就是让开发者自己去配置过滤器链,类似下面这样:

java 复制代码
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.build();
}

这样开发者更容易理解,自己是在配置配置 SecurityFilter 过滤器链,因为就是要配置这样一个 Bean。

SecurityFilterChain 是一个接口,这个接口只有一个实现类 DefaultSecurityFilterChain。

DefaultSecurityFilterChain 中有一个 requestMatcher,通过 requestMatcher 可以识别出来哪些请求需要拦截,拦截下来之后,经由该类的另外一个属性 filters 进行处理,这个 filters 中保存了我们配置的所有 Filter。

java 复制代码
public final class DefaultSecurityFilterChain implements SecurityFilterChain {

	private final RequestMatcher requestMatcher;

	private final List<Filter> filters;

}

因此,在配置 SecurityFilterChain 这个 Bean 的时候,我们甚至可以按照如下方式来写:

java 复制代码
@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 中扮演着非常重要的角色,其主要作用可以归纳为以下几点:

  1. 配置过滤器:在 Spring Security 中,过滤器链中的每一个过滤器都是通过 xxxConfigurer 来进行配置的,而这些 xxxConfigurer 实际上都是 SecurityConfigurer 的实现。这意味着 SecurityConfigurer 负责配置和管理安全过滤器链中的各个组件。
  2. 初始化和配置安全构建器:SecurityConfigurer 接口中定义了两个主要方法:init 和 configure。init 方法用于初始化安全构建器(SecurityBuilder),而 configure 方法则用于配置这个构建器。这两个方法共同确保了安全组件的正确设置和初始化。
  3. 提供扩展点:SecurityConfigurer 的三个主要实现类(SecurityConfigurerAdapter、GlobalAuthenticationConfigurerAdapter、WebSecurityConfigurer)为开发者提供了扩展 Spring Security 功能的点。特别是,大部分的 xxxConfigurer 都是 SecurityConfigurerAdapter 的子类,这使得开发者能够轻松地定制和扩展安全配置。
  4. 构建安全上下文:通过配置和组合不同的 SecurityConfigurer 实现,可以构建一个完整的安全上下文,包括身份验证、授权、加密等各个方面,从而确保应用程序的安全性。

换句话说,Spring Security 中的各种 Filter,其实都有各自对应的 xxxConfigurer,通过这些 xxxConfigurer 完成了对 Filter 的配置。

举几个简答的例子,如:

  • CorsConfigurer 负责配置 CorsFilter
  • RememberMeConfigurer 负责配置 RememberMeAuthenticationFilter
  • FormLoginConfigurer 负责配置 UsernamePasswordAuthenticationFilter

以上是前置知识。

三 SecurityBuilder

SecurityBuilder 是 Spring Security 框架中的一个核心接口,它体现了建造者设计模式,用于构建和配置安全组件。这个接口并不直接与安全功能如认证或授权的具体实现绑定,而是提供了一种灵活的方式来组织和装配这些功能组件,特别是在构建安全上下文和过滤器链过程中。

核心特点

  1. 灵活性与模块化 :通过 SecurityBuilder,开发者可以以模块化的方式添加、移除或替换安全配置中的各个部分,而不需要了解整个安全架构的细节。这促进了代码的复用性和可维护性。
  2. 层次结构 :在 Spring Security 中,SecurityBuilder及其子类形成了一个层次结构,允许配置像嵌套娃娃一样层层深入,每一个层级都可以贡献自己的配置逻辑,最终形成一个完整的安全配置。
  3. 安全上下文构建 :在认证过程中,SecurityBuilder 用于构造 SecurityContext,该上下文中包含了当前认证主体(通常是用户)的详细信息,以及主体所关联的权限和角色。
  4. 过滤器链构建 :在 Web 应用中,SecurityBuilder 还用于构建 FilterChainProxy,这是一个关键组件,负责组织和执行一系列的过滤器,这些过滤器负责处理 HTTP 请求的安全性,比如检查用户是否已经登录、是否有权限访问某个资源等。

简而言之,在 SecurityBuilder 的子类 AbstractConfiguredSecurityBuilder 中,有一个名为 configurers 的集合,这个集合中保存的就是我们前面所说的各种 xxxConfigure 对象。

AbstractConfiguredSecurityBuilder 收集到所有的 xxxConfigure 之后,将来会调用每个 xxxConfigure 的 configure 方法完成过滤器的构建。

另外很重要的一点,HttpSecurity 也是一个 SecurityBuilder。因此,HttpSecurity 其实就是帮我们收集各种各样的 xxxConfigure,并存入到 configurers 集合中,以备将来构建过滤器使用。

四 HttpSecurity

根据前面的介绍,我们知道,无论我们怎么配置,最终拿到手的一定是一个 DefaultSecurityFilterChain 对象,因为这是 SecurityFilterChain 的唯一实现类。

所以,HttpSecurity 其实就是在帮我们配置 DefaultSecurityFilterChain,我们看到 HttpSecurity 里边就有两个非常关键的属性:

java 复制代码
private List<OrderedFilter> filters = new ArrayList<>();
private RequestMatcher requestMatcher = AnyRequestMatcher.INSTANCE;

这两个恰恰就是构建 DefaultSecurityFilterChain 所需的关键参数。

事实确实如此,我们看到,在 HttpSecurity#performBuild 方法中,就是利用这两个参数构建出来了 DefaultSecurityFilterChain:

java 复制代码
@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 集合中。

这里松哥以我们最为常见的登录配置为例来和大家捋一捋这个流程。

新版的登录配置我们一般按照如下方式来配置:

java 复制代码
http.formLogin(f -> f.permitAll());

这个方法如下:

java 复制代码
public HttpSecurity formLogin(Customizer<FormLoginConfigurer<HttpSecurity>> formLoginCustomizer) throws Exception {
	formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer<>()));
	return HttpSecurity.this;
}

从这段源码可以看到,我们配置里边写的 lambda,其实就是在配置 FormLoginConfigurer 对象。

getOrApply 方法主要主要有两方面的作用:

  1. 确保这个 Bean 不会重复配置。
  2. 如果该 Bean 是第一次配置,那么将该对象添加到 SecurityBuilder 中(其实就是 HttpSecurity 对象自身),这个 SecurityBuilder 里边保存了所有配置好的 xxxConfigure 对象,将来在过滤器链构建的时候,会去遍历这些 xxxConfigure 对象并调用其 configure 方法,完成过滤器的构建。

在 FormLoginConfigurer#init 方法中,完成了和登录相关过滤器的配置,如:

  • 登录请求处理过滤器(构造方法中完成的)
  • 注销过滤器的配置
  • 登录失败端点配置
  • 登录页面的配置

这里涉及到的属性都会在对应的 xxxConfigurer 中完成配置。

当过滤器链开始构建的时候,会调用到所有 xxxConfigurer 的 configure 方法,在这个方法中,最终完成相关过滤器的创建,并将之添加到 HttpSecurity 的 filters 属性这个集合中。

FormLoginConfigurer 的 configure 方法在其父类中,如下:

java 复制代码
@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 方法,如下:

java 复制代码
@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,一般配置如下:

java 复制代码
.csrf(c -> c.disable());

那么很明显,这段代码其实就是调用了 CsrfConfigurer 的 disable 方法:

java 复制代码
public B disable() {
	getBuilder().removeConfigurer(getClass());
	return getBuilder();
}

该方法直接从 SecurityBuilder 的 configurers 集合中移除了 CsrfConfigurer,所以导致最终调用各个 xxxConfigure 的 configure 方法的时候,没有 CsrfConfigurer#configure 了,就导致 csrf 过滤器没有配置上,进而 CSRF filter 失效。

如果小伙伴们想要彻底学会 Spring Security,那么不妨看看我最近刚刚录完的 Spring Security+OAuth2 教程。

相关推荐
oradh1 小时前
Oracle XTTS实现跨版本迁移和升级(Oracle 11g单库升级至19C RAC集群)
数据库·oracle·11g升级19c·xtts跨版本迁移和升级
z123456789862 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
程序猿DD2 小时前
一个 API Key,统一调用大模型、生图和联网搜索
数据库·网关
TlSfoward3 小时前
抓包代理链路下的 TLS 指纹变化分析 TLSFOWARD抓包工具
数据库·爬虫·网络协议·搜索引擎·php
数智化管理手记4 小时前
账龄分析手工统计易遗漏?自动账龄分析工具怎么搭建
大数据·网络·数据库·人工智能·数据挖掘
LabVIEW开发4 小时前
NI Package Manager安装LabVIEW时InternalServerError错误
网络·数据库·labview·labview知识·labview功能·labview程序
SelectDB4 小时前
PostgreSQL 实时同步到 Apache Doris:一站式 CDC 解决方案
数据库
欲速不达88204 小时前
Mysql并发DML锁等待的优化
数据库·mysql
Database_Cool_5 小时前
AI Agent 长期记忆怎么存?阿里云 PolarDB-X 对接 Mem0 记忆框架实践
数据库·阿里云
会编程的土豆5 小时前
数据库范式
数据库·oracle