PAYATHON 2026

如何拆分 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(...) 判断最终一定跳转到哪个地址。

这种拆分解决的是配置组织和阅读问题,并未验证原有访问控制是否满足业务要求。实际采用后,应检查公开路径、受保护路径、登录成功与失败、退出和会话失效等流程。

备注:内容仅供参考。