Spring Security 文档精读:Filter 链注册机制与 OAuth2 权限迁移
2026/9/9 9:43:56 网站建设 项目流程

Spring Security 官网文档我前前后后翻过不下十遍,每次以为自己看懂了,新项目一开始还是会踩坑。最典型的一次是排查过滤器不生效,查了一周网上博客愣是没找到原因,最后翻回官网 Architecture 那一章,十分钟就定位到问题——原来是我对 DelegatingFilterProxy 的注册机制理解错了。从那以后我就养成了一个习惯:遇到 Spring Security 的问题,第一件事不是搜索,而是打开官方文档对应章节。

这篇东西不是文档翻译,也不是教程复读,而是从"怎么读文档"的角度出发,把 Spring Security 官网文档里那些真正重要的部分串一遍,重点说清楚三个热搜里反复出现的问题:Spring Security 的中文文档怎么找、Filter 链到底是怎么完成注册的、OAuth2 里的 hasScope 为什么在新版本里找不到了。

1. 为什么官网文档是学习 Spring Security 最好的地方

1.1 搜索博客的碎片化困境

Spring Security 的学习路径非常容易走偏。你在搜索引擎里输入"Spring Security 登录认证",能收到一堆博客文章,但几乎每一篇都只讲了某个具体场景:配置了 SecurityFilterChain、加了表单登录、放行了几个路径,然后就没有然后了。这些博客本身没有错,但 Spring Security 是一个高度抽象、分层极多的框架,单独看任何一个配置片段都无法建立完整的认知。

最典型的问题就是版本差异。Spring Boot 2.7 时代还在用authorizeRequests,到了 Spring Boot 3.x 就直接让你用authorizeHttpRequests;旧博客里全是WebSecurityConfigurerAdapter,新版本里这个类已经彻底移除了。搜索引擎给你的结果,往往混杂了三年前的写法和今年的最新 API,新手根本分不清哪个是当前可用方案。

官网文档恰好解决了这个问题。它只维护当前版本的内容,旧版本单独存档,章节之间有明确的逻辑递进,而且每个配置项、每个过滤器的职责都有官方定义。学习成本确实比看博客高,但换来的是准确性和系统性。

1.2 官网文档的阅读路线图

Spring Security 官方文档从 6.x 开始做了一个很清晰的结构划分。进入文档首页,你应该首先注意这几个方向:

  • Servlet Applications:基于 Spring MVC 的经典 Web 应用,这是绝大多数开发者的主战场,也是文档内容最丰富的一块。
  • Reactive Applications:基于 WebFlux 的响应式应用,写法上和 Servlet 版本有较大差异。
  • Getting Started:快速上手示例,用于建立第一印象。

我的建议是,第一次读文档不要直接扎进" Authentication "章节,先花半小时把Architecture完整读一遍。这一章用图文方式解释了请求从进入 Servlet 容器到最后被业务代码处理的完整过滤链,是整个框架的地基。地基没打牢之前,看后面的内容都很容易飘。

1.3 关于"中文文档"的正确打开方式

网络上能搜到一些 Spring Security 的中文翻译文档,但大部分翻译版本停留在 Spring Security 5.x 时代,甚至还有更早的 4.x 内容。框架 API 变化频繁,用旧文档指导新项目开发很容易掉坑。

如果英文阅读吃力,我推荐的做法是:用浏览器翻译功能看原文。现代浏览器的整页翻译对技术文档的翻译质量已经相当不错,术语基本保留,翻译后仍然能看懂逻辑。关键 API 的 javadoc 注释最好对照原文看,因为这些注释的措辞往往决定了方法的行为边界。另一个折中方案是:先用中文快速浏览章节结构,知道"哪一章大概在讲什么",再在需要深度理解时精读英文原版。

1.4 版本选择:如果你还想用老版本

很多公司项目还在 Spring Boot 2.x 时代,对应的 Spring Security 是 5.7/5.8 系列。这种情况不需要强行上 6.x 文档,官网文档都提供了版本切换入口,点开左侧导航栏底部的版本下拉框,可以找到 5.7、5.8 的历史版本。老项目中如果有配置要查,务必切到对应版本看,否则会看到很多方法名对不上、语义完全不同的内容。

2. 核心架构:Spring Security 的 Filter 链是如何完成注册的

