至暗时刻

之前的项目不是用上了最新最热的 SpringBoot 3.0 嘛,今天我脑子一热,看了一眼官网版本,直接把 SpringBoot 的版本从3.0.6升级到了3.1.1

重新构建 Gradle 之后启动项目,SpringBoot 没出问题,但启动项目时,依赖传递更新的新版 Spring Security 中有好几个方法移除并标记为弃用(大红色的警告,看上去真的很吓人),虽然代码还能够正常运行,但是本着弃用方法必然有替用方法的原理,故参考文档后写下该文。

原本项目中的 Security 配置代码如下(仅包含了弃用字段的配置),主要集中在SecurityFilterChain的配置 Bean 上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//关闭CSRF,设置无状态连接
http.csrf().disable();

//不通过Session获取SecurityContext
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

//配置异常处理器,处理认证失败的JSON响应
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);

//开启跨域请求
http.cors();
return http.build();
}

拨云见日

如原来的配置文件所示,这四个方法HttpSecurity.csrf(),HttpSecurity.sessionManagement(),HttpSecurity.exceptionHandling(),HttpSecurity.cors()都被标记为弃用了。而根据官网的信息,日后更新的 Spring Security 7 中会完全移除这些方法,Spring Security 6 中虽然两种方法都可以使用,但会产生方法已弃用的警告。

The Lambda DSL is present in Spring Security since version 5.2, and it allows HTTP security to be configured using lambdas.
You may have seen this style of configuration in the Spring Security documentation or samples. Let us take a look at how a lambda configuration of HTTP security compares to the previous configuration style.

新版本的Spring Security 6中,官方推荐使用 Lambda DSL 配置方法来配置 Spring Security。相比过去的配置方式,可以抛弃大量的.and()连接方法,同时 Lambda DSL 更符合现代的编程思想,能让配置方式更加简洁、清晰。

实际上从Spring Security 5.2中,部分方法就已经使用了 Lambda DSL。我在上个版本也已经使用HttpSecurity.authorizeHttpRequests()方法的 Lambda DSL 配置方式。该方法是最早实现且推荐使用 Lambda DSL 配置的方法之一。这次的更新将 DSL 的应用范围变得更大了,囊括了几乎所有常用的HttpSecurity配置。

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
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//关闭CSRF,设置无状态连接
http.csrf(AbstractHttpConfigurer::disable);

//不通过Session获取SecurityContext
http.sessionManagement((sessionManagement) -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

//允许匿名访问的接口
http.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(URL_WHITELIST).anonymous()
.requestMatchers(URL_PERMIT_ALL).permitAll()
.requestMatchers(URL_AUTHENTICATION_1).hasAuthority("admin")
.anyRequest().authenticated());
//添加过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

//配置异常处理器,处理认证失败的JSON响应
http.exceptionHandling((exceptionHandling) -> exceptionHandling
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler));

//开启跨域请求
http.cors((cors) -> cors.configure(http));
return http.build();
}

Lambda 方式的源码分析

新版本的 Spring Security 中,HttpSecurity的配置方法都是通过 Lambda 表达式实现的,要理解 Lambda 配置的原理,首先要从HttpSecurity开始。

HttpSecurity:核心配置类

HttpSecurity是 Spring Security 中最核心的配置类,它的详细继承关系如下,可以看到,HttpSecurity继承自 AbstractConfiguredSecurityBuilder,同时实现了SecurityBuilderHttpSecurityBuilder两个接口。

HttpSecurity

HttpSecurity 的定义如下:

1
2
3
4
5
6
7
8
public final class HttpSecurity
extends
AbstractConfiguredSecurityBuilder<
DefaultSecurityFilterChain,
HttpSecurity>
implements
SecurityBuilder<DefaultSecurityFilterChain>,
HttpSecurityBuilder<HttpSecurity>

该类最重要的职责是进行各种类型的安全配置,通过匿名函数传入各种各样的继承于AbstractHttpConfigurer的配置器,从这些 Configurer 中获取配置信息对HttpSecurity进行配置。

配置示例

这里以设置不通过 Session 获取 SecurityContext 的方法:HttpSecurity.sessionManagement()方法为例,在HttpSecurity中存在大量类似的方法,新版本中,这些方法都是通过 Lambda 表达式实现的,使用 Lambda 表达式接受配置类,对该配置类进行链式调用,从而实现自定义配置。

