Spring Security 学习笔记 2:架构

Spring Security 学习笔记 2:架构

Spring Security 的 Servlet 支持基于 Servlet Filters:

Spring Security 利用 Servlet Filter 实现:

主要的组件:

  • DelegatingFilterProxy:从 Spring 容器中加载 Filter 实例并调用
  • FilterChainProxy:特殊的 Filter,用于调用多个 SecurityFilterChain
  • SecurityFilterChain:SecurityFilter 的调用链,可以包含多个以对应不同的路径规则,执行时会根据请求路径依次匹配,执行匹配到的第一个调用链。

SecurityFilterChain 可以包含 0 个 SecurityFilter,比如如果希望程序忽略某个路径(不做处理),可以为其添加一个包含 0 个 SecurityFilter 的 SecurityFilterChain。

Security Filter

Security Filter 用于执行具体的调用链行为,最常见的是进行身份认证或授权。需要注意的是它们在调用链上的顺序相当关键,比如用于认证的 Filter 必须在用于授权的 Filter 之前,这点和 Servlet 的 Filter 是一样的。

通常使用HttpSecurity添加并构建一个SecurityFilterChain

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) {
        httpSecurity.csrf(Customizer.withDefaults())
                .httpBasic(Customizer.withDefaults())
                .formLogin(Customizer.withDefaults())
                .authorizeHttpRequests(authorize ->
                        authorize.anyRequest().authenticated());
        return httpSecurity.build();
    }
}

这里依次添加了三个过滤器:

  • CsrfFilter:防止 CSRF 攻击
  • 认证过滤器:用于身份认证
  • 授权过滤器:用于对请求进行授权

打印过滤器链

可以添加配置以打印 Spring Security 日志:

properties 复制代码
# 日志级别设置为 debug
logging.level.org.springframework.security=debug

打印的日志:

复制代码
2025-12-25T10:09:47.066+08:00 DEBUG 20476 --- [demo] [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with filters: DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, UsernamePasswordAuthenticationFilter, DefaultResourcesFilter, DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter, BasicAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, TenantFilter, ExceptionTranslationFilter, AuthorizationFilter

这里边是加载的 SecurityFitlerChain 中的 Security Filter 信息,可以从这里边看到 Filter 的先后顺序。

向过滤器链添加过滤器

HttpSecurity 提供了三种添加过滤器的方法:

  • #addFilterBefore(Filter, Class<?>) 在另一个过滤器之前添加你的过滤器
  • #addFilterAfter(Filter, Class<?>) 在另一个过滤器之后添加你的过滤器
  • #addFilterAt(Filter, Class<?>) 用你的过滤器替换另一个过滤器

假设需要添加一个检查当前用户是否可以访问特定租户信息的 Filter(多租户系统):

java 复制代码
public class TenantFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String tenantId = request.getHeader("X-Tenant-Id");
        boolean hasAccess = isUserAllowed(tenantId);
        if (hasAccess) {
            filterChain.doFilter(request, response);
            return;
        }
        throw new AccessDeniedException("Access denied");
    }

    private boolean isUserAllowed(String tenantId) {
        // 模拟检查用户是否允许访问租户
        return tenantId != null && !tenantId.isEmpty();
    }
}

这里通过请求头信息获取租户编码,然后检查当前用户是否可以访问,如果不行,就抛出异常。

现在添加这个 Filter 到过滤器链,因为需要获取当前用户信息,所以这个过滤器应当在认证过滤器之后:

java 复制代码
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) {
    // ...
    httpSecurity.addFilterAfter(new TenantFilter(), AnonymousAuthenticationFilter.class);
    return httpSecurity.build();
}

声明过滤器为 Spring Bean

通常不应当让 Security Filter 作为 Spring Bean 存在,因为这样做可能让 Filter 被调用两次,一次是 Spring Security 调用,一次是 Spring 容器调用。

如果一定要这么做(比如需要以依赖注入的方式使用 Filter),需要添加特殊设置,以告诉 Spring 容器这个 Filter 不应当被注册到容器:

java 复制代码
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter tenantFilter) {
    FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(tenantFilter);
    registration.setEnabled(false);
    return registration;
}
java 复制代码
@Component
public class TenantFilter implements Filter {
    // ...
}

这样就只有 Spring Security 会使用这个 Bean 作为 Filter。

自定义 Spring Security 过滤器

通常,会使用链式调用(DSL)的方式添加过滤器:

java 复制代码
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, TenantFilter tenantFilter) {
    httpSecurity.csrf(Customizer.withDefaults())
        .httpBasic(Customizer.withDefaults())
        .formLogin(Customizer.withDefaults())
        .authorizeHttpRequests(authorize ->
                               authorize.anyRequest().authenticated());
    httpSecurity.addFilterAfter(tenantFilter, AnonymousAuthenticationFilter.class);
    return httpSecurity.build();
}

如果要用自定义 Filter 取代原有的 Filter,可以:

