秋雨De blog

  • 首页
  • 留言板
  • 关于
  • rss
秋雨De blog
一个技术小白的个人博客
  1. 首页
  2. 未分类
  3. 正文

深入 Spring Security 6:从底层机制到动态模块化配置

2025年5月28日 210点热度 0人点赞 0条评论

一、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 就会自动为所有符合匹配规则的请求应用对应的过滤器链。
  • SecurityFilterChain
    每个 SecurityFilterChain 对象由两部分组成:
    1. RequestMatcher:定义该链所应用的请求范围(常见写法是匹配所有 URL,即 anyRequest())。
    2. 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)。
  • 典型实现: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) 的底层步骤(简化版):

  1. 创建 HttpSecurity 实例
    • Spring Security 通过内部联系人(如 EnableWebSecurityImportSelector)在启动时创建 HttpSecurity 对象。此时它内部的 sharedObjects Map 中已经放入了核心 Bean,比如 ApplicationContext、ObjectPostProcessor、AuthenticationConfiguration 等。
  2. 收集所有 SecurityConfigurer<SecurityFilterChain, HttpSecurity> Bean
    • Spring 容器扫描到各个实现了 SecurityConfigurer<SecurityFilterChain, HttpSecurity> 的 Bean,包含官方内置的配置器(如 CsrfConfigurer、FormLoginConfigurer 等)以及用户自定义的配置器(如 OnceFilterConfigurer、ExceptionHandlingConfigurer 等)。
    • 当调用 securityFilterChain(HttpSecurity http, List<SecurityConfigurer> securityConfigurers) 时,这个 List<SecurityConfigurer> 参数会自动注入容器中所有同类型 Bean。
  3. 依次调用各配置器的 init(http)
    • Spring 首先遍历 securityConfigurers 列表,对每个配置器执行 init(http)。这一步通常用于让配置器“预先准备共享对象”或“创建尚未完成的 BeanDefinition”。
  4. 依次调用各配置器的 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 内部的数据结构,还未真正实例化过滤链。
  5. 调用 http.build():生成最终的 SecurityFilterChain
    • 在所有配置器都完成 configure() 后,Spring 执行 http.build()。此时 HttpSecurity 会根据之前存储的各种配置元数据,进行以下操作:
      1. 汇总所有被注册的 Filter(按先后顺序,以及显式指定的 addFilterBefore/addFilterAfter 插入点),组织成一个 LinkedList<Filter>。
      2. 根据配置的 RequestMatcher(通常是所有请求),生成一个 SecurityFilterChainImpl,将上述 Filter 列表挂到该链上。
      3. 将生成的 SecurityFilterChainImpl 注册为一个 Spring Bean。
    • 由于 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 的执行顺序决定了请求的处理流程:
    1. CorsFilter 必须最先运行,否则跨域预检请求无法顺利通过;
    2. 自定义的 JwtTokenFilter 常常需要在 UsernamePasswordAuthenticationFilter 之前运行,以便在表单登录或 BasicAuth 之前先尝试 Token 验证;
    3. ExceptionTranslationFilter 必须包裹住所有可能抛出 AuthenticationException 或 AccessDeniedException 的 Filter,用于捕获并委派给统一的 AuthenticationEntryPoint、AccessDeniedHandler;
    4. 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 无状态认证的后端服务,需求点如下:

  1. JWT 无状态认证
    所有接口都使用 JWT 作为鉴权方式,不依赖 HttpSession。
  2. 自定义异常返回格式
    认证失败或无权限时,统一返回 { "success": false, "code": 401/403, "message": "具体错误描述" } 的 JSON。
  3. 自定义登出逻辑
    当客户端调用 /auth/logout 时,需要执行一些资源清理(比如将 Token 加黑名单),并返回特定格式的 JSON。
  4. URL 白名单
    某些 URL(如 /auth/login、/auth/register、Swagger 文档、静态资源)无需认证访问。
  5. 跨域动态配置
    不同环境(开发/测试/生产)可能使用不同的前端域名,需要在配置文件中灵活指定。
  6. 可扩展性
    将来可能需要新增短信登录、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 底层的深入分析与模块化拆分示例,我们可以得出下面几点实践心得与建议:

  1. 深刻理解 SecurityBuilder + SecurityConfigurer 机制
    • 明白 HttpSecurity 构建流程:init() → configure() → build()。
    • 掌握如何在子配置器中通过 addFilterBefore / addFilterAfter、.authenticationProvider()、.authorizeHttpRequests() 等方法将自定义逻辑注入到 HttpSecurity。
  2. 拆分粒度要适当
    • 功能模块化拆分的核心目的是降低单一类的复杂度,但也不要过度拆分。若某些功能耦合度较高,可考虑放在同一个配置器中。
    • 推荐拆分要遵循“单一职责、低耦合”的原则,例如将认证逻辑、授权规则、异常处理、跨域配置、静态资源忽略等拆成独立配置器。
  3. 利用 @ConditionalOnMissingBean 实现可覆盖
    • 在框架层面提供默认实现时,如 PasswordEncoder、TokenService、UserDetailsService 等,使用 @ConditionalOnMissingBean 注解,使业务方能够随时覆盖默认实现,而无需修改框架源码。
  4. 显式控制配置器与 Filter 顺序
    • 对于有先后依赖的配置器,要使用 @Order 注解。
    • 对于需要插入到特定 Filter 之前或之后的自定义 Filter,要在配置器的 configure() 方法里使用 http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class) 等明确指定插入点。
  5. 编写针对性自动化测试
    • 为每个 SecurityConfigurer 编写单独测试,使用 Spring Security Test 模块,模拟不同场景(无 Token、Token 过期、跨域预检、静态资源访问、登出等),验证配置是否生效。
    • 通过 @WebMvcTest 或 @SpringBootTest 结合 @AutoConfigureMockMvc,使用 MockMvc 发送不同类型的请求,校验响应状态与响应体。
  6. 监控与日志
    • 在自定义的 JwtTokenFilter、AuthenticationEntryPoint、AccessDeniedHandler 等关键组件中增加日志输出,便于线上问题排查。
    • 对于异常日志,比如 Token 校验失败、授权失败等,要打印必要的上下文信息(如请求路径、用户标识、异常消息),但注意不要将敏感信息(如明文密码、Token 内容)写入日志。
  7. 与分布式/网关模式结合
    • 在微服务架构中,如果使用 API Gateway 或 Spring Cloud Gateway 做统一鉴权和路由,可以将认证逻辑从各服务中剥离,只在网关层做 Token 校验、签发入口。各微服务只需做资源授权与细粒度权限控制。
    • 也可以将 JWT 校验封装到共享的 Starter 中,微服务依赖时自动获得鉴权功能;网关可以直接调用同样的 JwtTokenFilter 或基于 Spring Cloud Gateway 的 WebFlux 过滤器。

