在 Spring Security 里摸爬滚打久了,很多人都会遇到一个场景:配置类里写了一大串http.formLogin().and().csrf().disable(),每段配置都像是魔法咒语,能调能用,但说不清它们到底是怎么串起来的。尤其是第一次点进SecurityConfigurer这个接口的源码时,两个泛型参数砸过来,很容易一头雾水。
我当时啃这个类的动机很简单:想搞清楚HttpSecurity和WebSecurity到底谁先谁后、每个Configurer在哪个环节被触发,以及为什么自定义一个配置器就能改变整个过滤器链。这篇文章把我读源码的过程和一些踩过的坑整理出来,适合已经用过 Spring Security 基本功能、想进一步掌握扩展机制的开发者。读完你会明白SecurityConfigurer在架构里处于什么位置,init和configure两个方法各管什么,也就能自己写出整洁的自定义配置器。
1. SecurityConfigurer 在架构里的位置:Builder 与 Configurer 的分工
1.1 不先理解 Builder 模式,读这个类会很吃力
Spring Security 的整体构建逻辑用的是经典的 Builder 模式,但又和常见的UserBuilder、HttpClientBuilder不太一样。普通的 Builder 把所有构建参数和方法都堆在一个类里,而 Spring Security 把“构建对象”和“对构建过程的不同局部定制”拆成了两个角色。
这里要先说清楚两个接口:
SecurityBuilder<T>:负责定义整个安全对象的构建流程,好比是工程总指挥。它声明了build()方法,以及一系列内部管理方法。SecurityConfigurer<T, B extends SecurityBuilder<T>>:负责向 Builder 注入某一块具体的配置逻辑,好比是分包出去的专业施工队,只管自己负责的那部分。
HttpSecurity本身就是一个SecurityBuilder,最终要构建出FilterChainProxy里真正要执行的过滤器链。而FormLoginConfigurer、CsrfConfigurer、ExceptionHandlingConfigurer这些,都是挂在HttpSecurity名下的SecurityConfigurer。每一块功能一个配置器,彼此独立,再通过统一的流程组装起来。
这种设计最直接的好处是:新增一个安全功能,不需要去改HttpSecurity这个庞然大物,只要新写一个Configurer,把它apply到HttpSecurity上即可。框架本身的功能边界因此非常清晰。
1.2 接口定义与两个泛型参数到底表示什么
直接看接口源码:
public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> { void init(B builder) throws Exception; void configure(B builder) throws Exception; default AndSecurityConfigurer<O, B> and() { return new AndSecurityConfigurer<>(this); } }两个泛型参数是理解这个类的第一个门槛。
第一个参数O(我这里按最新的写法叫O,早期版本源码里写的是S,含义相同)代表最终构建出来的安全对象类型。对HttpSecurity来说,这个O就是DefaultSecurityFilterChain,因为HttpSecurity.build()最终构建出的就是一条具体的过滤器链。你可以粗暴理解为:O是“活干完之后交付的那个东西”。
第二个参数B是 Builder 本身的类型,约束当前配置器到底能挂在哪种 Builder 上。比如FormLoginConfigurer的签名是:
public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> {它的泛型约束就是HttpSecurityBuilder<H>,也就是说这个配置器只能用于HttpSecurity这类 Builder,不能随便挂到别的地方。
init(B builder)和configure(B builder)就是配置器被调用的两个时机,后面会专门展开。这里先记住一个结论:泛型参数是配置器的能力边界和交付契约,看任何一个Configurer的源码,先看它的类签名,基本就能判断它是干什么的。
1.3 配置器如何被持有和管理:configurers 列表的运作机制
AbstractConfiguredSecurityBuilder是大部分 Builder 的基类,它内部维护了一个Configurer的集合:
private final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>> configurers = new LinkedHashMap<>();注意这个数据结构是LinkedHashMap,key 是配置器的 Class 类型,value 是配置器实例的列表。用 Class 做 key 的好处是,后面getConfigurer(Class<C> clazz)能非常快速地找到对应类型的配置器。HttpSecurity在执行链式调用时,会不断往这个 Map 里塞配置器。
看一下HttpSecurity里非常典型的getOrApply逻辑:
private <C extends SecurityConfigurer<O, B>> C getOrApply(C configurer) throws Exception { C existing = getConfigurer(configurer.getClass()); if (existing != null) { return existing; } return apply(configurer); }apply最终会调用add:
private <C extends SecurityConfigurer<O, B>> void add(C configurer) { Class<? extends SecurityConfigurer<O, B>> clazz = configurer.getClass(); List<SecurityConfigurer<O, B>> configs = this.configurers.computeIfAbsent(clazz, key -> new ArrayList<>()); configs.add(configurer); }这里有个容易忽略的细节:同一个类型的配置器可以存在多个实例,所以 value 才是List而不是单个对象。绝大多数情况下一个类型只有一个实例,但你要知道这个设计,否则看到getConfigurers返回的是一个List时会觉得奇怪。
整个 Builder 的构建流程,简单说就是“初始化所有配置器 → 让配置器依次执行自己的逻辑 → 最后做后处理”。SecurityConfigurer就是在这个流程里被反复回调的棋子。
2. init 和 configure 两个方法:被源码瞒着的执行时机
2.1 init(B builder) 的真实用途:前置检查和状态准备
很多刚读源码的人把init理解成“初始化配置状态”,这个理解太泛。看实际调用链会更清楚。
AbstractConfiguredSecurityBuilder.doBuild()是核心入口:
private void doBuild() throws Exception { synchronized (this.configurers) { buildState = BuildState.INITIALIZING; beforeInit(); init(); buildState = BuildState.CONFIGURING; beforeConfigure(); configure(); buildState = BuildState.BUILDING; performBuild(); buildState = BuildState.BUILT; } }init()方法会遍历所有配置器并调用其init(builder):
private void init() throws Exception { Collection<SecurityConfigurer<O, B>> configurers = getConfigurers(); for (SecurityConfigurer<O, B> configurer : configurers) { configurer.init((B) this); } }所以init是构建流程中非常靠前的环节,此时 Builder 上各配置器尚未真正装配过滤器。配置器在这个阶段适合做什么呢?我梳理了源码里常见的几类用途:
- 前置校验:检查某些必要配置是否已经存在,不存在就抛出异常。例如
RememberMeConfigurer会检查AuthenticationManager是否可用。 - 给 Builder 注入共享对象:比如某些配置器会在
init阶段把自己需要的AuthenticationManager设置到 Builder 上,方便后续其他配置器使用。 - 修改 Builder 的默认行为:比如通过
builder.getAuthenticationRegistry()做一些提前注册。
HttpSecurity在formLogin()这类方法被调用时,返回的却是配置器本身,本质上就是为了让你在链式调用里直接设置配置器内部的属性,然后这些属性最终会在configure阶段落地。
一个比较容易犯的错:把耗时或依赖外部资源的逻辑放在init里。init发生在所有配置器真正干活之前,如果这里抛异常,整个 Builder 都无法完成构建。所以init里的逻辑应当尽量轻量、可快速失败。
2.2 configure(B builder) 的真实用途:过滤器装配发生在这里
configure是配置器的重头戏,所有真正改变过滤器链的代码都在这阶段执行。同样看doBuild的调用顺序,configure()发生在init()之后:
private void configure() throws Exception { Collection<SecurityConfigurer<O, B>> configurers = getConfigurers(); for (SecurityConfigurer<O, B> configurer : configurers) { configurer.configure((B) this); } }以AbstractHttpConfigurer的子类为例,它们绝大多数都在configure里完成“取得 HttpSecurity → 往过滤器链里塞过滤器”的动作。比如CsrfConfigurer的configure大致是:
@Override public void configure(H http) { CsrfFilter filter = new CsrfFilter(this.csrfTokenRepository); // ... 设置各种属性 ... http.addFilter(filter); }configure方法里能拿到完整的 Builder 引用(在这里是HttpSecurity),所以可以调用它的addFilter、addFilterBefore、addFilterAfter等方法。
要特别注意的是:init和configure的执行顺序对所有配置器来说是固定的吗?其实从doBuild的代码看,init阶段是“所有配置器依次 init”,configure阶段是“所有配置器依次 configure”。也就是说,不是“配置器 A 先 init 再 configure,然后配置器 B 再 init 再 configure”,而是“所有配置器先全部 init,再全部 configure”。这个顺序对理解跨配置器的协作非常重要。
举个例子,ExceptionHandlingConfigurer在init阶段会先检查有没有显式配置AuthenticationEntryPoint,没有的话就放一个默认的Http403ForbiddenEntryPoint。这样后续其他配置器在configure阶段需要用到ExceptionHandlingConfigurer的配置结果时,它已经准备好了。如果顺序反过来,就会出现依赖项尚未初始化的问题。
2.3 getConfigurer() 和 removeConfigurer() 的源码细节与 NPE 陷阱
AbstractConfiguredSecurityBuilder提供了几个查询配置器的方法,源码非常简单,但使用上坑不少:
public <C extends SecurityConfigurer<O, B>> C getConfigurer(Class<C> clazz) { List<SecurityConfigurer<O, B>> configs = this.configurers.get(clazz); if (configs == null) { return null; } if (configs.size() != 1) { throw new IllegalStateException("..."); } return (C) configs.get(0); }如果同类型配置器有多个实例,这个方法会直接抛IllegalStateException,而不是返回一个列表。要获取多个实例就得用getConfigurers(Class<C> clazz),它返回List<C>。
我自己踩过一个特别基础的坑:在自定义Configurer的configure方法里,试图通过http.getConfigurer(FormLoginConfigurer.class)来调整表单登录配置,结果居然拿到了null。排查了很久才发现,我的配置器是在formLogin()之前被apply到HttpSecurity上的,执行configure时FormLoginConfigurer还没被加入configurers集合。
这提醒我们:配置器之间的依赖必须考虑添加顺序。如果你非要拿到某个配置器,最好在目标配置器确实被注册之后再操作,或者干脆在configure阶段不要过度依赖兄弟配置器的存在,而是通过HttpSecurity暴露的其他方法完成协作。
removeConfigurer的方法名很直白,就是把指定类型的配置器从集合里移除。某些场景下有用,比如你想完全替换默认配置器。但一般情况下不建议随便用,移除之后框架可能会因为缺少某个默认行为而报出比较隐蔽的错误。
2.4 为什么配多个 Configurer 并不冲突:SharedObject 机制的补充说明
前面聊到配置器之间需要协作,但很多协作并不是直接互相拿Configurer,而是通过HttpSecurity上共享的SharedObject(即一系列注册在不同 key 下的共享数据)完成。例如AuthenticationManager就是通过setSharedObject注册、getSharedObject获取的。
@Override public <C> C getSharedObject(Class<C> sharedType) { return this.sharedObjects.get(sharedType); }SecurityConfigurer只是配置器的外壳,真正的数据流转靠的是 SharedObject。你在源码里看到某个Configurer从builder.getSharedObject(AuthenticationManager.class)拿东西,这不奇怪。理解了这一点,你就不会以为配置器之间的所有关系都能通过getConfigurer拿到。
3. 从 HttpSecurity.formLogin() 向源码深处走:Configurer 如何变成过滤器
3.1 formLogin() 的返回值为什么是一个 Configurer
先看一个特别容易让人困惑的 API:http.formLogin()。按照传统 Java 链式调用的习惯,你可能以为它返回的是HttpSecurity,但实际上它返回的是FormLoginConfigurer。
HttpSecurity里的写法是:
public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception { return getOrApply(new FormLoginConfigurer<>()); }getOrApply前面已经看过:如果已经存在同类型配置器,就返回已有的;否则apply加入后再返回。这个设计有一个非常核心的好处:允许你分段配置同一个配置器。
http.formLogin() .loginPage("/custom-login") .and() .formLogin() .defaultSuccessUrl("/home");第二次调用的formLogin()会拿到同一个FormLoginConfigurer实例,所以两步配置会叠加到同一个配置器上。如果每次调用都 new 一个新的,那第二次调用就把第一次的配置给覆盖了。这个“获取或创建”的语义是整个链式配置的基础。
3.2 从 AbstractAuthenticationFilterConfigurer 看过滤器装配的全过程
FormLoginConfigurer的真正核心逻辑大部分继承自AbstractAuthenticationFilterConfigurer。这个抽象类的configure方法是理解表单登录的钥匙:
@Override public void configure(H http) throws Exception { AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); // ... 构建认证过滤器 ... this.authFilter.setAuthenticationManager(authenticationManager); // ... 设置认证入口、成功处理器、失败处理器等 ... // 关键一行:把过滤器注册到 HttpSecurity http.addFilter(this.authFilter); }拆开来看它做了几件事:
- 从 SharedObject 里取
AuthenticationManager,这是认证的底层核心。 - 把用户配置的登录页、成功跳转地址、失败跳转地址等属性都灌进
UsernamePasswordAuthenticationFilter。 - 通过
http.addFilter()把这个过滤器挂到过滤器链上。
这段逻辑完美诠释了SecurityConfigurer的定位:它本身不直接做事,而是把“做什么事”所需要的材料和步骤组织好,最终把过滤器交给 Builder。
很多人一开始不知道formLogin()里配置的loginPage("/login")到底存在哪、什么时候生效,读了AbstractAuthenticationFilterConfigurer的源码就会明白,它被存在FormLoginConfigurer实例的字段里,直到configure阶段才被设置到过滤器上。
3.3 内置 Configurer 全家福:它们各自负责哪一段链
我整理了一个内置配置器的对照表,方便快速定位自己想看实现的功能:
| 配置器类 | 对应的入口 API | 主要作用 |
|---|---|---|
FormLoginConfigurer | http.formLogin() | 装配用户名密码登录过滤器 |
CsrfConfigurer | http.csrf() | 注入 CSRF 过滤器,校验 Token |
ExceptionHandlingConfigurer | http.exceptionHandling() | 配置认证入口与访问拒绝处理 |
RememberMeConfigurer | http.rememberMe() | 注入记住我过滤器 |
SessionManagementConfigurer | http.sessionManagement() | 配置会话固定保护、并发会话控制 |
LogoutConfigurer | http.logout() | 装配登出过滤器 |
AnonymousConfigurer | http.anonymous() | 配置匿名认证过滤器 |
ServletApiConfigurer | 自动配置 | 将 Servlet 相关对象注入安全上下文 |
CorsConfigurer | http.cors() | 加载 CORS 配置源并注入处理器 |
每个配置器内部结构不同,但落到最后几乎都是“构造一个过滤器 +http.addFilter或修改已有过滤器的行为”。如果你打算自定义功能,参考这些内置类是最快的路径,尤其是AbstractHttpConfigurer的子类,它们骨架极其整齐。
3.4 AndSecurityConfigurer 与 and() 方法的障眼法
还有一个值得留意的小细节:SecurityConfigurer接口默认给了and()方法,返回的是AndSecurityConfigurer。这个类设计得非常取巧:
public final class AndSecurityConfigurer<O, B extends SecurityBuilder<O>> extends AbstractSecurityConfigurer<O, B> { private final SecurityConfigurer<O, B> configurer; @Override public void configure(B builder) { // 实际啥也不干 } @Override public void init(B builder) { // 实际啥也不干 } }AndSecurityConfigurer本身不干任何事,它存在的唯一意义就是让你能这样写:
http.formLogin() .loginPage("/login") .and() .csrf().disable();and()把当前配置流程切回HttpSecurity,不是真正“合并”配置器。它的源码体现了链式 API 里非常实用的一种技巧:用一个空壳配置器来改变链式调用的上下文,而不是真的做复杂的状态转换。
4. 源码级自定义 Configurer 实战:给表单登录插一个图形验证码过滤器
4.1 扩展思路:照着 AbstractAuthenticationFilterConfigurer 的骨架搭
光读源码不动手,理解永远停留在表面。我自己练手时做的最多的功能是给表单登录加一个图形验证码校验。这个场景特别适合拿来演示SecurityConfigurer的用法,因为它在不破坏框架原有流程的前提下,往过滤器链里插入一个自定义过滤器。
我的设计思路是:写一个CaptchaConfigurer,它负责创建CaptchaFilter,并且用addFilterBefore把验证码过滤器放到UsernamePasswordAuthenticationFilter前面,确保验证码在校验用户名密码之前先通过。
为什么继承SecurityConfigurerAdapter?因为它提供了init(B builder)和configure(B builder)的空实现,我只重写需要的方法即可。
public class CaptchaConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Override public void configure(HttpSecurity http) { CaptchaFilter captchaFilter = new CaptchaFilter(); captchaFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); // 把我们的过滤器插在 UsernamePasswordAuthenticationFilter 之前 http.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class); } }关键点是http.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)。这个 API 意味着我们不需要自己去维护过滤器的排序,只要指定相对位置,Spring Security 会自动完成排序。
4.2 过滤器内部如何拿到请求上下文
剩下的问题就是CaptchaFilter该怎么获取请求、响应、以及验证码是否正确。
自定义过滤器最常见的路子是继承OncePerRequestFilter,这样能保证同一个请求只执行一次过滤逻辑。我在练手项目里是这么写的:
public class CaptchaFilter extends OncePerRequestFilter { private AuthenticationManager authenticationManager; public void setAuthenticationManager(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 只拦截登录请求 if (needValidate(request)) { String inputCaptcha = request.getParameter("captcha"); String sessionCaptcha = (String) request.getSession().getAttribute("captcha"); if (!validate(inputCaptcha, sessionCaptcha)) { // 校验失败:没有认证入口就返回错误 response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } } filterChain.doFilter(request, response); } private boolean needValidate(HttpServletRequest request) { return "/login".equals(request.getRequestURI()) && "POST".equalsIgnoreCase(request.getMethod()); } private boolean validate(String input, String session) { return input != null && input.equalsIgnoreCase(session); } }这个过滤器本身很简单,但它体现了SecurityConfigurer的价值:当你需要引入一个过滤器时,不是直接在某个WebSecurityConfigurerAdapter里@Bean方法里 new 一个 Filter 然后添加到 http 上,而是封装成配置器,让过滤器创建和装配逻辑待在它该在的位置上。
4.3 怎么把自定义 Configurer 接入配置
接入方式非常清爽,在扩展WebSecurityConfigurerAdapter的配置类里:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .apply(new CaptchaConfigurer()) .and() .formLogin() .permitAll(); } }apply返回的是HttpSecurity是错的,准确说apply(C configurer)返回的是配置器实例。所以我这里的链式写法里:apply(new CaptchaConfigurer())返回CaptchaConfigurer,再调用.and()切回HttpSecurity,之后继续配formLogin()。
如果想给CaptchaConfigurer加参数(比如要不要启用、验证码从哪个参数读取),完全可以在CaptchaConfigurer内部增加字段和 setter 方法,外部配置时链式调用即可:
http.apply(new CaptchaConfigurer()) .captchaParam("captcha") .and() .formLogin();这种封装让配置信息不会散落在各处的 Bean 定义里,而是集中在同一个配置器内部。
4.4 为什么这种方式比直接 new Filter + addFilterBefore 更值得推荐
有的读者可能会问:传一个Filter的@Bean,再用http.addFilterBefore(captchaFilter(), UsernamePasswordAuthenticationFilter.class),不是更简单吗?
区别在于配置行为的聚合和复用。直接addFilterBefore的方式每次都要重复写“过滤器实例化 + 过滤器装配”,而且过滤器的参数只能通过 setter 在 Bean 里设置,过滤器和它的配置参数是分离的。用SecurityConfigurer则把这两步内聚成一块可组合的逻辑单元。
另外,用配置器更方便做二次定制。假设项目里有多个安全过滤链需要用验证码,但不同链的验证码参数不同,直接用new CaptchaConfigurer().captchaParam("captcha")就能复用。而如果每个链都手工 addFilter,改动时容易漏改一处。
最后是源码阅读层面:你越是理解自定义配置器的模式,就越容易看懂 Spring Security 内置的那些Configurer。它们大多逃不出“构造过滤器 → 设置参数 → addFilter”这三板斧。
4.5 坑与陷阱:postProcess、顺序和异常
自定义配置器时最容易踩的坑有三个。
第一,configure里使用了http.getSharedObject(AuthenticationManager.class)但返回 null。这个通常是因为你没在容器里暴露AuthenticationManager,或者你的Configurer被apply到HttpSecurity的时机太早。解决办法是在配置类里定义一个AuthenticationManager的 Bean(从AuthenticationConfiguration借用),或者让Configurer的初始化逻辑延后到需要时再取。
第二,过滤器的OncePerRequestFilter默认会对所有请求执行过滤逻辑。如果你没在过滤器里排除登录页、静态资源、验证码图片生成接口,会把它们也拦下来。所以必须把“是否需要校验”的判定写清楚。我在needValidate里判断了 URI 和请求方法,这是一个基本但有效的过滤条件。
第三,继承SecurityConfigurerAdapter时泛型参数别写错。第一个参数是最终安全对象类型,对HttpSecurity场景就是DefaultSecurityFilterChain。写错了虽然也能编译,但 IDE 提示的类型推断会乱,后续维护很痛苦。
5. 源码阅读三板斧:断点位置、调用链追踪与学习路径
5.1 最值得打断点的四个位置
读SecurityConfigurer相关源码,不建议从头到尾一行一行读,建议带着“我到底想搞懂什么”去打断点调试。我自己常用下面四个位置:
第一个是AbstractConfiguredSecurityBuilder.doBuild()门口。在这个断点能看到整个构建流程的状态机流转:INITIALIZING→CONFIGURING→BUILDING→BUILT。你会亲眼看到 init 和 configure 的执行顺序。
第二个是HttpSecurity.formLogin()方法里。这里能看到getOrApply返回的到底是新建的配置器还是已存在的配置器。来回调用formLogin()多次时,这个断点能验证前面的“同一个实例”结论。
第三个是自定义CaptchaConfigurer.configure()方法里。在这里看HttpSecurity当前已经有多少个过滤器,通过http.getFilters()可以直观看到过滤器链条的前后顺序。
第四个是FilterChainProxy.doFilter里。等应用实际跑起来后,从浏览器发起请求,在这里能看到过滤器链被最终执行的样子。这能帮你把“配置阶段”和“运行阶段”对上号。
5.2 用调用层次快速定位实现类
一个非常实用的 IDE 技巧:在SecurityConfigurer接口的方法名上右键 → “Find Usages” 或 “Call Hierarchy”,能看到所有实现类的重写。比如你想知道configure(B builder)到底有多少子类实现了,这里会列出几十个类,非常符合预期。
当你想迅速搞清楚某个入口 API(比如http.csrf())到底构造了什么配置器、它又是怎么把过滤器装进去的,可以直接看HttpSecurity对应方法:
public CsrfConfigurer<HttpSecurity> csrf() throws Exception { ApplicationContext context = getContext(); return getOrApply(new CsrfConfigurer<>(context)); }从这里点进CsrfConfigurer的configure,马上就能看到内部的CsrfFilter创建与注册逻辑。
5.3 一条推荐的源码研读路径
我给身边同事的建议是不要从SecurityConfigurer直接开始,那条路线容易看到一个接口后就开始迷路。按这个顺序走会顺很多:
- 先看配置类入口(如
WebSecurityConfigurerAdapter子类的configure(HttpSecurity http))。 - 跟进
HttpSecurity.formLogin(),理解getOrApply与配置器的创建。 - 进到
FormLoginConfigurer/AbstractAuthenticationFilterConfigurer,找到configure方法。 - 绕回
AbstractConfiguredSecurityBuilder.doBuild(),理解 init 与 configure 的驱动时机。 - 再看
SecurityConfigurer接口本身,此时你会发现它异常简洁,核心全在子类里。
这样读的好处是,你有多个具体的类作为“锚点”,不会在读抽象方法时失去方向。每读一个具体配置器,都可以反推出通用规则。
5.4 顺着 Spring Security 版本演进读源码可获得额外视野
每次 Spring Security 大版本升级,SecurityConfigurer家族的实现细节都会有些调整。比如早期版本中WebSecurityConfigurerAdapter里的init会把AuthenticationManager配置好,新版本中则越来越倾向于把AuthenticationManager的创建交还容器管理。
我刚入行时读的是旧版源码,后来升到新版,最大的感触是:框架正在把更多“自动配置”逻辑往AuthenticationConfiguration和HttpSecurityConfiguration等地方迁移,SecurityConfigurer本身承担的职责越来越聚焦于单点过滤器的装配。这个趋势其实也反馈到我们的使用方式上:自定义配置器时应尽量小而专,避免在一个配置器里塞进太多跨功能逻辑。
6. 写代码之外的一点体会
读SecurityConfigurer源码是我个人认为理解 Spring Security 的“分水岭”。跨过这道坎后,再看WebSecurityConfigurerAdapter里的那一大串链式调用,不再是背 API,而是看穿了每个方法背后“获取或创建配置器 + 设置内部属性 + 最终在 configure 装配过滤器”的套路。
我自己的习惯是:每想扩展一个功能前,先找一个最相似的内置Configurer作为参考。比如做多因素认证时参考FormLoginConfigurer,做自定义访问决策时参考ExpressionUrlAuthorizationConfigurer。照着源码框架搭出来的代码,几乎不用太担心生命周期与过滤器顺序的问题。
最后分享一个刚入门时常被忽视的小技巧:调试时多看HttpSecurity的字段面板。在 IDE 的断点处展开configurers这个 Map,你就能看到当前有多少个配置器、每个配置器的状态字段是什么。比对着源码猜来猜去方便太多。祝你读源码顺利,遇到问题欢迎在评论区聊聊你调试到的奇怪行为。