CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
OAuth 2.0 与 OpenID Connect/课时/Resource Server
RawBlame
课时目录12 节

OAuth 2.0 与 OpenID Connect

  1. 01OAuth 是什么
  2. 02授权码流
  3. 03PKCE 与 SPA
  4. 04OpenID Connect
  5. 05Scope 与权限
  6. 06Refresh Token
  7. 07Spring OAuth2 Client
  8. 08Resource Server
  9. 09第三方登录
  10. 10OAuth vs 本站 JWT 登录
  11. 11常见安全问题
  12. 12综合练习

Resource Server

JWT 校验、@PreAuthorize 与 token introspection。

35 分钟

Resource Server

这一课你将能做到

  1. 配置 spring-boot-starter-oauth2-resource-server,用 issuer-uri 自动发现 JWKS,校验传入的 Bearer access_token
  2. 区分 JWT 本地验签与 opaque token introspection 两种模式,知道何时选哪个
  3. 用 @PreAuthorize("hasAuthority('SCOPE_xxx')") 做 scope 级控制,并与本站自研 JWT 保护的路径分区共存

为什么要学

Client 负责拿票;Resource Server 负责验票。当你对外开放 API——第三方应用拿着授权服务器发的 access_token 来调你的 /api/v1/tracks——你的 backend 就切换成了 Resource Server 角色:验签、看 scope、执行业务。

它与上一课是同一枚硬币的两面(一个去别人那登录,一个验证别人的票),也和本站自研 JWT 的验签逻辑同源——区别只在发行方变成了标准 IdP、授权语言变成了 scope、验签公钥来自 IdP 的 JWKS 而不是你自己的密钥。

面试角度:「JWT 怎么验」「为什么用 issuer-uri 而不是手填 jwks-uri」「RS 与 Client 能否共存」都是开放 API 场景的常考题。

先建立图像

外部 Client(第三方应用)
  │  Authorization: Bearer eyJ...
  ▼
你的 API(Resource Server)
  │ ① 取 token:解析 Authorization 头,格式不对 → 401
  │ ② 验签:用 iss 对应的 JWKS 公钥验签名,伪造/过期/aud 不符 → 401
  │ ③ 看授权:scope/权限够不够?不够 → 403
  │ ④ 执行 Controller
  ▼
200 业务成功

两种校验模式:

方式适用 token特点
JWT 本地验签JWT 形态的 access_token快,无网络往返;信任 iss + JWKS;撤销靠短寿命
Introspection不透明(opaque)token每次询问授权服务器「这票有效吗、scope 是啥」;可加缓存

跟着做

步骤 1:加依赖

<!-- 中文注释:Resource Server 与 Client 可同时存在,但职责与配置完全不同 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

步骤 2:配置 issuer

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com
          # Boot 启动时访问 {issuer-uri}/.well-known/openid-configuration
          # 自动拿到 jwks_uri;也可显式配 jwk-set-uri 跳过发现

步骤 3:独立一条 Resource Server 链(与自研 JWT 链分区)

package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
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 ResourceServerSecurityConfig {

    @Bean
    public SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/v1/**")                    // 只管开放 API 前缀
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(HttpMethod.GET, "/api/v1/public/**").permitAll()
                .anyRequest().authenticated())
            .oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()))
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }
}

说明:/admin/** 等站内接口继续走你现有的自研 JWT 链;两条链按 securityMatcher 分区,互不越界。

步骤 4:按 scope 做方法级授权

// 中文注释:@EnableMethodSecurity 已在配置类开启;scope 转换为 SCOPE_ 前缀权限
@RestController
@RequestMapping("/api/v1/tracks")
public class TrackApiController {

    @GetMapping
    @PreAuthorize("hasAuthority('SCOPE_tracks:read')")
    public List<Track> list() { /* ... */ }

    @PostMapping
    @PreAuthorize("hasAuthority('SCOPE_tracks:write')")
    public Track create(@RequestBody Track body) { /* ... */ }
}

步骤 5:用 curl 观察 401 / 403

# ① 无 Bearer → 401,响应头带 WWW-Authenticate
curl -i http://localhost:8080/api/v1/tracks

# ② 有效 JWT 但缺 scope → 403
curl -i http://localhost:8080/api/v1/tracks \
  -H "Authorization: Bearer VALID_BUT_NO_tracks:read_SCOPE"

# ③ 正常 → 200
curl -i http://localhost:8080/api/v1/tracks \
  -H "Authorization: Bearer VALID_WITH_tracks:read"

步骤 6:不透明 token 改走 introspection

# 中文注释:IdP 发的是不透明串时,本地验不了签,必须远程询问
spring:
  security:
    oauth2:
      resourceserver:
        opaquetoken:
          introspection-uri: https://auth.example.com/introspect
          client-id: ${RS_INTROSPECT_CLIENT_ID}
          client-secret: ${RS_INTROSPECT_CLIENT_SECRET}

配置后把链上的 oauth.jwt(...) 换成 oauth.opaqueToken(Customizer.withDefaults())。introspection 结果(active、scope、sub)可加本地缓存降低延迟。

