红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. Spring Framework 学习笔记
  3. 正文

Spring Security 学习笔记 2:架构

2026年1月4日 11点热度 0人点赞 0条评论

Spring Security 的 Servlet 支持基于 Servlet Filters:

filterchain

Spring Security 利用 Servlet Filter 实现:

multi securityfilterchain

主要的组件:

  • DelegatingFilterProxy:从 Spring 容器中加载 Filter 实例并调用

  • FilterChainProxy:特殊的 Filter,用于调用多个 SecurityFilterChain

  • SecurityFilterChain:SecurityFilter 的调用链,可以包含多个以对应不同的路径规则,执行时会根据请求路径依次匹配,执行匹配到的第一个调用链。

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

Security Filter

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

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

@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 日志:

# 日志级别设置为 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(多租户系统):

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 到过滤器链,因为需要获取当前用户信息,所以这个过滤器应当在认证过滤器之后:

@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 不应当被注册到容器:

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

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

自定义 Spring Security 过滤器

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

@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,可以:

@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 上添加一次,如果重复添加可能报错:

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:

httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
httpSecurity.addFilterAt(basicAuthenticationFilter, BasicAuthenticationFilter.class);

处理安全异常

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

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

AuthorizationFilter中的伪代码如下:

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

保存认证前的请求

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

请求缓存

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

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

防止请求被缓存

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

@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 错误,很可能你会发现一条有助于理解问题的日志信息。

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

logging.level.org.springframework.security=TRACE

参考资料

  • Spring Security

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: security spring
最后更新:2026年1月4日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

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

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号