Spring Security 该怎么玩 ?

写在前面

在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 请求在安全过滤链上经过层层关卡:

  1. 前置拦截:CsrfFilter 防御跨站攻击,HeaderWriterFilter 添加安全响应头
  2. 认证关卡:UsernamePasswordAuthenticationFilter 处理表单登录,BasicAuthenticationFilter 应对 HTTP Basic 认证
  3. 终极守卫: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:实现跨站请求伪造防护
LogoutFilter:处理用户登出逻辑
HeaderWriterFilter:添加安全相关的 HTTP 响应头
DisableEncodeUrlFilter:禁用 URL 编码(特殊场景使用)

会话相关

RequestCacheAwareFilter:处理请求缓存,用于登录后重定向到原始请求
WebAsyncManagerIntegrationFilter:集成 SecurityContext 到异步处理机制

上下文管理

SecurityContextHolderFilter:上下文管理
SecurityContextHolderAwareRequestFilter: 将上下文绑定到请求上

认证相关

UsernamePasswordAuthenticationFilter:处理表单登录认证逻辑
BasicAuthenticationFilter:处理 HTTP Basic 认证
AnonymousAuthenticationFilter:在没有已认证用户时提供匿名身份
DefaultLoginPageGeneratingFilter:生成默认登录页面
DefaultLogoutPageGeneratingFilter:生成默认登出页面

鉴权相关

AuthorizationFilter:基于权限或角色进行请求授权检查
ExceptionTranslationFilter:将安全异常转换为 HTTP 响应

查看请求所经过的过滤器列表

设置 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 实现鉴权。

如何实现?

  1. 匿名身份:在安全过滤器链中,认证过滤器之后会接入匿名过滤器(AnonymousAuthenticationFilter),若认证过滤器未识别有效身份,则通过匿名过滤器生成匿名身份 AnonymousAuthenticationToken,且 authenticated = true。
  2. “认证”权限:在鉴权环节,“是否认证”与“是否拥有角色”属于同一层级概念,表示拥有某项权限。因此,鉴权配置中通常最后会添加 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 个作用:

  1. 作为鉴权的入口,由鉴权过滤器 AuthorizationFilter 调用
  2. 作为验证器,验证请求用户身份是否符合要求

如上图,鉴权过滤器调用 RequestMatcherDelegatingAuthorizationManager 进行鉴权,在其内部维护了一个验证器列表,将鉴权的具体操作委托给它们。

老版本的 Spring Security 使用的是另一套方案,整体比较复杂。

从认证和鉴权的实现方案非常相似,鉴权的方案更加成熟一点,只依赖核心 AuthorizationManager 接口就完成了鉴权实现。 认证的实现显然有历史包袱(ProviderManager 的作用完全可以被 AuthenticationManager替代),未来说不定可能改进。

从踩坑到精通的避坑指南

鉴于 Spring Security 配置复杂性,开发时,按照以下流程会降低一些难度。

  1. 开启日志,查看 Spring Security 过滤器链执行顺序,必要时在跳转关键点(如认证、鉴权失败)进行断点分析。
  2. 自定义实现前,考虑 Spring Security 是否已实现相似功能,主流的认证、鉴权操作都已支持,个性化行为可以在Configuration 中 配置 SecurityFilterChain。
  3. 自定义实现,可以参考 Spring Security 已提供的实现,尽量复用非核心实现(如 session、rememberMe)。第一、减少实现难度,第二、有些框架内的代码未调用,会导致运行中的异常。

总结

Spring Security 提供的认证和鉴权功能已非常完善,涵盖 CORS、CSRF 等安全特性,且默认开启。它在保证开箱即用的同时,保持了极高的灵活性,各部分均可自定义,但也正因过度灵活导致自定义复杂度较高。不过,遵循一定的流程和方法可降低使用阻力,使框架更加易用。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值