讲清楚要点

  1. Resource Server 不发起授权跳转、不管登录页:它只消费 token——401/403 的语义要干净(401 未认证,403 权限不足)。
  2. JWT 本地验签的信任根是 issuer:issuer-uri 决定 JWKS 从哪来、iss 声明怎么比对;配错一个字符,全部请求验签失败。
  3. 默认校验链不能关:签名、exp、nbf、iss 自动校验;aud 校验建议显式加上(自定义 JwtValidator),防止「发给别的应用的 token」混进来。
  4. JWT 撤销是短板,用短寿命补:本地验签意味着无法即时作废单个 token;开放 API 的 access 寿命控制在 15-60 分钟,撤销需求转嫁给 refresh/授权层。
  5. scope 不足是授权问题,返回 403:当成 401 处理会把第三方应用踢进重新授权死循环。
  6. 与本站自研 JWT 分区共存:/api/v1/** 走 OAuth RS 链,/admin/** 走自研链;日志里标记 auth_kind=rs|site,排错先看路径再猜凭证种类。

你可能会懵的地方

  • 同一个应用既是 Client 又是 Resource Server,矛盾吗? 不矛盾,是两种身份:对 GitHub 你是 Client(去登录);对第三方开发者你是 RS(开放 API)。两套配置、两条链、路径分区。

  • 本站自研 JWT 和 OAuth 的 JWT 长得像,怎么区分? 看发行方与验签密钥:自研 JWT 用你自己的密钥签;OAuth access 用 IdP 的 JWKS。按路径分区后,每条链只认一种——绝不要写「什么都试一下」的万能验证器。

  • issuer-uri 配错的现象是什么? 启动期:无法拉取 /.well-known/openid-configuration 直接报错;运行期:验签全军覆没、全部 401。先 curl 该地址确认可达、域名拼写一致。

  • 要不要自己校验 aud? 建议。默认校验不含 aud 时,「发给应用 A 的 token」可以被拿来调应用 B 的资源(只要签名有效)。加一个 JwtAudienceValidator 限定预期 aud。

  • 高 QPS 下 introspection 会不会拖垮接口? 每请求一次远程调用确实贵。缓解:优先让 IdP 发 JWT(本地验签);必须用 opaque 时加短 TTL 缓存(如 30-60 秒),权衡撤销及时性。

错法对照

错法问题正解
只解析 JWT payload 不验签伪造 token 畅通无阻issuer/JWKS 验签 + 声明校验
/admin 与 /api/v1 共用一条混沌鉴权链两套凭证互相「碰巧能用」securityMatcher 分区 + 文档
缺 scope 返回 401调用方误判未登录,死循环已认证缺权限 → 403
把 id_token 当 API 的 Bearer受众错误,安全模型混乱API 只收 access_token
生产上关掉 exp/iss 校验「先跑起来」过期/跨站 token 全放行默认校验链保持开启

卡点急救

  1. 现象:启动直接报 issuer 无法解析 / connection refused。issuer-uri 地址不通或拼写错误。先 curl https://auth.example.com/.well-known/openid-configuration 确认可达且返回 JSON;本地 IdP 注意端口与容器网络。
  2. 现象:所有请求都 401,token 明明有效。排查四件套:服务器时钟(exp/nbf 全乱)、JWKS 缓存(IdP 换 key 未刷新)、alg 白名单(IdP 用 ES256 而你只放行 RS256)、securityMatcher 是否漏了该路径。
  3. 现象:接上 Resource Server 后 /admin 全 401。RS 链的 securityMatcher 写成了 /**,把管理接口也拦了。改回 /api/v1/**,并核对多条链的 @Order(更精确的 matcher 放前面)。

面试常问

  • JWT 本地验签和 token introspection 怎么选? token 是 JWT 且追求低延迟 → 本地验签(信任 iss+JWKS,撤销靠短寿命);token 是不透明串或需要即时撤销语义 → introspection(每次问授权服务器,可加缓存)。本质是「性能 vs 即时撤销」的权衡。

  • Resource Server 验 JWT 的完整流程? ① 从 Authorization 头取 Bearer token;② 按 issuer 拉取/缓存 JWKS 验签;③ 校验 exp/nbf/iss(及自定义 aud);④ 解析 scope 映射为 SCOPE_ 权限;⑤ 走方法/路径授权;⑥ 全程无会话,STATELESS。

  • 你们站自研 JWT 与 OAuth Resource Server 如何共存? 路径分区:站内接口走自研 JWT 链(自家密钥验签、角色鉴权),开放 API 前缀走 RS 链(IdP JWKS 验签、scope 鉴权);日志标记凭证种类,排错先看路径。两种凭证互不通用,也不会互相干扰。

自测三问

  1. JWT 本地验签与 introspection 各适合什么 token 形态?各自的撤销短板怎么补?
  2. 画出外部 Client 调用你 /api/v1/tracks 时的校验顺序与两个失败出口(401/403)。
  3. 为什么本站建议 admin 走自研 JWT,而开放 API 走 Resource Server?

小结一句话

Resource Server = 验 access_token(JWKS 验签或 introspection)、看 scope、再跑业务;与本站自研 JWT 链按路径分区、各管一段。

延伸

  • Spring Security:OAuth2 Resource Server
  • RFC 7662:Token Introspection
  • 下一课:第三方登录——把 GitHub、Google 两类 IdP 的差异收进统一适配层,落地真实产品规则。
加载本节练习…
← 上一课Spring OAuth2 Client下一课 →第三方登录