0%

springSecurity提供的过滤器

Spring Security 核心过滤器详解:过滤器链的工作机制与职责分工

Spring Security 的本质是一个过滤器链,所有 Web 安全功能(认证、授权、CSRF 防护等)均通过不同职责的过滤器协同实现。每个过滤器专注于单一功能,按特定顺序组成链,确保请求在到达业务代码前完成安全校验。从 “职责定义→工作流程→关键源码→使用场景” 四个维度,彻底讲透每个过滤器的作用与协作逻辑。

过滤器链的整体执行顺序

在分析单个过滤器前,需先明确过滤器链的默认执行顺序(Spring Security 自动维护,顺序不可随意调整),这是理解安全流程的基础:

执行顺序 过滤器名称 核心职责
1 SecurityContextPersistenceFilter 管理 SecurityContext(创建 / 清空)
2 CsrfFilter 防止 CSRF 攻击(验证 CSRF Token)
3 LogoutFilter 处理退出登录请求
4 UsernamePasswordAuthenticationFilter 处理表单登录(用户名 / 密码认证)
5 AnonymousAuthenticationFilter 为未认证用户分配匿名身份
6 SessionManagementFilter 管理 Session(防固定攻击、限制会话数量)
7 ExceptionTranslationFilter 捕获并处理认证 / 授权异常
8 FilterSecurityInterceptor 权限校验(判断用户是否有权访问资源)

注:实际项目中可能存在更多过滤器(如 RememberMeAuthenticationFilter、OAuth2 相关过滤器),但上述 8 个是最核心的基础过滤器。

核心过滤器详解

1. SecurityContextPersistenceFilter:安全上下文的 “管家”

核心职责:在请求开始时创建 SecurityContext(存储用户认证信息),在请求结束时清空 SecurityContextHolder(避免 ThreadLocal 内存泄漏),是整个安全流程的 “基础保障”。

工作流程
  1. 请求到达时:
    • HttpSession 中读取 SecurityContext(若存在,说明用户已登录);
    • 若不存在,创建新的 SecurityContext
    • SecurityContext 存入 SecurityContextHolder(ThreadLocal 存储,供后续过滤器使用)。
  2. 请求结束时:
    • SecurityContext 有变化(如用户刚登录),将其写入 HttpSession
    • 调用 SecurityContextHolder.clearContext(),清空当前线程的 SecurityContext
关键源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

// 确保同一请求只被处理一次(防止转发/重定向重复处理)
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

try {
// 1. 从 Session 中获取或创建 SecurityContext
SecurityContext contextBeforeChainExecution = repo.loadContext(new HttpRequestResponseHolder(request, response));
// 2. 存入 SecurityContextHolder(ThreadLocal)
SecurityContextHolder.setContext(contextBeforeChainExecution);

// 3. 执行后续过滤器链
chain.doFilter(request, response);
} finally {
// 4. 请求结束:清空当前线程的 SecurityContext
SecurityContextHolder.clearContext();
// 5. 保存 SecurityContext 到 Session(若有变化)
repo.saveContext(SecurityContextHolder.getContext(), request, response);
}
}
为什么重要?
  • SecurityContext 是整个安全流程的 “数据载体”,后续过滤器(如 UsernamePasswordAuthenticationFilterFilterSecurityInterceptor)均依赖它获取用户信息;
  • 清空 SecurityContextHolder 是为了避免 ThreadLocal 内存泄漏(线程池复用线程时,残留的 SecurityContext 会导致用户信息错乱)。

2. CsrfFilter:防跨站请求伪造(CSRF)的 “卫士”

核心职责:验证请求中的 CSRF Token,防止恶意网站利用用户已登录的会话发起非法请求(如伪造转账、修改密码)。Spring Security 4+ 默认为所有非 GET、HEAD、OPTIONS、TRACE 的请求开启 CSRF 防护。

