写在前面
在Web开发的江湖里,认证授权就像守护城门的卫兵——看似基础却暗藏玄机。当你以为“写个登录接口+Session存储”就能高枕无忧时,CSRF攻击、越权访问等暗箭早已潜伏在代码角落。Spring Security 作为Java生态的安全扛把子,不仅是解决登录认证的工具,更是一套攻防兼备的安全体系。
别再吐槽复杂!Spring Security到底有多能打?
有些文章对比安全认证授权框架时,得出 Spring Security 不支持 xx 功能的结论,有些片面之嫌。Spring Security 一个甚为诟病的问题是集成复杂,尤其在 Spring Boot 出现之前,需要大量配置 XML 以及硬编码配置,想想就很酸爽。
现在的版本早已脱胎换骨:
- 开箱即用:Spring Boot整合后,一行配置启动基础认证
- 灵活扩展:OAuth2、JWT、SSO等复杂场景都能通过扩展点实现
- 社区活跃:Spring 家族核心项目
对比轻量级安全框架,Spring Security的 “复杂” 在某个角度也是它的优势——后续出现新的认证协议、标准,那些预设好的“简单方案”反而会成为枷锁。
核心架构:吃透过滤器链,才算摸到 Security 的门槛
Spring Security 与其他框架有些不同,需要深入其架构了解各个组件构成才能灵活使用。
Servlet 过滤器
根据 Servlet 规范,客户端发起请求后会经过由多个过滤器构成的过滤器链,最终到达 Servlet 处理业务逻辑。过滤器对请求、响应进行加工,很多三方库都通过自定义 Filter 拦截处理请求,通过决策放行、禁止或修改请求。
Spring Security 也通过自定义一个 Spring Security Filter 来拦截请求,将请求转发到 Spring Security 核心进行处理、决策后续操作。

如果不使用三方安全库实现一个基于 Session 认证功能, 流程大体如下:

特别注意的是,除非是内部系统且用户体量极小,否则切勿轻易从零开始实现安全认证授权组件。因为这不仅涉及认证授权,更包含重要的安全环节,其中有很多坑需要规避。
安全过滤链
Spring Security 通过 Servlet Filter 过滤链拦截请求,这是整个处理流的入口。核心处理流程由 Spring Security 定义的安全过滤链实现,其设计复用了 Servlet 过滤链的逻辑,并直接使用其核心 Filter 类。
实际上,Spring Security 的安全防护、认证、鉴权功能都体现在安全过滤链中。每个安全过滤链中的 Filter 都可以执行 3 种行为:
- 不符合安全要求,直接结束当前请求(可能抛出异常或直接响应,如未认证返回 401 响应码)
- 当前 Filter 无法处理请求,放行到过滤链下一个 Filter 处理
- 当前 Filter 处理完成,放行到过滤链下一个 Filter 处理

