如何拆分 Spring Security v6 中冗长的 lambda 和链式配置?
可以。你提出的 Customizer<FormLoginConfigurer<HttpSecurity>> 写法成立。也可以在同一个 HttpSecurity 上分别调用配置方法,用方法引用把较长的配置移到私有方法中,最后调用一次 http.build() 即可。
为什么可以拆开调用?
原代码通过外层链式调用连续执行 CSRF、SecurityContext、请求授权、登录、会话、退出和 remember-me 配置。这些调用也可以分成多条语句。
例如:
http.csrf(AbstractHttpConfigurer::disable)
.securityContext(context -> context.requireExplicitSave(true));
可以改成:
http.csrf(AbstractHttpConfigurer::disable);
http.securityContext(context -> context.requireExplicitSave(true));
这里使用的仍是同一个 http,执行顺序也相同。拆分语句不会创建新的 SecurityFilterChain。
各配置方法接收的 Customizer<T> 是一个函数式接口,核心方法是 void customize(T t)。因此,lambda 可以赋给变量,也可以用参数类型匹配的 void 方法替代。
直接把 lambda 保存为 Customizer
可以将 lambda 保存为变量,并在块内把链式调用拆成独立语句:
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
Customizer<FormLoginConfigurer<HttpSecurity>> formLoginCustomizer = form -> {
form.loginPage("/login");
form.defaultSuccessUrl("/homepage.html");
form.failureUrl("/login?error=true");
form.successHandler(myAuthenticationSuccessHandler);
form.failureHandler(authenticationFailureHandler);
form.authenticationDetailsSource(authenticationDetailsSource);
form.permitAll();
};
http.formLogin(formLoginCustomizer);
这段代码应放在原配置类的方法中,沿用原有的处理器和依赖。Customizer 是配置回调,不需要为了这种拆分专门注册成 Spring Bean。
如果配置只使用一次,局部变量就够了。如果希望 filterChain 主要展示配置顺序,提取私有方法通常更清楚。
用方法引用整理完整配置
将较长的请求授权和表单登录配置提取成方法后,原有配置项和调用顺序保持不变。代码仍放在原配置类中,使用已有字段以及 sessionRegistry()、rememberMeServices() 方法。
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.web.SecurityFilterChain;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable);
http.securityContext(context -> context.requireExplicitSave(true));
http.authorizeHttpRequests(this::configureAuthorization);
http.formLogin(this::configureFormLogin);
http.sessionManagement(session -> {
session.invalidSessionUrl("/invalidSession.html");
session.maximumSessions(1)
.sessionRegistry(sessionRegistry());
});
http.logout(logout -> {
logout.logoutSuccessHandler(myLogoutSuccessHandler);
logout.invalidateHttpSession(true);
logout.logoutSuccessUrl("/logout.html?logSucc=true");
logout.deleteCookies("JSESSIONID");
logout.permitAll();
});
http.rememberMe(remember -> {
remember.rememberMeServices(rememberMeServices());
});
return http.build();
}
private void configureAuthorization(
AuthorizeHttpRequestsConfigurer<HttpSecurity>
.AuthorizationManagerRequestMatcherRegistry authz) {
authz.requestMatchers(HttpMethod.GET, "/roleHierarchy")
.hasRole("STAFF");
authz.requestMatchers(
"/login*",
"/logout*",
"/signin/**",
"/signup/**",
"/customLogin",
"/user/registration*",
"/registrationConfirm*",
"/expiredAccount*",
"/registration*",
"/badUser*",
"/user/resendRegistrationToken*",
"/forgetPassword*",
"/user/resetPassword*",
"/user/savePassword*",
"/updatePassword*",
"/user/changePassword*",
"/emailError*",
"/resources/**",
"/old/user/registration*",
"/successRegister*",
"/qrcode*",
"/user/enableNewLoc*"
).permitAll();
authz.requestMatchers("/invalidSession*")
.anonymous();
authz.requestMatchers("/user/updatePassword*")
.hasAuthority("CHANGE_PASSWORD_PRIVILEGE");
authz.requestMatchers("/console")
.hasAuthority("READ_PRIVILEGE");
authz.anyRequest()
.hasAuthority("READ_PRIVILEGE");
}
private void configureFormLogin(FormLoginConfigurer<HttpSecurity> form) {
form.loginPage("/login");
form.defaultSuccessUrl("/homepage.html");
form.failureUrl("/login?error=true");
form.successHandler(myAuthenticationSuccessHandler);
form.failureHandler(authenticationFailureHandler);
form.authenticationDetailsSource(authenticationDetailsSource);
form.permitAll();
}
this::configureFormLogin 在这里与 form -> configureFormLogin(form) 的作用相同。方法名说明配置用途,具体参数留在对应的方法中查看。
请求授权方法的参数类型较长,是因为回调接收的是 AuthorizationManagerRequestMatcherRegistry,而不是外层的 AuthorizeHttpRequestsConfigurer。如果不想显式写出这个类型,可以把授权配置保留在局部 lambda 中,由 Java 推断参数类型。
拆分时需要保留哪些关系?
请求匹配规则要保持原有顺序。授权配置按顺序匹配请求,移动可能重叠的规则会影响最终应用的权限要求。anyRequest() 应留在具体规则之后。
各方法返回的 builder 可能不同。例如:
session.maximumSessions(1)
.sessionRegistry(sessionRegistry());
这里的 sessionRegistry(...) 是在 maximumSessions(1) 返回的并发会话配置对象上调用的。这两行保留局部链式调用,可以直接看出它们的从属关系。
登录和退出部分同时包含 URL 配置及自定义处理器。上面的重构保留了原调用顺序;自定义处理器的行为仍取决于其实现,不能仅凭 defaultSuccessUrl(...) 或 logoutSuccessUrl(...) 判断最终一定跳转到哪个地址。
这种拆分解决的是配置组织和阅读问题,并未验证原有访问控制是否满足业务要求。实际采用后,应检查公开路径、受保护路径、登录成功与失败、退出和会话失效等流程。
备注:内容仅供参考。