1
2
3
4
public HttpSecurity sessionManagement(Customizer<SessionManagementConfigurer<HttpSecurity>> sessionManagementCustomizer) throws Exception {
sessionManagementCustomizer.customize(getOrApply(new SessionManagementConfigurer<>()));
return HttpSecurity.this;
}

其主要的处理流程如下:

  1. 调用Customizer接口的customize()方法,对 Configurer 对象进行配置

  2. 通过getOrApply()方法获取或者应用一个 Configurer 对象至HttpSecurity

  3. 返回该HttpSecurity对象,以便进行链式调用配置

其中几个核心步骤的源码分析如下:

customize():接受配置类

HttpSecurity中的大量方法都会接受一个类似于Customizer<xxxConfigurer<HttpSecurity>>类型的参数,Customizer是一个函数式接口,通过声明@FunctionalInterface注解来标注。

1
2
3
4
5
6
7
@FunctionalInterface
public interface Customizer<T> {
void customize(T t);
static <T> Customizer<T> withDefaults() {
return (t) -> {};
}
}

该 class 中只有一个customize(T t)方法,该方法接受一个泛型参数,该方法的作用是对泛型参数进行配置。在 Spring Security 中用于支持 Lambda DSL 的匿名函数配置方法。

可以注意的是该接口提供了一个withDefaults()方法,该方法返回一个包含空方法的对象,这样做的目的是实现 SpringBoot“约定优于配置”的编程思想,如果不需要配置HttpSecurity,则可以直接使用withDefaults()方法,如下例所示:

1
http.sessionManagement(Customizer.withDefaults());

该方法返回的Customizer对象的customize()方法是一个空方法,不会对HttpSecurity进行任何配置。

getOrApply():获取或应用配置器

该方法定义在HttpSecurity内部,属于该类的一个私有方法,该方法的作用是获取或者应用一个SecurityConfigurerAdapter对象。

1
2
3
4
5
6
7
8
private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply(C configurer)
throws Exception {
C existingConfig = (C) getConfigurer(configurer.getClass());
if (existingConfig != null) {
return existingConfig;
}
return apply(configurer);
}

其中核心的getConfigurer()定义在其父类AbstractConfiguredSecurityBuilder中,该方法的作用是获取SecurityConfigurerAdapter对象,并检测该配置类是否已经在 Spring Security 中配置过,如果未被配置,则返回null,使得该方法调用apply()方法。

1
2
3
4
5
6
public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer) throws Exception {
configurer.addObjectPostProcessor(this.objectPostProcessor);
configurer.setBuilder((B) this);
add(configurer);
return configurer;
}

apply()方法的作用也很明显,当该SecurityConfigurerAdapter未被配置时,将该对象添加到 Spring Security 中,并返回该对象。这样的设计使得 Spring Security 的配置类可以被多次配置,而不会产生冲突。

Lambda DSL 的优势

General From ChatGPT

在 Java 中使用 Lambda 表达式的优势有以下几点:

  1. 简洁性:使用 Lambda 表达式可以大大减少代码的冗余,提高代码的可读性和可维护性。相比传统的匿名内部类,Lambda 表达式可以更简洁地表达函数式接口的实现。

  2. 函数式编程支持:Lambda 表达式是 Java 对函数式编程的一种支持,可以方便地处理函数作为参数传递、函数作为返回值等函数式编程的特性。通过 Lambda 表达式,可以更轻松地编写函数式风格的代码。

  3. 代码灵活性:使用 Lambda 表达式可以将行为作为参数传递给方法,从而使得代码更加灵活。可以根据需要在运行时传递不同的行为,而不需要编写多个具体的实现类或匿名内部类。

  4. 并行处理:Lambda 表达式可以与 Java 8 引入的 Stream API 结合使用,使得并行处理数据变得更加容易。通过使用 Lambda 表达式和 Stream API,可以更方便地编写并行化的代码,提高程序的性能。

  5. 内部迭代:传统的迭代方式需要显式地编写循环结构,而 Lambda 表达式可以隐式地进行迭代。通过使用 Lambda 表达式,可以更加简洁地处理集合元素的迭代操作。

写在最后

其实对于个人而言,Lambda DSL 这样的方式确实能够提供更好的可读性

但是对于团队而言,这样的方式可能会导致团队成员之间的代码风格不统一,导致代码可读性下降,所以在团队中使用 Lambda DSL 的方式需要谨慎。

人的惰性是无穷的,很多时候我们都会选择最简单的方式来完成工作,而不是最好的方式。