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

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

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

学习

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

更多

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

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
OAuth 2.0 与 OpenID Connect/课时/Scope 与权限
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综合练习

Scope 与权限

scope 粒度、最小权限。

25 分钟

Scope 与权限

这一课你将能做到

  1. 解释 scope 是「用户可见、可同意也可拒绝」的授权粒度,并说明 token 里实际批准的 scope 可能少于申请值
  2. 为自己的 API 设计一套 scope 命名(如 tracks:read / tracks:write),与 OIDC scope 分开、可文档化
  3. 在 Spring Resource Server 用 SCOPE_ 前缀的 @PreAuthorize 做细粒度控制,说清 401 与 403 在 scope 场景下的差别

为什么要学

同意页上「此应用将能够:读取你的公开资料、获取你的邮箱地址」——每个复选框背后就是一个 scope。scope 是用户与权限之间唯一的产品化接口:申请多了,用户不敢点同意,合规风险也大;申请少了,功能中途再弹一次授权,体验割裂。

而 scope 之所以不只是「展示文案」,是因为 Resource Server 必须把它变成可执行的访问控制。API 只验「有没有登录」、不看 scope,等于把最小权限原则废掉——一个只被授权读课表的第三方应用,就能顺手调你的写接口。

后端与全栈都绕不开它:对接第三方 IdP 时 scope 决定你能拿到什么资料;自建开放 API 时 scope 决定第三方能做什么。面试里「scope 和 role 的区别」「401 与 403 什么时候用哪个」是权限设计的基础题。

先建立图像

Client 请求:scope=openid profile tracks:read tracks:write
                 │
                 ▼
用户在同意页看到两个复选框:「读取公开资料」「创建/管理课表」
                 │
                 ▼
token 响应:scope=openid profile tracks:read
            (tracks:write 被用户拒绝或 IdP 未批准)
                 │
                 ▼
Resource Server 逐条比对:
  GET  /api/v1/tracks  需要 SCOPE_tracks:read  → 有 → 200
  POST /api/v1/tracks  需要 SCOPE_tracks:write → 无 → 403

三类 scope 不要混为一谈:

类型例子谁消费
OIDC 标准范围openid profile emailClient 认人 / 取资料
IdP 预置范围GitHub 的 read:user、Google 的 drive.file调 IdP 自己的 API
自建 API 范围tracks:read tracks:write你自己的 Resource Server

跟着做

步骤 1:为你的功能设计 scope 字符串

在笔记里写出(本站示例):

只读课表列表/详情 → tracks:read
创建/更新/删除课表 → tracks:write
OIDC 登录取资料   → openid profile email

命名规则:短、稳定、可文档化;用 资源:动作 的冒号风格;避免一次给 admin:* 这种通配。scope 一旦发出给第三方用,改名就是破坏性变更,起名要像设计数据库字段一样慎重。

步骤 2:观察 authorize 与 token 响应里的 scope

# authorize 请求(申请)
GET /authorize?...&scope=openid%20profile%20tracks:read

# token 响应(实际批准)
{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid profile tracks:read"
}

写下一句结论:业务代码必须以响应里的 scope 为准,不能假设「我请求了就一定有」。用户可以拒绝、IdP 可以裁剪、管理员可以配置更严的策略。

步骤 3:Resource Server 按 scope 拦截

// 中文注释:Spring 默认把 JWT 里的 scope 映射成 SCOPE_ 前缀的 GrantedAuthority
@Configuration
@EnableMethodSecurity   // 使 @PreAuthorize 生效
public class MethodSecurityConfig { }

@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) { /* ... */ }
}

步骤 4:用 curl 对照三种结果

# ① 不带 token → 401(未认证)
curl -i http://localhost:8080/api/v1/tracks

# ② 带只含 tracks:read 的 token 调写接口 → 403(已认证,权限不足)
curl -i -X POST http://localhost:8080/api/v1/tracks \
  -H "Authorization: Bearer READ_ONLY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"slug":"demo"}'

# ③ 带含 tracks:write 的 token → 200(业务成功)
curl -i -X POST http://localhost:8080/api/v1/tracks \
  -H "Authorization: Bearer WRITER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"slug":"demo"}'

把「401 vs 403」的对照写进错法表——这是前端与后端最容易打架的地方。

步骤 5:scope(OAuth 层)与本站角色(RBAC 层)分层

