1. 一个类看懂 Spring Security 的配置组装逻辑
Spring Security 里,WebSecurityConfigurerAdapter 大概是出场率最高的一个类。不管是教程还是公司项目,基本都能看到这种写法:继承 WebSecurityConfigurerAdapter,重写 configure(HttpSecurity http),然后在里面配上一段 antMatchers 和 authorizeRequests。但绝大多数人在这样写的时候,其实并不清楚这个类是怎么被加载的,三个 configure 方法之间到底是什么关系,为什么有些配置写了却没用。
这篇文章就围绕这个类的源码来拆,适合有基本 Spring Boot Web 开发经验、被自定义安全配置折磨过的读者。读完之后,你至少能回答三个问题:WebSecurityConfigurerAdapter 是谁实例化的?它的三个 configure 方法执行顺序由谁决定?多适配器共存时请求到底走了哪条过滤器链?
先说结论:WebSecurityConfigurerAdapter 本质是一个适配器,它把 Spring Security 的核心构建流程包装成了几个可重写的钩子方法。它的底层是 WebSecurityConfigurer 接口,而 WebSecurityConfigurer 接口又继承了 SecurityConfigurer<Filter, WebSecurity>。这个泛型签名很关键,它说明这套配置器的最终产出物是一个 Filter,而这个 Filter 就是要注册到 Servlet 容器里的 springSecurityFilterChain。
1.1 接口与抽象类的继承结构
打开 Spring Security 5.x 的源码,类声明是这样的:
public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer { private HttpSecurity http; private AuthenticationManagerBuilder authenticationBuilder; private AuthenticationConfiguration authenticationConfiguration; private AuthenticationManager authenticationManager; private UserDetailsService userDetailsService; private boolean disableDefaults; // ... }而接口的定义更简短:
public interface WebSecurityConfigurer extends SecurityConfigurer<Filter, WebSecurity> { AuthenticationManager authenticationManager() throws Exception; UserDetailsService userDetailsServiceBean() throws Exception; }这里能看到一个容易忽略的点:WebSecurityConfigurer 接口只强制要求实现两个方法,一个是获取 AuthenticationManager,一个是获取 UserDetailsService。至于我们熟悉的三个 configure 方法,其实不是接口定义的,而是适配器自己加的扩展点。换句话说,WebSecurityConfigurerAdapter 把接口没有覆盖到的、但绝大多数开发者需要的配置入口,都做成了带默认实现的钩子,你重写哪个,哪个就生效。
这样的设计非常符合适配器模式的本意:不强制你实现全部方法,而是给一个“还算能用”的默认实现,你只需要针对差异部分做覆盖。
1.2 适配器模式的真正含义
你可以把 WebSecurityConfigurerAdapter 理解成一个“总包工头”。Spring Security 的底层有两条非常复杂的构建链路,一条是 HttpSecurity,负责组装过滤器链;另一条是 WebSecurity,负责把所有过滤器链汇总成一个 FilterChainProxy。正常情况下,这两条链路有一大堆内部步骤,普通开发者根本不需要关心。
适配器做的就是把这些内部步骤藏起来,对外只留几个口子:
- configure(HttpSecurity):主战场,配置 URL 授权规则、登录方式、CSRF 等。
- configure(AuthenticationManagerBuilder):配置用户存储、密码编码器、自定义认证逻辑。
- configure(WebSecurity):配置全局级过滤规则,典型的如忽略静态资源。
这三个方法各有各的调用时机和生命周期,后面我会逐个展开。如果你只是背配置不看源码,很容易把三者的边界搞混,最常见的错误就是在 configure(WebSecurity) 里写 authorizeRequests,或者在 configure(HttpSecurity) 里写 ignoring,这都属于用错了口子。
1.3 三个 configure 方法的角色划分
用一句话概括三个方法的分工:
- configure(HttpSecurity) 负责“单条过滤器链长什么样”。
- configure(AuthenticationManagerBuilder) 负责“认证管理器用什么数据源和策略”。
- configure(WebSecurity) 负责“哪些请求根本不进过滤器链”。
其中 configure(WebSecurity) 的执行时机最早,它在 WebSecurity 构建阶段就被调用;configure(HttpSecurity) 的调用发生在 HttpSecurity 的构建阶段;configure(AuthenticationManagerBuilder) 则在 getHttp() 创建 HttpSecurity 之前就会被触发,因为 HttpSecurity 内部需要引用一个可用的认证管理器。
这三者的执行顺序不是随意的,而是被 WebSecurity 和 HttpSecurity 这两个构建器层层传递的自然结果。接下来我先深入看三个方法各自的源码实现,再看启动装配全流程。
2. 三大 configure 钩子逐段解析:默认配置的来源
2.1 configure(HttpSecurity):过滤器链的主战场
先看默认实现。如果你继承 WebSecurityConfigurerAdapter 但一个方法都不重写,Spring Security 会给你一套默认配置。它的逻辑就写在 getHttp() 方法里,默认调用如下:
http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .and() .httpBasic();这段代码的意思是:所有请求都要认证;提供表单登录和 HTTP Basic 两种认证方式。Spring Boot 项目里,如果不做任何安全配置,启动后访问任意接口就会跳转到登录页,原因就在这里。
你重写 configure(HttpSecurity http) 时,默认行为会被完全覆盖,而不是叠加。所以很多人第一次写的时候容易踩坑:自己只配了一个 matching 规则,发现其他路径全都匿名可访问了,因为没有再调用 anyRequest().authenticated()。
从源码视角看,HttpSecurity 本身是一个 SecurityBuilder,它的构建过程会调用所有已注册的 AbstractHttpConfigurer 的 init 和 configure 方法。比如 authorizeRequests() 会注册一个 ExpressionUrlAuthorizationConfigurer,formLogin() 会注册一个 FormLoginConfigurer。最终构建出来的产物是 DefaultSecurityFilterChain,里面按顺序排着一堆过滤器。
2.2 configure(AuthenticationManagerBuilder):认证管理器怎么建立
这个方法的默认实现是空的:
protected void configure(AuthenticationManagerBuilder auth) throws Exception { }不要以为空实现就不重要。真正有意思的是它什么时候被调用,以及调用完之后 AuthenticationManager 去了哪里。
在 getHttp() 方法里,有一段核心逻辑:
this.localConfigureAuthenticationBldr = new AuthenticationManagerBuilder(objectPostProcessor); this.localConfigureAuthenticationBldr.parentAuthenticationManager(authenticationManager);这里创建了一个局部的 AuthenticationManagerBuilder,并且把全局的 authenticationManager 设置为它的 parent。然后这段代码会立刻调用 configure(localConfigureAuthenticationBldr):
configure(localConfigureAuthenticationBldr);也就是说,你在 configure(AuthenticationManagerBuilder) 里写的 userDetailsService、passwordEncoder、authenticationProvider,实际上作用于一个局部构建器。如果配置完成后这个局部构建器里有实际的认证数据源,后续 getAuthenticationManager() 会优先用它构建出一个独立的 AuthenticationManager;如果局部构建器是空的,就 fallback 到全局那个 AuthenticationManager。
这个设计让我经常跟人解释一个现象:为什么使用多个适配器时,在其中某个适配器里配置了 userDetailsService,对另一个适配器的认证逻辑完全不生效。因为每个适配器都有自己独立的局部构建器,它们的 parent 才是共享的全局 AuthenticationManager。
2.3 configure(WebSecurity):一个经常被误用的入口
第三个 configure 方法默认也是空的:
protected void configure(WebSecurity web) throws Exception { }它唯一常见的用途是:
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**", "/js/**", "/images/**"); }这里要理解 ignoring 和 permitAll 的本质区别。permitAll 是“请求虽然进了过滤器链,但经过授权规则时直接放行”。ignoring 是“请求根本不进 Spring Security 的过滤器链”,像静态资源这种完全无状态的请求,连 CSRF 过滤器都不需要经过。
源码层面,WebSecurity.ignoring() 会创建一个 IgnoredRequestConfigurer,最终这些忽略规则会被收集到 FilterChainProxy 中,作为第一批判断条件。请求到达时,先检查是否命中 ignore 列表,命中就直接跳过。而 permitAll 是在 AuthorizationFilter 或 FilterSecurityInterceptor 里通过投票器放行,前者轻量得多。
所以性能敏感、完全不需要安全上下文的路径,用 ignoring 更合适;但如果你希望在过滤器链里做一些统一处理,比如打日志、加请求头,那就不该 ignoring,改用 permitAll 更合理。
2.4 AuthenticationManager 的四种获取姿势
读这个类源码时,另一个高频困惑是 AuthenticationManager 到底怎么拿。适配器里相关的方法有好几个,它们的用途完全不同:
| 方法 | 作用 | 使用场景 |
|---|---|---|
| authenticationManager() | 接口方法,获取认证管理器 | 内部框架调用,一般不用 |
| getAuthenticationManager() | 从局部或全局构建器中解析认证管理器 | 自定义配置时获取认证管理器 |
| authenticationManagerBean() | 显式暴露 AuthenticationManager 为 Spring Bean | 需要注入到自己的 Service 里 |
| configure(AuthenticationManagerBuilder) | 往局部构建器注册认证数据源 | 最常用的用户配置入口 |
其中 authenticationManagerBean() 的实现比较有意思,它带了一个 @Bean 注解,编译后实际上等于主动声明一个 Bean。源码里有一段注释,大意是:只要这个方法被子类调用,最终生成的 AuthenticationManager 会被暴露到容器中。如果你在项目里自定义了过滤器,需要手动调用认证管理器做认证,就可以继承适配器并写一个 authenticationManagerBean() 方法。
需要特别注意的是,一旦你重写了 authenticationManager() 方法,返回的 AuthenticationManager 优先级更高,configure(AuthenticationManagerBuilder) 里写的局部配置可能就不会生效。这是一个隐蔽的坑,等看到多适配器那一节再细说。
3. 启动时一路追踪:从注解到 FilterChainProxy
学源码不能只盯着类内部,还得看它是怎么被装配起来的。我从 @EnableWebSecurity 出发,把启动时的完整链路走一遍。
3.1 @EnableWebSecurity 到底导入了什么
WebSecurityConfigurerAdapter 本身不干活,它需要被外部触发才会构建。触发点是 @EnableWebSecurity:
@Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class }) public @interface EnableWebSecurity { }其中 WebSecurityConfiguration 是核心配置类,它负责创建 springSecurityFilterChain Bean。SpringWebMvcImportSelector 负责在 WebMVC 场景下导入额外配置,比如把认证信息注入到 JSP 标签或 MVC 参数解析器。
这里有一个细节值得注意:Spring Boot 项目通常不需要显式加 @EnableWebSecurity,因为 Spring Boot 的自动配置类在 classpath 存在 Spring Security 时,会自动引入安全配置。但如果你在项目里手动加了 @EnableWebSecurity,自动配置的后缀逻辑会尊重你的显式声明,避免重复。
3.2 Spring Boot 自动配置的默认适配器如何生效
Spring Boot 的 SpringBootWebSecurityConfiguration 里有一个很有意思的条件逻辑。它的作用是:如果容器里没有任何 WebSecurityConfigurerAdapter 类型的 Bean,就自动注册一个空的适配器,让系统能跑起来并套用默认安全配置。
大致逻辑如下:
@Bean @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() { return new WebSecurityConfigurerAdapter() {}; }注意这里会创建一个匿名适配器,方法体是空的。这意味着什么?意味着一个只提供了默认行为的适配器会被加到构建流程里,最终的默认配置就源于 2.1 节里那段 authorizeRequests + formLogin + httpBasic。
只要你自定义了一个继承 WebSecurityConfigurerAdapter 的配置类,这个条件装配就不再生效,自动配置的默认适配器退出舞台。所以你写的适配器会完全接管安全策略,这一点从 Bean 装配层面就定死了。
3.3 WebSecurityConfiguration 的构建入口:springSecurityFilterChain
接下来看 WebSecurityConfiguration 的核心方法:
@Bean(name = "springSecurityFilterChain") public Filter springSecurityFilterChain() throws Exception { boolean hasConfigurers = webSecurityConfigurers != null && !webSecurityConfigurers.isEmpty(); if (!hasConfigurers) { WebSecurityConfigurerAdapter adapter = new WebSecurityConfigurerAdapter() {}; webSecurityConfigurers.add(adapter); } for (SecurityConfigurer<Filter, WebSecurity> securityConfigurer : webSecurityConfigurers) { webSecurity.apply(securityConfigurer); } return webSecurity.build(); }这段代码是整套装配流程的中枢。webSecurityConfigurers 这个列表是 Spring 容器自动收集的:所有类型为 SecurityConfigurer<Filter, WebSecurity> 的 Bean 会被注入到这个字段里。你自定义的继承 WebSecurityConfigurerAdapter 的配置类,就是通过这一机制被收集进来的。
然后 webSecurity.apply(configurer) 把每个配置器都挂到 WebSecurity 上。最后调用 webSecurity.build() 把整个配置流程跑起来,返回一个 Filter,即 springSecurityFilterChain。这个 Bean 会被 Servlet 容器注册为过滤器,所有请求进来先经过它。
没有配置器时,代码里也会手动补一个空适配器,这个兜底逻辑和 Spring Boot 自动配置里的兜底逻辑是双保险,防止用户把配置环境搞坏。
3.4 build() 的执行顺序:init 与 configure 的先后逻辑
SecurityBuilder 的 build 过程遵循一个固定模式,先是 doBuild,再是 init,然后是 configure。WebSecurity 的 doBuild 会对每一个注册进来的 SecurityConfigurer 依次调用 init 和 configure。
对适配器而言:
public void init(WebSecurity web) throws Exception { HttpSecurity http = getHttp(); web.addSecurityFilterChainBuilder(http) .postBuildAction(() -> { FilterSecurityInterceptor securityInterceptor = http.getSharedObject(FilterSecurityInterceptor.class); web.securityInterceptor(securityInterceptor); }); } @Override public void configure(WebSecurity web) throws Exception { }init 阶段做了一件关键的事:调用 getHttp(),强制创建 HttpSecurity,并把 HttpSecurity 作为一个 SecurityFilterChainBuilder 注册到 WebSecurity 中。configure(WebSecurity web) 保持空实现,把扩展机会留给子类。
这个顺序解释了为什么 configure(WebSecurity) 里的代码不参与 HttpSecurity 的构建。WebSecurity 的 configure 阶段在构建全局过滤器链,而 HttpSecurity 的构建实际上已经在上一步 init 中被触发。所以在 WebSecurity.configure 里想改 HttpSecurity 的规则是做不到的。
3.5 getHttp() 创建 HttpSecurity 的关键细节
再回头看 getHttp(),它是理解适配器的核心瓶颈。方法内部首先是缓存判断:
if (this.http != null) { return this.http; }同一个适配器实例,HttpSecurity 只创建一次,避免重复构建。接着创建局部 AuthenticationManagerBuilder:
AuthenticationEventPublisher eventPublisher = getAuthenticationEventPublisher(); this.localConfigureAuthenticationBldr = new AuthenticationManagerBuilder(objectPostProcessor); this.localConfigureAuthenticationBldr.parentAuthenticationManager(authenticationManager) .authenticationEventPublisher(eventPublisher);这里的 objectPostProcessor 是 Spring 容器里的后处理器,所有内部创建的对象都会经过它,以便进行 AOP 增强或者自定义处理。parentAuthenticationManager 把全局认证管理器设置为局部构建器的父级,这样局部构建器即使没有任何数据源,也能通过父级完成认证。
紧接着调用 configure(localConfigureAuthenticationBldr),就会触发你重写的 configure(AuthenticationManagerBuilder) 方法。之后创建 HttpSecurity:
HttpSecurity http = new HttpSecurity(objectPostProcessor, localConfigureAuthenticationBldr, sharedObjects);注意这个构造函数的参数:objectPostProcessor 负责后处理所有过滤器,localConfigureAuthenticationBldr 负责在过滤器链需要认证管理器时提供实例,sharedObjects 是共享对象池,各配置器之间通过它传数据。
创建完后,默认配置开始注入:
http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();然后调用 configure(http),让你重写的内容覆盖默认规则。最后记住实例并返回。
所以整条链路是:WebSecurity.init -> 适配器.init -> getHttp -> 创建局部 AuthenticationManagerBuilder -> 调用 configure(AuthenticationManagerBuilder) -> 创建 HttpSecurity -> 调用 configure(HttpSecurity)。
4. 多适配器共存:SecurityFilterChain 的匹配顺序与陷阱
4.1 一个适配器对应一条过滤器链
很多项目不止一个适配器,尤其是前后端分离加上管理后台时,会拆出好几套安全规则。你要清楚,每个 WebSecurityConfigurerAdapter 实例都会通过 init 阶段注册一个 HttpSecurity,而每个 HttpSecurity 构建出一个 DefaultSecurityFilterChain。
这就意味着,你有几个适配器,就有几条过滤器链。最终它们都被放在 FilterChainProxy 里,由它统一调度。
DefaultSecurityFilterChain 内部保存了两样东西:一个 RequestMatcher 和一个 List
4.2 @Order 排序与先匹配先服务
多适配器共存时,顺序非常重要。WebSecurityConfiguration 收集配置器列表后,会通过 AnnotationAwareOrderComparator 进行排序。默认情况下,WebSecurityConfigurerAdapter 上标记了一个 @Order(100),如果你没有显式指定,多个适配器的相对顺序就是不确定的,这会带来随机性的 bug。
请求进来时,FilterChainProxy 会按顺序遍历所有 DefaultSecurityFilterChain,命中第一个 RequestMatcher 匹配的链就使用它,后面的链完全不执行。
这跟 Spring MVC 里拦截器的顺序认知完全不同。MVC 拦截器是按顺序都执行一遍,安全过滤器链则是遇到第一个匹配就短路。所以当你配了多个适配器,一定要给每个适配器明确的 @Order,否则两个适配器的 antMatcher 规则一旦重叠,谁先执行完全没有保障。
4.3 antMatcher 与 authorizeRequests 的混淆点
这是我在实际项目中遇到最多的问题。看这段错误配置:
@Configuration @Order(1) public class ApiSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/api/**").hasRole("ADMIN") .anyRequest().authenticated(); } }很多人以为这是“只对 /api/** 生效的适配器”,其实不是。适配器没写 http.antMatcher("/api/**") 时,它的 RequestMatcher 是匹配所有请求的。也就是说,任何请求只要走到这条链,都会进入这条链的授权规则,找不到匹配规则时落到 anyRequest().authenticated()。
正确的做法是,在链的层面先限定匹配范围:
http.antMatcher("/api/**") .authorizeRequests() .antMatchers("/api/**").hasRole("ADMIN") .anyRequest().authenticated();这样 /api/** 以外的请求会跳到下一条链,不会在这里被 anyRequest 拦下来。两者一个是链的入口条件,一个是链内部的 URL 级授权规则,千万别混。
4.4 多适配器下 AuthenticationManager 的归属问题
多适配器下另一个诡异问题是认证管理器归属。刚才讲过,每个适配器的 getHttp() 会创建独立的局部 AuthenticationManagerBuilder,并设置全局 AuthenticationManager 作为 parent。
如果适配器 A 在 configure(AuthenticationManagerBuilder) 里注册了 userDetailsService,适配器 B 没有注册,那么 A 对应的过滤器链会使用自己构建的认证管理器,B 就 fallback 到全局那个。这本来没问题,但如果你同时在多个适配器里都注册了 userDetailsService,并且没有指定 @Order,Spring 容器会在收集 UserDetailsService 时直接报冲突。
解决方案有两个层面:一是全局共享,把 UserDetailsService 定义成独立 Bean,只在一个适配器里引用;二是每个适配器配各自的用户存储,但要显式区分数据源,别让容器无从选择。
5. 源码级排错:实测过的五个坑和定位方式
读源码不是为了背概念,是为了踩坑时能快速定位。我把自己实际排过的五个问题整理成速查,每个问题都标记了从源码哪个位置入手。
5.1 权限规则不生效,先检查是不是被更靠前的过滤器链“截胡”
现象:配了一个适配器放行 /public/**,但访问 /public/a 还是跳登录。
这时把断点打在 FilterChainProxy.doFilter 上,看它遍历 securityFilterChains 时第一个匹配的是谁。绝大多数情况是前面还有一个 @Order 更小的适配器,它的 RequestMatcher 匹配所有请求,把 /public/** 给吞了。
定位思路:打开 actuator 的 mappings 端点或者临时打日志,输出所有 DefaultSecurityFilterChain 的 RequestMatcher 和过滤器数量,一目了然。
curl http://localhost:8080/actuator/mappings但更直接的做法是看 FilterChainProxy 的字段 securityFilterChains,在调试器里展开每个链的 matcher。
5.2 PasswordEncoder 报错:DelegatingPasswordEncoder 的默认行为
现象:程序启动正常,一登录就报 There is no PasswordEncoder mapped for the id "null"。
这个报错跟 WebSecurityConfigurerAdapter 的关系在于,你通过适配器的 configure(AuthenticationManagerBuilder) 配置了 userDetailsService,但没给这个构建器指定 passwordEncoder。Spring Security 默认使用 DelegatingPasswordEncoder,它要求数据库里的密码必须带 {id} 前缀,比如 {bcrypt}xxx。如果存的是明文,DelegatingPasswordEncoder 解析不到算法 id,就报这个错。
源码位置在 AbstractDaoAuthenticationProvider 的密码比对逻辑里。解决方式有两种:密码存储时直接写入 {bcrypt} 前缀,或者显式声明 PasswordEncoder Bean:
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }如果你显式声明了 PasswordEncoder Bean,自动配置会把它应用到全局,适配器里的 configure(AuthenticationManagerBuilder) 也可以直接用 auth.userDetailsService(...).passwordEncoder(passwordEncoder)。
5.3 静态资源与 404:WebSecurity.ignoring 和 permitAll 的选择
现象:登录后访问一个不存在的 URL,居然被重定向回登录页。
这其实不是 bug,而是安全过滤器链的正常表现。Spring Security 默认对没有匹配到任何规则的 URL 执行 authenticated,未认证就跳登录。404 页面本身也是一个请求,同样会被拦截。
解决时先想清楚需求。如果只是希望某些路径不鉴权,用 permitAll 更安全,因为请求会经过过滤器链,可以继续记录安全日志或者做后续审计。如果是纯静态资源,追求性能和完全旁路,才考虑 web.ignoring()。
5.4 两个适配器导致认证配置丢失:WebSecurity 构建时的 Configurer 列表
现象:项目里有两个适配器,其中一个适配器里配置了表单登录页和登录成功跳转,但实际访问时发现另一个适配器的行为生效了。
这种情况的根因是请求命中了更靠前的适配器。当你把两个适配器的匹配范围通过 antMatcher 分开后,问题一般会消失。源码层面,问题出在 WebSecurityConfiguration 收集全部 SecurityConfigurer 并按 @Order 排序,两条链各自维护自己的登录配置,不存在“配置覆盖”一说,只有“谁先命中”。
所以排查思路不是找覆盖关系,而是看请求实际命中了哪条链。可以先在每条适配器的 configure(HttpSecurity) 里加上 http.authorizeRequests().anyRequest().authenticated(),然后用不同路径验证匹配结果。
5.5 自定义认证逻辑被覆盖:AuthenticationManager Bean 的暴露与否
现象:写了一个继承适配器的配置类,在 configure(AuthenticationManagerBuilder) 里定义了 authenticationProvider,结果自己写的 Controller 里注入 AuthenticationManager 后,调 authenticate 时抛异常,说不支持当前的 UsernamePasswordAuthenticationToken。
原因是 Controller 里注入的 AuthenticationManager 是全局那个,它不一定包含你在局部适配器里定义的 authenticationProvider。源码里 localConfigureAuthenticationBldr 构建出来的 AuthenticationManager 只在当前 HttpSecurity 的过滤器链内部共享,并不会自动注册为容器 Bean。
解决方式是显式暴露 Bean:
@Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }这样适配器内部构建的认证管理器会被提升为全局 Bean,Controller 里注入的就是同一个实例,自定义 provider 才会生效。
6. 从适配器到 Lambda DSL:这套设计的演进与迁移建议
6.1 适配器模式:一个聪明的历史妥协
看完整套源码,你会承认 WebSecurityConfigurerAdapter 确实是一个聪明的设计。它把 Spring Security 复杂的构建过程包装成简单的继承方式,让大多数开发者只需要关注业务规则,不用理解 FilterChainProxy、SecurityBuilder、SecurityConfigurer 这些底层概念。
但这种设计也有代价。继承意味着单点扩展,一个类里塞太多配置时会变得臃肿。而且适配器的初始化时序是隐式的,getHttp() 里的调用链很长,新手很难理解为什么 configure(AuthenticationManagerBuilder) 在 configure(HttpSecurity) 之前执行。源码里那一大堆 protected 方法,本质上是为了向后兼容而保留的历史包袱。
6.2 SecurityFilterChain Bean 时代的新写法
Spring Security 5.2 之后,官方推荐不再继承适配器,而是直接声明 SecurityFilterChain Bean:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/public/**").permitAll() .anyRequest().authenticated() .and() .formLogin(); return http.build(); }这种写法把 HttpSecurity 当成一个普通的 Bean 方法参数注入,不再有适配器的生命周期负担。多个过滤器链也是同理,按方法名字和 @Order 排列即可。配多套规则时,这样的表达方式比继承多个适配器更直白,也不容易出现 AuthenticationManager 归属不清的问题。
如果项目还在用 Spring Boot 2.7 之前的版本,你依然可以用适配器;如果是新项目,强烈建议直接采用 SecurityFilterChain Bean 的方式写,因为新版本源码已经把大量适配器内部的默认逻辑迁到了 HttpSecurity 的自动配置中,迁移到新写法可以避免未来升级时的磕绊。
6.3 迁移时的实测心得与建议
我自己从适配器迁移到 SecurityFilterChain Bean 时,遇到的主要坑有两个。第一,原来在 configure(AuthenticationManagerBuilder) 里写的 userDetailsService 要改成独立的 Bean,然后用 AuthenticationManagerBuilder 注入或者直接依赖 UserDetailsService Bean。第二,原来通过 authenticationManagerBean() 暴露的 AuthenticationManager 要直接用 AuthenticationConfiguration.getAuthenticationManager() 获取,这个类在自动配置中已经注册。
迁移的思路其实很清晰:把适配器拆成三层,一是 UserDetailsService 和 PasswordEncoder 的 Bean,二是 SecurityFilterChain 方法,三是需要认证管理器的自定义组件单独注入。拆完之后,代码可读性好了很多,排查问题时也更容易定位是哪条链出了问题。
如果你还在面向 Spring Security 的配置苦战,我的建议是从源码出发,先理解适配器三条调用链的时序,再逐步过渡到新的 Lambda DSL 写法。底层原理没变,变的只是表达方式。