HttpSecurity 是如何组装过滤器链的

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

一 SecurityFilterChain

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

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

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

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

1
2
3
4
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.build();
}

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

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

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

1
2
3
4
5
6
7
public final class DefaultSecurityFilterChain implements SecurityFilterChain {

private final RequestMatcher requestMatcher;

private final List<Filter> filters;

}

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

1
2
3
4
@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 里边就有两个非常关键的属性:

1
2
private List<OrderedFilter> filters = new ArrayList<>();
private RequestMatcher requestMatcher = AnyRequestMatcher.INSTANCE;

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
@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 集合中。

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

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

1
http.formLogin(f -> f.permitAll());

这个方法如下:

1
2
3
4
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 方法在其父类中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@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 方法,如下:

1
2
3
4
5
6
@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,一般配置如下:

1
.csrf(c -> c.disable());

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

1
2
3
4
public B disable() {
getBuilder().removeConfigurer(getClass());
return getBuilder();
}

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

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

这套教程基本上把 Spring Security 的方方面面以及 OAuth2 都讲到了。最重要的是,贴合了很多小伙伴们日常常见的开发场景,比如短信验证码登录、JWT+Redis 登录、微信 OAuth2 登录等等都有讲到。

视频是基于目前最新版的 Spring Security6 录制的,Spring Security6 在 API 层面的变化还是蛮大的,引入了大量的 Lambda 表达式去简化配置,很多旧版的写法在 Spring Security6 中被废弃,并在将来在 Spring Security7 中会移除相关的 API,所以说最近的 Spring Security 更新还是蛮激进的。

另一方面就是这套视频包含了全新的基于目前最新版 Spring Security 录制的 OAuth2 教程,松哥在 2020 年的时候出过图文版的 OAuth2 教程,但是,现在新版的 OAuth2 也有很多变化,不仅仅是 API 层面的变化,授权模式也发生了一些变化,传统的密码模式、简化模式现在都不再推荐,转而引入了 PKCE 模式,并且利用 OIDC 简化用户信息获取。同时,在 2020 年还处于萌芽状态的 Spring Security OAuth2 Server 这个项目,目前也趋于成熟,也可以直接使用了,这些松哥都在视频中和大家做了详细介绍。在 OAuth2 环节我也和大家分享了如何使用微信的 OAuth2 登录。

总之这一套教程,让大家彻底理解系统的安全管理。

之前有小伙伴说我直接自己写过滤器,既灵活还简单。我并不反对这种做法,但是有一个前提就是你很牛,你自己写的过滤器有考虑到计时攻击,有考虑到 XSS 攻击,有考虑到点击劫持,有考虑跨站请求伪造。。。等等太多了。当然,这些问题如果你都没有考虑到,那么 Spring Security 都有帮你考虑到并提供解决方案!

理解了 Spring Security,再去看市面上其他的安全管理框架,都会豁然开朗。

不同于松哥之前其他教程的交付方式,这套教程使用微信群答疑并且不再使用一机一码绑定设备播放,小伙伴们可以在任意设备上登录自己的账号就可以学习了,很便捷。

下面是两个试看视频:

这套视频是付费的,¥499,有需要的小伙伴加微信备注 499

松哥在 2021 年出版了《深入浅出 Spring Security》(清华大学出版社,2021),所以这套视频教程绝对够专业。