/api/v1/**(开放 API) → OAuth access_token + SCOPE_xxx(对第三方应用的授权)
/admin/**(站内管理)  → 本站自研 JWT + 角色 ADMIN(对站内用户的授权)

两层各管各的:联邦用户可能有 tracks:read 但没有 ADMIN 角色;
管理员登录也不应该自动获得对外 API 的 scope。

步骤 6:写给第三方的 scope 说明(文档片段)

tracks:read  — 读取公开课表列表与详情
tracks:write — 创建/更新课表(需申请审核,默认不发放)
openid       — 获取标准化 id_token,用于登录

讲清楚要点

  1. scope 是用户同意的边界,所以默认只申请当前功能真正需要的权限——最小权限原则在这里落地。
  2. 用户可以拒绝部分 scope,所以 Client 必须实现「功能降级」:没有 email 就展示默认头像昵称,而不是整个流程报错。
  3. OIDC scope 与业务 API scope 语义不同,命名空间要分开:profile 管资料,tracks:read 管课表,别用一个 user 词打天下。
  4. 同意页展示不等于执行,Resource Server 必须再验一次:token 可以被裁剪、被降级,API 端的 @PreAuthorize 才是最后防线。
  5. Spring 默认把 scope 映射成 SCOPE_xxx:@PreAuthorize 字符串漏了前缀,规则永不命中且不报错——最隐蔽的坑之一。
  6. 敏感操作要单独 scope:删除、改密这类操作可拆成 tracks:delete 并叠加二次确认,避免「一个写 scope 一把梭」。

你可能会懵的地方

  • scope 和角色(ROLE_ADMIN)是不是一回事? 都能表达权限,但来源不同:scope 来自「用户对某个 Client 的授权」(OAuth 层);角色来自「你站内用户模型」(RBAC 层)。联邦用户可能有 scope 而没有站内角色;反之亦然。

  • token 里没有 scope 字段怎么办? JWT 可能用 scope(空格分隔字符串)或 scp(数组);不透明 token 需要调 introspection 端点查询。先查你对接的 IdP 文档确认格式。

  • 为什么我申请了 email,id_token 里却没有? 三种可能:用户拒绝了;该 claim 只在 UserInfo 端点返回;scope 忘了加 openid。逐一排查。

  • Client Credentials 模式也有 scope 吗? 有,但语义是「应用自身的权限」,没有终端用户参与、没有同意页,通常由管理员在 IdP 侧预配置——更要严控发放。

  • scope 请求了但响应里少了一项,该报错还是继续? 看功能关键度:核心 scope 缺失可以中止并提示用户重新授权;边缘 scope 缺失应降级继续。把判定逻辑写在 Client 侧,别指望 Resource Server 替你想。

错法对照

错法问题正解
一次申请所有 scope「以后方便」用户不信任,合规风险大按功能增量申请
API 只验 JWT 签名不看 scope签名合法 ≠ 有权操作验签 + 过期 + scope 三步都走
把 openid 当成业务写权限OIDC 范围与 API 范围混用两套命名空间分开设计
@PreAuthorize("hasAuthority('tracks:read')") 漏 SCOPE_规则永不命中且静默使用默认转换器的 SCOPE_tracks:read
缺 scope 时返回 401前端误判「未登录」,死循环跳登录已认证但权限不足 → 403

卡点急救

  1. 现象:前端收到 403 后疯狂跳登录页。前端把一切 401/403 都当「没登录」处理了。约定清楚:401 才清会话去登录;403 提示「权限不足/需要申请更高 scope」。后端也别把权限不足包装成 401。
  2. 现象:@PreAuthorize 写了但完全不生效。两个排查点:配置类有没有 @EnableMethodSecurity;authority 字符串是不是漏了 SCOPE_ 前缀(Spring 默认转换器会把 scope 变成 SCOPE_xxx)。
  3. 现象:用户拒绝了部分 scope,应用某个页面直接崩。Client 侧没做降级。处理:以 token 响应里的 scope 为准渲染功能开关;对「核心功能必需的 scope」被拒的场景,给出解释页并引导重新授权。

面试常问

  • scope 和 RBAC 角色的区别? scope 是 OAuth 层的授权粒度,表达「用户同意某应用能做什么」,随授权发放、可撤销、用户可见;role 是业务系统内部的权限模型,表达「这个本地账号是什么身份」。开放 API 用 scope 隔离第三方,站内管理用角色控制用户,两者可以共存但不能互相顶替。

  • 401 和 403 分别什么时候返回? 401 = 未认证(没带凭证、凭证无效或过期),响应应带 WWW-Authenticate 头;403 = 已认证但权限不足(scope 不够、角色不对)。把 403 当 401 返回会引发前端「清会话 → 跳登录 → 登录完还是 403」的死循环。

  • 如何设计一套可演进的 scope 命名? 资源:动作(tracks:read/tracks:write)或 资源.动作;固定小写、提前文档化、发放可审计;新增能力加新 scope 而不是改旧语义;给敏感动作拆独立 scope。核心是「scope 语义一旦对外发布就是契约」。

自测三问

  1. 为什么 token 响应中的 scope 可能比 authorize 请求的少?Client 应如何应对?
  2. OIDC 的 openid 与自建的 tracks:write 分别服务谁?
  3. 无 token 与「token 缺 scope」访问写接口,状态码分别应该是什么?

小结一句话

Scope = 用户可见、可拒绝、可撤销的授权粒度;申请要克制,API 端必须用 SCOPE_ 规则强制校验,401 与 403 各司其职。

延伸

  • RFC 6749 §3.3:Access Token Scope
  • OpenID Connect:Scope Claims
  • 下一课:Refresh Token——access 设多短才安全?过期了如何无感续期?
加载本节练习…
← 上一课OpenID Connect下一课 →Refresh Token