"Spring Security filter 是如何完成注册的"这个搜索词在近期热度很高。我猜问这个问题的人,十有八九都遇到过自定义 Filter 不生效、或者过滤链里多了一个自己没见过的过滤器这类诡异问题。这一节就针对 Filter 注册机制做个完整拆解。

2.1 从 Servlet 过滤器到 DelegatingFilterProxy

Spring Security 在 Web 应用中的工作基础是 Servlet 规范中的 Filter。正常情况下,一个过滤器要在 web.xml 或 Servlet 容器里注册,容器收到请求后按照注册顺序执行过滤器。但 Spring Security 的设计目标是完全脱离容器配置,不依赖 web.xml。

为了让 Spring 容器里的 Bean 能够被 Servlet 容器调用,Spring 提供了一个桥接过滤器:DelegatingFilterProxy。这个过滤器本身会被注册到 Servlet 容器中,但它只是一个"空壳",真正干活的时候,它会去 Spring 容器里查找名字为springSecurityFilterChain的 Bean,然后把请求委托给它。

这里的关键点是:DelegatingFilterProxy 是一个"代理",它自己不做任何安全校验,它只负责从 ApplicationContext 里找目标 Bean 并转发请求。很多人在项目里自定义了一个 SecurityFilterChain 就以为万事大吉,却没有考虑到这个 Bean 是否真的能被 Servlet 容器感知。

2.2 谁是 springSecurityFilterChain?FilterChainProxy 的秘密

顺着 DelegatingFilterProxy 往下追,目标 Bean 的名字是springSecurityFilterChain,它的实际类型是FilterChainProxy

FilterChainProxy实现了 Filter 接口,但它不是普通的过滤器,它是一个"过滤器链容器"。这个类内部维护了一个有序的SecurityFilterChain列表,每个 SecurityFilterChain 对应一个 RequestMatcher 和一组过滤器对象。请求进来时,FilterChainProxy 会遍历列表,找到第一个匹配当前请求的 SecurityFilterChain,然后执行它内部维护的那组过滤器。

换句话说,你在配置类里写的:

@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .formLogin(Customizer.withDefaults()); return http.build(); }

这个filterChain方法返回的SecurityFilterChain对象,会被框架收集起来,和框架内置的其他 SecurityFilterChain(比如保护错误页面、静态资源的那几条默认链)一起,在FilterChainProxy内部按顺序排列。

2.3 Spring Boot 自动注册的关键环节

在 Spring Boot 项目中,DelegatingFilterProxy 也不是手动添加到 web.xml 里的,而是由自动配置完成的。Spring Boot 的SecurityFilterAutoConfiguration会检测到容器中存在springSecurityFilterChain这个 Bean,然后自动创建一个FilterRegistrationBean,把 DelegatingFilterProxy 注册到 Servlet 容器中,注册顺序默认是所有过滤器中最靠后的(Ordered.LOWEST_PRECEDENCE - 1),这样做的目的是确保请求先经过业务过滤器,最后再进入 Spring Security 的过滤链。

这里有一个容易让人犯迷糊的地方:@EnableWebSecurity 导入的配置类也会做初始化工作,Spring Boot 的自动配置则负责把过滤器注册进容器。如果脱离了 Spring Boot(比如纯 Spring 项目),你需要手动在 AbstractSecurityWebApplicationInitializer 的子类里完成初始化,它会替你完成 DelegatingFilterProxy 的注册。

2.4 用日志验证过滤器链路是否注册成功

注册是否成功,最直接的办法是开启 DEBUG 日志:

logging.level.org.springframework.security=DEBUG

启动项目后,你会看到类似这样的日志输出:

