1. 从一次 401 说起:过滤器链到底卡在哪
很多 Java 后端同学第一次把 Spring Security 和 OAuth2 拼在一起时,都会遇到一个很迷惑的现象:明明请求头里带了Authorization: Bearer xxx,接口却还是返回 401,日志里也看不出所以然。这个问题的根源,往往不是 Token 本身有问题,而是没搞清楚 Spring Security 的过滤器链和 OAuth2 资源服务器到底谁先谁后、谁负责什么。
简单说,Spring Security 是一套「认证 + 授权」的框架,核心是一组按顺序执行的过滤器链(FilterChain)。它管的是「你是谁、你能干什么」。而 OAuth2 是一套授权协议,它定义了怎么拿 Token、怎么用 Token 访问资源。在 Spring 生态里,OAuth2 资源服务器的能力是「挂」在 Spring Security 过滤器链上的一个环节,它负责从请求头里解析 Bearer Token,然后交给 Spring Security 的上下文去判断权限。
所以两者的关系可以这样理解:Spring Security 是骨架和调度中心,OAuth2 是其中一种具体的认证方式。你配置的SecurityFilterChain决定了哪些请求需要认证、用哪种方式认证;而 OAuth2 资源服务器配置决定了 Token 怎么校验、校验通过后用户身份和权限怎么放进SecurityContext。
这篇文章面向正在做 Spring Boot 后端、需要把 OAuth2 资源服务器接进 Spring Security 的开发者。我会给出一套可以直接复制的SecurityConfig和application.yml骨架,然后用 curl 验证 Token 校验是否真的生效,最后把常见的坑列出来。适合谁:已经知道 JWT 大概长什么样、但被过滤器链顺序和配置类搞晕的人。
2. 前置准备:依赖、密钥与 TaoToken 接入信息
在写配置之前,先把依赖和基础信息准备好。这里我用 Spring Boot 3.x + Spring Security 6.x 的组合,OAuth2 资源服务器用spring-boot-starter-oauth2-resource-server,这是目前官方推荐的方式,不再依赖老的spring-security-oauth2那一套。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency>如果你手上还没有可用的 Token 签发环境,或者想先拿一个标准格式的 JWT 来验证资源服务器的校验逻辑,可以用 TaoToken 的模型对话或 API Keys 页面生成一个测试用的 Key,再配合它的接入文档确认请求头格式。官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api 。注意 API 地址不带 UTM 参数,直接用它做 base url 即可。
资源服务器校验 Token 有两种主流方式:一种是 JWT 本地校验(用公钥验签,不查授权中心),另一种是 opaque token 远程校验(把 Token 转发给授权中心 introspect 接口)。本地校验性能好、无网络依赖,适合大多数场景。下面我以 JWT 本地校验为主线,因为它最能体现「过滤器链 + OAuth2」的衔接点。
你需要准备一个 JWK Set URI,也就是授权中心暴露公钥的地址,形如https://your-auth-server/oauth2/jwks。如果你用的是 TaoToken 这类服务,可以在接入文档里找到对应的 JWKS 地址。拿到之后填进application.yml。
3. 可复制配置:SecurityFilterChain 与 application.yml 骨架
先看application.yml,这里定义了资源服务器的 issuer 和 jwk-set-uri,Spring Security 会自动拉取公钥并缓存。
spring: security: oauth2: resourceserver: jwt: issuer-uri: https://your-auth-server jwk-set-uri: https://your-auth-server/oauth2/jwks server: port: 8080注意issuer-uri和jwk-set-uri二选一即可,如果两个都写,Spring 会优先用jwk-set-uri。issuer-uri的好处是它会自动校验 Token 里的iss字段,安全性更高。
接下来是核心的SecurityConfig。这里我用 Spring Security 6 的 Lambda DSL 写法,SecurityFilterChainBean 取代了老的WebSecurityConfigurerAdapter。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**").permitAll() .requestMatchers("/user/**").hasAuthority("SCOPE_user") .requestMatchers("/admin/**").hasAuthority("SCOPE_admin") .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .jwtAuthenticationConverter(jwtAuthenticationConverter()) ) ); return http.build(); } private org.springframework.core.convert.converter.Converter< org.springframework.security.oauth2.jwt.Jwt, org.springframework.security.authentication.AbstractAuthenticationToken> jwtAuthenticationConverter() { org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter converter = new org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter(); org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter authoritiesConverter = new org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter(); authoritiesConverter.setAuthorityPrefix("SCOPE_"); authoritiesConverter.setAuthoritiesClaimName("scope"); converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); return converter; } }这段配置里最关键的是.oauth2ResourceServer(oauth2 -> oauth2.jwt(...))。它做的事情是:在 Spring Security 过滤器链中插入BearerTokenAuthenticationFilter,这个过滤器会从Authorization头里取出 Bearer Token,然后交给JwtAuthenticationProvider去验签、解析 claims,最后把结果放进SecurityContext。后面的authorizeHttpRequests规则才有东西可判断。
jwtAuthenticationConverter的作用是把 JWT 里的scope字段映射成 Spring Security 的权限。默认情况下,JwtGrantedAuthoritiesConverter会把 scope 值加上SCOPE_前缀,所以配置里写hasAuthority("SCOPE_user")才能匹配上。如果你 Token 里的权限字段叫authorities或roles,改setAuthoritiesClaimName即可。
4. 验证请求:用 curl 确认 Token 校验真的生效
配置写完,启动应用。接下来用 curl 做三组验证,分别对应「无 Token」「有 Token 但权限不够」「有 Token 且权限正确」。
第一组,不带 Token 访问受保护接口:
curl -i http://localhost:8080/user/profile预期返回401 Unauthorized,响应头里会有WWW-Authenticate: Bearer。这说明BearerTokenAuthenticationFilter没找到 Token,直接判定未认证。
第二组,带一个格式正确但 scope 不含user的 Token:
curl -i -H "Authorization: Bearer <your-token>" http://localhost:8080/user/profile预期返回403 Forbidden。注意这里和 401 的区别:401 是「你没认证」,403 是「你认证了但没权限」。如果这里返回 401,说明 Token 验签失败,检查jwk-set-uri是否可达、Token 是否过期。
第三组,带一个 scope 包含user的 Token:
curl -i -H "Authorization: Bearer <your-token>" http://localhost:8080/user/profile预期返回200 OK和业务数据。到这一步,说明过滤器链、OAuth2 资源服务器、权限映射三者已经串通了。
如果你想更直观地看 Token 解析结果,可以在 Controller 里注入Jwt对象:
import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ProfileController { @GetMapping("/user/profile") public String profile(@AuthenticationPrincipal Jwt jwt) { return "subject=" + jwt.getSubject() + ", scope=" + jwt.getClaimAsString("scope"); } }访问成功后返回的字符串里能看到sub和scope,这就证明 Token 里的信息确实被解析并传到了业务层。
5. 本篇常见错排查:401、403 与过滤器顺序
第一个高频错误:配置了oauth2ResourceServer但依然返回 401,且日志里没有验签失败信息。这通常是jwk-set-uri写错或网络不通,Spring 拉不到公钥,验签直接失败。排查方法是在启动日志里搜jwk,看有没有Fetching JWK Set相关记录。如果用的是 TaoToken 的接入文档里的 JWKS 地址,确认路径没有多余斜杠。
第二个错误:hasAuthority("user")匹配不上。原因是默认权限前缀是SCOPE_,你写user自然匹配不到SCOPE_user。要么改配置里的setAuthorityPrefix(""),要么在规则里写全SCOPE_user。我建议保留前缀,因为这样能区分 OAuth2 的 scope 和其他来源的权限。
第三个错误:自定义了一个Filter想手动解析 Token,结果和BearerTokenAuthenticationFilter冲突。记住,一旦启用oauth2ResourceServer,Token 解析这件事就交给它了,你的自定义过滤器应该放在它之后,从SecurityContext里拿已经解析好的Authentication,而不是重新解析一遍请求头。
第四个错误:SessionCreationPolicy.STATELESS没配,导致 Spring Security 尝试创建 Session,在分布式环境下出现状态不一致。资源服务器必须是无状态的,这一行不能省。
第五个错误:issuer-uri配了但 Token 里的iss对不上,返回 401。这种情况日志里会有Jwt issuer validation failed。检查授权中心实际签发的iss值,和配置里的是否完全一致,包括末尾斜杠。
6. 把 Token 校验接进你的编码链路
到这里,Spring Security 和 OAuth2 的衔接点应该清楚了:SecurityFilterChain负责定义规则和顺序,oauth2ResourceServer负责插入 Token 解析过滤器,JwtAuthenticationConverter负责把 claims 翻译成权限。三者缺一不可。
如果你接下来要在本地长期跑这套资源服务器,或者用它对接多个下游服务做联调,建议把 Token 的获取和刷新也纳入日常流程。TaoToken 的 Coding Plan 适合这种需要反复验证 Token 校验、又不想每次手动换 Key 的场景,入口在 https://taotoken.net/api 对应的控制台里可以找到。需要生成新的测试 Key 时,直接去 API Keys 页面操作,接入文档里有完整的请求头示例。
最后留一个实用技巧:在application.yml里把logging.level.org.springframework.security=DEBUG打开,启动后你会看到过滤器链的完整顺序,以及每个请求经过了哪些过滤器。这比任何文档都直观,排障时先看这行日志,能省掉大半猜测时间。