工作原理
  1. 生成 CSRF Token:用户登录时,Spring Security 生成一个随机的 CSRF Token,存入 HttpSessionSecurityContext,并通过表单隐藏域或 Cookie 传递给前端;
  2. 验证 CSRF Token:
    • 前端提交非安全请求(POST、PUT、DELETE 等)时,需在请求头(如 X-CSRF-TOKEN)或请求参数中携带 CSRF Token;
    • CsrfFilter 从请求中提取 Token,与 HttpSession 中的 Token 对比,不一致则拒绝请求(返回 403 Forbidden)。
关键源码(简化版)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);

// 1. 从请求中提取 CSRF Token
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = (csrfToken == null);

// 2. 若没有 Token,生成新 Token 并保存到 Session
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);

// 3. 判断请求是否需要 CSRF 验证(非 GET/HEAD/OPTIONS/TRACE 请求需要)
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}

// 4. 从请求中提取用户提交的 Token
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}

// 5. 对比 Token:不一致则抛出异常
if (!csrfToken.getToken().equals(actualToken)) {
throw new InvalidCsrfTokenException(csrfToken, actualToken);
}

// 6. Token 验证通过,执行后续过滤器
filterChain.doFilter(request, response);
}
常见场景与配置
  • 前后端分离项目:通常关闭 CSRF 防护(前端通过 Token 认证,无 Session),配置 http.csrf().disable()

  • 传统表单项目:需在表单中添加隐藏域携带 CSRF Token:

    1
    2
    3
    4
    <form method="post" action="/user/update">
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
    <!-- 其他表单字段 -->
    </form>

3. LogoutFilter:处理退出登录的 “执行者”

核心职责:拦截退出登录请求(默认 /logout),执行退出逻辑(销毁 Session、清空 SecurityContext、删除 “记住我” Cookie 等)。

工作流程
  1. 拦截请求:匹配退出登录路径(默认 /logout,可通过 http.logout().logoutUrl("/custom-logout") 自定义);
  2. 执行退出逻辑:
    • 调用 LogoutHandler 链(如 SecurityContextLogoutHandler 清空 SecurityContextCookieClearingLogoutHandler 删除 Cookie);
    • 触发 LogoutSuccessHandler(默认重定向到登录页,可自定义为返回 JSON 结果);
  3. 重定向或返回结果:退出成功后,默认重定向到 /login?logout,可通过 logoutSuccessUrl("/index") 自定义。
关键配置示例
1
2
3
4
5
6
7
8
9
@Override
protected void configure(HttpSecurity http) throws Exception {
http.logout()
.logoutUrl("/user/logout") // 自定义退出路径
.logoutSuccessUrl("/login?success") // 退出成功跳转路径
.invalidateHttpSession(true) // 销毁当前 Session
.deleteCookies("JSESSIONID", "remember-me") // 删除指定 Cookie
.permitAll(); // 允许匿名访问退出路径
}

4. UsernamePasswordAuthenticationFilter:表单登录的 “核心”

核心职责:拦截表单登录请求(默认 /login 的 POST 请求),提取用户名 / 密码,封装成 UsernamePasswordAuthenticationToken,委托 AuthenticationManager 完成认证,是表单认证流程的入口

工作流程(回顾认证流程核心)
  1. 拦截请求:仅处理 POST 方法的 /login 请求(可通过 loginProcessingUrl("/user/login") 自定义);
  2. 提取凭证:从请求参数中获取用户名(默认 username)和密码(默认 password);
  3. 封装 Token:创建未认证的 UsernamePasswordAuthenticationTokensetAuthenticated(false));
  4. 委托认证:调用 AuthenticationManager.authenticate(token),触发后续认证逻辑(如 DaoAuthenticationProvider 校验密码);
  5. 处理结果:
    • 认证成功:调用 successfulAuthentication(),将认证信息存入 SecurityContext,重定向到首页;
    • 认证失败:调用 unsuccessfulAuthentication(),清空 SecurityContext,重定向到登录页并显示错误。
关键源码(核心方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 1. 仅允许 POST 方法
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("不支持的认证方法:" + request.getMethod());
}

// 2. 提取用户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);
username = (username != null) ? username.trim() : "";
password = (password != null) ? password : "";

// 3. 封装未认证的 Token
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authRequest);