DefaultSecurityFilterChain - Validated match pattern [/**] DefaultSecurityFilterChain - Adding security filter UsernamePasswordAuthenticationFilter DefaultSecurityFilterChain - Adding security filter ExceptionTranslationFilter DefaultSecurityFilterChain - Adding security filter AuthorizationFilter

这组日志表明你的 SecurityFilterChain 已经组装完成并被 FilterChainProxy 管理。如果这里的过滤器数量和你配置的不一致(比如少了某个自定义过滤器),说明注册链路出了问题。

2.5 常见的自定义 Filter 不生效的原因

实际开发中最常见的自定义 Filter 不生效,无非以下几种情况:

  1. 自定义过滤器实现了 Filter 接口,但没有交给 Spring 容器管理。FilterChainProxy 只认 SecurityFilterChain 里添加的过滤器,你通过 @Component 注册的普通 Filter 走的是 Servlet 容器过滤器链,两者是并行的,不是你直觉中的"串行"。
  2. 在 HttpSecurity 里用 addFilterBefore 添加了过滤器,但方法参数类型不匹配addFilterBefore要求传入的过滤器必须实现jakarta.servlet.Filter,而且这里的"before"指的是在 SecurityFilterChain 内部顺序中的 before,不是 Servlet 容器过滤器链的顺序。
  3. 多个 SecurityFilterChain 时,自定义过滤器加到了错误的那条链上。加在哪条链取决于你是在哪个SecurityFilterChain实例的 HttpSecurity 上 add 的。如果请求匹配了其他链,那你的过滤器根本不会执行。
  4. 过滤器注册顺序不对FilterRegistrationBean的 order 属性会被所有过滤器使用,而 Spring Security 默认用最低优先级注册。如果不小心给了自定义 Filter 一个更高优先级(数值更小),请求还没进安全链就被拦截了。

在动手改代码之前,先想清楚:这个过滤器到底应该在 Servlet 容器层执行,还是在 Spring Security 的 SecurityFilterChain 内执行?这个问题的答案直接决定了你的注册方式。

3. 官网文档里认证与授权章节的正确打开方式

3.1 Authentication 章节的阅读逻辑

Spring Security 官方文档的Authentication章节,按我说的"体系阅读法"拆开看,是这样一条线:

  • 先理解AuthenticationManager是总入口,接口的authenticate方法接收一个Authentication对象,返回一个已认证的Authentication对象。
  • ProviderManager是 AuthenticationManager 的默认实现,它维护了一组AuthenticationProvider,逐个尝试去认证请求。
  • 每个AuthenticationProvider只处理一种特定的凭证类型。DaoAuthenticationProvider是其中最常见的一个,它会通过UserDetailsService加载用户信息,然后对密码做比对(密码编码器由PasswordEncoder负责)。

整条链路可以用一句话串起来:登录请求来了,AuthenticationManager 找到合适的 AuthenticationProvider,Provider 从 UserDetailsService 拿用户数据,比对凭证,比对通过就返回完整 Authentication 对象。之后通过 SecurityContextHolder 把它保存在当前线程的 SecurityContext 里。

3.2 授权部分的三个层级

授权是 Spring Security 里最容易产生命名混乱的部分,因为授权可以发生在三个完全不同的层级:

  • URL 级授权:用authorizeHttpRequests配置,针对"路径"做控制。比如/admin/**只有 ADMIN 角色能访问。
  • 方法级授权:在 Service 或 Controller 方法上打注解,比如@PreAuthorize("hasRole('ADMIN')"),这是最常用、最灵活的方式。
  • 对象级授权:通过 ACL 模块实现,控制到单条数据记录的读写权限,非常重量级,大多数业务场景用不到。

新版本的文档在 Authorization 章节里会按这个层次展开。对绝大多数项目,你只需要重点看 URL 级和方法级就够了。

3.3 @EnableMethodSecurity 时代的方法安全

方法安全这块,5.6 之前的老写法是用@EnableGlobalMethodSecurity,从 5.6 开始官方引入@EnableMethodSecurity作为替代。6.x 版本里已经找不到@EnableGlobalMethodSecurity了。

@EnableMethodSecurity提供了三个可选项:

@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
  • securedEnabled = true:启用@Secured注解,写法是@Secured("ROLE_ADMIN")
  • jsr250Enabled = true:启用 JSR-250 规范注解,比如@RolesAllowed
  • 默认启用的@PreAuthorize/@PostAuthorize不受这两个开关控制,它一直生效。

实际开发中,@PreAuthorize("hasAuthority('USER_DELETE')")这种写法最灵活,它能直接写 SpEL 表达式,可以调用方法参数、做逻辑组合。如果你的项目只需要简单的角色判断,@Secured也够用。但需要注意的是:在 SecurityFilterChain 中,hasRole('ADMIN')相当于hasAuthority('ROLE_ADMIN')hasRole只是自动帮你加了ROLE_前缀的语法糖。

3.4 认证授权章节最容易读漏的几个点

官网文档里有些知识点不在显眼的标题下面,但实战非常关键:

  • CSRF 保护:Spring Security 默认开启 CSRF 防护,REST API 如果走会话认证,必须携带 CSRF Token,否则 POST 请求会被拒。用 JWT 的接口通常不需要 CSRF,因为天然免疫这种攻击。
  • Session 管理SessionCreationPolicy的控制对无状态服务很重要,STATELESS模式下 Spring Security 不会再创建 Session,但仍然会尝试从请求头里解析 Bearer Token。
  • 异常处理ExceptionTranslationFilter是框架处理认证/授权异常的枢纽,它会把AccessDeniedExceptionAuthenticationException转成 HTTP 响应或重定向。自定义 401/403 返回体时,你要处理的其实是这个过滤器之后的AuthenticationEntryPointAccessDeniedHandler

4. OAuth2 文档的困惑:hasScope 方法去哪了

4.1 让无数人困惑的往事

"Spring Security OAuth2 没有 hasScope 方法了吗"——这个坑太经典了。

在 Spring Security 5.x 的早期版本(5.7 及以前),OAuth2 资源服务器配置里确实有hasScope()方法。因为网上的教程和许多开源项目都是那个时期写的,所以搜索出来的代码大部分是这种写法:

http.oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .jwtAuthenticationConverter(jwtAuthenticationConverter()) ) .accessDeniedHandler(...) ); @PreAuthorize("hasScope('read')")

这种写法在 Spring Security 5.7 确实是合法的。但从 5.8 开始,官方把hasScopehasAnyScope标记为 deprecated,到了 6.x 直接移除了。所以你现在打开新项目的依赖,看到的基础是不包含这些方法的。

4.2 为什么官方要移除 hasScope

因为scope本质就是 authority 的一种特殊形式。OAuth2 中,JWT(或 OAuth2 token)里有个scope字段,它代表客户端被授权的权限范围。在 Spring Security 内部,这些 scope 会被转换成带SCOPE_前缀的SimpleGrantedAuthority对象。

也就是说:

  • scope = read在 Spring Security 内部就是authority = SCOPE_read
  • hasScope("read")等价于hasAuthority("SCOPE_read")

官方认为没必要维护两套 API,所以统一收敛到hasAuthority/hasAnyAuthority。这样权限模型就统一了:不管这个权限来自角色、scope 还是自定义权限字段,最终都是 authority。你不需要再区分"这是 role 还是 scope"来选不同的方法,只需要知道它的前缀规则即可。

4.3 新版本里的正规写法

在新版本中,正确的写法有两种。

如果是配置类里针对路径的授权:

http.authorizeHttpRequests(auth -> auth .requestMatchers("/api/invoices").hasAuthority("SCOPE_invoice.read") .anyRequest().authenticated() );

如果是在方法注解上用:

@PreAuthorize("hasAuthority('SCOPE_read')") public List<Invoice> getInvoices() { // ... }

注意SCOPE_前缀是必须的。很多人在迁移时只是机械地删除 hasScope 换成 hasAuthority,忘记加前缀,结果权限全部失效,接口全部返回 403。

4.4 在文档里精准定位 OAuth2 相关章节

官网文档在Servlet Applications -> OAuth2模块下分了几大块:

  • OAuth2 Client:客户端模式,用于接入第三方登录、获取 token。
  • OAuth2 Resource Server:资源服务器模式,用于校验 JWT 或 opaque token。
  • OAuth2 Authorization Server:授权服务器,Spring Security 不再直接支持,需要迁移到 Spring Authorization Server 项目。

你要根据项目角色选对应章节。资源服务器这块,重点看 JWT 那一节,里面讲了JwtDecoderJwtAuthenticationConverter以及自定义 token 解析的方式。

如果项目里有调用用户信息(userinfo)、刷新 token 等,就属于 OAuth2 Client 章节的范畴。里面的OAuth2LoginAuthenticationFilter的过滤位置、OAuth2AuthorizedClientManager的配置方式,都是实际开发中绕不开的细节。

4.5 实际项目迁移时需要动的代码

假设你正在把一个 Spring Boot 2.7 + Spring Security 5.7 的项目升级到 Spring Boot 3.x + Spring Security 6.x,涉及 hasScope 的改动主要是这几处:

  1. 所有方法上的@PreAuthorize("hasScope('read')")改成@PreAuthorize("hasAuthority('SCOPE_read')")
  2. 所有 SecurityFilterChain 里的.hasScope("read")改成.hasAuthority("SCOPE_read")
  3. 如果你的自定义 AuthenticationConverter 之前没有添加 SCOPE_ 前缀,升级后要检查JwtGrantedAuthoritiesConverter的默认行为。Spring Security 6.x 默认会把scope字段转换成SCOPE_xxx形式的 authority,但如果你重写了 converter,可能就丢了这个默认行为,导致权限判断不一致。
  4. 检查是否有其他地方依赖了旧版的OAuth2AccessToken相关 API,这类 API 在 6.x 中的包名和类名有变化。

实际测试下来,最隐蔽的问题不在编译期,而是运行时:编译过了,但权限全部 403。原因就是 scope 前缀没有被加进 authority。

5. 从文档到实战:我读 Spring Security 文档的几点心得

5.1 文档里没有明确说清的"隐藏规则"

  • 过滤链的顺序就是安全策略的顺序。文档不会刻意提醒你,但addFilterBefore/addFilterAfter的"之前""之后"是相对 SecurityFilterChain 内部的顺序而言,不是相对 Servlet 层级的顺序。如果你在 Servlet 容器里也加了过滤器,两条链是平行的,不会互相嵌套。
  • 任何一条 SecurityFilterChain 不生效,先看 RequestMatcher。FilterChainProxy 是"拿请求去匹配第一条匹配的链",一旦匹配就固定使用这条链。所以如果前一条链的requestMatcher覆盖范围太广,后面的链永远不会被触发,这在多链配置里最容易出问题。
  • 授权表达式本质都是 authority 判断。hasRole、hasScope、hasAuthority,底层都是对GrantedAuthority的匹配。文档会把它们分开写,是为了让你读起来有语义区分。实战中我建议统一用hasAuthority,前缀规则自己控制,排查问题的时候思路最清晰。

5.2 按需求驱动阅读,不按章节顺序硬啃

如果你要做的功能是"给现有系统加一个手机号登录",没必要从文档开头读起。我的做法是:先把需求拆成几个关键词——手机号验证码 -> 自定义认证流程 -> AuthenticationProvider;短信验证码校验 -> 自定义逻辑;登录成功后返回自定义 Token -> 该走 JWT 还是 session。再凭这几个关键词,直接在文档里搜对应章节。

文档的全文搜索功能很好用,直接搜AuthenticationProviderUserDetailsService,能快速定位到具体小节。读的时候只精读那几节的内容,其余先跳过。这样读文档的效率比从头读到尾高得多。

5.3 利用官方 Sample 和测试代码辅助理解

只有文档还不够,我还强烈建议配合官方 Sample 项目来学习。Spring Security 官方仓库(spring-projects/spring-security)的samples目录下有大量可运行的示例代码,每一个示例对应文档里的某个具体场景。比如你想找"JWT 资源服务器 + method security 配合"的完整示例,直接去那里找。

读示例代码的时候,不要只盯着 SecurityFilterChain 的配置,要把示例的测试代码也看了。Spring Security 官方对测试的支持(spring-security-test)很完善,@WithMockUserSecurityMockMvcRequestPostProcessors这些工具在调试权限问题时能派上大用场。文档的 Test 章节对这一部分有完整介绍。

5.4 版本升级时最实用的一个技巧

Spring Security 5.8 官方给了一个迁移指南文档,专门讲从 5.7 到 5.8 再到 6.0 的 API 变化。这篇文档在官网导航里叫Migration Guide,是版本升级前的必读材料。里面列了一个"已移除/已废弃 API 对照表",按表逐项检查项目代码,能省掉大量搜报错信息的时间。

最后一个切身的体会:Spring Security 的文档属于"越看越薄"的类型。第一次读觉得信息量巨大、名词满天飞,但当你在项目里真的踩过几个坑再回去读同一段内容,会突然发现作者把每种情况都写得很清楚,只是当初没注意到。所以如果你现在觉得看不懂,不用焦虑,也别急着换博客,你只是缺一些真实的实战经验。把文档留在手边,遇到问题回头翻一翻,几个项目下来,自然就通了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询