通过从底层代码的剖析出发,我们看到了 Spring Security 6.x 的构建逻辑:“将每个安全功能实现为一个可独立注册的配置器(SecurityConfigurer),并在启动时统一组装成最终的过滤链”。基于这一设计,采用“多 SecurityConfigurer 模块化配置”的方式,不仅能够消除单一配置类的臃肿,也赋予了框架高度的可扩展性与可维护性。希望本篇文章能帮助你充分理解 Spring Security 的底层原理,并在实际项目中将动态配置方式灵活运用。祝编码顺利,安全无忧!

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 暂无
最后更新:2025年5月28日

fallrain

种一棵树最好的时间是十年前,其次是现在。

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

fallrain

种一棵树最好的时间是十年前,其次是现在。

文章目录
  • 一、Spring Security 底层架构与核心类分析
    • 1.1 FilterChainProxy 与 SecurityFilterChain 的角色
    • 1.2 底层配置接口:SecurityBuilder、SecurityConfigurer、SecurityConfigurerAdapter
    • 1.3 HttpSecurity 的构建流程
  • 二、为何要基于多个 SecurityConfigurer 做动态配置
    • 2.1 避免单一配置类臃肿、职责分散
    • 2.2 支持“零代码”新增/替换功能
    • 2.3 灵活控制过滤器顺序与依赖关系
  • 三、示例:从底层逻辑到动态配置实战
    • 3.1 项目需求概览
    • 3.2 构建各功能模块的 SecurityConfigurerAdapter
    • 3.3 主配置类:SecurityConfiguration
  • 四、从底层原理到动态配置方式的思考
    • 4.1 底层设计哲学:插件式构建
    • 4.2 “零代码”新增与覆盖:可插拔的扩展性
    • 4.3 单一职责、清晰可维护
    • 4.4 灵活控制 Filter 顺序
    • 4.5 适合微服务与 Starter 化
  • 五、总结与实践建议
友情连接
猫饭范文泉博客迎風别葉CODING手艺人ScarSu
归档
  • 2025 年 5 月
  • 2025 年 4 月
  • 2025 年 3 月
  • 2024 年 12 月
  • 2024 年 10 月
  • 2024 年 5 月
  • 2023 年 2 月
  • 2022 年 11 月
  • 2022 年 3 月
  • 2021 年 12 月
  • 2021 年 8 月
  • 2021 年 5 月
  • 2021 年 4 月
  • 2021 年 3 月
  • 2020 年 12 月
  • 2020 年 11 月
  • 2020 年 8 月
  • 2020 年 5 月
  • 2019 年 12 月
  • 2019 年 3 月

吉ICP备18007356号

吉公网安备22020302000184号

Theme Kratos Made By Seaton Jiang

COPYRIGHT © 2025 秋雨De blog ALL RIGHTS RESERVED