// 4. 委托 AuthenticationManager 认证
return this.getAuthenticationManager().authenticate(authRequest);
}
自定义配置示例
1
2
3
4
5
6
7
8
9
10
11
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html") // 自定义登录页面
.loginProcessingUrl("/user/login") // 自定义登录请求路径
.usernameParameter("userName") // 自定义用户名参数名(默认 username)
.passwordParameter("passWord") // 自定义密码参数名(默认 password)
.defaultSuccessUrl("/home") // 登录成功默认跳转路径
.failureUrl("/login.html?error") // 登录失败跳转路径
.permitAll();
}

5. AnonymousAuthenticationFilter:匿名用户的 “身份卡”

核心职责:为未认证的用户分配一个 “匿名身份”(AnonymousAuthenticationToken),让 Spring Security 的认证流程能统一处理 “已登录用户” 和 “未登录用户”,避免流程断裂。

为什么需要匿名身份?

Spring Security 的所有请求都需经过 “认证流程”,若用户未登录,SecurityContext 中无 Authentication 对象,后续过滤器(如 FilterSecurityInterceptor)会无法判断用户身份,导致流程异常。匿名身份本质是 “占位符”,让流程能正常执行。

工作流程
  1. 检查身份:从 SecurityContextHolder 中获取 Authentication
  2. 分配匿名身份:若 Authenticationnull(用户未登录),创建 AnonymousAuthenticationToken(包含默认角色 ROLE_ANONYMOUS),存入 SecurityContext
  3. 执行后续流程:匿名身份会被后续过滤器识别,按 “匿名用户” 的权限规则处理(如允许访问 /login,拒绝访问 /admin)。
关键源码(简化版)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

// 1. 若已认证(有 Authentication),直接放行
if (SecurityContextHolder.getContext().getAuthentication() == null) {
// 2. 创建匿名身份 Token(默认角色 ROLE_ANONYMOUS)
Authentication anonymousAuth = new AnonymousAuthenticationToken(
key, anonymousUser, authorities);
// 3. 存入 SecurityContext
SecurityContextHolder.getContext().setAuthentication(anonymousAuth);
}

// 4. 执行后续过滤器
chain.doFilter(request, response);
}
匿名用户的权限控制

可通过 permitAll() 允许匿名用户访问特定路径,或通过 anonymous() 明确指定:

1
2
3
4
5
6
7
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login", "/register").permitAll() // 允许匿名访问登录/注册页
.antMatchers("/anonymous/**").anonymous() // 仅允许匿名用户访问
.anyRequest().authenticated();
}

6. SessionManagementFilter:Session 的 “管理员”

核心职责:管理 HTTP Session,主要处理两大问题:

  1. Session 固定攻击(Session Fixation):防止攻击者通过固定 Session ID 劫持用户会话;
  2. 会话数量控制:限制同一用户同时登录的会话数量(如只允许一个设备登录)。
核心功能:防 Session 固定攻击

Session 固定攻击流程:攻击者诱导用户使用攻击者生成的 Session ID 登录,登录后攻击者用相同 Session ID 即可劫持会话。SessionManagementFilter 通过以下方式防御:

  • 登录时更换 Session ID:用户登录成功后,生成新的 Session ID,废弃旧 ID(默认开启);

  • 配置示例:

    1
    2
    http.sessionManagement()
    .sessionFixation().migrateSession(); // 登录时迁移 Session(更换 Session ID)
核心功能:限制会话数量
1
2
3
4
http.sessionManagement()
.maximumSessions(1) // 同一用户最多允许 1 个会话
.expiredUrl("/login?expired") // 会话过期后跳转路径
.maxSessionsPreventsLogin(true); // 达到最大会话数时,禁止新登录(false 表示踢掉旧会话)

7. ExceptionTranslationFilter:异常的 “翻译官”

核心职责:捕获并处理认证 / 授权过程中抛出的异常(如 AuthenticationExceptionAccessDeniedException),将异常 “翻译” 为用户可理解的响应(如重定向到登录页、返回 403 页面),是安全流程的 “异常处理中心”。