HTTP 请求在安全过滤链上经过层层关卡:
- 前置拦截:CsrfFilter 防御跨站攻击,HeaderWriterFilter 添加安全响应头
- 认证关卡:UsernamePasswordAuthenticationFilter 处理表单登录,BasicAuthenticationFilter 应对 HTTP Basic 认证
- 终极守卫:AuthorizationFilter 根据角色权限决定放行与否
安全过滤链剖析
开启日志打印过滤器列表是学习分析 Spring Security 最重要且便捷的手段。
打印当前启用过滤器列表
设置 org.springframework.security 包日志等级为 debug
logging.level.org.springframework.security=DEBUG
应用启动时,会打印当前 Spring Security 启用的过滤器列表及其顺序:
2025-05-31T19:19:11.014+08:00 DEBUG 20612 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with filters: DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter, BasicAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, ExceptionTranslationFilter, AuthorizationFilter
以上是未进行任何配置时启用的过滤器链,下面简要分析各个过滤器的作用:
|
作用 |
过滤器 |
|
安全相关 |
CsrfFilter:实现跨站请求伪造防护 |
|
会话相关 |
RequestCacheAwareFilter:处理请求缓存,用于登录后重定向到原始请求 |
|
上下文管理 |
SecurityContextHolderFilter:上下文管理 |
|
认证相关 |
UsernamePasswordAuthenticationFilter:处理表单登录认证逻辑 |
|
鉴权相关 |
AuthorizationFilter:基于权限或角色进行请求授权检查 |
查看请求所经过的过滤器列表
设置 org.springframework.security 包日志等级为 trace
logging.level.org.springframework.security=TRACE
启动后,在未认证情况下,访问 http://localhost:8080,日志打印如下(省略部分信息):
//== 请求 "/" 地址
Securing GET /
Invoking DisableEncodeUrlFilter (1/15)
Invoking WebAsyncManagerIntegrationFilter (2/15)
Invoking SecurityContextHolderFilter (3/15)
Invoking HeaderWriterFilter (4/15)
Invoking CsrfFilter (5/15)
//== 请求 "/" 经过以上 5 个过滤器,共 15 个过滤器
Invoking LogoutFilter (6/15)
Did not match request to Ant [pattern='/logout', POST]
Invoking UsernamePasswordAuthenticationFilter (7/15)
Did not match request to Ant [pattern='/login', POST]
Invoking DefaultLoginPageGeneratingFilter (8/15)
Invoking DefaultLogoutPageGeneratingFilter (9/15)
Did not render default logout page since request did not match [Ant [pattern='/logout', GET]]
Invoking BasicAuthenticationFilter (10/15)
Did not process authentication request since failed to find username and password in Basic Authorization header
Invoking RequestCacheAwareFilter (11/15)
matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided
//== 以上过滤器未匹配请求,都不会执行
Invoking SecurityContextHolderAwareRequestFilter (12/15)
Invoking AnonymousAuthenticationFilter (13/15)
// 以下所有操作由 AuthorizationFilter 和 ExceptionTranslationFilter 配合完成
Invoking ExceptionTranslationFilter (14/15)
Invoking AuthorizationFilter (15/15)
Authorizing GET /
Checking authorization on GET / using org.springframework.security.authorization.AuthenticatedAuthorizationManager@39247f63
Did not find SecurityContext in HttpSession FD9DE3B4A78316C33B2453D31B9F36B8 using the SPRING_SECURITY_CONTEXT session attribute
Created SecurityContextImpl [Null authentication]
Created SecurityContextImpl [Null authentication]
Set SecurityContextHolder to AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=FD9DE3B4A78316C33B2453D31B9F36B8], Granted Authorities=[ROLE_ANONYMOUS]]
//== 以上由授权过滤器验证当前为匿名用户,鉴权失败,抛出异常
Sending AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=FD9DE3B4A78316C33B2453D31B9F36B8], Granted Authorities=[ROLE_ANONYMOUS]] to authentication entry point since access is denied
//== 由 ExceptionTranslationFilter 处理鉴权异常,并进行后续处理
Saved request http://localhost:8080/?continue to session
//== 保存当前请求,用于登录成功后重放请求
Trying to match using And [Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@57249300, matchingMediaTypes=[application/xhtml+xml, image/*, text/html, text/plain], useEquals=false, ignoredMediaTypes=[*/*]]]
Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint@66f5a02e
Redirecting to http://localhost:8080/login
//== 重定向到登录地址
通过过程日志分析 Spring Security 处理流。特别当处理过程不符合预期配置时,可以在关键 Filter 上打断点排查。
认证
用户认证由命名为 xxAuthenticationFilter 的过滤器处理,开箱即用的情况下会启用基于用户名密码表单认证的 UsernamePasswordAuthenticationFilter 和基于用户名密码 Basic 认证的 BasicAuthenticationFilter。
Authentication
Authentication 表示用户认证请求,登录时需将请求中的凭证转为该对象。同时,Authentication 也用于表示已认证的对象,区别在于认证状态标识。Authentication 自身是一个接口,需按场景定义实现类:
- 认证前:Authentication 作为携带凭证的对象,如用户名密码认证的 UsernamePasswordAuthenticationToken
- 认证后:Authentication 作为已认证对象,额外携带权限等信息,且 isAuthenticated = true
Spring Security 认证和鉴权分离设计,认证时仅需通过 setAuthenticated(boolean isAuthenticated) 设置已认证状态(认证通过设为 true,失败设为 false),框架不关注该值的设置方式。
认证处理流程

认证 Filter 负责将请求转为 Authentication 类型并进行认证。认证操作由 AuthenticationManager 组件负责,成功则返回新的 Authentication 类并调用成功处理器,失败则调用失败处理器。因此,整个认证流程可分为 4 个部分:提取认证参数、认证、成功处理、失败处理。
1. 提取认证参数
用户提交认证时,将从用户请求中提取出凭证信息来构造出 Authentication。 如使用用户名密码进行认证时,将从HttpServletRequest中提取出用户名和密码来构造 UsernamePasswordAuthentication
2. 认证
认证过程由 AuthenticationManager 接口对应的组件负责,该接口只有一个方法。
Authentication authenticate(Authentication authentication) throws AuthenticationException
- 入参为请求中提取出的 Authentication
- 出参为认证成功后的 Authentication,转到成功处理器进行后续处理
- 异常 AuthenticationException 表示当前认证失败(一般由于凭证不正确导致),该异常由“认证 Filter” 捕获,并转到失败处理器进行后续处理
AuthenticationManager 默认实现为 ProviderManager, 它自身并未进行真实的认证操作,而是将这个操作委托给一个系列的 AuthenticationProvider。