java 复制代码
@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity,
                                                   TenantFilter tenantFilter,
                                                   AuthenticationManager authManager
                                                   ) {
        httpSecurity.csrf(Customizer.withDefaults())
                .formLogin(Customizer.withDefaults())
                .authorizeHttpRequests(authorize ->
                        authorize.anyRequest().authenticated());
        httpSecurity.addFilterAfter(tenantFilter, AnonymousAuthenticationFilter.class);
        BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter(authManager);
        httpSecurity.addFilterAt(basicAuthenticationFilter, BasicAuthenticationFilter.class);
        return httpSecurity.build();
    }

同一个 Filter 只能在 FilterChain 上添加一次,如果重复添加可能报错:

java 复制代码
httpSecurity.csrf(Customizer.withDefaults())
    .httpBasic(Customizer.withDefaults())
    .formLogin(Customizer.withDefaults())
    .authorizeHttpRequests(authorize ->
                           authorize.anyRequest().authenticated());
httpSecurity.addFilterAfter(tenantFilter, AnonymousAuthenticationFilter.class);
BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter(authManager);
httpSecurity.addFilterAt(basicAuthenticationFilter, BasicAuthenticationFilter.class);

此时可以移除原来的 Filter 添加,或者利用 DSL 关闭 Filter:

java 复制代码
httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
httpSecurity.addFilterAt(basicAuthenticationFilter, BasicAuthenticationFilter.class);

处理安全异常

查看日志可以看到AuthorizationFilter过滤器前有一个ExceptionTranslationFilter过滤器。该过滤器可以捕获其后产生的AccessDeniedExceptionAuthenticationException异常。

如果是未登录或AuthenticationException异常,会重定向到登录页面进行登录,如果是AccessDeniedException异常,访问会被拒绝,AccessDeniedHandler会被调用。

AuthorizationFilter中的伪代码如下:

java 复制代码
try {
	filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
	if (!authenticated || ex instanceof AuthenticationException) {
		startAuthentication();
	} else {
		accessDenied();
	}
}

保存认证前的请求

当一个请求没有认证信息且需要访问需要认证的资源时,需要保存该请求以便在认证成功后重新发起。在 Spring Security 中,这是通过使用 HttpServletRequest 来实现的,具体由 RequestCache 实现。

请求缓存

举例说明,比如我们需要缓存请求参数中有continue的 HTTP 请求,以便在用户认证后再次执行该请求:

java 复制代码
@Bean
public DefaultSecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
    HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
    requestCache.setMatchingRequestParameterName("continue");
    httpSecurity.requestCache(cache -> cache.requestCache(requestCache));
    return httpSecurity.build();
}

防止请求被缓存

如果不想请求被缓存,即禁用请求缓存功能,可以:

java 复制代码
@Bean
public SecurityFilterChain securityFilterChain2(HttpSecurity httpSecurity) throws Exception {
    RequestCache requestCache = new NullRequestCache();
    httpSecurity.requestCache(cache -> cache.requestCache(requestCache));
    return httpSecurity.build();
}

RequestCacheAwareFilter

该过滤器使用 RequestCache 来重放原始请求。

日志记录

Spring Security 提供了在 DEBUG 和 TRACE 级别对所有与安全相关的事件的全面日志记录。这在调试应用程序时非常有用,因为 Spring Security 不会在响应体中添加任何关于请求被拒绝原因的详细信息。如果你遇到 401 或 403 错误,很可能你会发现一条有助于理解问题的日志信息。

可以通过修改配置启用日志:

properties 复制代码
logging.level.org.springframework.security=TRACE

参考资料

相关推荐
阿Y加油吧8 小时前
二刷 LeetCode:300. 最长递增子序列 & 152. 乘积最大子数组 复盘笔记
笔记·算法·leetcode
y = xⁿ8 小时前
Redis八股学习日记:数据结构;跳表的底层;Reids的事务机制
数据结构·redis·学习
炽烈小老头8 小时前
【每天学习一点算法 2026/04/29】最长连续序列
学习·算法
库奇噜啦呼8 小时前
【iOS】源码学习-类与对象底层原理
学习·ios·cocoa
不灭锦鲤9 小时前
网络安全学习第98天
学习·安全
阿Y加油吧9 小时前
二刷 LeetCode:5. 最长回文子串 & 1143. 最长公共子序列 复盘笔记
笔记·算法·leetcode
JAVA面经实录9179 小时前
Spring AI 高频开发万能 Prompt 合集 + 生产级工具类
java·人工智能·spring·prompt
星幻元宇VR9 小时前
VR自行车骑行模拟系统|让交通安全教育“骑”进现实
科技·学习·安全·vr
JAVA面经实录9179 小时前
如何选择适合项目的「限流 / 熔断 / 降级」方案
java·spring·kafka·sentinel·guava
知识分享小能手9 小时前
R语言入门学习教程,从入门到精通,R语言数值关系数据可视化 - 完整知识点(5)
学习·信息可视化·r语言