核心架构
理解 Spring Security 的核心架构是掌握整个框架的基础。本章将深入讲解 Spring Security 在 Servlet 环境下的工作原理,包括过滤器链、核心组件以及请求处理流程。
Servlet 过滤器基础
Spring Security 的 Servlet 支持建立在 Servlet 过滤器(Filter)之上。在学习 Spring Security 之前,我们需要先理解过滤器的工作原理。
什么是过滤器?
当一个 HTTP 请求到达应用时,Servlet 容器会创建一个过滤器链(FilterChain),其中包含处理该请求的多个过滤器实例和最终的 Servlet。在 Spring MVC 应用中,这个 Servlet 就是 DispatcherServlet。
过滤器链的工作方式是:每个过滤器可以在请求到达目标 Servlet 之前或之后执行自定义逻辑。过滤器有三个核心能力:
- 阻止请求传递:过滤器可以选择不调用后续的过滤器或 Servlet,而是直接生成响应
- 修改请求或响应:过滤器可以在请求传递给下游组件之前修改
HttpServletRequest或HttpServletResponse - 执行前后逻辑:过滤器可以在调用下游组件前后各执行一段代码
一个典型的过滤器实现如下:
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 1. 在请求传递给下游之前执行的逻辑
System.out.println("请求处理前...");
// 2. 将请求传递给下一个过滤器或 Servlet
chain.doFilter(request, response);
// 3. 在请求处理完成后执行的逻辑
System.out.println("请求处理后...");
}
}
这个例子展示了过滤器的典型结构。chain.doFilter(request, response) 这行代码是关键,它将控制权传递给过滤器链中的下一个组件。如果不调用这行代码,请求处理就会在此终止。
过滤器的执行顺序
由于每个过滤器只影响下游的过滤器和 Servlet,因此过滤器的执行顺序至关重要。假设有两个过滤器 A 和 B,如果 A 在 B 之前注册,那么执行顺序是:
- A 的前置逻辑
- B 的前置逻辑
- Servlet 处理请求
- B 的后置逻辑
- A 的后置逻辑
这种洋葱模型的设计使得过滤器能够灵活地在请求处理的不同阶段介入。
DelegatingFilterProxy:连接容器与 Spring
为什么需要 DelegatingFilterProxy?
Servlet 容器和 Spring 容器有着不同的生命周期管理机制。Servlet 容器通过自己的标准注册过滤器实例,但它并不了解 Spring 的 Bean。这就产生了一个问题:如何让 Servlet 容器调用 Spring 管理的过滤器 Bean?
Spring 提供了 DelegatingFilterProxy 来解决这个问题。它是一个特殊的过滤器,充当 Servlet 容器和 Spring 容器之间的桥梁。
工作原理
DelegatingFilterProxy 的核心思路是:通过标准的 Servlet 容器机制注册它,但它会将所有工作委托给一个 Spring Bean 来完成。
// DelegatingFilterProxy 的简化伪代码
public class DelegatingFilterProxy implements Filter {
private Filter delegate;
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 延迟获取 Spring Bean(懒加载)
if (delegate == null) {
delegate = (Filter) applicationContext.getBean("myFilterBean");
}
// 将工作委托给 Spring Bean
delegate.doFilter(request, response);
}
}
这种设计有两个重要的好处:
首先,它支持延迟加载。Servlet 容器需要在启动时注册过滤器,但 Spring Bean 通常是在 ContextLoaderListener 加载完成后才可用。通过委托代理,实际的过滤器 Bean 可以在需要时才被创建。
其次,它让过滤器能够享受 Spring 的依赖注入能力。作为 Spring Bean,过滤器可以注入其他服务、配置等。
FilterChainProxy:Spring Security 的核心
FilterChainProxy 的角色
FilterChainProxy 是 Spring Security Servlet 支持的核心组件。它是一个特殊的过滤器,通过 SecurityFilterChain 将请求委托给多个过滤器实例处理。
由于 FilterChainProxy 本身是一个 Spring Bean,它通常被包装在 DelegatingFilterProxy 中,从而被 Servlet 容器识别和调用。
为什么需要 FilterChainProxy?
你可能会问,为什么不直接将安全过滤器注册到 Servlet 容器?FilterChainProxy 提供了几个关键优势:
统一的入口点:所有 Spring Security 的 Servlet 支持都从这里开始。在调试时,在 FilterChainProxy 处设置断点是排查问题的最佳起点。
自动清理:FilterChainProxy 会在请求处理完成后自动清理 SecurityContext,避免内存泄漏。
防火墙保护:它应用 Spring Security 的 HttpFirewall,保护应用免受某些类型的攻击。
灵活的匹配:在 Servlet 容器中,过滤器只能基于 URL 进行匹配。而 FilterChainProxy 可以基于 HttpServletRequest 中的任何内容(如请求头、请求方法等)来决定使用哪个安全过滤器链。
SecurityFilterChain:安全过滤器链
概念理解
SecurityFilterChain 被 FilterChainProxy 用来确定当前请求应该调用哪些 Spring Security 过滤器。
一个应用可以配置多个 SecurityFilterChain,FilterChainProxy 会根据 RequestMatcher 的匹配结果选择合适的链。只有第一个匹配的链会被调用。
多链配置示例
假设我们需要为 API 请求和普通页面请求配置不同的安全策略:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// API 安全链:匹配 /api/** 路径
@Bean
@Order(1) // 优先级更高
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable()) // API 无状态,禁用 CSRF
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
// Web 安全链:匹配其他所有请求
@Bean
@Order(2)
public SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults()) // 启用 CSRF 保护
.formLogin(Customizer.withDefaults()) // 表单登录
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
);
return http.build();
}
}
在这个配置中,/api/messages/ 请求会匹配第一条链(/api/**),而 /messages/ 请求则会匹配第二条链。注意第一条链只有 3 个安全过滤器,而第二条链可能有更多,每条链都可以独立配置。
忽略特定请求
如果某些请求不需要 Spring Security 的保护,可以配置一个空的过滤器链:
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring()
.requestMatchers("/static/**", "/public/**");
}
这些请求将完全绕过 Spring Security 的处理。
安全过滤器详解
过滤器执行顺序
Spring Security 的过滤器按照特定的顺序执行,这个顺序很重要。例如,执行认证的过滤器必须在执行授权的过滤器之前。
考虑以下配置:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
);
return http.build();
}
这个配置会产生以下过滤器顺序:
CsrfFilter- 防护 CSRF 攻击- 认证过滤器(
UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter等) AuthorizationFilter- 执行授权检查
查看当前配置的过滤器
在应用启动时,Spring Security 会在 DEBUG 级别打印配置的过滤器列表:
2023-06-14T08:55:22.321-03:00 DEBUG 76975 --- [main] o.s.s.web.DefaultSecurityFilterChain
: Will secure any request with [DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter,
SecurityContextHolderFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter,
UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter,
DefaultLogoutPageGeneratingFilter, BasicAuthenticationFilter, RequestCacheAwareFilter,
SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter,
ExceptionTranslationFilter, AuthorizationFilter]
要启用详细日志,可以在配置文件中添加:
logging:
level:
org.springframework.security: TRACE
添加自定义过滤器
当需要添加自定义过滤器时,确定其位置很关键。一个经验法则是:
| 过滤器类型 | 放置位置 | 原因 |
|---|---|---|
| 安全防护过滤器 | SecurityContextHolderFilter 之后 | 此时安全上下文已加载 |
| 认证过滤器 | LogoutFilter 之后 | 此时已完成登出检查,可以开始认证 |
| 授权过滤器 | AnonymousAuthenticationFilter 之后 | 此时所有认证已完成 |
例如,添加一个检查租户权限的过滤器:
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String tenantId = req.getHeader("X-Tenant-Id");
if (hasAccessToTenant(tenantId)) {
chain.doFilter(request, response); // 继续处理
return;
}
throw new AccessDeniedException("无权访问该租户");
}
}
// 配置过滤器
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ... 其他配置
.addFilterAfter(new TenantFilter(), AnonymousAuthenticationFilter.class);
return http.build();
}
避免过滤器重复注册
如果将过滤器声明为 Spring Bean(使用 @Component 或 @Bean),Spring Boot 会自动将其注册到嵌入式容器。这可能导致过滤器被调用两次:一次由容器调用,一次由 Spring Security 调用。
解决方案是禁用容器的自动注册:
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false); // 禁用容器注册
return registration;
}
异常处理机制
ExceptionTranslationFilter 的作用
ExceptionTranslationFilter 负责将安全相关的异常转换为适当的 HTTP 响应。它处理两类异常:
AuthenticationException:认证异常,表示用户未认证或认证失败AccessDeniedException:授权异常,表示用户已认证但没有足够的权限
处理流程
当请求处理过程中抛出安全异常时,ExceptionTranslationFilter 会捕获并处理:
- 首先尝试执行过滤器链
filterChain.doFilter(request, response) - 如果抛出
AuthenticationException:- 清除
SecurityContextHolder - 保存当前请求(用于认证成功后重放)
- 调用
AuthenticationEntryPoint引导用户认证
- 清除
- 如果抛出
AccessDeniedException:- 如果用户未认证,启动认证流程
- 如果用户已认证,调用
AccessDeniedHandler返回 403 错误
// ExceptionTranslationFilter 的简化伪代码
try {
filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication(); // 启动认证
} else {
accessDenied(); // 返回 403
}
}
请求缓存机制
为什么需要请求缓存?
当用户尝试访问需要认证的资源时,系统会引导用户进行认证。认证成功后,理想的情况是能够回到用户最初想要访问的页面。这就需要请求缓存机制。
RequestCache 的工作方式
默认情况下,Spring Security 使用 HttpSessionRequestCache 将原始请求保存在 HTTP Session 中。认证成功后,RequestCacheAwareFilter 会从缓存中取出原始请求并重放。
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("continue"); // 自定义参数名
http
// ...
.requestCache(cache -> cache
.requestCache(requestCache)
);
return http.build();
}
禁用请求缓存
在某些场景下(如前后端分离架构),可能不需要保存原始请求:
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
http
// ...
.requestCache(cache -> cache
.requestCache(new NullRequestCache())
);
return http.build();
}
SecurityContext 管理
SecurityContext 的概念
SecurityContext 是 Spring Security 存储当前用户认证信息的地方。它包含一个 Authentication 对象,该对象代表了当前已认证用户的身份和权限信息。
SecurityContextHolder 的使用
SecurityContextHolder 是访问 SecurityContext 的静态工具类:
// 获取当前用户信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
// 手动设置安全上下文(通常不推荐)
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
推荐的注入方式
从 Spring Security 6 开始,推荐通过依赖注入获取 SecurityContext:
@Controller
public class UserController {
private final SecurityContextHolderStrategy securityContextHolderStrategy;
public UserController(SecurityContextHolderStrategy strategy) {
this.securityContextHolderStrategy = strategy;
}
@GetMapping("/profile")
public String profile(Model model) {
Authentication auth = securityContextHolderStrategy.getContext().getAuthentication();
model.addAttribute("username", auth.getName());
return "profile";
}
}
这种方式避免了静态访问带来的潜在问题,特别是在多应用上下文的场景下。
小结
Spring Security 的核心架构建立在 Servlet 过滤器之上,通过以下组件协同工作:
- DelegatingFilterProxy:连接 Servlet 容器和 Spring 容器
- FilterChainProxy:Spring Security 的统一入口点
- SecurityFilterChain:定义请求与安全过滤器的映射关系
- 安全过滤器:按特定顺序执行认证、授权、防护等功能
- ExceptionTranslationFilter:处理安全异常并生成响应
- RequestCache:缓存原始请求用于认证后重放
- SecurityContext:存储当前用户的认证信息
理解这些组件的工作原理和它们之间的关系,是深入学习 Spring Security 各项功能的基础。在后续章节中,我们将基于这些基础知识,详细探讨认证、授权、会话管理等核心功能的具体实现。