所以,按照 Spring Security 的设计,自定义一个认证操作,只需要实现对应的 AuthenticationProvider 即可。
3. 成功处理
按照 Spring Security 的要求,需要在成功时处理几个工作。
- 设置安全上下文
- 处理 rememberMe
- 发布认证事件
- 成功处理
通过认证过程获得返回的 Authentication 对象。可以按照实际需求来决定直接返回响应码还是转发到指定页面。 常见场景:
- jwt 令牌:直接生成令牌并写入到响应中
- session:将 sessionId 写入响应 header 头(可能直接复用当前 session )
- 服务端渲染:转发到指定模板上进行渲染
4. 失败处理
按照 Spring Security 的要求,需要在成功时处理几个工作
- 清空安全上下文
- 清空 rememberMe
- 失败处理
从实际需求来决定直接返回响应码还是转发到指定页面 常见场景:
- 前后端分离:返回 401 状态码
- 服务端渲染:转发到登录界面,并传递失败原因
内置的用户名凭证认证流程
Spring Security 提供了很多开箱即用的认证方式,用户名凭证的认证较为通用也比较有代表性。比如:用户名密码登录、手机号验证码登录都可以基于该内置实现配置完成。
其他需要定制的认证方式都可以通过参考它进行编写代码实现。

如上图,Spring Security 提供了多种开箱即用的认证方式,用户名凭证认证较为通用(如用户名密码登录、手机号验证码登录可基于此实现),其他定制认证方式可参考其实现逻辑。
ProviderManager 将认证过程委托给一系列 AuthenticationProvider。Spring Security 为实现开箱即用,启动时自动配置父级 ProviderManager,当当前 ProviderManager 中的 AuthenticationProvider 均无法处理认证请求时,会尝试交给父级 ProviderManager 处理。

自定义认证流程
上述的认证流程中AuthenticationManager的认证接口实现是可选的。该接口内部实现逻辑高度可配置,导致配置块有些分散,不同位置的配置可能还需要搭配使用。
在认证逻辑简单的情况下,完全可以抛弃 AuthenticationManager, 在 Filter 自己实现一套认证代码。 伪代码如下:
public class AuthFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// 1. 提取参数
String username = request.getParamter("username");
String pwd = request.getParamter("password");
// 2. 自定义认证逻辑
boolean authenticated = "root".equals(username) && "pwd".equals(pwd);
if (authenticated) {
// 构造认证成功的 Authentication
Authentication result = new CustomAuthentication(root, pwd, authorities);
result.setAuthenticated(true);
// 3. 成功处理
successfulAuthentication()
} else {
// 4. 失败处理
unsuccessfulAuthentication()
}
}
}
使用时,需将自定义 Filter 注册到安全过滤链的合适位置。但需注意,这种方式会抛弃 Spring Security 的部分内置特性(如 session、rememberMe)。
鉴权
认证、鉴权分离
Spring Security 将认证和鉴权分离设计极为巧妙,二者相互独立。通俗地说,可以使用第三方认证方案,仅借助 Spring Security 实现鉴权。
如何实现?
- 匿名身份:在安全过滤器链中,认证过滤器之后会接入匿名过滤器(AnonymousAuthenticationFilter),若认证过滤器未识别有效身份,则通过匿名过滤器生成匿名身份 AnonymousAuthenticationToken,且 authenticated = true。
- “认证”权限:在鉴权环节,“是否认证”与“是否拥有角色”属于同一层级概念,表示拥有某项权限。因此,鉴权配置中通常最后会添加 anyRequest().authenticated()(限制其他请求不能为匿名用户)。
ExceptionTranslationFilter 协作
鉴权过程由 2 个过滤器配合完成:AuthorizationFilter 负责鉴权,符合权限要求则放行,否则抛出异常,该异常由 ExceptionTranslationFilter 捕获并处理,伪代码如下: 伪代码如下:
// 整个 try-catch 块由 ExceptionTranslationFilter 负责
try {
// 内部由 AuthorizationFilter 负责
boolean hasAuthority = check()
if(!hasAuthority) {
throw new AccessDeniedException()
}
} catch(Exception e) {
if (e instanceof AccessDeniedException) {
// 无授权响应
send403()
}
if (e instanceof AuthenticationException) {
// 发起认证
send401()
}
throw e;
}
这种将鉴权过程与结果处理分离的设计,个人觉得有些不妥。鉴权流程与响应处理本应属于同一环节,分离后导致两个过滤器相互依赖,无法拆分(而认证环节的处理集中在认证过滤器中,内聚性更强)。
鉴权架构