工作流程
  1. 捕获异常:通过 try-catch 捕获后续过滤器链抛出的异常;
  2. 分类处理:
    • 认证异常(AuthenticationException):如用户名密码错误、用户不存在,重定向到登录页;
    • 授权异常(AccessDeniedException):如用户已登录但无权限,若用户是匿名用户则重定向到登录页,否则返回 403 页面;
  3. 委托处理:实际处理逻辑委托给 AuthenticationEntryPoint(处理认证异常)和 AccessDeniedHandler(处理授权异常)。
关键源码(简化版)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

try {
// 1. 执行后续过滤器链,捕获异常
chain.doFilter(request, response);
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
// 2. 提取 Throwable 中的 AuthenticationException 或 AccessDeniedException
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);

if (ase != null) {
// 3. 处理认证异常(如重定向到登录页)
handleAuthenticationException(request, response, chain, ase);
} else {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
if (ase != null) {
// 4. 处理授权异常(如返回 403 页面)
handleAccessDeniedException(request, response, chain, ase);
} else {
// 5. 其他异常,直接抛出
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new RuntimeException(ex);
}
}
}
}
自定义异常处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint((request, response, ex) -> {
// 自定义认证异常处理(如返回 JSON 结果)
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{\"code\":401,\"msg\":\"请先登录\"}");
})
.accessDeniedHandler((request, response, ex) -> {
// 自定义授权异常处理(如返回 403 JSON)
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{\"code\":403,\"msg\":\"无访问权限\"}");
});
}

8. FilterSecurityInterceptor:权限校验的 “裁判”

核心职责:Spring Security 过滤器链的最后一个过滤器,负责判断当前用户是否有权访问目标资源,是权限控制的 “最终裁判”。

工作流程
  1. 封装请求:将 HttpServletRequestHttpServletResponseFilterChain 封装为 FilterInvocation
  2. 获取资源权限:通过 SecurityMetadataSource 获取目标资源(如 /admin/**)所需的权限(如 ROLE_ADMIN);
  3. 权限决策:调用 AccessDecisionManager,对比当前用户的权限(从 SecurityContext 中获取)与资源所需权限;
  4. 处理结果:
    • 有权限:放行请求,执行后续过滤器链(最终到达 Controller);
    • 无权限:抛出 AccessDeniedException,由 ExceptionTranslationFilter 处理。
关键源码(核心方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}

public void invoke(FilterInvocation fi) throws IOException, ServletException {
// 1. 检查是否已处理过该请求(防止重复校验)
if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
return;
}

// 2. 标记请求已处理
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);

InterceptorStatusToken token = null;
try {
// 3. 核心:权限校验(委托给父类 AbstractSecurityInterceptor)
token = super.beforeInvocation(fi);
// 4. 权限校验通过,放行请求
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
// 5. 清理权限校验状态
super.finallyInvocation(token);
}

// 6. 权限校验后的处理(如触发事件)
super.afterInvocation(token, null);
}
权限配置示例
1
2
3
4
5
6
7
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN") // /admin/** 需 ROLE_ADMIN 角色
.antMatchers("/user/**").hasAnyRole("ADMIN", "USER") // /user/** 需 ADMIN 或 USER 角色
.anyRequest().authenticated(); // 其他请求需认证
}

总结:过滤器链的协作逻辑

Spring Security 过滤器链的核心设计思想是 “单一职责 + 链式协作”,每个过滤器专注于一个功能,按顺序协同完成安全控制:

  1. 准备阶段SecurityContextPersistenceFilter 创建安全上下文,CsrfFilter 处理 CSRF Token;
  2. 认证阶段LogoutFilter 处理退出,UsernamePasswordAuthenticationFilter 处理登录,AnonymousAuthenticationFilter 分配匿名身份;
  3. 管理阶段SessionManagementFilter 管理 Session;
  4. 校验阶段FilterSecurityInterceptor 进行权限校验;
  5. 异常处理ExceptionTranslationFilter 捕获并处理全流程异常

欢迎关注我的其它发布渠道