【Spring Security】安全过滤链
Spring Security
- 安全过滤链(Security Filter Chain)
- Security Filter Chain 概览
- 默认核心过滤器职责
- 自定义过滤器的添加与位置控制
- FilterChainProxy
安全过滤链(Security Filter Chain)
Security Filter Chain 概览
Spring Security 的“执行载体”
Spring Security 的核心本质其实是一条 Servlet Filter 链。
当请求进入 Spring Boot 应用时,Spring Security 会把自己的一组过滤器“挂”到 Servlet 的过滤器机制上。
换句话说:
Security 的认证与授权逻辑不是直接写在 Controller 里,而是由一连串 Filter 在请求进入 Controller 之前完成。
请求接入流程(Spring MVC 与 Security)
我们先看一个从浏览器到 Controller 的典型请求路径:
浏览器请求↓
Tomcat (Servlet 容器)↓
DispatcherServlet(Spring MVC)↓
HandlerMapping → Controller
但当你引入 Spring Security 后,路径会变成这样:
浏览器请求↓
Tomcat↓
DelegatingFilterProxy(代理 Filter)↓
FilterChainProxy(Security 的总调度器)↓
SecurityFilterChain(多条安全链之一)↓
15+ 内置过滤器依次执行(认证/授权/异常处理)↓
DispatcherServlet↓
Controller
所以说:Spring Security 并不是“接管”Controller,而是通过过滤器链包裹了整个请求过程。
DelegatingFilterProxy 与 FilterChainProxy 的关系
这两个名字很像,容易混淆,我们分清楚:
组件 | 所属层 | 职责 |
---|---|---|
DelegatingFilterProxy | Servlet 容器层(Tomcat) | 是一个“桥梁”,把 Servlet 的过滤调用转交给 Spring 容器中的 Bean |
FilterChainProxy | Spring Security 内部 | 是真正的安全过滤器调度器,负责执行所有 SecurityFilterChain |
执行顺序大概是:
Tomcat → DelegatingFilterProxy → FilterChainProxy → SecurityFilterChain → 各种Security Filter
重点:
DelegatingFilterProxy
是一个普通的 Servlet 过滤器,它会查找 Spring 容器中名为 "springSecurityFilterChain"
的 Bean,然后把过滤任务交给它。这个 Bean 就是 FilterChainProxy。
SecurityFilterChain 的组成与匹配机制
一个应用可以有多个 SecurityFilterChain
每条链对应一组安全配置。
例如:
@Configuration
@EnableWebSecurity
public class MultiSecurityConfig {@BeanSecurityFilterChain apiChain(HttpSecurity http) throws Exception {http.securityMatcher("/api/**") // 只匹配 API 请求.authorizeHttpRequests(auth -> auth.anyRequest().authenticated()).httpBasic();return http.build();}@BeanSecurityFilterChain webChain(HttpSecurity http) throws Exception {http.securityMatcher("/web/**") // 只匹配 Web 页面.authorizeHttpRequests(auth -> auth.anyRequest().permitAll()).formLogin();return http.build();}
}
当请求进入时:
- 如果路径是
/api/**
→ 使用第一条链; - 如果路径是
/web/**
→ 使用第二条链。
匹配机制:
FilterChainProxy
会逐条检查 SecurityFilterChain
,找到第一个 .matches(request)
为 true
的链,然后只执行那一条。
请求从进入到放行的简化流程图
默认核心过滤器职责
Spring Security 的整个认证与授权机制,都是靠一条巨大的 “过滤链”(Security Filter Chain) 实现的。
所有的 HTTP 请求(如 /login
, /user
, /api/**
)都会经过这些过滤器层层处理。
每个过滤器(Filter
)负责一块安全逻辑:
- 有的做登录认证;
- 有的做登出;
- 有的检查权限;
- 有的防止 CSRF;
- 有的加安全 Header;
- 有的包装请求对象。
底层类: org.springframework.security.web.FilterChainProxy
它负责:
- 根据 URL 匹配不同的过滤链;
- 依次执行每个过滤器;
- 控制异常处理与认证授权的流程。
Spring Security 默认过滤器链(15+ 核心过滤器)
-
WebAsyncManagerIntegrationFilter
-
WebAsyncManagerIntegrationFilter
是 Spring Security 过滤器链中的第一个核心过滤器,主要解决 异步请求处理中的安全上下文传递问题,确保异步线程(如@Async
方法、异步控制器)能正确获取当前用户的认证信息。 -
作用是 将请求线程的
SecurityContext
与 Spring Web 的异步管理器(WebAsyncManager
)绑定,确保异步线程执行时能继承原始请求的安全上下文。 -
具体工作流程:
-
请求到达时:过滤器拦截请求,从
SecurityContextHolder
中获取当前线程的SecurityContext
(包含用户认证信息)。 -
绑定到异步管理器:将
SecurityContext
存入WebAsyncManager
(Spring 管理异步请求的核心组件)的属性中,确保异步任务启动时能拿到这个上下文。 -
异步线程执行时:当异步任务(如
@Async
方法)开始执行前,WebAsyncManager
会将绑定的SecurityContext
重新设置到异步线程的SecurityContextHolder
中。 -
异步任务结束后:自动清除异步线程的
SecurityContextHolder
,避免线程池中的线程被复用后导致安全上下文泄露。
-
-
代码示例:假设一个异步控制器方法
@RestController public class AsyncController {@GetMapping("/async")public CompletableFuture<String> asyncRequest() {// 此处为请求线程,能正常获取 SecurityContextAuthentication auth = SecurityContextHolder.getContext().getAuthentication();String username = auth.getName();// 异步处理(由其他线程执行)return CompletableFuture.supplyAsync(() -> {// 若没有 WebAsyncManagerIntegrationFilter,此处 auth 会为 nullAuthentication asyncAuth = SecurityContextHolder.getContext().getAuthentication();return "异步线程获取的用户名:" + asyncAuth.getName();});} }
-
有
WebAsyncManagerIntegrationFilter
:异步线程中asyncAuth
能正确获取用户名。 -
无此过滤器:异步线程中
asyncAuth
为null
(安全上下文丢失)。
-
-
-
SecurityContextHolderFilter
-
SecurityContextHolderFilter
是 Spring Security 6.x 及以上版本新增的核心过滤器(替代了旧版的SecurityContextPersistenceFilter
),负责 管理SecurityContext
的生命周期,是整个安全上下文(用户认证信息)在请求过程中传递的 “核心管家”。 -
作用是 在请求开始时加载
SecurityContext
到SecurityContextHolder
,在请求结束时保存SecurityContext
并清理资源,确保用户认证状态能跨请求共享(如登录后,后续请求无需重复认证)。 -
工作流程(分两步)
-
请求到达时:加载 SecurityContext 到线程
当一个请求进入应用时,过滤器会:
- 从 Session 中获取之前保存的
SecurityContext
(如果存在,比如用户已登录); - 若 Session 中没有
SecurityContext
(如首次访问或未登录用户),则创建一个空的SecurityContext
; - 将获取到的
SecurityContext
设置到SecurityContextHolder
中(绑定到当前请求线程的ThreadLocal
变量)。
此时,后续的过滤器(如认证、授权过滤器)和控制器(
Controller
)就能通过SecurityContextHolder.getContext()
轻松获取用户认证信息。 - 从 Session 中获取之前保存的
-
请求结束时:保存并清理 SecurityContext
当请求处理完成(响应返回给客户端)时,过滤器会:
- 从
SecurityContextHolder
中取出当前线程的SecurityContext
; - 将
SecurityContext
保存回 Session(确保下次请求能复用认证状态); - 清除
SecurityContextHolder
中的SecurityContext
(避免线程池复用线程时导致的信息泄露)。
- 从
-
-
代码示例:假设用户已登录,在控制器中获取用户信息
@RestController public class UserController {@GetMapping("/user")public String getCurrentUser() {// 直接从 SecurityContextHolder 获取认证信息// 这些信息正是由 SecurityContextHolderFilter 从 Session 加载到线程中的Authentication auth = SecurityContextHolder.getContext().getAuthentication();return "当前登录用户:" + auth.getName();} }
-
如果没有
SecurityContextHolderFilter
:SecurityContextHolder.getContext()
会返回空,无法获取用户信息; -
有此过滤器:即使是跨请求(如登录后访问多个接口),
SecurityContext
也能通过 Session 保存,实现认证状态的延续。
-
-
-
HeaderWriterFilter
-
HeaderWriterFilter
是 Spring Security 过滤器链中的第三个核心过滤器,主要职责是 自动向 HTTP 响应头中添加安全相关的头部信息,通过浏览器的内置安全机制增强应用的安全性,属于 “防御性安全措施”。 -
HeaderWriterFilter
通过内部的HeaderWriter
接口实现响应头的添加,默认情况下会添加以下关键安全头(可通过配置自定义或禁用):响应头 作用说明 X-Content-Type-Options: nosniff
禁止浏览器对响应内容进行 “MIME 类型嗅探”。例如:防止将本应作为文本的 .txt
文件被浏览器误认为可执行脚本(text/plain
→text/html
),减少 XSS 风险。X-Frame-Options: DENY
禁止当前页面被嵌入到任何 <iframe>
或<frame>
中,防御 “点击劫持” 攻击(攻击者诱导用户点击嵌入在恶意页面中的合法按钮)。可选值:DENY
(完全禁止)、SAMEORIGIN
(仅允许同域嵌入)。X-XSS-Protection: 1; mode=block
启用浏览器内置的 XSS 过滤器。- 1
表示启用;-mode=block
表示检测到 XSS 攻击时,直接阻止页面加载(而非尝试修复),避免部分攻击绕过过滤。Strict-Transport-Security: max-age=31536000; includeSubDomains
(可选)启用 HTTP 严格传输安全(HSTS),强制浏览器后续仅通过 HTTPS 访问本域名及子域名,防止 “中间人攻击” 降级到 HTTP。- max-age
:HSTS 规则在浏览器中的缓存时间(秒);- 需通过配置显式启用(默认不添加)。 -
工作流程
-
请求处理阶段:当请求经过
HeaderWriterFilter
时,它会暂存当前的响应对象(HttpServletResponse
)。 -
响应返回前:在响应即将发送给客户端时,
HeaderWriterFilter
调用内部的HeaderWriter
实现类,向响应头中添加预设的安全头。 -
自定义扩展:开发者可通过配置添加自定义头(如
Content-Security-Policy
)或修改默认头的取值。
-
-
代码示例:自定义或修改响应头
在 Spring Security 配置类中,可通过
headers()
方法定制HeaderWriterFilter
的行为@Configuration @EnableWebSecurity public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http// 配置响应头.headers(headers -> headers// 修改 X-Frame-Options 为 SAMEORIGIN(允许同域嵌入).frameOptions(frame -> frame.sameOrigin())// 启用 HSTS 头.httpStrictTransportSecurity(hsts -> hsts.includeSubDomains(true).maxAgeInSeconds(31536000) // 1年)// 添加自定义头(如内容安全策略).addHeaderWriter(new StaticHeadersWriter("Content-Security-Policy", "default-src 'self'; script-src 'self'"))// 禁用某个默认头(如 X-XSS-Protection).xssProtection(xss -> xss.disable()));return http.build();} }
-
-
CorsFilter
-
CorsFilter
是 Spring Security 中处理跨域资源共享(CORS) 的过滤器,用于解决前端应用(如 Vue、React)与后端 API 不在同一域名下时的跨域请求限制问题。它并非默认启用,需通过配置显式开启,是前后端分离项目中的常用组件。 -
CorsFilter
会拦截所有请求,根据预设的跨域规则(如允许的来源、方法、请求头等)进行校验,并在响应中添加对应的 CORS 头,具体工作流程如下:-
拦截请求,提取跨域信息:从请求中解析跨域相关参数,如:
Origin
头:表示请求来自哪个域名(如http://localhost:3000
);Access-Control-Request-Method
头:表示前端请求使用的 HTTP 方法(如 POST、PUT);Access-Control-Request-Headers
头:表示前端请求携带的自定义头(如Authorization
)。
-
根据配置的跨域规则进行校验:检查请求的来源、方法、头等是否符合后端配置的允许规则(如是否在允许的域名列表中)。
-
生成 CORS 响应头:若校验通过,向响应中添加允许跨域的头信息,常见的有:
响应头 作用说明 Access-Control-Allow-Origin
指定允许跨域请求的来源(如 http://localhost:3000
或*
表示允许所有)。Access-Control-Allow-Methods
指定允许跨域请求的 HTTP 方法(如 GET, POST, PUT, DELETE
)。Access-Control-Allow-Headers
指定允许跨域请求携带的自定义头(如 Authorization, Content-Type
)。Access-Control-Allow-Credentials
表示是否允许跨域请求携带 Cookie( true
或false
),配合前端withCredentials
使用。Access-Control-Max-Age
指定预检请求(OPTIONS)的结果缓存时间(秒),避免频繁校验。 -
处理预检请求(OPTIONS):对于 “非简单请求”(如 POST 且 Content-Type 为
application/json
、使用自定义头、PUT/DELETE 方法等),浏览器会先发送一次 OPTIONS 预检请求,验证后端是否允许跨域。CorsFilter
会处理预检请求,返回上述 CORS 头,无需业务逻辑介入。
-
-
代码示例:启用与配置
在 Spring Security 中,需通过
.cors()
显式启用CorsFilter
,并配置跨域规则:@Configuration @EnableWebSecurity public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http// 启用 CORS 过滤器.cors(cors -> cors// 配置跨域规则.configurationSource(corsConfigurationSource()))// 其他配置(如关闭 CSRF,前后端分离常用).csrf(csrf -> csrf.disable());return http.build();}// 定义跨域规则@Beanpublic CorsConfigurationSource corsConfigurationSource() {CorsConfiguration config = new CorsConfiguration();// 允许的来源(生产环境需指定具体域名,避免 * 带来的安全风险)config.setAllowedOrigins(Arrays.asList("http://localhost:3000"));// 允许的方法config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));// 允许的请求头(包括自定义头,如 Authorization)config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));// 允许携带 Cookie(如需)config.setAllowCredentials(true);// 预检请求缓存时间(3600秒 = 1小时)config.setMaxAge(3600L);// 应用到所有路径UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", config);return source;} }
-
-
CsrfFilter
-
CsrfFilter
是 Spring Security 中用于防御跨站请求伪造(CSRF)攻击的核心过滤器,默认启用,主要针对 “修改型请求”(如 POST、PUT、DELETE 等)进行安全校验,是 Web 应用常见的安全防护手段。 -
CsrfFilter
通过令牌校验机制防止 CSRF 攻击,确保请求是用户 “主动且知情” 发起的,而非恶意网站伪造的。其核心逻辑是:- 为每个用户会话生成一个唯一的 CSRF 令牌(随机字符串),存储在 Session 中。
- 要求所有 “修改型请求”(非 GET、HEAD、OPTIONS、TRACE)必须携带此令牌。
- 校验请求中的令牌与 Session 中存储的令牌是否一致,不一致则拒绝请求。
-
工作流程
- 生成与存储令牌:
- 用户首次访问应用时,
CsrfFilter
会生成一个 CSRF 令牌(如743f3a3b-2e11-4d4b-9f2d-21655b9f2d1a
),存储在 Session 中(HttpSessionCsrfTokenRepository
是默认实现)。 - 令牌会通过 请求属性(
_csrf
)暴露给前端(如用于表单隐藏域或请求头)。
- 用户首次访问应用时,
- 前端携带令牌:前端在发起 “修改型请求” 时,必须通过以下方式之一携带令牌:
- 表单提交:通过隐藏域
<input type="hidden" name="_csrf" value="${_csrf.token}"/>
。 - AJAX 请求:通过请求头(如
X-CSRF-TOKEN
)携带令牌值。
- 表单提交:通过隐藏域
- 后端校验令牌:
CsrfFilter
拦截请求,提取请求中的令牌(从参数_csrf
或头X-CSRF-TOKEN
中获取)。- 对比请求令牌与 Session 中存储的令牌:
- 一致:放行请求,继续处理。
- 不一致或缺失:抛出
CsrfException
,拒绝请求(默认返回 403 Forbidden)。
- 生成与存储令牌:
-
代码示例:自定义 CSRF 行为
@Configuration @EnableWebSecurity public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.csrf(csrf -> csrf// 自定义令牌存储库(默认是 HttpSessionCsrfTokenRepository).csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())// 忽略某些路径的 CSRF 校验(如第三方回调、API 接口).ignoringRequestMatchers("/api/webhook/**", "/public/**")// 自定义 CSRF 异常处理器(返回 JSON 而非默认页面).accessDeniedHandler((request, response, ex) -> {response.setContentType("application/json;charset=UTF-8");response.getWriter().write("{\"error\":\"CSRF 令牌验证失败\"}");}));return http.build();} }
CookieCsrfTokenRepository
:将令牌存储在 Cookie 中(而非 Session),适合前后端分离场景(无 Session 时仍可传递令牌)。ignoringRequestMatchers
:对无需 CSRF 保护的接口(如公开 API、第三方回调)关闭校验,减少不必要的开销。
-
何时需要禁用 CSRF?
CsrfFilter
默认启用,但以下场景通常会禁用:- 前后端分离 + JWT 无状态认证:JWT 令牌通常通过请求头(如
Authorization
)传递,且无 Session,CSRF 攻击风险极低(攻击方无法获取 JWT 令牌)。 - 纯 API 服务:仅对外提供 API 且客户端不是浏览器(如移动端 App),不存在 CSRF 攻击的前提(浏览器自动携带 Cookie)。
禁用方式:
http.csrf(csrf -> csrf.disable());
- 前后端分离 + JWT 无状态认证:JWT 令牌通常通过请求头(如
-
-
LogoutFilter
-
LogoutFilter
是 Spring Security 中专门处理用户登出流程的核心过滤器,负责拦截登出请求并执行一系列清理操作,确保用户安全退出系统。它在过滤器链中位于 CSRF 过滤器之后,认证相关过滤器之前,是登出功能的 “总调度器”。 -
用户发起登出请求时,
LogoutFilter
会按固定逻辑完成以下操作,确保用户认证状态被彻底清除:- 清除安全上下文:删除
SecurityContextHolder
中存储的当前用户认证信息(Authentication
),使当前线程无法再获取用户身份。 - 使会话失效:调用
HttpSession.invalidate()
销毁当前用户会话,防止会话被复用。 - 删除关联 Cookie:移除与认证相关的 Cookie(如
JSESSIONID
、“记住我” 功能的remember-me
Cookie)。 - 清除 “记住我” 令牌:如果启用了 “记住我” 功能,会通知
RememberMeServices
失效对应的令牌,避免用户再次通过 “记住我” 自动登录。 - 触发登出成功处理器:登出完成后,调用
LogoutSuccessHandler
处理后续逻辑(如重定向到登录页、返回登出成功的 JSON 响应)。
- 清除安全上下文:删除
-
工作流程
- 拦截登出请求:
LogoutFilter
会拦截预设的登出路径(默认是GET /logout
,可通过配置修改路径和请求方法)。例如:用户点击 “退出登录” 按钮,前端发送POST /api/logout
请求,LogoutFilter
会捕获该请求。 - 执行登出操作:调用配置的
LogoutHandler
列表(默认包含以下处理器,可自定义扩展):SecurityContextLogoutHandler
:负责清除SecurityContext
和使会话失效(核心处理器)。CookieClearingLogoutHandler
:删除指定的 Cookie(如配置了.deleteCookies("JSESSIONID")
)。RememberMeServices
(若启用):清除 “记住我” 令牌。
- 处理登出结果:登出操作完成后,由
LogoutSuccessHandler
决定后续行为:- 传统 Web 应用:默认重定向到登录页(附加
?logout
参数,如/login?logout
)。 - 前后端分离应用:通常返回 JSON 响应(如
{"success": true, "message": "登出成功"}
),由前端处理跳转。
- 传统 Web 应用:默认重定向到登录页(附加
- 拦截登出请求:
-
代码示例:自定义登出行为
@Configuration @EnableWebSecurity public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.logout(logout -> logout// 自定义登出请求路径(默认是 /logout).logoutUrl("/api/logout")// 允许的请求方法(默认只支持 GET,前后端分离常用 POST).logoutMethod(HttpMethod.POST)// 登出时删除的 Cookie.deleteCookies("JSESSIONID", "remember-me")// 清除“记住我”令牌(需注入 RememberMeServices).rememberMeServices(rememberMeServices())// 登出成功后不保存请求缓存(避免重定向到登出前的页面).clearAuthentication(true).invalidateHttpSession(true)// 自定义登出成功处理器(前后端分离返回 JSON).logoutSuccessHandler((request, response, authentication) -> {response.setContentType("application/json;charset=UTF-8");response.getWriter().write("{\"success\":true, \"message\":\"登出成功\", \"redirect\":\"/login\"}");})// 登出失败处理器(如令牌无效时).logoutFailureHandler((request, response, exception) -> {response.setStatus(HttpServletResponse.SC_BAD_REQUEST);response.getWriter().write("{\"error\":\"登出失败:" + exception.getMessage() + "\"}");}));return http.build();}// 假设启用了“记住我”功能,需配置对应的 RememberMeServices@Beanpublic RememberMeServices rememberMeServices() {return new PersistentTokenBasedRememberMeServices("remember-me-key",userDetailsService(),persistentTokenRepository());}// 其他依赖 Bean(省略实现)@Bean public UserDetailsService userDetailsService() { ... }@Bean public PersistentTokenRepository persistentTokenRepository() { ... } }
-
-
OAuth2AuthorizationRequestRedirectFilter
-
OAuth2AuthorizationRequestRedirectFilter
是 Spring Security 中处理 OAuth2 认证流程第一步(授权请求重定向) 的过滤器,仅在集成第三方登录(如 GitHub、Google、微信登录等)时启用。它的核心作用是:拦截前端发起的第三方登录请求,生成符合 OAuth2 协议的授权 URL,并将用户重定向到第三方授权服务器的登录授权页面。 -
核心职责
- 拦截第三方登录请求:监听预设的第三方登录触发路径(默认格式为
/oauth2/authorization/{registrationId}
,其中registrationId
是第三方服务的唯一标识,如github
、wechat
)。例如:用户点击 “使用 GitHub 登录”,前端跳转至/oauth2/authorization/github
,此请求会被该过滤器拦截。 - 构建 OAuth2 授权请求:根据预配置的第三方服务信息(如客户端 ID、客户端密钥、授权范围、回调地址等),生成符合 OAuth2 协议的授权请求参数,包括:
response_type=code
(授权码模式,最常用的 OAuth2 模式);client_id
(你的应用在第三方平台的唯一标识);redirect_uri
(授权成功后第三方服务器回调你的应用的地址);scope
(请求的权限范围,如user:email
表示获取用户邮箱);state
(随机字符串,用于防止 CSRF 攻击,后续会验证)。
- 重定向到第三方授权页面:将上述参数拼接为第三方授权服务器的授权 URL(如 GitHub 的
https://github.com/login/oauth/authorize?client_id=xxx&redirect_uri=xxx&...
),并将用户浏览器重定向到该 URL,引导用户在第三方平台完成登录和授权。
- 拦截第三方登录请求:监听预设的第三方登录触发路径(默认格式为
-
工作流程(以 GitHub 登录为例)
- 用户触发第三方登录:前端页面点击 “GitHub 登录”,请求后端路径
/oauth2/authorization/github
。 - 过滤器拦截并生成授权请求:
OAuth2AuthorizationRequestRedirectFilter
识别到registrationId=github
,从配置中读取 GitHub 第三方服务的信息(客户端 ID、回调地址等)。- 生成随机
state
参数,存储到 Session 或OAuth2AuthorizationRequestRepository
中(用于后续回调验证)。 - 拼接授权 URL:
https://github.com/login/oauth/authorize?client_id=你的GitHub客户端ID&redirect_uri=https://你的应用域名/login/oauth2/code/github&response_type=code&scope=user:email&state=随机字符串
。
- 重定向到 GitHub 授权页:用户浏览器被重定向到上述 URL,显示 GitHub 的登录页面(若用户已登录 GitHub,则直接显示授权确认页,询问是否允许你的应用获取其邮箱等信息)。
- 后续流程:用户授权后,GitHub 会将用户重定向回你的应用的回调地址(如
/login/oauth2/code/github
),并携带code
(授权码)和state
参数,后续由OAuth2LoginAuthenticationFilter
处理(验证state
、用code
换令牌等)。
- 用户触发第三方登录:前端页面点击 “GitHub 登录”,请求后端路径
-
代码示例:启用 GitHub 登录
要启用
OAuth2AuthorizationRequestRedirectFilter
,需引入 OAuth2 客户端依赖并配置第三方服务信息:-
引入依赖(Maven)
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency>
-
配置第三方服务信息(application.yml)
spring:security:oauth2:client:registration:# registrationId 为 githubgithub:client-id: 你的GitHub客户端ID # 在GitHub开发者平台申请client-secret: 你的GitHub客户端密钥scope: user:email # 请求的权限范围provider:github:authorization-uri: https://github.com/login/oauth/authorize # 授权页URL(GitHub固定)token-uri: https://github.com/login/oauth/access_token # 令牌获取URL(GitHub固定)user-info-uri: https://api.github.com/user # 用户信息URL(GitHub固定)
-
Spring Security 配置类
@Configuration @EnableWebSecurity public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())// 启用 OAuth2 登录,自动注册 OAuth2AuthorizationRequestRedirectFilter 等过滤器.oauth2Login(oauth2 -> oauth2// 自定义登录页(可选,默认会生成一个登录页,包含第三方登录按钮).loginPage("/custom-login")// 自定义回调地址(需与第三方平台配置的回调地址一致,默认是 /login/oauth2/code/{registrationId}).redirectUri("/login/oauth2/code/github"));return http.build();} }
-
-
注意事项
registrationId
的作用:用于区分不同的第三方服务(如同时集成 GitHub 和 Google 登录时,registrationId
分别为github
和google
),过滤器通过它定位对应的第三方配置。state
参数的安全性:过滤器生成的state
会存储在 Session 中,后续回调时会验证请求中的state
与存储的是否一致,防止 CSRF 攻击和请求伪造。- 回调地址的一致性:配置的
redirect_uri
必须与在第三方平台(如 GitHub 开发者中心)注册的回调地址完全一致,否则授权服务器会拒绝回调。 - 仅支持授权码模式:该过滤器主要用于 OAuth2 的 “授权码模式”(最安全的模式),不支持简化模式等其他模式。
-
-
OAuth2LoginAuthenticationFilter
-
OAuth2LoginAuthenticationFilter
是 Spring Security 中处理 OAuth2 第三方登录流程第二步(授权回调与认证) 的核心过滤器,专门用于接收第三方授权服务器的回调请求,完成授权码验证、令牌获取和用户身份认证,最终生成用户在当前应用中的认证信息。 -
核心职责
- 拦截第三方授权回调请求:监听预设的回调路径(默认格式为
/login/oauth2/code/{registrationId}
,其中registrationId
是第三方服务标识,如github
)。例如:用户在 GitHub 授权后,被重定向到https://你的应用域名/login/oauth2/code/github?code=xxx&state=xxx
,此请求会被该过滤器拦截。 - 验证回调参数的合法性:
- 校验
state
参数:确保回调请求中的state
与之前发送授权请求时存储的state
一致(防止 CSRF 攻击和请求伪造)。 - 提取
code
参数:获取第三方服务器返回的授权码(后续用于换取令牌)。
- 校验
- 用授权码换取访问令牌:向第三方授权服务器的令牌端点(如 GitHub 的
https://github.com/login/oauth/access_token
)发送请求,携带授权码、客户端 ID、客户端密钥等参数,换取访问令牌(access_token)、刷新令牌(refresh_token)等信息。 - 获取第三方用户信息:使用获取到的访问令牌,调用第三方服务器的用户信息端点(如 GitHub 的
https://api.github.com/user
),获取用户的基本信息(用户名、邮箱、头像等)。 - 创建本地认证信息:将第三方用户信息转换为 Spring Security 可识别的
Authentication
对象(OAuth2AuthenticationToken
),包含用户身份、权限等信息,并将其存入SecurityContextHolder
,标记用户在当前应用中已登录。 - 处理认证结果:
- 认证成功:触发
AuthenticationSuccessHandler
(默认重定向到应用首页,可自定义为返回 JSON 或跳转到指定页面)。 - 认证失败:触发
AuthenticationFailureHandler
(默认返回错误页面,可自定义为返回错误信息)。
- 认证成功:触发
- 拦截第三方授权回调请求:监听预设的回调路径(默认格式为
-
工作流程(以 GitHub 登录为例,承接上一步的授权重定向)
-
第三方授权回调:用户在 GitHub 授权后,被重定向到
https://你的应用域名/login/oauth2/code/github?code=abc123&state=xyz456
。 -
过滤器拦截并验证参数:
OAuth2LoginAuthenticationFilter
拦截请求,提取code=abc123
和state=xyz456
。- 验证
state
:对比请求中的xyz456
与之前存储在 Session 中的state
(由OAuth2AuthorizationRequestRedirectFilter
生成),一致则继续。
-
换取访问令牌:过滤器向 GitHub 的令牌端点发送请求:
POST https://github.com/login/oauth/access_token 参数:code=abc123client_id=你的GitHub客户端IDclient_secret=你的GitHub客户端密钥redirect_uri=https://你的应用域名/login/oauth2/code/github
GitHub 返回令牌信息:
access_token=gho_xxx&token_type=bearer
。 -
获取用户信息:过滤器使用
access_token=gho_xxx
调用 GitHub 用户信息接口:GET https://api.github.com/user Header: Authorization: bearer gho_xxx
GitHub 返回用户信息(如
login=zhangsan
、email=zhangsan@github.com
)。 -
创建本地认证对象:将 GitHub 用户信息转换为
OAuth2AuthenticationToken
,包含用户名zhangsan
、权限ROLE_USER
等,并设置到SecurityContextHolder
中,用户在当前应用中登录成功。 -
跳转至首页:通过
AuthenticationSuccessHandler
重定向到应用首页(如/home
)。
-
-
代码示例:自定义 OAuth2 登录回调行为
在 Spring Security 配置中,可通过
oauth2Login()
定制OAuth2LoginAuthenticationFilter
的行为:@Configuration @EnableWebSecurity public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated()).oauth2Login(oauth2 -> oauth2// 自定义回调路径(需与第三方平台配置一致).redirectUri("/oauth2/callback/{registrationId}")// 自定义认证成功处理器(如返回JSON或跳转到指定页面).successHandler((request, response, authentication) -> {// authentication 为 OAuth2AuthenticationToken,包含第三方用户信息String username = authentication.getName();response.sendRedirect("/welcome?user=" + username);})// 自定义认证失败处理器.failureHandler((request, response, exception) -> {response.setContentType("application/json;charset=UTF-8");response.getWriter().write("{\"error\":\"第三方登录失败:" + exception.getMessage() + "\"}");})// 自定义用户信息映射(将第三方用户信息转换为本地UserDetails).userInfoEndpoint(userInfo -> userInfo.userService(oauth2UserService())));return http.build();}// 自定义用户信息服务:将第三方用户信息转换为本地UserDetailsprivate OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {return new DefaultOAuth2UserService() {@Overridepublic OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {OAuth2User oauth2User = super.loadUser(userRequest);// 从第三方用户信息中提取字段(如GitHub的"login"对应用户名,"email"对应邮箱)Map<String, Object> attributes = oauth2User.getAttributes();String username = (String) attributes.get("login");String email = (String) attributes.get("email");// 构建本地用户信息(可添加自定义权限)return User.withUsername(username).password("N/A") // 第三方登录无需本地密码.authorities("ROLE_USER").build();}};} }
-
注意事项
- 回调路径的一致性:配置的回调路径(如
/login/oauth2/code/github
)必须与在第三方平台(如 GitHub 开发者中心)注册的回调地址完全一致,否则第三方服务器会拒绝回调。 - 用户信息的映射:第三方返回的用户信息字段(如 GitHub 的
login
、微信的nickname
)可能不同,需通过userInfoEndpoint().userService()
自定义映射逻辑,将其转换为本地应用的UserDetails
或OAuth2User
对象。 - 令牌的存储与刷新:过滤器默认会将访问令牌、刷新令牌等信息存储在
OAuth2AuthorizedClientService
中,可用于后续访问第三方 API(如调用 GitHub 接口获取用户仓库)。若令牌过期,会自动使用刷新令牌获取新令牌(需第三方支持)。 - 仅用于授权码模式:该过滤器仅支持 OAuth2 的 “授权码模式”(最安全的模式),与
OAuth2AuthorizationRequestRedirectFilter
配合完成完整的第三方登录流程。
- 回调路径的一致性:配置的回调路径(如
-
-
UsernamePasswordAuthenticationFilter
-
UsernamePasswordAuthenticationFilter
是 Spring Security 中处理 基于用户名 / 密码的表单登录 的核心过滤器,默认拦截POST /login
请求,负责从请求中提取用户名和密码,执行认证逻辑,并处理认证成功 / 失败的后续操作。它是传统账号密码登录场景的 “核心处理器”。 -
核心职责
- 拦截登录请求:专门拦截预设的登录路径(默认是
POST /login
,可通过配置修改路径和请求方法),仅处理携带用户名和密码的登录请求。 - 提取认证凭证:从请求参数中提取
username
和password
(默认参数名,可自定义),构建未认证的UsernamePasswordAuthenticationToken
对象(包含用户名、密码,但未设置认证状态)。 - 执行认证逻辑:将未认证的
UsernamePasswordAuthenticationToken
交给AuthenticationManager
(认证管理器)处理,由其调用UserDetailsService
加载用户信息,并通过PasswordEncoder
校验密码是否匹配。 - 处理认证结果:
- 认证成功:触发
AuthenticationSuccessHandler
(默认重定向到首页,可自定义为返回 JSON 令牌等),并将认证通过的Authentication
对象存入SecurityContextHolder
。 - 认证失败:触发
AuthenticationFailureHandler
(默认返回错误页面,可自定义为返回错误信息)。
- 认证成功:触发
- 拦截登录请求:专门拦截预设的登录路径(默认是
-
工作流程(传统表单登录为例)
- 用户提交登录表单:前端通过表单(
method="POST"
,action="/login"
)提交用户名和密码,请求格式为application/x-www-form-urlencoded
(如username=admin&password=123456
)。 - 过滤器拦截并提取凭证:
UsernamePasswordAuthenticationFilter
拦截POST /login
请求,从请求参数中获取username=admin
和password=123456
,构建UsernamePasswordAuthenticationToken
(未认证状态)。 - 调用认证管理器:过滤器将令牌交给
AuthenticationManager
,后者委托UserDetailsService
加载用户信息(如从数据库查询admin
的加密密码$2a$10$xxx
),再通过PasswordEncoder
比对提交的密码与存储的加密密码是否一致。 - 处理认证结果:
- 成功:
AuthenticationManager
返回认证通过的Authentication
令牌(包含用户权限等信息),过滤器调用AuthenticationSuccessHandler
重定向到/home
,并将令牌存入SecurityContextHolder
(标记用户已登录)。 - 失败:
AuthenticationManager
抛出异常(如BadCredentialsException
),过滤器调用AuthenticationFailureHandler
重定向到/login?error
,显示错误信息。
- 成功:
- 用户提交登录表单:前端通过表单(
-
代码示例:自定义登录行为
可通过 Spring Security 配置定制过滤器的行为,适应不同场景(如修改登录路径、自定义参数名、返回 JSON 等):
@Configuration @EnableWebSecurity public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth.requestMatchers("/login").permitAll() // 登录页允许匿名访问.anyRequest().authenticated()).formLogin(form -> form// 自定义登录请求路径(默认是 /login).loginProcessingUrl("/api/login")// 自定义用户名/密码参数名(默认是 username/password).usernameParameter("user").passwordParameter("pass")// 认证成功处理器(前后端分离返回 JSON 令牌).successHandler((request, response, authentication) -> {response.setContentType("application/json;charset=UTF-8");// 生成 JWT 令牌(示例)String token = generateJwt(authentication);response.getWriter().write("{\"success\":true, \"token\":\"" + token + "\"}");})// 认证失败处理器.failureHandler((request, response, exception) -> {response.setContentType("application/json;charset=UTF-8");response.getWriter().write("{\"error\":\"登录失败:" + exception.getMessage() + "\"}");}));return http.build();}// 示例:生成 JWT 令牌(实际需引入 JWT 库实现)private String generateJwt(Authentication authentication) {return "jwt-token-for-" + authentication.getName();}// 配置密码编码器(必须配置,否则启动报错)@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}// 配置用户详情服务(从数据库加载用户,示例为内存用户)@Beanpublic UserDetailsService userDetailsService() {UserDetails user = User.withUsername("admin").password(passwordEncoder().encode("123456")).roles("ADMIN").build();return new InMemoryUserDetailsManager(user);} }
前后端分离场景的适配问题与解决
默认的
UsernamePasswordAuthenticationFilter
仅支持application/x-www-form-urlencoded
格式(表单提交),而前后端分离项目通常使用application/json
格式传递用户名和密码,此时需要 自定义过滤器替换它,示例如下:// 自定义 JSON 登录过滤器 public class JsonUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {// 从 JSON 中提取用户名和密码try {Map<String, String> credentials = new ObjectMapper().readValue(request.getInputStream(), Map.class);String username = credentials.get("username");String password = credentials.get("password");// 构建未认证令牌,调用认证管理器UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);return this.getAuthenticationManager().authenticate(token);} catch (IOException e) {throw new AuthenticationServiceException("解析登录参数失败", e);}} }// 在配置中替换默认过滤器 @Configuration public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.addFilterAt(new JsonUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)// 其他配置...} }
-
注意事项
- 请求格式限制:默认仅支持
application/x-www-form-urlencoded
(表单),JSON 格式需自定义过滤器(如上例)。 - 登录路径与方法:默认拦截
POST /login
,可通过loginProcessingUrl()
修改路径,但必须使用 POST 方法(GET 方法不安全,会暴露密码)。 - 与 CSRF 的配合:若未禁用 CSRF,登录请求需携带 CSRF 令牌(如表单隐藏域或请求头),否则会被
CsrfFilter
拦截。 - 密码加密:必须配置
PasswordEncoder
(如BCryptPasswordEncoder
),否则认证时会抛出IllegalArgumentException
(Spring Security 5+ 强制要求)。
- 请求格式限制:默认仅支持
-
-
DefaultLoginPageGeneratingFilter
-
DefaultLoginPageGeneratingFilter
是 Spring Security 中一个辅助性过滤器,主要作用是:当开发者未自定义自定义自定义登录页面时,自动生成一个默认的 HTML 登录表单页面,避免因缺少登录页导致的 404 错误或认证流程中断。 -
核心职责
- 检测是否存在自定义登录页:过滤器会判断开发者是否通过
.formLogin().loginPage("/custom-login")
配置了自定义登录页。如果已配置,此过滤器不生效;如果未配置,它会自动介入。 - 生成默认登录表单页面:当用户访问需要认证的资源(如
/home
)时,若未登录,会被重定向到默认登录路径(默认/login
)。此时,该过滤器会生成一个包含以下元素的 HTML 登录页面:- 用户名输入框(
name="username"
); - 密码输入框(
name="password"
); - 提交按钮(
type="submit"
); - CSRF 令牌隐藏域(
name="_csrf"
,用于防御 CSRF 攻击); - “记住我” 选项(若启用
rememberMe()
配置); - 错误提示区域(当登录失败时显示错误信息,如 “用户名或密码错误”)。
- 用户名输入框(
- 处理登录页请求:拦截对默认登录路径(
/login
)的 GET 请求,返回生成的登录页面 HTML;对于 POST 请求(实际提交登录表单),则放行给UsernamePasswordAuthenticationFilter
处理。
- 检测是否存在自定义登录页:过滤器会判断开发者是否通过
-
工作流程
- 用户访问受保护资源:例如用户未登录时访问
/dashboard
,Spring Security 会判断用户未认证,将其重定向到默认登录页路径/login
。 - 过滤器拦截登录页请求:
DefaultLoginPageGeneratingFilter
拦截GET /login
请求,检测到未配置自定义登录页,开始生成默认登录页面。 - 生成并返回登录页面:动态生成包含用户名、密码输入框和 CSRF 令牌的 HTML 页面,响应给浏览器,用户看到默认登录表单。
- 用户提交登录表单:用户输入账号密码后提交,表单以
POST /login
发送请求,此请求会被UsernamePasswordAuthenticationFilter
拦截处理(DefaultLoginPageGeneratingFilter
不处理 POST 请求)。 - 登录失败场景:若认证失败(如密码错误),用户会被重定向回
/login?error
,过滤器会在页面中显示错误信息(从请求参数中读取错误原因)。
- 用户访问受保护资源:例如用户未登录时访问
-
默认登录页面的特点
- 极简风格:页面为纯 HTML 样式,无复杂 CSS,仅包含核心登录元素,适合快速测试或演示。
- 自动适配配置:
- 若启用了 “记住我” 功能(
.rememberMe()
),页面会自动添加 “记住我” 复选框(name="remember-me"
)。 - 若配置了 OAuth2 第三方登录,页面会自动添加第三方登录按钮(如 “使用 GitHub 登录”)。
- 若启用了 “记住我” 功能(
- CSRF 保护:自动包含 CSRF 令牌隐藏域,避免登录请求被 CSRF 攻击。
-
如何禁用默认登录页?
在实际项目中(尤其是生产环境),通常需要使用自定义登录页,此时可通过以下方式禁用
DefaultLoginPageGeneratingFilter
:@Configuration @EnableWebSecurity public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated()).formLogin(form -> form// 配置自定义登录页路径(此时 DefaultLoginPageGeneratingFilter 自动失效).loginPage("/custom-login")// 允许匿名访问自定义登录页.permitAll());return http.build();} }
- 配置
loginPage("/custom-login")
后,Spring Security 会认为开发者已提供自定义登录页,DefaultLoginPageGeneratingFilter
不再生效。 - 需确保
/custom-login
路径对应一个有效的控制器(返回自定义登录页面 HTML)。
- 配置
-
-
DefaultLogoutPageGeneratingFilter
-
DefaultLogoutPageGeneratingFilter
是 Spring Security 中的一个辅助过滤器,作用与DefaultLoginPageGeneratingFilter
类似 —— 当开发者未自定义登出页面时,自动生成一个默认的 HTML 登出确认页面,用于提示用户确认是否执行登出操作。 -
核心职责
- 检测是否存在自定义登出页:过滤器会判断开发者是否通过配置自定义了登出页面(如
/custom-logout
)。如果已配置,此过滤器不生效;如果未配置,它会自动介入。 - 生成默认登出确认页面:当用户访问默认登出路径(默认
/logout
,GET 方法)时,过滤器会生成一个简单的 HTML 页面,包含:- 登出确认提示(如 “是否确定要退出登录?”);
- 一个提交按钮(点击后发送 POST 请求到
/logout
,触发实际登出逻辑); - CSRF 令牌隐藏域(因登出是 “修改型操作”,需通过 CSRF 校验,防止恶意登出)。
- 处理登出页请求:仅拦截对默认登出路径(
/logout
)的 GET 请求,返回生成的登出确认页面;对于 POST 请求(实际执行登出),则放行给LogoutFilter
处理。
- 检测是否存在自定义登出页:过滤器会判断开发者是否通过配置自定义了登出页面(如
-
工作流程
- 用户触发登出:例如用户点击 “退出登录” 链接,前端导航到
/logout
(GET 请求)。 - 过滤器拦截并生成页面:
DefaultLogoutPageGeneratingFilter
拦截GET /logout
请求,检测到未配置自定义登出页,生成默认登出确认页面并返回给浏览器。 - 用户确认登出:用户在页面中点击 “确认登出” 按钮,表单以
POST /logout
发送请求(携带 CSRF 令牌)。 - 执行实际登出逻辑:
POST /logout
请求被LogoutFilter
拦截,执行清除上下文、失效会话等登出操作(见LogoutFilter
详解)。 - 登出成功跳转:登出完成后,由
LogoutSuccessHandler
处理后续逻辑(默认重定向到/login?logout
)。
- 用户触发登出:例如用户点击 “退出登录” 链接,前端导航到
-
默认登出页面的特点
- 简单交互:仅包含确认提示和提交按钮,无复杂样式,适合快速测试。
- CSRF 保护:自动包含 CSRF 令牌隐藏域,确保登出请求是用户主动发起的(防止恶意网站伪造登出请求)。
- 依赖默认登出路径:仅对默认的
/logout
路径生效,若通过.logout().logoutUrl("/api/logout")
自定义了登出路径,此过滤器可能不生效。
-
如何禁用或替换默认登出页?
在实际项目中,通常不需要默认登出页,可通过以下方式处理:
-
使用自定义登出页:配置自定义登出页路径,
DefaultLogoutPageGeneratingFilter
会自动失效:@Configuration @EnableWebSecurity public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.logout(logout -> logout// 自定义登出页面路径(GET 请求访问).logoutPage("/custom-logout")// 登出处理路径(POST 请求,实际执行登出).logoutUrl("/do-logout").permitAll());return http.build();} }
需确保
/custom-logout
对应一个控制器,返回自定义的登出确认页面。 -
前后端分离项目:无页面直接登出:前后端分离项目通常通过 API 直接触发登出(无需确认页面),可禁用默认登出页并配置登出接口:
@Configuration public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.logout(logout -> logout.logoutUrl("/api/logout") // 登出接口(POST 请求).logoutMethod(HttpMethod.POST).logoutSuccessHandler((req, res, auth) -> {// 直接返回 JSON 响应,无需页面跳转res.getWriter().write("{\"success\":true}");}).permitAll());return http.build();} }
-
-
-
BasicAuthenticationFilter
-
BasicAuthenticationFilter
是 Spring Security 中处理 HTTP Basic 认证 的核心过滤器,专门拦截携带Authorization: Basic <凭证>
请求头的请求,解析并校验用户名密码,完成认证流程。它适用于简单的内部系统、API 测试或设备间通信场景(无需复杂登录页面)。 -
核心职责
- 拦截并解析认证凭证:检测请求头中是否存在
Authorization: Basic <凭证>
,若存在则解码凭证,提取用户名和密码;若不存在或格式不正确,则不处理(后续过滤器可能触发未认证逻辑)。 - 执行认证逻辑:构建未认证的
UsernamePasswordAuthenticationToken
(包含提取的用户名和密码),交给AuthenticationManager
执行校验(通过UserDetailsService
加载用户信息,PasswordEncoder
比对密码)。 - 处理认证结果:
- 认证成功:将认证通过的
Authentication
对象存入SecurityContextHolder
,放行请求。 - 认证失败:触发
AuthenticationEntryPoint
,返回401 Unauthorized
响应,并在响应头中添加WWW-Authenticate: Basic realm="Realm"
,提示浏览器弹出登录框。
- 认证成功:将认证通过的
- 拦截并解析认证凭证:检测请求头中是否存在
-
工作流程
- 客户端发起请求:客户端发送请求,携带
Authorization: Basic YWRtaW46MTIzNDU2
头。 - 过滤器拦截并解码:
BasicAuthenticationFilter
拦截请求,解码凭证得到用户名admin
和密码123456
。 - 执行认证校验:构建
UsernamePasswordAuthenticationToken
并交给AuthenticationManager
,校验用户名密码是否匹配。 - 认证成功处理:校验通过后,将认证信息存入
SecurityContextHolder
,请求继续传递到后续过滤器和 Controller。 - 认证失败处理:若密码错误或用户不存在,返回
401
响应,浏览器弹出登录框让用户重新输入凭证。 - 无凭证处理:若请求无
Authorization
头,过滤器不干预,后续ExceptionTranslationFilter
会触发AuthenticationEntryPoint
,返回401
并提示输入凭证。
- 客户端发起请求:客户端发送请求,携带
-
代码示例:启用 Basic 认证
在 Spring Security 中,需通过
.httpBasic()
显式启用BasicAuthenticationFilter
,示例如下:@Configuration @EnableWebSecurity public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated() // 所有请求需认证)// 启用 HTTP Basic 认证.httpBasic(basic -> basic// 自定义认证失败响应(默认返回 401 并提示登录框).authenticationEntryPoint((request, response, authException) -> {response.setContentType("application/json;charset=UTF-8");response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getWriter().write("{\"error\":\"认证失败:请提供正确的用户名和密码\"}");}));return http.build();}// 配置密码编码器(强制要求)@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}// 配置用户信息(示例为内存用户)@Beanpublic UserDetailsService userDetailsService() {UserDetails user = User.withUsername("admin").password(passwordEncoder().encode("123456")).roles("ADMIN").build();return new InMemoryUserDetailsManager(user);} }
-
注意事项
- 安全性较低:
- 凭证仅经过 Base64 编码(非加密),若传输未使用 HTTPS,凭证易被拦截破解。
- 凭证会随每次请求发送,增加泄露风险。
- 不支持 “登出”(除非客户端主动清除凭证,或服务端通过其他方式拒绝旧凭证)。
- 适用场景:适合内部系统、API 测试、物联网设备通信等对安全性要求不高,且需要简单认证的场景,不推荐用于面向公众的生产系统。
- 与表单登录的共存:Spring Security 支持同时启用 Basic 认证和表单登录,客户端可选择任意一种方式认证(浏览器默认优先弹出 Basic 登录框,若用户取消则可能触发表单登录重定向)。
- 跨域场景注意:若存在跨域请求,需配合
CorsFilter
配置跨域规则,确保Authorization
头能被正常传递和处理。
- 安全性较低:
-
-
RequestCacheAwareFilter
-
RequestCacheAwareFilter
是 Spring Security 过滤器链中的重要成员,主要负责 缓存用户认证前的原始请求,并在用户成功登录后自动重定向回该请求,提升用户体验。它解决了 “用户访问受保护资源时被拦截到登录页,登录后需手动返回原资源” 的问题。 -
简单来说,它的核心作用是:“记住” 用户在未登录时想要访问的页面,等用户登录成功后,自动帮用户跳转到那个页面。
具体职责包括:
- 缓存未认证请求:当用户未登录却访问受保护资源(如
/dashboard
)时,RequestCacheAwareFilter
会将该请求(包含路径、参数等信息)缓存起来(默认存储在HttpSession
中)。 - 登录后重定向到原请求:用户登录成功后,过滤器会从缓存中取出之前保存的请求,将用户重定向到该请求对应的资源(如
/dashboard
),而非默认首页。 - 避免重复缓存:仅缓存 “需要认证的 GET 请求”(非修改型请求),避免缓存 POST 等可能产生副作用的请求(防止重复提交表单)。
- 缓存未认证请求:当用户未登录却访问受保护资源(如
-
工作流程
- 用户访问受保护资源:例如用户未登录时直接访问
/dashboard
(需认证)。 - 拦截并缓存请求:
- Spring Security 发现用户未认证,触发拦截(由
ExceptionTranslationFilter
处理)。 RequestCacheAwareFilter
将/dashboard
请求的详细信息(URL、参数、请求方法等)缓存到HttpSession
中(通过RequestCache
接口实现,默认是HttpSessionRequestCache
)。
- Spring Security 发现用户未认证,触发拦截(由
- 重定向到登录页:用户被重定向到登录页(如
/login
)。 - 用户登录成功:用户输入正确的用户名密码,完成认证。
- 恢复缓存的请求:
- 登录成功后,
RequestCacheAwareFilter
从缓存中取出之前保存的/dashboard
请求。 - 自动将用户重定向到
/dashboard
,而非默认的首页(如/home
)。
- 登录成功后,
- 清除缓存:重定向后,缓存的请求会被自动清除,避免重复重定向。
- 用户访问受保护资源:例如用户未登录时直接访问
-
代码示例:自定义缓存行为
默认情况下,
RequestCacheAwareFilter
已能满足大部分需求,也可通过配置自定义其行为:@Configuration @EnableWebSecurity public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth.requestMatchers("/login").permitAll().anyRequest().authenticated()).formLogin(form -> form// 登录成功后,若有缓存请求则重定向到该请求,否则重定向到 /home.defaultSuccessUrl("/home", false) // 禁用缓存(始终重定向到 defaultSuccessUrl)// .successHandler((req, res, auth) -> res.sendRedirect("/home")))// 自定义 RequestCache(可选).requestCache(requestCache -> requestCache.requestCache(customRequestCache()));return http.build();}// 自定义请求缓存(例如只缓存特定路径的请求)@Beanpublic RequestCache customRequestCache() {HttpSessionRequestCache cache = new HttpSessionRequestCache();// 只缓存 /dashboard 路径的请求cache.setRequestMatcher(new AntPathRequestMatcher("/dashboard"));return cache;} }
defaultSuccessUrl("/home", false)
中,false
表示 “若有缓存请求则优先重定向到该请求,否则到/home
”;若设为true
,则始终重定向到/home
(忽略缓存)。- 若通过
successHandler
自定义登录成功逻辑,需手动处理缓存请求(如调用requestCache.getRequest()
获取并重定向)。
-
注意事项
- 只缓存 GET 请求:默认仅缓存 GET、HEAD 等 “安全方法” 的请求,不缓存 POST、PUT 等可能修改数据的请求(防止登录后重复提交表单导致的数据重复)。
- 缓存存储位置:默认使用
HttpSessionRequestCache
,将请求信息存储在 Session 中,适合有状态应用;无状态应用(如 JWT)可自定义RequestCache
实现(如存储在 Redis 中)。 - 禁用缓存场景:若希望登录后始终跳转到固定页面(如首页),可通过以下方式禁用缓存:
- 配置
defaultSuccessUrl("/home", true)
(强制跳转默认页)。 - 使用自定义
successHandler
并忽略缓存请求。
- 配置
-
-
SecurityContextHolderAwareRequestFilter
-
SecurityContextHolderAwareRequestFilter
是 Spring Security 中一个请求包装过滤器,它的核心作用是对原始HttpServletRequest
进行包装,添加与安全相关的便捷方法,让开发者能更简单地在控制器(或其他组件)中获取当前用户的认证信息和权限状态。 -
核心职责
-
包装原始请求:拦截所有请求,将原始
HttpServletRequest
包装为SecurityContextHolderAwareRequestWrapper
,后续过滤器和控制器接收到的都是这个包装后的请求对象。 -
提供安全相关快捷方法:包装后的请求对象新增了以下常用方法(这些方法内部会自动从
SecurityContextHolder
中获取认证信息):方法 作用说明 getUserPrincipal()
获取当前登录用户的 Principal
对象(即Authentication
中的principal
,通常是UserDetails
)。getRemoteUser()
获取当前登录用户的用户名(字符串形式)。 isUserInRole(String role)
判断当前用户是否拥有指定角色(角色名需省略前缀 ROLE_
,如传入ADMIN
等价于判断ROLE_ADMIN
)。isAuthenticated()
判断当前用户是否已认证(返回 boolean
)。
-
-
工作流程
- 请求经过过滤器:当请求进入过滤器链并到达
SecurityContextHolderAwareRequestFilter
时,过滤器会拦截请求。 - 包装请求对象:过滤器创建
SecurityContextHolderAwareRequestWrapper
实例,将原始HttpServletRequest
作为参数传入,完成包装。包装后的对象会替代原始请求,继续在过滤器链中传递。 - 控制器中使用快捷方法:控制器(
@Controller
)接收的HttpServletRequest
实际上是包装后的对象,开发者可直接调用新增的安全方法,无需手动访问SecurityContextHolder
。
- 请求经过过滤器:当请求进入过滤器链并到达
-
代码示例:使用包装后的请求方法
传统方式(未使用过滤器):
@RestController public class UserController {@GetMapping("/user")public String getUserInfo() {// 需手动从 SecurityContextHolder 获取认证信息Authentication auth = SecurityContextHolder.getContext().getAuthentication();String username = auth.getName();boolean isAdmin = auth.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));return "用户名:" + username + ",是否为管理员:" + isAdmin;} }
使用
SecurityContextHolderAwareRequestFilter
后的简化方式:@RestController public class UserController {@GetMapping("/user")public String getUserInfo(HttpServletRequest request) {// 直接通过请求对象的快捷方法获取String username = request.getRemoteUser(); // 获取用户名boolean isAdmin = request.isUserInRole("ADMIN"); // 判断是否有 ADMIN 角色(自动补全 ROLE_ 前缀)Principal principal = request.getUserPrincipal(); // 获取用户 Principal 对象return "用户名:" + username + ",是否为管理员:" + isAdmin;} }
-
特点
- 透明包装:开发者无需修改控制器代码,只需通过注入的
HttpServletRequest
即可调用新增方法,对现有逻辑无侵入。 - 角色名自动补全:调用
isUserInRole("ADMIN")
时,过滤器会自动补全为ROLE_ADMIN
(与 Spring Security 中角色的存储格式一致),简化角色判断。 - 依赖
SecurityContext
:所有快捷方法的实现都依赖SecurityContextHolder
中的SecurityContext
,如果用户未认证(如匿名用户),getRemoteUser()
会返回null
,isUserInRole()
会返回false
。
- 透明包装:开发者无需修改控制器代码,只需通过注入的
-
-
AnonymousAuthenticationFilter
-
AnonymousAuthenticationFilter
是 Spring Security 过滤器链中的核心过滤器,它的核心作用是:为未认证的用户(即未登录用户)自动分配一个 “匿名用户” 身份,确保系统中所有请求都有明确的认证状态(已认证或匿名),避免因 “无身份” 导致的逻辑漏洞或null
指针异常。 -
核心职责
- 检测未认证请求:拦截所有请求,检查
SecurityContextHolder
中是否存在有效的Authentication
对象(即用户是否已登录)。 - 分配匿名身份:若不存在有效认证信息(用户未登录),则自动创建一个
AnonymousAuthenticationToken
(匿名认证令牌),包含:- 用户名:默认
anonymousUser
(可自定义)。 - 权限:默认
ROLE_ANONYMOUS
(可自定义)。
- 用户名:默认
- 存储匿名身份:将
AnonymousAuthenticationToken
存入SecurityContextHolder
,使后续过滤器和控制器能识别 “匿名用户” 身份。
- 检测未认证请求:拦截所有请求,检查
-
工作流程
- 用户未登录访问资源:例如用户未登录时访问
/home
(公开资源)。 - 过滤器检测认证状态:
AnonymousAuthenticationFilter
检查SecurityContextHolder
,发现没有Authentication
对象(用户未登录)。 - 创建并设置匿名令牌:生成
AnonymousAuthenticationToken
(用户名为anonymousUser
,角色为ROLE_ANONYMOUS
),并设置到SecurityContextHolder
中。 - 后续组件处理匿名用户:
- 授权过滤器(如
FilterSecurityInterceptor
)根据规则判断匿名用户是否有权访问资源(如permitAll()
允许访问)。 - 控制器中可通过
SecurityContextHolder
或HttpServletRequest
方法(如getRemoteUser()
)获取匿名用户信息(返回anonymousUser
)。
- 授权过滤器(如
- 用户未登录访问资源:例如用户未登录时访问
-
代码示例:识别匿名用户
@RestController public class HomeController {@GetMapping("/home")public String home() {Authentication auth = SecurityContextHolder.getContext().getAuthentication();if (auth instanceof AnonymousAuthenticationToken) {// 匿名用户(未登录)return "欢迎访问首页,您当前是匿名用户(" + auth.getName() + ")";} else {// 已认证用户return "欢迎回来," + auth.getName() + "!";}} }
- 未登录用户访问
/home
:返回欢迎访问首页,您当前是匿名用户(anonymousUser)
。 - 登录用户访问
/home
:返回欢迎回来,admin!
。
默认的匿名用户名和角色可通过配置修改:
@Configuration @EnableWebSecurity public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth.requestMatchers("/home").permitAll() // 允许匿名用户访问首页.anyRequest().authenticated())// 自定义匿名用户配置.anonymous(anonymous -> anonymous.username("guest") // 自定义匿名用户名(默认 anonymousUser).authorities("ROLE_GUEST") // 自定义匿名用户角色(默认 ROLE_ANONYMOUS).principal("guest-principal") // 自定义 principal 对象);return http.build();} }
- 配置后,未登录用户的用户名为
guest
,角色为ROLE_GUEST
,控制器中可获取到这些自定义信息。
- 未登录用户访问
-
注意事项
-
匿名用户也是 “已认证” 的一种特殊状态:
AnonymousAuthenticationToken
的isAuthenticated()
方法返回true
(标记为已认证),但本质是 “匿名认证”。这是为了统一处理逻辑 —— 无论是真实用户还是匿名用户,SecurityContext
中都有非null
的Authentication
对象。 -
与
permitAll()
的关系:permitAll()
允许所有用户访问,包括匿名用户(ROLE_ANONYMOUS
)和已认证用户。若需仅允许匿名用户访问(如登录页),可结合anonymous()
配置:.authorizeHttpRequests(auth -> auth.requestMatchers("/login").anonymous() // 仅匿名用户可访问登录页(已登录用户会被拒绝) )
-
禁用匿名用户:通常不建议禁用,但可通过
anonymous().disable()
关闭匿名用户功能。禁用后,未登录用户的SecurityContext
中会没有Authentication
对象,需手动处理null
情况。
-
-
-
SessionManagementFilter
-
SessionManagementFilter
是 Spring Security 中负责 会话安全管理 的核心过滤器,主要处理与用户会话相关的安全策略,如会话并发控制、会话超时处理、防御会话固定攻击等,确保用户会话的安全性和合规性。 -
核心职责
会话(
HttpSession
)是 Web 应用中保存用户状态的关键机制,但也存在安全风险(如会话劫持、多设备同时登录等)。SessionManagementFilter
的核心职责是通过一系列策略管控会话生命周期,具体包括:- 会话并发控制:限制同一用户的最大并发会话数(如禁止同一账号在多设备同时登录)。
- 会话固定攻击防御:用户登录时更换会话 ID(
Session ID
),防止攻击者利用初始会话 ID 劫持会话。 - 会话超时 / 失效处理:检测到无效会话(如超时、被强制下线)时,触发指定策略(如重定向到登录页)。
- 会话创建策略执行:根据配置的会话创建策略(如 “需要时创建”“从不创建”)管理会话的创建时机。
-
工作流程
- 拦截请求并检查会话状态:过滤器拦截所有请求,从
SecurityContextHolder
中获取当前用户的认证信息(Authentication
),并检查当前会话(HttpSession
)的状态。 - 处理会话并发:若配置了并发会话限制(如
maximumSessions(1)
),过滤器会检查当前用户的活跃会话数:- 未超过限制:正常处理请求。
- 超过限制:根据策略处理(如让最早的会话失效,或拒绝新登录),并触发
SessionInformationExpiredStrategy
(如提示 “账号在其他设备登录”)。
- 防御会话固定攻击:当用户完成登录(从匿名状态变为已认证状态)时,过滤器会触发会话 ID 更换(
session.regenerateId()
),并将旧会话中的数据迁移到新会话,使攻击者持有的旧会话 ID 失效。 - 检测会话失效:若会话已超时或被标记为失效(如管理员强制踢下线),过滤器会清除
SecurityContextHolder
中的认证信息,并触发InvalidSessionStrategy
(如重定向到/login?invalid-session
)。 - 执行会话创建策略:根据配置的
sessionCreationPolicy
(如IF_REQUIRED
“需要时创建”、NEVER
“从不创建”)决定是否创建新会话,避免不必要的会话创建(如无状态应用)。
- 拦截请求并检查会话状态:过滤器拦截所有请求,从
-
代码示例:自定义会话管理策略
@Configuration @EnableWebSecurity public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated()).formLogin(Customizer.withDefaults())// 配置会话管理.sessionManagement(session -> session// 会话并发控制:最多允许1个并发会话.maximumSessions(1)// 超过并发限制时,是否拒绝新登录(true:拒绝;false:让旧会话失效).maxSessionsPreventsLogin(false)// 并发会话失效策略(如旧会话被踢时的处理).expiredSessionStrategy(event -> {HttpServletResponse response = event.getResponse();response.setContentType("text/html;charset=UTF-8");response.getWriter().write("您的账号在其他设备登录,已被迫下线!<a href='/login'>重新登录</a>");}).and()// 会话固定攻击防御:登录时更换会话ID(默认启用).sessionFixation().migrateSession()// 会话创建策略:仅在需要时创建会话(默认值).sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)// 会话超时处理策略(默认1800秒,即30分钟).invalidSessionStrategy((request, response) -> {response.sendRedirect("/login?session-expired");}));return http.build();} }
- 会话并发控制:
maximumSessions(n)
:限制同一用户最多n
个并发会话(n=1
表示单设备登录)。maxSessionsPreventsLogin(true)
:超过限制时拒绝新登录(返回 401);false
(默认)则使最早的会话失效。
- 会话固定攻击防御:
sessionFixation().migrateSession()
(默认):登录时创建新会话并迁移数据,旧会话失效。sessionFixation().newSession()
:登录时创建全新会话(不迁移数据)。sessionFixation().none()
:禁用防御(不推荐,存在安全风险)。
- 会话创建策略:
SessionCreationPolicy.IF_REQUIRED
(默认):需要时创建会话(如登录后)。SessionCreationPolicy.ALWAYS
:每次请求都创建会话(不推荐,浪费资源)。SessionCreationPolicy.NEVER
:从不主动创建会话,但如果请求带来会话则使用。SessionCreationPolicy.STATELESS
:完全无状态(不使用会话,适合 JWT 等令牌认证)。
- 会话超时:默认超时时间由 Servlet 容器配置(如 Tomcat 默认 30 分钟),可通过
server.servlet.session.timeout=30m
(Spring Boot)修改。超时后由invalidSessionStrategy
处理(如重定向到登录页)。
- 会话并发控制:
-
注意事项
- 传统 Web 应用:依赖会话存储认证状态,需通过
SessionManagementFilter
配置并发控制、超时策略等,保障会话安全。 - 前后端分离 + JWT:通常采用
STATELESS
策略(禁用会话),此时该过滤器作用有限(但仍会处理剩余的会话相关逻辑)。 - 会话共享:分布式系统中会话共享(如 Redis 存储)时,需确保
SessionManagementFilter
的策略在集群中生效(如并发控制依赖共享存储)。
- 传统 Web 应用:依赖会话存储认证状态,需通过
-
-
ExceptionTranslationFilter
-
ExceptionTranslationFilter
是 Spring Security 过滤器链中专门处理认证与授权异常的核心过滤器,它像一个 “安全异常处理器”,负责捕获过滤器链中抛出的安全相关异常(如未登录、权限不足),并将其转换为用户友好的响应(如重定向到登录页、返回 403 错误),避免异常直接暴露给用户或导致系统崩溃。 -
核心职责
- 捕获安全相关异常:拦截过滤器链中(主要是后续的
FilterSecurityInterceptor
等)抛出的AuthenticationException
(认证异常)和AccessDeniedException
(授权异常)。 - 区分异常类型并处理:
- 认证异常(如未登录、登录失败):引导用户进行认证(如重定向到登录页、返回 401 提示)。
- 授权异常(如已登录但无权限):返回权限不足的响应(如 403 Forbidden)。
- 暂存请求信息:对于因未认证被拦截的请求,会暂存请求信息(通过
RequestCache
),以便用户登录后能重定向回原请求(配合RequestCacheAwareFilter
)。
- 捕获安全相关异常:拦截过滤器链中(主要是后续的
-
工作流程
- 拦截请求并执行后续过滤器:
ExceptionTranslationFilter
本身不处理请求,而是先执行过滤器链中的后续过滤器(如FilterSecurityInterceptor
)。 - 捕获异常:若后续过滤器抛出
AuthenticationException
或AccessDeniedException
,则被该过滤器捕获。 - 处理认证异常(
AuthenticationException
):- 例如:用户未登录访问
/dashboard
,FilterSecurityInterceptor
发现未认证,抛出AuthenticationException
。 - 过滤器调用
AuthenticationEntryPoint
(认证入口点):- 传统 Web 应用:默认重定向到登录页(如
/login?login_error
),并暂存原请求(/dashboard
)。 - 前后端分离应用:可自定义返回
401 Unauthorized
JSON 响应(如{"error": "请先登录"}
)。
- 传统 Web 应用:默认重定向到登录页(如
- 例如:用户未登录访问
- 处理授权异常(
AccessDeniedException
):- 例如:用户已登录但无
ADMIN
角色,却访问/admin
,抛出AccessDeniedException
。 - 过滤器判断用户是否已认证:
- 已认证但无权限:调用
AccessDeniedHandler
,默认返回403 Forbidden
。 - 未认证(如匿名用户):先执行认证流程(同步骤 3),再判断权限。
- 已认证但无权限:调用
- 例如:用户已登录但无
- 拦截请求并执行后续过滤器:
-
代码示例:自定义异常处理
可通过配置自定义认证 / 授权异常的处理逻辑,适应不同场景(如前后端分离返回 JSON):
@Configuration @EnableWebSecurity public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth.requestMatchers("/login").permitAll().requestMatchers("/admin").hasRole("ADMIN").anyRequest().authenticated()).formLogin(Customizer.withDefaults())// 配置异常处理.exceptionHandling(ex -> ex// 处理认证异常(未登录).authenticationEntryPoint((request, response, authException) -> {// 前后端分离:返回 401 JSONresponse.setContentType("application/json;charset=UTF-8");response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getWriter().write("{\"error\":\"请先登录\", \"code\":401}");})// 处理授权异常(权限不足).accessDeniedHandler((request, response, accessDeniedException) -> {// 前后端分离:返回 403 JSONresponse.setContentType("application/json;charset=UTF-8");response.setStatus(HttpServletResponse.SC_FORBIDDEN);response.getWriter().write("{\"error\":\"权限不足\", \"code\":403}");}));return http.build();} }
AuthenticationEntryPoint
:处理未认证异常的策略接口,默认实现是LoginUrlAuthenticationEntryPoint
(重定向到登录页)。自定义实现可返回 JSON、跳转自定义页面等。AccessDeniedHandler
:处理授权异常的策略接口,默认实现是AccessDeniedHandlerImpl
(返回 403 错误页面)。自定义实现可返回 JSON、记录审计日志等。- 与
RequestCache
配合:处理未认证异常时,会通过RequestCache
暂存当前请求信息(如/dashboard
),用户登录成功后,RequestCacheAwareFilter
会将其重定向回该请求。
-
-
FilterSecurityInterceptor
-
FilterSecurityInterceptor
是 Spring Security 过滤器链中的最后一道防线,也是负责最终权限校验的核心过滤器。它的核心作用是:根据开发者配置的安全规则(如 “哪些路径需要哪些权限”),对请求进行权限判断,决定是否允许访问目标资源。 -
核心职责
- 获取当前请求的安全规则:根据请求的路径(如
/admin
),匹配开发者配置的安全规则(如hasRole("ADMIN")
),确定访问该资源所需的权限。 - 提取当前用户的权限:从
SecurityContextHolder
中获取当前登录用户的认证信息(Authentication
),提取用户拥有的权限(Authorities
)。 - 执行权限判断:通过
AccessDecisionManager
(访问决策管理器)对比 “资源所需权限” 与 “用户拥有的权限”,决定是否允许访问:- 权限匹配:放行请求,允许访问目标资源(如控制器方法)。
- 权限不匹配:抛出
AccessDeniedException
(授权异常),由ExceptionTranslationFilter
处理(返回 403 错误)。
- 获取当前请求的安全规则:根据请求的路径(如
-
工作流程
-
请求到达过滤器链末尾:经过前面的过滤器(如认证、会话管理等)后,请求到达
FilterSecurityInterceptor
(过滤器链的最后一步)。 -
获取请求对应的安全配置:
过滤器通过
SecurityMetadataSource
(安全元数据源)解析当前请求的路径(如/admin
),找到对应的安全规则(如hasRole("ADMIN")
)。 -
获取用户权限:从
SecurityContextHolder
中获取Authentication
对象,提取用户的权限列表(如ROLE_USER
)。 -
权限判断:
- 将 “资源所需权限”(
ROLE_ADMIN
)和 “用户拥有的权限”(ROLE_USER
)交给AccessDecisionManager
。 AccessDecisionManager
对比后发现权限不匹配,抛出AccessDeniedException
。
- 将 “资源所需权限”(
-
处理权限不足:
- 异常被
ExceptionTranslationFilter
捕获,返回403 Forbidden
响应(或自定义提示)。 - 若权限匹配,则放行请求,最终到达
DispatcherServlet
并调用对应的控制器方法。
- 异常被
-
-
代码示例:定义安全规则
@Configuration @EnableWebSecurity public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth// 公开资源:允许所有用户访问.requestMatchers("/", "/public/**").permitAll()// 管理员资源:仅 ADMIN 角色可访问.requestMatchers("/admin/**").hasRole("ADMIN")// 用户资源:仅 USER 或 ADMIN 角色可访问.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")// 其他所有资源:需认证(登录即可,无特定角色要求).anyRequest().authenticated()).formLogin(Customizer.withDefaults());return http.build();} }
SecurityMetadataSource
:负责解析请求路径与安全规则的映射关系(如/admin/**
对应hasRole("ADMIN")
),默认实现是ExpressionBasedFilterInvocationSecurityMetadataSource
(支持 SpEL 表达式,如hasRole()
、permitAll()
)。AccessDecisionManager
:权限决策的核心组件,根据 “资源所需权限” 和 “用户拥有的权限” 决定是否允许访问。默认使用AffirmativeBased
(只要有一个决策器同意就允许访问),包含三个决策器:RoleVoter
:判断用户角色是否匹配(如ROLE_ADMIN
)。AuthenticatedVoter
:判断用户是否已认证(如isAuthenticated()
)。WebExpressionVoter
:支持 SpEL 表达式判断(如hasPermission()
)。
RunAsManager
:可选组件,用于临时切换用户身份(如某些操作需要以管理员身份执行),一般很少使用。- 当用户(
ROLE_USER
)访问/admin/dashboard
时,FilterSecurityInterceptor
会匹配到hasRole("ADMIN")
规则,发现用户权限不足,抛出AccessDeniedException
,最终返回 403。 - 当用户(
ROLE_ADMIN
)访问/admin/dashboard
时,权限匹配,请求被放行。
-
扩展场景:自定义权限判断
如果默认的权限判断逻辑无法满足需求(如基于数据库的动态权限),可通过自定义
SecurityMetadataSource
或AccessDecisionManager
实现:// 示例:自定义 AccessDecisionManager(简化版) public class CustomAccessDecisionManager implements AccessDecisionManager {@Overridepublic void decide(Authentication auth, Object object, Collection<ConfigAttribute> attrs) throws AccessDeniedException {// 1. 获取用户权限(auth.getAuthorities())// 2. 获取资源所需权限(attrs)// 3. 自定义逻辑判断(如从数据库查询动态权限)boolean hasPermission = checkPermission(auth, attrs);if (!hasPermission) {throw new AccessDeniedException("权限不足");}}private boolean checkPermission(Authentication auth, Collection<ConfigAttribute> attrs) {// 实现自定义权限校验逻辑// ...}// 其他方法省略(supports) }// 在配置中替换默认决策管理器 @Configuration public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated()).with(new Customizer<ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry>() {@Overridepublic void customize(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) {registry.accessDecisionManager(new CustomAccessDecisionManager());}});return http.build();} }
-
执行流程举例(以表单登录为例)
-
请求受保护资源
/dashboard
→ 被FilterSecurityInterceptor
拦下:未认证
→ 抛出AuthenticationException
-
ExceptionTranslationFilter
捕获异常
→ 重定向/login
-
用户提交
/login
→UsernamePasswordAuthenticationFilter
捕获请求
→ 交由AuthenticationManager
执行认证
→ 成功 →SecurityContext
写入认证信息 -
登录成功后
→RequestCacheAwareFilter
重定向回最初请求的页面
→ 后续请求经过SecurityContextHolderFilter
自动加载认证信息 -
用户登出
/logout
→LogoutFilter
清理SecurityContext
、Session、Cookie
自定义过滤器的添加与位置控制
在 Spring Security 中,请求进入系统后,会依次经过十几层安全过滤器(比如认证、授权、异常处理、登出等)。
而我们可以在这些默认过滤器之间 插入自定义 Filter —— 实现日志记录、验证码校验、请求签名验证、黑名单校验等个性化安全逻辑。
核心 API:三种添加方式
Spring Security 提供了 3 种方式来控制你自定义过滤器在链中的位置:
方法 | 作用 | 示例 |
---|---|---|
addFilterBefore(filter, class) | 把自定义过滤器放在指定过滤器 之前 | 验证码验证放在登录过滤器前 |
addFilterAfter(filter, class) | 放在指定过滤器 之后 | 日志过滤放在认证后 |
addFilterAt(filter, class) | 直接替换指定过滤器 | 替换默认的用户名密码过滤器 |
示例:自定义日志过滤器
public class LoggingFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain)throws ServletException, IOException {String uri = request.getRequestURI();Authentication auth = SecurityContextHolder.getContext().getAuthentication();String username = (auth != null && auth.isAuthenticated())? auth.getName(): "匿名用户";System.out.println("请求路径: " + uri + " | 当前用户: " + username);filterChain.doFilter(request, response);}
}
- 继承
OncePerRequestFilter
:保证每次请求只执行一次; - 记录请求路径:通过
request.getRequestURI()
获取当前请求的 URI(如/api/user
)。 - 获取当前用户:
- 从
SecurityContextHolder.getContext()
中获取Authentication
对象(Spring Security 存储用户认证信息的核心对象)。 - 判断用户是否已认证(
auth != null && auth.isAuthenticated()
):- 已认证:通过
auth.getName()
获取用户名(如登录用户admin
)。 - 未认证:标记为 “匿名用户”(注意:如果启用了
AnonymousAuthenticationFilter
,未登录用户会被分配anonymousUser
身份,此时auth.getName()
会返回anonymousUser
,而非这里的 “匿名用户” 字符串,可根据需求调整)。
- 已认证:通过
- 从
- 打印日志:输出 “请求路径 + 当前用户” 的日志信息,然后通过
filterChain.doFilter()
放行请求,保证过滤器链继续执行。
在配置类中注册过滤器
@Configuration
@EnableWebSecurity
public class SecurityConfig {@Autowiredprivate LoggingFilter loggingFilter;@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated()).formLogin(withDefaults());// 添加到 UsernamePasswordAuthenticationFilter 之前http.addFilterBefore(loggingFilter, UsernamePasswordAuthenticationFilter.class);return http.build();}
}
-
@EnableWebSecurity
:开启 Spring Security 的核心功能,自动注册 Security 相关的过滤器链和组件。 -
authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
:配置所有请求(anyRequest()
)都需要认证(authenticated()
)才能访问,即未登录用户访问任何接口都会被拦截到登录页。 -
formLogin(withDefaults())
:启用默认的表单登录功能:- 提供默认登录页(
/login
,GET 请求访问)。 - 登录表单提交到
/login
(POST 请求),由UsernamePasswordAuthenticationFilter
处理认证。 - 认证成功后默认重定向到之前访问的受保护页面(或
/
首页)。
- 提供默认登录页(
-
addFilterBefore(loggingFilter, UsernamePasswordAuthenticationFilter.class)
:将自定义的LoggingFilter
插入到 Security 过滤器链中,且位于UsernamePasswordAuthenticationFilter
之前。这意味着:
LoggingFilter
会先于 “用户名密码认证过滤器” 执行,即在用户提交的登录请求被认证处理前,就会记录请求日志。
FilterChainProxy
FilterChainProxy 是什么?
在 Spring Security 中,所有的安全过滤逻辑都最终交给一个“超级过滤器”来统一调度,那就是:
org.springframework.security.web.FilterChainProxy
它的作用是:
将不同的安全配置(即不同的
SecurityFilterChain
)映射到不同的请求 URL。
换句话说:
它是整个安全过滤体系的“总指挥官”,负责把每一个 HTTP 请求分配到正确的安全规则链上去执行。
可以这样理解:
FilterChainProxy 的主要职责
FilterChainProxy
主要做三件事:
任务 | 说明 |
---|---|
路由 | 根据请求 URL 找到匹配的 SecurityFilterChain |
调度 | 执行该链上定义的一组过滤器 |
保护 | 处理异常与上下文清理,确保安全上下文一致性 |
核心成员结构
-
关键字段
public class FilterChainProxy extends GenericFilterBean {private final List<SecurityFilterChain> filterChains;// SecurityContext 管理过滤器上下文private FilterChainProxy.FilterChainValidator filterChainValidator;... }
filterChains
就是所有安全链的集合,类型是:List<SecurityFilterChain>
-
SecurityFilterChain 接口
public interface SecurityFilterChain {boolean matches(HttpServletRequest request);List<Filter> getFilters(); }
matches()
判断当前请求是否命中该安全链。getFilters()
返回该链中要执行的过滤器。
执行流程
FilterChainProxy
的执行流程可概括为 “接收请求 → 匹配过滤器链 → 执行链中过滤器 → 放行请求”,具体步骤如下:
-
接收请求,进入
doFilter
方法当请求到达 Servlet 容器时,
FilterChainProxy
作为注册的过滤器之一,其doFilter(ServletRequest, ServletResponse, FilterChain)
方法被调用。- 参数
FilterChain
是Servlet 容器的原始过滤器链(包含非 Spring Security 的其他过滤器,如日志过滤器、编码过滤器等)。
- 参数
-
构建
FirewalledRequest
包装请求(安全加固)FilterChainProxy
首先通过HttpFirewall
(默认StrictHttpFirewall
)对原始请求进行包装,生成FirewalledRequest
:- 作用:防御恶意请求(如包含特殊字符的 URL、非法 HTTP 方法),若请求非法则直接拒绝(抛出异常)。
-
匹配最合适的
SecurityFilterChain
FilterChainProxy
内部持有多个SecurityFilterChain
实例(由开发者通过@Bean
定义,如SecurityFilterChain
配置类),它会遍历这些链,找到第一个与当前请求匹配的链:- 匹配规则:每个
SecurityFilterChain
通过requestMatcher
定义其负责的 URL 路径(如/api/**
),FilterChainProxy
调用SecurityFilterChain.matches(request)
判断是否匹配。 - 示例:若请求路径为
/api/user
,则匹配requestMatcher("/api/**")
的SecurityFilterChain
。
- 匹配规则:每个
-
执行匹配到的
SecurityFilterChain
中的过滤器找到匹配的
SecurityFilterChain
后,FilterChainProxy
会创建一个内部过滤器链执行器(VirtualFilterChain
),并执行该链中的所有 Spring Security 过滤器(如CsrfFilter
、UsernamePasswordAuthenticationFilter
等):VirtualFilterChain
是 Spring Security 内部的过滤器链实现,它会按顺序执行SecurityFilterChain
中的过滤器。- 每个过滤器执行完成后,通过
filterChain.doFilter()
触发下一个过滤器,直至链中所有过滤器执行完毕。
-
执行 Servlet 容器的原始过滤器链
当
SecurityFilterChain
中的所有过滤器执行完毕后,VirtualFilterChain
会调用原始的 Servlet 过滤器链(步骤 1 中的FilterChain
),让请求继续经过容器中的其他过滤器(如自定义的日志过滤器、Spring MVC 的DispatcherServlet
),最终到达目标资源(如控制器方法)。 -
处理响应(可选)
请求处理完成后,响应会按过滤器链的反向顺序返回,每个过滤器可在响应返回时进行额外处理(如添加响应头、记录日志)。
组件说明
SecurityFilterChain
:每个SecurityFilterChain
包含一组Filter
(Spring Security 核心过滤器,如认证、授权过滤器)和一个RequestMatcher
(匹配请求路径)。开发者可通过@Bean
定义多个SecurityFilterChain
,实现不同路径的差异化安全配置。示例:定义两个过滤器链,分别处理
/api/**
和/admin/**
:@Configuration public class SecurityConfig {// 处理 /api/** 路径的过滤器链@Beanpublic SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {http.requestMatchers(m -> m.antMatchers("/api/**")).authorizeHttpRequests(auth -> auth.anyRequest().authenticated()).httpBasic(Customizer.withDefaults());return http.build();}// 处理 /admin/** 路径的过滤器链@Beanpublic SecurityFilterChain adminFilterChain(HttpSecurity http) throws Exception {http.requestMatchers(m -> m.antMatchers("/admin/**")).authorizeHttpRequests(auth -> auth.anyRequest().hasRole("ADMIN")).formLogin(Customizer.withDefaults());return http.build();} }
HttpFirewall
:用于请求安全校验,默认实现StrictHttpFirewall
会禁止包含分号(;
)、反斜杠(\
)等特殊字符的 URL,防止路径遍历攻击。若需允许特殊字符,可自定义HttpFirewall
。