从上图可以看出,鉴权实现分为两部分:识别用户请求对应的权限验证器、验证权限。
配置
鉴权配置建立请求匹配器和验证器的映射关系,解决“用户请求如何找到合适的权限验证器”问题。以下面配置为例:
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(request -> request
// IpAddressMatcher 直接具体的RequestMatcher, 可以自定义
.requestMatchers(new IpAddressMatcher("192.168.1.15")).denyAll()
// MvcRequestMatcher ant 风格路径
.requestMatchers("/admin/**").hasRole("ADMIN")
// AnyRequestMatcher
.anyRequest().authenticated());
return http.build();
通过 requestMatchers() 定义请求匹配器,后接权限验证器。例如,requestMatchers(new IpAddressMatcher("192.168.1.15")).denyAll() 表示:IP 为 “192.168.1.15” 的请求将由 “拒绝验证器” 处理。
请求匹配器:RequestMatcher
配置 requestMatchers() 最终会解析为 RequestMatcher 实例,Spring Security 已提供多种场景的实现:
|
方法 |
特点 |
|
antMatchers |
最常使用,支持 Ant 风格路径表达式,简单灵活(如 /api/**)。 |
|
mvcMatchers |
与 Spring MVC 路径匹配规则一致,支持路径变量(如 /users/{id})。 |
|
其他RequestMatcher实现 |
自定义匹配逻辑,可检查请求头、参数、IP 等。 |
验证器 AuthorizationManager
匹配请求后,AuthorizationManager 验证当前用户身份是否满足条件。hasRole("ADMIN")、denyAll() 等最终会解析为 AuthorizationManager 实例(或等价的 lambda 表达式)。
该接口只有一个核心方法:
AthorizationDecision check(Supplier<Authentication> authentication, T context)
- authentication:已认证的用户信息
- context:请求上下文(如请求本身、所需权限等)
- 返回值:AuthorizationDecision 包含布尔值,表示是否通过鉴权
架构实现
整体架构的实现和认证架构非常相似。AuthorizationManager 在鉴权中起到 2 个作用:
- 作为鉴权的入口,由鉴权过滤器 AuthorizationFilter 调用
- 作为验证器,验证请求用户身份是否符合要求

如上图,鉴权过滤器调用 RequestMatcherDelegatingAuthorizationManager 进行鉴权,在其内部维护了一个验证器列表,将鉴权的具体操作委托给它们。
老版本的 Spring Security 使用的是另一套方案,整体比较复杂。
从认证和鉴权的实现方案非常相似,鉴权的方案更加成熟一点,只依赖核心 AuthorizationManager 接口就完成了鉴权实现。 认证的实现显然有历史包袱(ProviderManager 的作用完全可以被 AuthenticationManager替代),未来说不定可能改进。
从踩坑到精通的避坑指南
鉴于 Spring Security 配置复杂性,开发时,按照以下流程会降低一些难度。
- 开启日志,查看 Spring Security 过滤器链执行顺序,必要时在跳转关键点(如认证、鉴权失败)进行断点分析。
- 自定义实现前,考虑 Spring Security 是否已实现相似功能,主流的认证、鉴权操作都已支持,个性化行为可以在Configuration 中 配置 SecurityFilterChain。
- 自定义实现,可以参考 Spring Security 已提供的实现,尽量复用非核心实现(如 session、rememberMe)。第一、减少实现难度,第二、有些框架内的代码未调用,会导致运行中的异常。
总结
Spring Security 提供的认证和鉴权功能已非常完善,涵盖 CORS、CSRF 等安全特性,且默认开启。它在保证开箱即用的同时,保持了极高的灵活性,各部分均可自定义,但也正因过度灵活导致自定义复杂度较高。不过,遵循一定的流程和方法可降低使用阻力,使框架更加易用。
2万+

被折叠的 条评论
为什么被折叠?



