OIDC / OAuth 2.0 认证授权体系详解
基于本次会话全部疑问整理,涵盖 ID Token、Access Token、Passkey、SSO、Scope、资源服务器验证、架构职责等主题。
目录
- 基础概念:OAuth 2.0 与 OIDC 的关系
- Access Token 与 ID Token 的区别
- 无密码认证与 Passkey
- OIDC 规范的是"认证协议层"而非"认证手段"
- ID Token 的生命周期与应用场景
- Access Token 如何标识"用户是谁"
- 资源服务器的验证流程
- Introspection 接口
- 标准 Scope 及使用场景
- 性能优化:避免每次请求两次远程调用
- SSO 会话建立机制(Cookie 视角)
- SSO 切换业务系统的触发机制
- 架构职责:纯后端 API 服务 vs 登录流程
- IdP 的端点区分:/authorize 与 /introspect
- 常见误区汇总
1. 基础概念:OAuth 2.0 与 OIDC 的关系
OAuth 2.0 解决授权,OIDC 在其之上解决认证。
| 维度 | OAuth 2.0 | OIDC |
|---|---|---|
| 目标 | 授权(Authorization) | 认证(Authentication)+ 授权 |
| 核心产物 | Access Token | Access Token + ID Token |
| 回答的问题 | "这个应用能访问什么?" | "用户是谁?" + "能访问什么?" |
| 用户身份 | 未定义 | 标准化(ID Token / UserInfo) |
OIDC 不是修改 OAuth 2.0,而是扩展它——复用 OAuth 2.0 的授权流程,在其上增加认证所需的协议元素:
- ID Token:JWT 格式,明确告诉客户端"用户是谁、何时认证"
- UserInfo 端点:用 access token 换取用户身份信息
- 标准 scope(
openid等):请求认证时带openid - 标准 claims:
sub、name、email、auth_time等 - Discovery 文档:
/.well-known/openid-configuration - JWKS 端点:标准方式获取公钥验证 ID Token 签名
- 多种认证流程:Authorization Code、Implicit、Hybrid 等
关键纠正:OIDC 规定 access token 是给资源服务器用的,客户端不应解析它来获取身份;身份信息应来自 ID Token 或 UserInfo。
2. Access Token 与 ID Token 的区别
即使 access token 是 JWT 格式,仍然需要 ID token。 两者用途、受众、语义完全不同,不能互相替代。
| 维度 | ID Token | Access Token |
|---|---|---|
| 用途 | 证明"用户是谁"(认证) | 访问资源(授权) |
受众(aud) |
客户端(client_id) |
资源服务器(API) |
| 消费者 | 客户端应用 | 资源服务器 |
| 规范 | OIDC 核心规范 | OAuth 2.0 |
| 是否必须 | OIDC 中必须 | OAuth 中必须 |
为什么不能合并:
- 受众不同是根本原因:ID Token 的
aud是客户端,Access Token 的aud是资源服务器。一个 token 同时满足两个受众违反"一个 token 一个受众"原则,带来安全风险(客户端拿它访问 API,或 API 读到只该给客户端的身份信息)。 - 语义不同:ID Token 是"身份断言"(sub、name、email、auth_time),Access Token 是"权限凭证"(scope、资源访问权)。
- 生命周期与撤销策略不同:ID Token 通常短时效、一次性身份证明;Access Token 可能有效期更长,需要能被资源服务器验证甚至撤销。
关于"客户端解码 access token 拿 sub":技术上可以,但规范上不允许、设计上不合理。OIDC 明确客户端不应解析 access token,它只应原样传给资源服务器;access token 里可能根本没有客户端需要的用户信息(name、email);客户端信任 access token 里的身份信息是不安全的。
3. 无密码认证与 Passkey
3.1 无密码认证(Passwordless Authentication)
不需要用户输入传统密码,用其他凭证验证身份:
| 方式 | 原理 |
|---|---|
| 魔法链接(Magic Link) | 邮箱收到一次性登录链接 |
| 邮箱/短信 OTP | 一次性验证码 |
| WebAuthn / Passkeys | 设备生物识别或 PIN 解锁密钥对 |
| 硬件安全密钥 | 物理 USB/NFC 设备(YubiKey) |
| 生物识别 | 指纹、人脸(Windows Hello、Face ID) |
| 推送通知 | 手机上确认登录 |
优势:更安全(无密码可泄露/撞库/暴力破解,天然抗钓鱼)、体验更好、降低运维成本。
与 OIDC 的关系:无密码认证是认证方式(how),OIDC 是认证协议(how identity is conveyed),两者是不同层次,可结合使用——用户通过 Passkey/Magic Link 完成无密码认证,认证成功后 IdP 按 OIDC 协议签发 ID Token 和 Access Token。
3.2 Passkey 的实现
Passkey 基于 FIDO2 标准,底层是非对称加密和质询-响应(Challenge-Response)机制。
注册阶段(创建密钥对):
- 设备为网站生成一对唯一密钥:私钥 + 公钥
- 私钥保存在设备安全芯片/密码管理器,永不离开设备
- 公钥发送给网站服务器,与账户关联存储
登录阶段(签名质询):
- 服务器生成随机质询(Challenge)发给设备
- 设备要求本地验证(指纹/面容/PIN)解锁私钥,同时证明"用户存在"
- 设备用私钥对质询做数字签名,发回服务器
- 服务器用存储的公钥验证签名,通过即登录成功
安全性:防钓鱼(签名绑定域名)、防泄露(服务器只存公钥)、防重放(质询随机)。
跨设备:通过密码管理器(iCloud 钥匙串、Google 密码管理工具)端到端加密同步 Passkey;或使用硬件密钥(私钥永不离开硬件)。
本质:用"留在设备上的私钥"替代"需要发送给服务器的密码",用密码学签名证明"你就是你"。
4. OIDC 规范的是"认证协议层"而非"认证手段"
OIDC 规范的是"认证结果如何表达和传递",而不是"用户具体用什么方式完成认证"(账密、Passkey、短信验证码)。
OIDC 不规定"怎么认证":用户可以用账密、Passkey、OTP、社交登录、企业 SSO、硬件密钥——OIDC 完全不做规定,只要求 IdP 在认证完成后按标准签发 ID Token。
OIDC 规范的是:
| 内容 | 说明 |
|---|---|
| 认证请求 | 客户端如何发起(scope=openid、response_type、nonce) |
| 认证流程 | Authorization Code、Implicit、Hybrid |
| 认证结果 | 通过 ID Token 表达"谁、何时、如何认证" |
| 身份信息 | UserInfo 端点、标准 claims |
| 会话管理 | Session Management、Front/Back-Channel Logout |
| 发现与密钥 | Discovery、JWKS |
ID Token 里的 amr(authentication methods references) 可声明用了哪种认证方式(如 pwd、otp、mfa),但这只是结果描述,不是规定 IdP 必须怎么实现账密认证。
账密认证由谁规范:IdP 自身实现(Keycloak、Auth0、Okta)、SAML(企业 SSO)、WebAuthn/FIDO2(Passkey)、NIST 800-63(安全等级 AAL1/2/3)等。
类比:OIDC 像"身份证签发和查验的标准"——规定身份证长什么样、怎么查验、有哪些字段,但不规定你是通过考试、面试还是刷脸来证明身份(那是发证机关的职责)。
5. ID Token 的生命周期与应用场景
5.1 核心定位:登录事件的"身份证"
认证是"登录时确认你是谁"(一次性),鉴权是"每次请求确认你能访问什么"(反复发生)。
| 层面 | 发生时机 | 用什么 |
|---|---|---|
| 用户认证 | 登录那一刻,一次性 | ID Token |
| 每次 API 请求的鉴权 | 每次访问资源时 | Access Token |
ID Token 的职责是:在认证完成那一刻,把"用户是谁、何时认证、如何认证"告诉客户端。客户端验证签名、nonce、aud、iss、exp 后,就完成了这次登录的确认,然后建立自己的会话(如 session cookie)。ID Token 是"登录事件的产物",不是"每次请求的凭证"。
5.2 客户端拿到 ID Token 后的验证步骤
| 步骤 | 目的 |
|---|---|
| 验签 | 用 IdP 的 JWKS 公钥验证签名 |
校验 iss |
确认签发者 |
校验 aud |
确认是发给自己的(aud = client_id) |
校验 nonce |
防重放 |
校验 exp |
确认未过期 |
| 读取 claims | sub、name、email、picture 等 |
5.3 典型应用场景
场景 1:单点登录(SSO)
用户在 IdP 登录一次,访问多个应用自动登录。每个 App 各自向 IdP 要一个 aud 匹配自己 client_id 的 ID Token,基于共享的 SSO 会话免去重复登录。
场景 2:客户端展示用户信息
登录后显示"张三,你好"、头像、邮箱,直接从 ID Token claims 读或调 UserInfo,不需要调业务 API。
场景 3:前端 SPA 判断登录状态
登录后解析 ID Token 判断是否已登录、展示用户信息。注意:SPA 里 ID Token 通常只用于展示和判断登录状态,访问 API 仍用 Access Token。
场景 4:后端服务作为 OIDC 客户端
后端 Web 应用走 OIDC 登录,验证 ID Token 确认用户身份,建立服务端 session,后续请求靠 session。
场景 5:关联本地账户
首次 OIDC 登录时,用 ID Token 的 sub 映射本地账户(比 email 更可靠,email 可能变)。
场景 6:前端和后端共享登录态
前端把 ID Token 发给自己的后端,后端验证后建立会话。
5.4 ID Token vs Access Token 的场景分工
| 场景 | 用 ID Token | 用 Access Token |
|---|---|---|
| 判断用户是否登录 | ✅ | ❌ |
| 展示用户名/头像 | ✅ | ❌ |
| 关联本地账户 | ✅(用 sub) |
❌ |
| SSO 识别同一用户 | ✅(用 sub) |
❌ |
| 调用业务 API | ❌ | ✅ |
| 访问用户资源 | ❌ | ✅ |
| 向后端证明登录态 | ✅ | ❌ |
5.5 后续请求与 token 过期
- 后续请求不再需要 ID Token,客户端用自己的会话记住身份。
- 每次访问 API 带 Access Token,资源服务器验证它来鉴权(不是重新认证)。
- Access Token 过期:用 Refresh Token 换新的 Access Token,不需要新 ID Token(用户身份没变)。
- 只有会话彻底失效(Refresh Token 也过期) 时,才重新走 OIDC 流程拿新 ID Token。
5.6 完整生命周期
| 阶段 | 产物 | 作用 |
|---|---|---|
| 用户登录 | ID Token | 一次性确认身份,客户端建立会话 |
| 首次访问 API | Access Token | 鉴权访问资源 |
| Access Token 过期 | Refresh Token → 新 Access Token | 续期,不重新认证 |
| 会话彻底失效 | 重新走 OIDC | 再次认证,拿新 ID Token |
6. Access Token 如何标识"用户是谁"
Access Token 通常不直接"知道用户是谁",它知道的是"这次授权代表哪个主体(subject)以及被授予了什么权限"。
6.1 JWT 格式的 Access Token
| Claim | 含义 |
|---|---|
sub |
主体标识——这就是"谁",通常是用户 ID |
scope / scp |
被授予的权限范围 |
aud |
受众(哪个资源服务器) |
iss |
签发者(IdP) |
exp / iat |
有效期、签发时间 |
client_id |
哪个客户端发起的 |
sub 是标识,不是资料:它只是让资源服务器知道"这个操作代表哪个用户",资源服务器通常不需要知道用户叫什么。
6.2 不透明(Opaque)Token
本身不含信息,资源服务器需调用 IdP 的 Introspection 端点(RFC 7662) 查询元数据,返回包含 sub、scope、active 等。
6.3 资源服务器如何使用这个"谁"
- 权限判断:结合
scope和sub决定能否访问。 - 资源归属判断:如
GET /users/{id}/orders,检查sub是否等于{id},防越权。 - 审计/记录:知道哪个用户发起的操作。
6.4 为什么资源服务器不直接看 ID Token
- 受众不对:ID Token 的
aud是客户端,不是资源服务器。 - 语义不对:ID Token 是身份断言,不是访问凭证。
- 安全边界:若 API 接受 ID Token,客户端就能拿它访问 API,违背"一个 token 一个受众"。
6.5 一个常见误解
"资源服务器怎么知道 sub=user123 对应哪个真实用户?"——它不需要知道。资源服务器只信任 IdP 签发的 sub 作为用户标识,用它做权限和归属判断。真实用户资料由客户端通过 ID Token / UserInfo 获取并展示。双方通过同一个 sub 指代同一用户。
7. 资源服务器的验证流程
站在资源服务器角度,收到 access token 后有两个刚需:
| 需求 | 本质 | 靠什么保证 |
|---|---|---|
| Token 有效且经过认证 | 真实性 + 有效性 | 签名验证、issuer、有效期、受众 |
| 明确知道是哪个用户 | 主体标识 | sub(或 Introspection 返回) |
7.1 验证检查项
| 检查项 | 目的 |
|---|---|
| 签名验证 | 用 IdP 公钥(JWKS)验签,确认是 IdP 签发、未篡改 |
iss |
确认签发者是可信 IdP |
aud |
确认 token 是发给我这个资源服务器的(防拿给别的 API 的 token 来访问) |
exp / nbf |
确认未过期、已生效 |
scope / scp |
确认有访问该接口所需权限 |
| (可选)Introspection | 不透明 token 时查询 active 状态 |
aud 检查非常关键:确保客户端不能拿发给别的服务的 token 来访问你的 API。
7.2 完整验证流程
收到 access token
↓
解析 token(JWT 直接解,opaque 走 Introspection)
↓
验签(JWKS 公钥)—— 确保是 IdP 签发、未篡改
↓
检查 iss —— 是可信 IdP 吗
↓
检查 aud —— 是发给我这个 API 的吗
↓
检查 exp/nbf —— 在有效期内吗
↓
检查 scope —— 有访问该资源的权限吗
↓
提取 sub —— 这是哪个用户
↓
按 sub 做数据查询 / 归属校验
↓
返回资源
7.3 安全要点:防止越权(IDOR)
- ❌ 错误:
GET /users/{id}/orders,直接信任路径里的{id} - ✅ 正确:用 token 里的
sub去查订单,或校验{id} == sub
用户身份必须以 token 里的 sub 为准,而不是请求参数,否则攻击者拿自己的合法 token 改路径就能看别人的订单。
7.4 为什么不能让客户端直接告诉我"我是 user123"
客户端可以撒谎。资源服务器只信任 IdP 签名的 token,不信任客户端自报的身份。这正是 access token 存在的意义:由可信第三方(IdP)背书的、可验证的主体标识 + 权限。
8. Introspection 接口
Introspection 接口的核心请求参数就是 token,即待查询的 Access Token。
8.1 规范定义(RFC 7662)
| 参数 | 是否必须 | 说明 |
|---|---|---|
token |
必须 | 待自省的 token 字符串(Access Token 本身) |
token_type_hint |
可选 | 提示类型(如 access_token),用于优化查询 |
8.2 请求示例
POST /introspect
Content-Type: application/x-www-form-urlencoded
token={{accessToken}}
8.3 注意事项
调用 Introspection 时,资源服务器自身也需要身份认证(Client Secret / Basic / 私有密钥 JWT),以证明有权限查询 token 状态。这属于"调用接口的凭证",与"被查询的 token"是两个不同概念。
8.4 返回结果
JSON,包含 active(是否活跃)、scope、sub 等。
9. 标准 Scope 及使用场景
OIDC 中 openid 是唯一必须包含的 scope,声明这是 OIDC 请求并返回 sub。其他均为可选:
| Scope | 返回的主要 Claims | 典型场景 |
|---|---|---|
openid |
sub |
必须。所有 OIDC 请求的基础,唯一标识用户 |
profile |
name, family_name, picture, locale |
界面展示姓名、头像、语言偏好 |
email |
email, email_verified |
需要邮箱(通知、账户关联) |
address |
address |
邮政地址(电商、物流) |
phone |
phone_number, phone_number_verified |
手机号(短信通知、二次验证) |
offline_access |
(请求 Refresh Token) | 用户不在场时(后台任务)持续访问资源 |
注意:sub 是唯一可靠的用户标识符;email 或 phone_number 可能变化,不应用作跨系统关联用户的唯一键。
10. 性能优化:避免每次请求两次远程调用
问题:API 后端每次收到请求,都同步调用认证服务器(验证 token)和授权服务器(检查权限),引入两次网络往返,高并发下成为瓶颈,并将 API 可用性绑定到外部服务延迟。
优化方案:
方案一(首选):本地验证 JWT
如果 Access Token 是 JWT,资源服务器用 IdP 公布的公钥(JWKS)在本地验证签名、iss、aud、exp。JWKS 公钥应缓存(TTL 5-10 分钟,或遇未知 kid 时刷新)。绝大多数请求是纯本地计算,零网络调用。
方案二:缓存 Introspection 结果
如果 Access Token 不透明、必须调 Introspection,资源服务器应根据 exp 和自身策略缓存响应,缓存有效期内对同一 token 的请求直接使用缓存。
方案三:混合模式
绝大多数请求走本地 JWT 验证;仅对高风险/敏感操作(管理员操作、支付)额外调用 Introspection 或检查吊销列表,获取实时吊销状态。
11. SSO 会话建立机制(Cookie 视角)
11.1 两种 Cookie,别混淆
| Cookie | 存在哪 | 作用 |
|---|---|---|
| IdP 的 SSO Cookie | IdP 域名下(如 accounts.google.com) |
记住"用户已在 IdP 登录",SSO 核心 |
| 各 App 的会话 Cookie | 各 App 自己域名下 | 记住"用户已登录本 App" |
SSO 的关键是第一个:IdP 域名下的 Cookie,让 IdP 在用户访问不同 App 时都能认出"已登录"。
11.2 第一次登录:SSO Cookie 如何建立
用户访问 App A,未登录
→ App A 重定向到 IdP:https://idp.com/authorize?client_id=appa&...
浏览器请求 IdP 的 /authorize
→ 带上 idp.com 域名下所有 Cookie(此时无 SSO Cookie)
→ IdP 发现无有效会话,返回登录页
用户输入账密,IdP 验证通过
IdP 做两件事:
a) 在 idp.com 域名下种 SSO Cookie
Set-Cookie: SSO_SESSION=abc123; Domain=idp.com; HttpOnly; Secure; SameSite=Lax
b) 生成授权码,重定向回 App A:
https://appa.com/callback?code=xyz
App A 用 code 换 token
→ 拿到 ID Token(aud=appa)+ Access Token
→ 验证 ID Token,建立自己的会话 Cookie(appa.com 域名下)
状态:idp.com → SSO_SESSION=abc123 ✅;appa.com → APP_SESSION=... ✅
11.3 第二次登录:SSO Cookie 如何复用
用户访问 App B,未登录
→ App B 重定向到 IdP:https://idp.com/authorize?client_id=appb&...
浏览器请求 IdP 的 /authorize
→ 自动带上 idp.com 域名下 Cookie,包括 SSO_SESSION=abc123
→ IdP 验证有效,认出"已登录"
→ 跳过登录页!
IdP 直接生成授权码,重定向回 App B:
https://appb.com/callback?code=def
App B 用 code 换 token
→ 拿到 ID Token(aud=appb)+ Access Token
→ 建立自己的会话 Cookie(appb.com 域名下)
关键点:第 2 步浏览器自动带上 IdP 域名的 Cookie,这是 SSO"免登录"的根本原因。
11.4 为什么浏览器会自动带 IdP 的 Cookie
因为重定向目标是 idp.com/authorize,浏览器对 idp.com 发请求时会带上所有 idp.com 域名的 Cookie。这是 HTTP Cookie 的基本机制,与 App 无关。
11.5 SSO Cookie 的关键属性
| 属性 | 作用 |
|---|---|
Domain=idp.com |
只在 IdP 域名下发送,各 App 拿不到 |
HttpOnly |
JS 无法读取,防 XSS 窃取 |
Secure |
只在 HTTPS 下发送 |
SameSite=Lax |
允许顶级导航(重定向)时携带,保证 SSO 跳转能带上 |
Path=/ |
全站有效 |
注意 SameSite:设成 Strict 时,从 App 重定向到 IdP 可能不带 Cookie,SSO 失效。所以通常用 Lax。
11.6 注销:SSO Cookie 的清除
用户在 App A 点注销
→ App A 清除自己的会话 Cookie(appa.com)
→ 重定向到 IdP 的 logout 端点
IdP 清除 SSO Cookie(idp.com)
→ 其他 App 下次重定向到 IdP 时不再自动登录
(可选)通知其他 App 注销
→ Front-Channel Logout:通过 iframe 通知各 App
→ Back-Channel Logout:IdP 直接调用各 App 的注销端点
如果只清 App 的 Cookie、不清 IdP 的 SSO Cookie,用户下次访问 App B 时可能又被自动登录,注销不彻底。
11.7 总结
| 问题 | 答案 |
|---|---|
| SSO 靠什么记住登录? | IdP 域名下的 SSO Cookie |
| 为什么访问 App B 能免登录? | 重定向到 IdP 时浏览器自动带上 SSO Cookie |
| 每个 App 的 ID Token 一样吗? | 不一样,aud 各自匹配自己的 client_id |
| App 能读到 SSO Cookie 吗? | 不能,Domain 限定在 IdP,且 HttpOnly |
| 注销要注意什么? | 必须清 IdP 的 SSO Cookie,否则会自动重新登录 |
12. SSO 切换业务系统的触发机制
SSO 不是"一个 ID Token 发给所有 App",而是"每个 App 各自向 IdP 要一个专属于自己(aud 匹配)的 ID Token,但 IdP 基于共享的 SSO 会话免去重复登录"。
12.1 每个 App 是独立的 OIDC 客户端
每个 App 必须在 IdP 上单独注册,成为独立 OIDC 客户端,拥有唯一 client_id。
12.2 切换时的触发流程
用户点击"业务系统 2"按钮/链接,本质是让浏览器导航到业务系统 2 的 URL。业务系统 2 发现用户未登录(无本地会话),将其重定向到 IdP 的 /authorize 端点。
这个重定向就是标准的 OIDC 授权请求,URL 包含 client_id(业务系统 2 的)、redirect_uri、scope=openid、response_type=code 等参数。
SSO 的"无感":浏览器重定向到 IdP 的 /authorize 时,自动携带 IdP 域名下的 SSO Cookie。IdP 识别后确认已登录,跳过登录页面,直接生成授权码重定向回业务系统 2。
12.3 会拿到 ID Token 和 Access Token 吗?
会。业务系统 2 用授权码去 IdP 的 token 端点换取 token 时,返回的就是专属于业务系统 2 的 ID Token(aud 是业务系统 2 的 client_id)和 Access Token。
12.4 为什么每个 App 的 ID Token 不同
OIDC 强制要求验证 aud,ID Token 里的 aud 必须与当前 App 的 client_id 匹配,否则拒绝接受。这是安全隔离:若 App B 能接受发给 App A 的 ID Token,从 App A 泄露的 token 就可用来冒充用户登录 App B。
12.5 SSO 体验为什么"感觉一次"
用户体验到的"只登录一次",是因为 IdP 在服务端维持了 SSO 会话(Cookie)。第一次登录时 IdP 种下 SSO Cookie;后续每个 App 重定向到 IdP 时浏览器自动带上,IdP 跳过登录页直接签发新 ID Token。用户看到的只是页面快速跳转,但每个 App 背后都各自完成了一次独立的 OIDC 流程。
13. 架构职责:纯后端 API 服务 vs 登录流程
13.1 核心矛盾与澄清
作为纯后端 API 开发者,常被要求"不接收或处理任何 Cookie 或浏览器行为",但 OIDC/SSO 中却有大量 Cookie 和重定向行为。根本原因:把"纯后端 API 服务"和"承载用户登录流程的 Web 应用"混在了一起。
OIDC/SSO 中的 Cookie 和重定向发生在负责处理用户登录的组件上,而不是被保护的业务 API 上。
13.2 角色分离
| 组件 | 职责 | 是否涉及 Cookie/重定向 |
|---|---|---|
| IdP | 认证用户,维护 SSO 会话 | 是(IdP 域下 SSO Cookie) |
| 前端 / BFF | 发起 OIDC 授权请求,处理回调,持有 token 或会话 | 是(浏览器重定向、会话 Cookie) |
| 业务 API | 验证 Access Token,返回业务数据 | 否(只读 Bearer 头) |
访问业务系统 2 时,是前端或 BFF 被重定向到 IdP,IdP 通过 SSO Cookie 识别用户,签发授权码,前端/BFF 换 Access Token。拿到 Access Token 后,前端/BFF 才把它放进 Authorization 头调用业务 API。API 看到的始终只是 Bearer Token,没有 Cookie 参与。
13.3 为什么会有"API 处理 Cookie"的错觉
很可能是因为代码/配置把两个不同角色的职责放在同一个物理进程里:一个 ASP.NET Core 应用同时承载用户登录(Cookie 认证/OIDC 中间件)和业务 API。OIDC 中间件默认对未认证请求发起 Challenge 重定向而非返回 401,让写 API 的人困惑。
这不是 OIDC 要求 API 处理 Cookie,而是该应用同时承担了"Web 前端"和"API 后端"两个角色。
13.4 纯后端 API 的职责边界
- 只做一件事:接收带
Authorization: Bearer的请求,验证 Access Token(本地 JWT 或 Introspection),提取sub和scope,返回数据。 - 不配置 Cookie 认证:不启用 Cookie 中间件或 OIDC 登录中间件。
- 未认证一律返回 401:让前端/BFF 去处理重定向到 IdP 的逻辑。
- Token 获取和 Cookie 管理是前端/BFF 的职责:API 只信任 IdP 签发的 Access Token。
13.5 关于"登录接口放在业务 API 中"的现实情况
把登录接口放在业务 API 服务里,需要看这个"登录接口"具体在做什么:
情况一:代理/编排接口(并非真正的认证)
接收前端传来的授权码,代替前端去 IdP 的 /token 端点换取 token,或建立服务端会话。真正的密码校验和 token 签发依然在 IdP。这本质是 BFF 逻辑,放在 API 服务里合理。
情况二:认证后的业务处理
前端在 IdP 完成登录拿到 token 后,调用业务 API 的 /login 同步用户信息、初始化配置、建立本系统业务会话。这属于业务逻辑,放在 API 服务里没问题。
情况三:历史遗留的自建认证
如果 /login 真的接收用户名密码并自己校验、自己签发 token,说明没有使用 OIDC/SSO,或 OIDC 只是外壳。这是值得重构的信号。
架构建议:认证逻辑应分离到独立 Auth Service 或 API 网关;鉴权(校验 token 和权限)放在各业务服务里。
14. IdP 的端点区分:/authorize 与 /introspect
它们不是同一个接口,职责完全不同。
14.1 /authorize:面向用户的前端入口
负责与用户交互,目标是获取授权码(或 token)。接收浏览器重定向过来的请求,参数含 client_id、redirect_uri、scope 等。SSO 场景中,业务系统发现用户未登录时,把浏览器重定向到这里,由 IdP 检查 SSO Cookie 并决定是否免登录。不用于后端服务之间验证 token。
14.2 Token 验证:后端调用的独立端点
API 服务端验证前端传来的 Access Token 是否有效,应调用 Introspection 端点(通常是 /introspect),遵循 RFC 7662,专门"允许受保护的资源(即 API 服务)向授权服务器查询某个 token 的元数据"。
- 调用方式:API 服务作为后端,携带自己的凭证(client credentials)调用,把待验证的 token 作为参数传入。
- 返回结果:JSON,包含
active(是否活跃)、scope、sub等。
14.3 关键区分
| 端点 | 谁访问 | 处理什么 |
|---|---|---|
/authorize |
浏览器 | "人"的登录 |
/introspect |
后端 API 服务 | "机器"对 token 的校验 |
之前担心的"每次请求都调用认证+授权接口"的性能问题,指的就是调用类似 /introspect 这样的后端接口。优化思路:如果是 JWT,优先在 API 服务本地验签,避免网络开销。
15. 常见误区汇总
| # | 误区 | 正确认知 |
|---|---|---|
| 1 | Access Token 是 JWT 就不需要 ID Token | 需要。受众、语义、生命周期不同,不能互换 |
| 2 | 客户端可以解析 Access Token 拿用户信息 | 不应。Access Token 对客户端应视为不透明,身份来自 ID Token |
| 3 | 后续请求用 Access Token 重新认证 | 后续是鉴权,不是重新认证;认证在登录时已完成 |
| 4 | ID Token 每次请求都要用 | 不用。登录时消费一次建立会话,后续靠会话 |
| 5 | Access Token 过期必须重新登录 | 用 Refresh Token 续期,不需要新 ID Token |
| 6 | 资源服务器需要知道用户姓名邮箱 | 不需要。只需 sub 做权限/归属判断 |
| 7 | 可以用请求参数里的用户 ID 查数据 | 必须以 token 的 sub 为准,防 IDOR 越权 |
| 8 | OIDC 规定了账密怎么认证 | 不规定。OIDC 只规范认证结果如何表达和传递 |
| 9 | 一个 ID Token 发给所有 SSO 应用 | 每个 App 各自有 aud 匹配自己 client_id 的 ID Token |
| 10 | 业务 API 需要处理 Cookie 和重定向 | 不需要。那是前端/BFF 的职责,API 只认 Bearer Token |
| 11 | /authorize 用于后端验证 token |
不是。后端验证用 /introspect,/authorize 是浏览器登录入口 |
| 12 | 每次请求都调认证+授权接口 | 优先本地 JWT 验签;不透明 token 才 Introspection + 缓存 |
| 13 | SSO 注销只清 App 的 Cookie 即可 | 必须清 IdP 的 SSO Cookie,否则会自动重新登录 |
| 14 | SPA 里 ID Token 可用于调 API | 调 API 用 Access Token;ID Token 仅用于展示和判断登录状态 |
| 15 | email 可作为跨系统关联用户的唯一键 |
sub 才是唯一可靠标识;email 可能变化 |
浙公网安备 33010602011771号