一、Spring Security 底层架构与核心类分析
要理解动态配置方式的价值,首先需要深入 Spring Security 的底层实现,弄清楚它是如何在启动时将各种安全组件组装成一条完整的过滤链(Filter Chain)的。本节将从底层核心类、组件交互流程等方面展开剖析。
1.1 FilterChainProxy
与 SecurityFilterChain
的角色
FilterChainProxy
Spring Security 的拦截机制基于 Servlet 过滤器(Filter)实现。启动时,Spring Boot 会自动注册一个FilterChainProxy
,它本质是一个特殊的javax.servlet.Filter
,但内部维护着多个SecurityFilterChain
。- 当一个 HTTP 请求到达时,
FilterChainProxy.doFilter(request, response, chain)
会遍历容器中所有的SecurityFilterChain
,检查每个链的RequestMatcher
是否与当前请求匹配。 - 如果匹配,就将该
SecurityFilterChain
中的所有子Filter
(按预定顺序)串联成一条链,交给 Servlet 容器继续执行,完成一整套安全逻辑。 - 如果不匹配,则跳过该链,继续检查下一个
SecurityFilterChain
。 - 只要容器里存在一个或多个
SecurityFilterChain
Bean,FilterChainProxy
就会自动为所有符合匹配规则的请求应用对应的过滤器链。
- 当一个 HTTP 请求到达时,
SecurityFilterChain
每个SecurityFilterChain
对象由两部分组成:RequestMatcher
:定义该链所应用的请求范围(常见写法是匹配所有 URL,即anyRequest()
)。List<Filter>
:一组按照先后顺序排列的javax.servlet.Filter
实例,比如:SecurityContextPersistenceFilter
(加载和清理 SecurityContext)CorsFilter
(处理跨域预检)JwtTokenFilter
(自定义的 Token 校验)UsernamePasswordAuthenticationFilter
(表单登录认证)ExceptionTranslationFilter
(捕获异常并委派给 EntryPoint/AccessDeniedHandler)FilterSecurityInterceptor
(执行 URL-权限决策)
只有当某个SecurityFilterChain
的RequestMatcher
与当前请求匹配时,才会执行它内含的Filter
列表,从而完整地处理认证、授权、异常转换、请求放行等流程。
1.2 底层配置接口:SecurityBuilder
、SecurityConfigurer
、SecurityConfigurerAdapter
在 Spring Security 6.x 中,官方已废弃了之前的 WebSecurityConfigurerAdapter
,转而引入更灵活的 “Builder + Configurer” 设计。核心思想是将每一块安全功能封装在一个独立的配置器里,然后由 Spring 自动组装成最终的过滤链。下面逐一介绍几个关键接口/抽象类。
1.2.1 SecurityBuilder
public interface SecurityBuilder<O> {
O build() throws Exception;
<C> C getSharedObject(Class<C> sharedType);
<C> void setSharedObject(Class<C> sharedType, C object);
AuthenticationConfiguration getAuthenticationConfiguration();
}
- 作用:
SecurityBuilder
表示一个安全构建器,它内部维护了:- 一组“共享对象”(Shared Objects),比如
ApplicationContext
、AuthenticationManagerBuilder
、ObjectPostProcessor
等; - 以及若干注册在对应构建器上的子配置器(
SecurityConfigurer
)。
- 一组“共享对象”(Shared Objects),比如
- 典型实现:
HttpSecurity
实现了SecurityBuilder<SecurityFilterChain>
,它会在内部收集所有向其注册的配置器,并在调用build()
时把这些配置器应用到自身,最终生成一个SecurityFilterChain
。
1.2.2 SecurityConfigurer
public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> {
void init(B builder) throws Exception;
void configure(B builder) throws Exception;
}
- 作用:将一种安全功能“插件式”地整合到一个
SecurityBuilder
(如HttpSecurity
)中。init(B builder)
:在真正构建流程中“注册前”阶段,可以注册某些 BeanDefinition、准备共享对象等。configure(B builder)
:在“注册后”阶段,向构建器(即HttpSecurity
)中注入具体的 Filter、AuthenticationProvider、授权策略等。
1.2.3 SecurityConfigurerAdapter
public abstract class SecurityConfigurerAdapter<O, B extends SecurityBuilder<O>>
implements SecurityConfigurer<O, B> {
@Autowired
private ObjectPostProcessor<Object> objectPostProcessor;
public void init(B builder) throws Exception { /* 默认空实现 */ }
public void configure(B builder) throws Exception { /* 默认空实现 */ }
protected <T> T postProcess(T object) {
return objectPostProcessor.postProcess(object);
}
protected <C> C getBuilder(Class<C> cls) {
return (C) builder.sharedObjects.get(cls);
}
}
- 作用:为用户自定义配置器提供便利的抽象基类,子类可以只重写
init()
或configure()
。 - 在
configure(B builder)
中,往往通过调用builder
提供的各类 DSL 方法(如addFilterBefore()
、authenticationProvider()
、authorizeHttpRequests()
等)来将自定义逻辑注册到HttpSecurity
中。
1.3 HttpSecurity
的构建流程
了解了 SecurityBuilder
与 SecurityConfigurer
的概念后,我们来看看在 Spring Boot 启动时,执行一个 @Bean SecurityFilterChain securityFilterChain(HttpSecurity http)
的底层步骤(简化版):
- 创建
HttpSecurity
实例- Spring Security 通过内部联系人(如
EnableWebSecurityImportSelector
)在启动时创建HttpSecurity
对象。此时它内部的sharedObjects
Map 中已经放入了核心 Bean,比如ApplicationContext
、ObjectPostProcessor
、AuthenticationConfiguration
等。
- Spring Security 通过内部联系人(如
- 收集所有
SecurityConfigurer<SecurityFilterChain, HttpSecurity>
Bean- Spring 容器扫描到各个实现了
SecurityConfigurer<SecurityFilterChain, HttpSecurity>
的 Bean,包含官方内置的配置器(如CsrfConfigurer
、FormLoginConfigurer
等)以及用户自定义的配置器(如OnceFilterConfigurer
、ExceptionHandlingConfigurer
等)。 - 当调用
securityFilterChain(HttpSecurity http, List<SecurityConfigurer> securityConfigurers)
时,这个List<SecurityConfigurer>
参数会自动注入容器中所有同类型 Bean。
- Spring 容器扫描到各个实现了
- 依次调用各配置器的
init(http)
- Spring 首先遍历
securityConfigurers
列表,对每个配置器执行init(http)
。这一步通常用于让配置器“预先准备共享对象”或“创建尚未完成的 BeanDefinition”。
- Spring 首先遍历
- 依次调用各配置器的
configure(http)
- 当所有配置器的
init()
执行完毕后,Spring 会再次遍历securityConfigurers
,对每个配置器执行configure(http)
,让它们往HttpSecurity
中写入各自的安全逻辑。 - 具体来说,
configure()
中常见的操作有:http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class)
http.authenticationProvider(customProvider)
http.csrf().disable()
http.authorizeHttpRequests().antMatchers("/public/**").permitAll().anyRequest().authenticated()
http.logout().logoutUrl("/logout").logoutSuccessHandler(...)
http.exceptionHandling().authenticationEntryPoint(...).accessDeniedHandler(...)
http.cors().configurationSource(source)
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
- 此时,这一步骤只是把“配置元数据”存入
HttpSecurity
内部的数据结构,还未真正实例化过滤链。
- 当所有配置器的
- 调用
http.build()
:生成最终的SecurityFilterChain
- 在所有配置器都完成
configure()
后,Spring 执行http.build()
。此时HttpSecurity
会根据之前存储的各种配置元数据,进行以下操作:- 汇总所有被注册的 Filter(按先后顺序,以及显式指定的
addFilterBefore
/addFilterAfter
插入点),组织成一个LinkedList<Filter>
。 - 根据配置的
RequestMatcher
(通常是所有请求),生成一个SecurityFilterChainImpl
,将上述 Filter 列表挂到该链上。 - 将生成的
SecurityFilterChainImpl
注册为一个 Spring Bean。
- 汇总所有被注册的 Filter(按先后顺序,以及显式指定的
- 由于
FilterChainProxy
会扫描容器中所有SecurityFilterChain
Bean,一旦发现这个新注册的 Bean,就会在真正处理 HTTP 请求时根据匹配规则执行对应的过滤链。
- 在所有配置器都完成
综上,Spring Security 6.x 在启动时执行的伪流程如下:
text复制编辑HttpSecurity http = new HttpSecurity(sharedObjects...);
List<SecurityConfigurer> configurers = container.getBeansOfType(SecurityConfigurer.class);
for (SecurityConfigurer cfg : configurers) {
cfg.init(http);
}
for (SecurityConfigurer cfg : configurers) {
cfg.configure(http);
}
SecurityFilterChain chain = http.build();
container.registerBean(chain);
正因为底层采用“先调用 init()
→ 再调用 configure()
→ 最后 build()
”的机制,Spring Security 实现了将各安全功能以插件形式动态组合到最终的过滤链中。理解这一点后,便能领会模块化拆分配置器的价值。
二、为何要基于多个 SecurityConfigurer
做动态配置
了解了底层“Builder + Configurer”的工作机制后,就很容易引出“为什么要基于多个独立的 SecurityConfigurer
实现动态组合”的思路。下面从几个方面详细阐述这样做的好处。
2.1 避免单一配置类臃肿、职责分散
- 问题场景
在 Spring Security 5.x 及以前,我们常见的做法是继承WebSecurityConfigurerAdapter
,然后重写configure(HttpSecurity http)
、configure(AuthenticationManagerBuilder auth)
两个方法。随着功能不断叠加,一个configure(HttpSecurity)
方法可能包含:- CSRF 禁用或配置
- CORS 配置
- URL 授权规则(
.authorizeHttpRequests()
) - 登录方式(
.formLogin()
、.oauth2Login()
、.httpBasic()
) - 登出设置(
.logout()
) - 异常处理(
.exceptionHandling()
) - 自定义 JWT 过滤器的插入(
.addFilterBefore()
) - Session 管理策略(
.sessionManagement()
) - 静态资源或 Swagger 放行(
.antMatchers("/css/**", "/swagger-ui/**").permitAll()
) - 甚至邮件验证码、短信登录等扩展逻辑……
最终导致这个方法膨胀成数百行,难以维护、难以协作。
- 模块化拆分
将各个功能点拆分成多个SecurityConfigurerAdapter
:OnceFilterConfigurer
:仅负责注册JwtTokenFilter
;ExceptionHandlingConfigurer
:仅负责设置authenticationEntryPoint
和accessDeniedHandler
;CorsConfigurer
:仅负责读取配置文件并构造CorsConfigurationSource
,然后调用http.cors().configurationSource(source)
;LogoutConfigurer
:仅负责将LogoutFilter
注册到链中,以及设置LogoutSuccessHandler
;ExpressionUrlAuthorizationConfigurer
:仅负责.authorizeHttpRequests().antMatchers(...).permitAll()...
;SessionManagementConfigurer
:仅负责.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;- 以及:
WebSecurityCustomizer
负责忽略静态资源、白名单路径,使得这些 URL 直接跳过所有安全过滤; - 其他必要组件如
PasswordEncoder
、UserDetailsService
也各自独立成 Bean。
2.2 支持“零代码”新增/替换功能
@ConditionalOnMissingBean
实现覆盖
如果我们在每个关键 Bean(如PasswordEncoder
、UserDetailsService
、各个子SecurityConfigurer
)上添加@ConditionalOnMissingBean
注解,就能让业务方在项目中自行覆盖默认实现: java复制编辑@Bean @ConditionalOnMissingBean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
如果业务方需要改用 Argon2,只需在项目中再注册一个Argon2PasswordEncoder
Bean(例如通过注解@Bean public PasswordEncoder passwordEncoder() { return new Argon2PasswordEncoder(); }
),Spring 容器会自动跳过默认的BCryptPasswordEncoder
。- “插件式”加载自定义配置器
任何实现了SecurityConfigurer<SecurityFilterChain, HttpSecurity>
的 Bean,只要被 Spring 扫描,就会自动注入到List<SecurityConfigurer>
。主配置类不用修改,就能“零侵入”地将新功能插入安全链。例如:- 业务方想新增“短信验证码登录”功能,只要新建一个
SmsLoginConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>
,在其中注册SmsAuthenticationFilter
与SmsAuthenticationProvider
,并将该类标注为@Component
或放在@Configuration
中,Spring 就会自动纳入配置链中。 - 如果后续要切换 JWT 到 OAuth2 资源服务器模式,只需要新增一个
OAuth2ResourceServerConfigurer
Bean,Spring 会在启动时把它与其他配置器统一apply()
到HttpSecurity
,不需要动主配置类一行代码。
- 业务方想新增“短信验证码登录”功能,只要新建一个
2.3 灵活控制过滤器顺序与依赖关系
- Filter 顺序的重要性
在安全过滤链里,不同 Filter 的执行顺序决定了请求的处理流程:CorsFilter
必须最先运行,否则跨域预检请求无法顺利通过;- 自定义的
JwtTokenFilter
常常需要在UsernamePasswordAuthenticationFilter
之前运行,以便在表单登录或 BasicAuth 之前先尝试 Token 验证; ExceptionTranslationFilter
必须包裹住所有可能抛出AuthenticationException
或AccessDeniedException
的 Filter,用于捕获并委派给统一的AuthenticationEntryPoint
、AccessDeniedHandler
;FilterSecurityInterceptor
是最后一道关卡,根据authorizeHttpRequests()
定义的 URL-权限映射来执行决策,拒绝后抛出AccessDeniedException
。
- 通过
addFilterBefore
/addFilterAfter
指定插入点
当我们在某个子SecurityConfigurerAdapter
的configure(HttpSecurity http)
中调用: java复制编辑http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
就能让JwtTokenFilter
始终在UsernamePasswordAuthenticationFilter
之前执行,而不依赖于容器加载顺序。这种显式指定插入点的方法,保证了多个模块拆分后依旧能正确定位 Filter 顺序。 - 使用
@Order
控制配置器加载顺序
如果某些SecurityConfigurer
之间有依赖,比如 “异常处理” 希望包裹住所有会抛异常的 Filter,需要优先注册,也可以给它加上: java复制编辑@Order(Ordered.HIGHEST_PRECEDENCE) @Component public class ExceptionHandlingConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Override public void configure(HttpSecurity http) throws Exception { http.exceptionHandling()...; } }
这样确保ExceptionHandlingConfigurer
的init()
与configure()
最先运行。其他配置器再按默认加载顺序依次加入。通过显式指定插入点与@Order
,我们能灵活地管理各子模块在过滤链中的先后关系。
三、示例:从底层逻辑到动态配置实战
为了将前面分析的概念落到实处,下面结合一个完整示例,演示如何从底层原理出发,逐步实现“基于多个 SecurityConfigurer
的动态配置”。
3.1 项目需求概览
假设我们要实现一个 JWT 无状态认证的后端服务,需求点如下:
- JWT 无状态认证
所有接口都使用 JWT 作为鉴权方式,不依赖 HttpSession。 - 自定义异常返回格式
认证失败或无权限时,统一返回{ "success": false, "code": 401/403, "message": "具体错误描述" }
的 JSON。 - 自定义登出逻辑
当客户端调用/auth/logout
时,需要执行一些资源清理(比如将 Token 加黑名单),并返回特定格式的 JSON。 - URL 白名单
某些 URL(如/auth/login
、/auth/register
、Swagger 文档、静态资源)无需认证访问。 - 跨域动态配置
不同环境(开发/测试/生产)可能使用不同的前端域名,需要在配置文件中灵活指定。 - 可扩展性
将来可能需要新增短信登录、OAuth2、社交登录等功能,希望新增时无需改主配置类,只需添加新的配置器即可。
3.2 构建各功能模块的 SecurityConfigurerAdapter
根据需求,将各功能拆分为独立模块,并实现对应的 SecurityConfigurerAdapter
或相关 Bean。
3.2.1 静态资源与白名单忽略:WebSecurityCustomizer
@Bean
@ConditionalOnMissingBean
public WebSecurityCustomizer webSecurityCustomizer(
SecurityConfigParams securityConfigParams,
@Value("${swagger.start}") Boolean swaggerStart,
@Value("${spring.datasource.druid.stat-view-servlet.enabled}") Boolean druidViewStart,
@Value("${spring.profiles.active}") String active) {
return web -> {
// 放行 application.yml 中配置的开放 URL
web.ignoring().antMatchers(securityConfigParams.getOpenUrl());
// 如果开启 Swagger,额外放行 swagger 相关资源
if (swaggerStart) {
web.ignoring().antMatchers(securityConfigParams.getSwaggerRes());
}
// 如果开启 Druid,额外放行 druid 监控页面
if (druidViewStart) {
web.ignoring().antMatchers(securityConfigParams.getDruidRes());
}
// 如果项目里包含 Flowable,放行 Flowable 资源
if (active.contains("flowable")) {
web.ignoring().antMatchers(securityConfigParams.getFlowableRes());
}
};
}
- 说明:
WebSecurityCustomizer
在 Spring Security 启动时最早生效,用于告诉容器“对某些 URL 不要进入任何安全过滤链”。直接跳过所有 Filter,因此访问这些路径时既无认证也无授权逻辑。
3.2.2 跨域配置:CorsConfigurer
@Bean
@ConditionalOnMissingBean
public CorsConfigurer<HttpSecurity> corsConfigurer(
@Value("${cors.start}") Boolean corsStart,
@Value("${cors.start.filter}") Boolean corsStartFilter,
@Value("${cors.origin}") String[] corsOrigin,
@Value("${cors.port}") String corsPort,
@Value("${cors.header}") String corsHeader,
@Value("${cors.methods}") String corsMethods,
@Value("${cors.file.uploading}") Boolean corsFileUploading) {
CorsConfigurer<HttpSecurity> corsConfigurer = new CorsConfigurer<>();
if (!corsStart) {
// 未开启 CORS,直接返回空配置器
return corsConfigurer;
}
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration configuration = new CorsConfiguration();
if (corsStartFilter) {
// 细粒度模式:按允许域名、方法、Header 等逐一添加
for (String origin : corsOrigin) {
configuration.addAllowedOrigin(origin);
}
configuration.addAllowedOrigin("http://127.0.0.1:" + corsPort);
configuration.addAllowedOrigin("http://localhost:" + corsPort);
if ("*".equals(corsHeader)) {
configuration.addAllowedOriginPattern(corsHeader);
}
configuration.addAllowedMethod(corsMethods);
configuration.setAllowCredentials(corsFileUploading);
source.registerCorsConfiguration("/**", configuration);
} else {
// 全放行模式:允许任意源、Header、方法
configuration.addAllowedOriginPattern("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.setAllowCredentials(true);
source.registerCorsConfiguration("/**", configuration);
}
corsConfigurer.configurationSource(source);
return corsConfigurer;
}
- 原理:Spring Security 在构建过滤链时,会检测是否存在
CorsConfigurationSource
,若存在则自动插入对应的CorsFilter
,确保跨域请求能被正确处理。
3.2.3 JWT 过滤器注册:OnceFilterConfigurer
public class OnceFilterConfigurer<T extends HttpSecurityBuilder<T>>
extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, T> {
private final ApplicationContext context;
public OnceFilterConfigurer(ApplicationContext context) {
this.context = context;
}
/**
* 在过滤链中将 JwtTokenFilter 插入到 UsernamePasswordAuthenticationFilter 之前
*/
public void tokenFilter() {
JwtTokenFilter jwtFilter = context.getBean(JwtTokenFilter.class);
this.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
public void configure(T http) throws Exception {
// 如果需要额外统一操作,也可以在这里调用 tokenFilter()
}
}
- 使用方式:
@Bean
@ConditionalOnMissingBean
public OnceFilterConfigurer<HttpSecurity> OncePerRequestConfigurer(ApplicationContext applicationContext) {
OnceFilterConfigurer<HttpSecurity> cfg = new OnceFilterConfigurer<>(applicationContext);
cfg.tokenFilter(); // 将 JwtTokenFilter 注册到过滤链中
return cfg;
}
- 说明:
JwtTokenFilter
的职责是从请求头中获取 JWT,校验并将认证信息填充到SecurityContextHolder
。通过addFilterBefore
明确指定其插入点,保证它在UsernamePasswordAuthenticationFilter
之前执行。
3.2.4 异常处理:ExceptionHandlingConfigurer
@Bean
@ConditionalOnMissingBean
public ExceptionHandlingConfigurer<HttpSecurity> exceptionHandlingConfigurer() {
ExceptionHandlingConfigurer<HttpSecurity> cfg = new ExceptionHandlingConfigurer<>();
// 认证失败(未登录或 Token 失效)
cfg.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(200);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(JSON.toJSONString(
new Result().setResult(false).setCode(401).setMsg(authException.getMessage())
));
})
// 权限不足
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(200);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(JSON.toJSONString(
new Result().setResult(false).setCode(403).setMsg(accessDeniedException.getMessage())
));
});
return cfg;
}
- 说明:当任何 Filter 抛出
AuthenticationException
或AccessDeniedException
时,ExceptionTranslationFilter
会捕获并委派给这里配置的AuthenticationEntryPoint
或AccessDeniedHandler
,从而返回统一的 JSON 格式。
3.2.5 Session 管理:SessionManagementConfigurer
@Bean
@ConditionalOnMissingBean
public SessionManagementConfigurer<HttpSecurity> sessionManagementConfigurer() {
SessionManagementConfigurer<HttpSecurity> cfg = new SessionManagementConfigurer<>();
cfg.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return cfg;
}
- 说明:由于使用 JWT 无状态认证,不需要 HttpSession,将 Session 策略设置为
STATELESS
,任何尝试创建或使用 Session 的操作都会被禁止。
3.2.6 登出逻辑:LogoutConfigurer
@Bean
@ConditionalOnMissingBean
public LogoutConfigurer<HttpSecurity> logoutConfigurer(ApplicationContext applicationContext,
SecurityConfigParams securityConfigParams) {
LogoutConfigurer<HttpSecurity> cfg = new LogoutConfigurer<>();
cfg.logoutUrl(securityConfigParams.getLogoutUrl())
.logoutSuccessHandler(new LogoutSuccessHandlerImpl(applicationContext));
return cfg;
}
- 说明:当请求到
/auth/logout
(由securityConfigParams.getLogoutUrl()
指定)时,Spring Security 会自动执行LogoutFilter
,并调用用户定义的LogoutSuccessHandlerImpl
执行清理操作并返回结果。
3.2.7 URL 授权规则:ExpressionUrlAuthorizationConfigurer
@Bean
@ConditionalOnMissingBean
public ExpressionUrlAuthorizationConfigurer<HttpSecurity> expressionUrlAuthorizationConfigurer(
ApplicationContext applicationContext,
SecurityConfigParams securityConfigParams) {
ExpressionUrlAuthorizationConfigurer<HttpSecurity> cfg =
new ExpressionUrlAuthorizationConfigurer<>(applicationContext);
cfg.getRegistry()
.antMatchers("/error").permitAll()
.antMatchers(securityConfigParams.getOpenUrl()).permitAll()
.anyRequest().authenticated();
return cfg;
}
- 说明:注册
authorizeHttpRequests()
相关逻辑,将/error
与配置文件中securityConfigParams.getOpenUrl()
指定的白名单路径放行,其余任何请求都要求认证。
3.2.8 密码编码器与用户详情服务
@Bean
@ConditionalOnMissingBean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@ConditionalOnMissingBean
public UserDetailsService UserDetailsService() {
return new UserDetailsServiceImpl();
}
- 说明:提供默认
BCryptPasswordEncoder
与简单的UserDetailsService
实现。若业务方在项目中自行定义相同类型 Bean,就会覆盖默认实现。
3.2.9 构建 AuthenticationManager
@Bean
public AuthenticationManager authenticationManager(
HttpSecurity httpSecurity,
List<AuthenticationProvider> authenticationProviders,
UserDetailsService userDetailsService) throws Exception {
AuthenticationManagerBuilder builder =
httpSecurity.getSharedObject(AuthenticationManagerBuilder.class);
// 注册所有 AuthenticationProvider
authenticationProviders.forEach(builder::authenticationProvider);
// 最后再指定 UserDetailsService(可选:默认的 DaoAuthenticationProvider 会使用它)
builder.userDetailsService(userDetailsService);
return builder.build();
}
- 说明:显式地收集所有
AuthenticationProvider
(包括DaoAuthenticationProvider
、可能存在的自定义 Provider),再通过userDetailsService
完成用户名/密码认证。构建出的AuthenticationManager
会被注入到UsernamePasswordAuthenticationFilter
(如有)或其他需要认证的 Filter 中。
3.3 主配置类:SecurityConfiguration
整合上述各子模块,实现主配置类 SecurityConfiguration
,负责将所有子配置器 “apply” 到 HttpSecurity
并生成最终的 SecurityFilterChain
。
@Configuration
public class SecurityConfiguration {
// —— 基础 Bean 定义 —— //
@Bean
@ConditionalOnMissingBean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@ConditionalOnMissingBean
public UserDetailsService UserDetailsService() {
return new UserDetailsServiceImpl();
}
@Bean
public TokenService tokenService(ApplicationContext applicationContext) {
return new JwtTokenService(applicationContext);
}
// —— 子功能配置器 —— //
@Bean
@ConditionalOnMissingBean
public OnceFilterConfigurer<HttpSecurity> OncePerRequestConfigurer(ApplicationContext applicationContext) {
OnceFilterConfigurer<HttpSecurity> cfg = new OnceFilterConfigurer<>(applicationContext);
cfg.tokenFilter(); // 注册 JwtTokenFilter
return cfg;
}
@Bean
@ConditionalOnMissingBean
public LogoutConfigurer<HttpSecurity> logoutConfigurer(ApplicationContext applicationContext,
SecurityConfigParams securityConfigParams) {
LogoutConfigurer<HttpSecurity> cfg = new LogoutConfigurer<>();
cfg.logoutUrl(securityConfigParams.getLogoutUrl())
.logoutSuccessHandler(new LogoutSuccessHandlerImpl(applicationContext));
return cfg;
}
@Bean
@ConditionalOnMissingBean
public ExpressionUrlAuthorizationConfigurer<HttpSecurity> expressionUrlAuthorizationConfigurer(
ApplicationContext applicationContext,
SecurityConfigParams securityConfigParams) {
ExpressionUrlAuthorizationConfigurer<HttpSecurity> cfg =
new ExpressionUrlAuthorizationConfigurer<>(applicationContext);
cfg.getRegistry()
.antMatchers("/error").permitAll()
.antMatchers(securityConfigParams.getOpenUrl()).permitAll()
.anyRequest().authenticated();
return cfg;
}
@Bean
@ConditionalOnMissingBean
public ExceptionHandlingConfigurer<HttpSecurity> exceptionHandlingConfigurer() {
ExceptionHandlingConfigurer<HttpSecurity> cfg = new ExceptionHandlingConfigurer<>();
cfg.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(200);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(JSON.toJSONString(
new Result().setResult(false).setCode(401).setMsg(authException.getMessage())
));
}).accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(200);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(JSON.toJSONString(
new Result().setResult(false).setCode(403).setMsg(accessDeniedException.getMessage())
));
});
return cfg;
}
@Bean
@ConditionalOnMissingBean
public SessionManagementConfigurer<HttpSecurity> sessionManagementConfigurer() {
SessionManagementConfigurer<HttpSecurity> cfg = new SessionManagementConfigurer<>();
cfg.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return cfg;
}
@Bean
@ConditionalOnMissingBean
public HeadersConfigurer<HttpSecurity> headersConfigurer() {
HeadersConfigurer<HttpSecurity> cfg = new HeadersConfigurer<>();
cfg.frameOptions().disable();
return cfg;
}
@Bean
@ConditionalOnMissingBean
public CorsConfigurer<HttpSecurity> corsConfigurer(
@Value("${cors.start}") Boolean corsStart,
@Value("${cors.start.filter}") Boolean corsStartFilter,
@Value("${cors.origin}") String[] corsOrigin,
@Value("${cors.port}") String corsPort,
@Value("${cors.header}") String corsHeader,
@Value("${cors.methods}") String corsMethods,
@Value("${cors.file.uploading}") Boolean corsFileUploading) {
CorsConfigurer<HttpSecurity> cfg = new CorsConfigurer<>();
if (!corsStart) {
return cfg;
}
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsCfg = new CorsConfiguration();
if (corsStartFilter) {
for (String origin : corsOrigin) {
corsCfg.addAllowedOrigin(origin);
}
corsCfg.addAllowedOrigin("http://127.0.0.1:" + corsPort);
corsCfg.addAllowedOrigin("http://localhost:" + corsPort);
if ("*".equals(corsHeader)) {
corsCfg.addAllowedOriginPattern(corsHeader);
}
corsCfg.addAllowedMethod(corsMethods);
corsCfg.setAllowCredentials(corsFileUploading);
source.registerCorsConfiguration("/**", corsCfg);
} else {
corsCfg.addAllowedOriginPattern("*");
corsCfg.addAllowedHeader("*");
corsCfg.addAllowedMethod("*");
corsCfg.setAllowCredentials(true);
source.registerCorsConfiguration("/**", corsCfg);
}
cfg.configurationSource(source);
return cfg;
}
// —— 构建 AuthenticationManager —— //
@Bean
public AuthenticationManager authenticationManager(
HttpSecurity httpSecurity,
List<AuthenticationProvider> authenticationProviders,
UserDetailsService userDetailsService) throws Exception {
AuthenticationManagerBuilder builder =
httpSecurity.getSharedObject(AuthenticationManagerBuilder.class);
authenticationProviders.forEach(builder::authenticationProvider);
builder.userDetailsService(userDetailsService);
return builder.build();
}
// —— 核心过滤链 —— //
@Bean
@ConditionalOnMissingBean
public SecurityFilterChain securityFilterChain(
HttpSecurity httpSecurity,
ApplicationContext applicationContext,
List<SecurityConfigurer> securityConfigurers,
UserDetailsService userDetailsService) throws Exception {
// 1. 将所有子 SecurityConfigurer 依次 apply 到 httpSecurity 中
for (SecurityConfigurer configurer : securityConfigurers) {
httpSecurity.apply(configurer);
}
// 2. 全局关闭请求缓存和 CSRF(采用 JWT 无状态)
return httpSecurity
.requestCache(cache -> cache.disable())
.csrf(csrf -> csrf.disable())
.build();
}
// —— 本地化消息源 —— //
@Bean
public ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource msg = new ReloadableResourceBundleMessageSource();
msg.setBasename("classpath:org/springframework/security/messages");
msg.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return msg;
}
}
- 在 Spring 启动时,容器会扫描到上述所有
@Bean
。 - 所有实现了
SecurityConfigurer<SecurityFilterChain, HttpSecurity>
的 Bean(如OnceFilterConfigurer
、ExceptionHandlingConfigurer
、CorsConfigurer
、LogoutConfigurer
、ExpressionUrlAuthorizationConfigurer
等)会被收集到List<SecurityConfigurer>
参数中。 - 调用
securityFilterChain(...)
方法时,先依次apply(configurer)
,让它们分别执行init(http)
+configure(http)
,将自定义 Filter、Handler、授权规则等装载到HttpSecurity
。 - 最后调用
.requestCache().disable()
、.csrf().disable()
,再执行http.build()
,生成完整的SecurityFilterChain
并注册到容器,供FilterChainProxy
调度使用。
四、从底层原理到动态配置方式的思考
通过上面详尽的底层剖析与示例演示,我们可以归纳出“为什么要基于多个 SecurityConfigurer
做动态配置”的几点关键思路与实践建议。
4.1 底层设计哲学:插件式构建
- Spring Security 6.x 底层已经将“安全功能”抽象为“多个配置器(
SecurityConfigurer
)”,通过HttpSecurity.apply(configurer)
依次应用,最终完成过滤链的组装。 - 正因为底层如此设计,开发者无需把所有逻辑硬塞到一个
configure(HttpSecurity)
方法里,而是可以把各功能拆分到多个配置器——仿佛给安全框架装“插件”:每个插件只做单一的事,最后由框架统一组装。
4.2 “零代码”新增与覆盖:可插拔的扩展性
- 只要某个模块实现了
SecurityConfigurer<SecurityFilterChain, HttpSecurity>
并被 Spring 扫描,就会自动注入到List<SecurityConfigurer>
,然后被HttpSecurity.apply()
,无需修改主配置类。 - 对于关键组件使用
@ConditionalOnMissingBean
,可以让业务侧在项目中自定义 Bean 来覆盖默认实现。这样即便是框架层面提供了默认逻辑,也能让业务根据实际需求“零代码”修改或替换。 - 示例场景:
- 默认使用
BCryptPasswordEncoder
,但若业务项目需要使用更高安全性的Argon2PasswordEncoder
,只需在业务代码里定义一个Argon2PasswordEncoder
Bean,即可替换框架默认。 - 如果某个业务不需要 JWT,而改成 OAuth2 资源服务器模式,只要提供一个
OAuth2ResourceServerConfigurer
Bean,Spring 会自动纳入,不必去改动原有代码。
- 默认使用
4.3 单一职责、清晰可维护
- 将登录、登出、Token 过滤、异常处理、CORS、会话管理、URL 授权、静态资源忽略等功能拆分成独立的配置器,各自只负责一块逻辑。
- 代码结构更加模块化,阅读者只需关注自己感兴趣的那一块,不会因上下文过多而迷失。维护时只要修改相应配置器即可,降低耦合度。
4.4 灵活控制 Filter 顺序
- 在各子
SecurityConfigurerAdapter
中使用addFilterBefore
/addFilterAfter
指定插入点,彻底摆脱了容器加载顺序对 Filter 顺序的影响。 - 若子配置器之间有先后依赖,也可以使用
@Order
注解来显式控制配置器在容器中的加载顺序。 - 典型示例:
ExceptionHandlingConfigurer
可以使用@Order(Ordered.HIGHEST_PRECEDENCE)
,保证其init()
与configure()
在所有其他 Filter 插入之前运行,这样后续所有抛出的认证/授权异常都能被正确捕获并转为统一 JSON。
4.5 适合微服务与 Starter 化
- 如果想为多个微服务提供统一的安全方案,只需将上述所有
SecurityConfigurer
类及主配置打包成一个 Maven/Gradle 依赖(如security-starter
)。各个微服务只需在pom.xml
或build.gradle
中引入此 Starter,就能获得统一的安全配置。 - 业务方如果要覆盖某个 Bean(如
TokenService
、UserDetailsService
等),只需在自己项目中声明同类型 Bean 即可,轻松覆盖 Differential Config。无需在每个微服务中复制粘贴整个配置类。
五、总结与实践建议
基于对 Spring Security 底层的深入分析与模块化拆分示例,我们可以得出下面几点实践心得与建议:
- 深刻理解
SecurityBuilder
+SecurityConfigurer
机制- 明白
HttpSecurity
构建流程:init()
→configure()
→build()
。 - 掌握如何在子配置器中通过
addFilterBefore
/addFilterAfter
、.authenticationProvider()
、.authorizeHttpRequests()
等方法将自定义逻辑注入到HttpSecurity
。
- 明白
- 拆分粒度要适当
- 功能模块化拆分的核心目的是降低单一类的复杂度,但也不要过度拆分。若某些功能耦合度较高,可考虑放在同一个配置器中。
- 推荐拆分要遵循“单一职责、低耦合”的原则,例如将认证逻辑、授权规则、异常处理、跨域配置、静态资源忽略等拆成独立配置器。
- 利用
@ConditionalOnMissingBean
实现可覆盖- 在框架层面提供默认实现时,如
PasswordEncoder
、TokenService
、UserDetailsService
等,使用@ConditionalOnMissingBean
注解,使业务方能够随时覆盖默认实现,而无需修改框架源码。
- 在框架层面提供默认实现时,如
- 显式控制配置器与 Filter 顺序
- 对于有先后依赖的配置器,要使用
@Order
注解。 - 对于需要插入到特定 Filter 之前或之后的自定义 Filter,要在配置器的
configure()
方法里使用http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class)
等明确指定插入点。
- 对于有先后依赖的配置器,要使用
- 编写针对性自动化测试
- 为每个
SecurityConfigurer
编写单独测试,使用Spring Security Test
模块,模拟不同场景(无 Token、Token 过期、跨域预检、静态资源访问、登出等),验证配置是否生效。 - 通过
@WebMvcTest
或@SpringBootTest
结合@AutoConfigureMockMvc
,使用 MockMvc 发送不同类型的请求,校验响应状态与响应体。
- 为每个
- 监控与日志
- 在自定义的
JwtTokenFilter
、AuthenticationEntryPoint
、AccessDeniedHandler
等关键组件中增加日志输出,便于线上问题排查。 - 对于异常日志,比如 Token 校验失败、授权失败等,要打印必要的上下文信息(如请求路径、用户标识、异常消息),但注意不要将敏感信息(如明文密码、Token 内容)写入日志。
- 在自定义的
- 与分布式/网关模式结合
- 在微服务架构中,如果使用 API Gateway 或 Spring Cloud Gateway 做统一鉴权和路由,可以将认证逻辑从各服务中剥离,只在网关层做 Token 校验、签发入口。各微服务只需做资源授权与细粒度权限控制。
- 也可以将 JWT 校验封装到共享的 Starter 中,微服务依赖时自动获得鉴权功能;网关可以直接调用同样的
JwtTokenFilter
或基于 Spring Cloud Gateway 的 WebFlux 过滤器。
通过从底层代码的剖析出发,我们看到了 Spring Security 6.x 的构建逻辑:“将每个安全功能实现为一个可独立注册的配置器(SecurityConfigurer
),并在启动时统一组装成最终的过滤链”。基于这一设计,采用“多 SecurityConfigurer
模块化配置”的方式,不仅能够消除单一配置类的臃肿,也赋予了框架高度的可扩展性与可维护性。希望本篇文章能帮助你充分理解 Spring Security 的底层原理,并在实际项目中将动态配置方式灵活运用。祝编码顺利,安全无忧!
文章评论