OAuth2 与 OIDC:从委托授权到安全登录¶
本文讲协议原理:应用为什么要跳转登录、授权码怎样换成令牌、各类令牌交给谁,以及浏览器和移动端应守住哪些安全边界。它不是某个 Java 项目的部署教程,不需要先安装软件,也不包含真实客户端凭据。
还不熟悉“认证、授权、Session、Cookie”的读者,先读认证和授权及其中的浏览器会话。JWT 的结构、签名和字段解释统一见 JWT 与 JWKS。
先记住三件事:OAuth2 主要解决“允许应用访问什么”,OIDC(OpenID Connect,建立在 OAuth2 之上的身份层)补充“这次登录的用户是谁”,登录成功仍不等于可以访问所有人的数据。
1. 为什么需要 OAuth2,谁参与其中¶
假设“相册打印应用”要读取用户在“云相册”中的照片。如果用户直接把云相册密码交给打印应用,打印应用可能得到远超打印所需的权限;泄漏影响整个账号,用户也很难只撤销这一应用的访问权。
OAuth 2.0 是委托授权框架:用户在云相册的授权服务器完成认证与授权,打印应用取得有范围和有效期限制的 Access Token(访问令牌),凭它读取允许访问的资源,无需接触用户的云相册密码。机器调用也可以使用 OAuth2,但不一定有用户参与。RFC 6749 §1
1.1 四个基本角色¶
| 角色 | 职责 | 相册例子 |
|---|---|---|
| Resource Owner,资源所有者 | 有权同意他人访问资源的主体,通常是用户 | 照片所属用户 |
| Client,客户端 | 希望取得授权、调用受保护资源的应用 | 相册打印应用 |
| Authorization Server,授权服务器 | 核实授权请求,必要时认证用户,并签发令牌 | 云相册的授权中心 |
| Resource Server,资源服务器 | 提供受保护的应用程序接口(API),验证令牌并检查访问权限 | 云相册的照片 API |
这里的“客户端”是协议角色,不只指手机或浏览器:Java 后端也可以是 OAuth2 客户端。浏览器经常只是帮用户打开页面、携带重定向结果;真正保存令牌并调用 API 的客户端可能在后端。
“资源”是照片、订单等被保护的数据或能力,不是第五个协议角色。同一系统可以同时承担授权服务器和资源服务器,但“能否发令牌”与“能否读这张照片”仍是不同职责。
1.2 公开客户端与机密客户端¶
| 类型 | 能否可靠保管长期凭据 | 常见形式 |
|---|---|---|
| Public Client,公开客户端 | 不能把随应用分发的秘密可靠地藏起来 | 原生 App、纯浏览器单页应用(Single Page Application,SPA) |
| Confidential Client,机密客户端 | 可以在受控服务端保管凭据并认证自身 | Java 后端、带受控后端的 Web 应用 |
Client ID(客户端标识)是公开编号,告诉授权服务器“哪个应用在申请”;Client Secret(客户端秘密)是机密客户端认证自身的一种凭据,不是用户密码,也不证明当前用户是谁。写进 JavaScript 或 App 安装包的共享 Secret,不能因为代码混淆就变成可信秘密。RFC 6749 §2.1
1.3 对应到 Spring Security 的职责¶
读 Java 代码时,可以先按职责认组件:OAuth2 Client 支持应用发起授权与取得令牌;Authorization Server 支持授权端点和令牌签发;Resource Server 支持 API 验证访问令牌。添加资源服务器支持不会让应用自动获得签发令牌的能力,一个应用也可同时配置多个角色。
具体依赖和配置取决于项目采用的 Spring 版本;Spring Authorization Server 官方 SPA 指南展示了公开客户端与 PKCE 的配置边界,不应把客户端 Secret 交给浏览器。
2. 读懂授权请求里的常用参数¶
授权端点是用户通过浏览器完成授权的入口;令牌端点是客户端提交授权码等凭据、取得令牌的接口。两者不是同一个步骤。
| 参数或概念 | 在本流程中的作用 |
|---|---|
client_id |
标识已注册的客户端 |
redirect_uri |
授权完成后,把浏览器送回客户端的回调地址 |
response_type=code |
请求授权端点返回授权码 |
scope |
申请的权限范围,多个值用空格分隔;openid 表示发起 OIDC 认证 |
| Consent,授权确认 | 用户确认应用申请的能力;是否再次展示页面取决于已有授权与服务端策略 |
| Authorization Code,授权码 | 短期、一次性的中间凭据,只用于兑换令牌,不能直接调用 API |
grant_type |
令牌端点识别本次取令牌方式;例如 authorization_code 或 client_credentials |
回调地址由客户端注册时约定,不是访问者随意填写的“登录后跳哪里”。授权服务器应精确匹配已注册地址;原生 App 的本机回环回调端口有专门例外,见原生 App 约束。登录后返回某个业务页面,应由客户端另行保存、限制为允许的站内路径,不能把回调做成任意网址跳转器。RFC 9700 §2.1
state、nonce、code_challenge 和 code_verifier 都与本次请求的安全绑定有关,下面放在实际流程中解释。
3. 授权码 + PKCE:按实际顺序理解¶
PKCE 的全称是 Proof Key for Code Exchange,可理解为“兑换授权码时出示一次性证明”。它让拿到授权码的一方还必须持有本次事务保存的原始随机值,降低授权码被截获或注入后的冒用风险。
3.1 流程开始前要具备什么¶
客户端应事先取得 Client ID,注册准确的回调地址,并知道可信授权服务器的端点及支持能力。不要从未经验证的外部输入选择令牌端点,否则可能把凭据发给攻击者。
下面以公开客户端使用 OIDC 为例,同时显式使用 state、nonce 和 PKCE,便于看清各自的位置;实际实现应交给成熟的 OAuth2/OIDC 客户端库维护事务和校验。纯 OAuth2 授权不使用 OIDC 的 nonce 和 ID Token。
3.2 一次完整请求¶
- 客户端建立本次授权事务。 分别生成新的、难以猜测的
state、nonce和code_verifier,安全绑定到发起流程的浏览器会话或 App 本次操作,设置有效期;不同用户、标签页或登录尝试不能混用同一份状态。 - 生成 PKCE 挑战。 客户端用
S256方法从code_verifier计算code_challenge,暂时保留原始 verifier,不把它放入授权请求。 - 打开授权页面。 浏览器访问授权端点,带上
client_id、redirect_uri、response_type=code、scope、state、nonce、code_challenge和code_challenge_method=S256。 - 授权服务器认证用户并决定授权。 用户在授权服务器的可信页面完成登录;已有有效会话时可能不必再输密码。需要确认权限时,用户同意或拒绝授权。用户密码不交给申请照片的客户端。
- 浏览器回调客户端。 成功时携带一次性
code与原样返回的state;失败时可能携带协议错误。客户端先检查回调对应当前事务,使用本例的state校验,拒绝不匹配、过期或重复回调。 - 客户端兑换令牌。 向可信令牌端点发送
grant_type=authorization_code、code、相同的redirect_uri、client_id和原始code_verifier。机密客户端还须按注册方式认证自身;PKCE 不替代客户端认证。 - 授权服务器校验。 检查授权码是否有效、未被使用、属于该客户端及回调地址,再验证 verifier 与授权请求保存的 challenge 是否匹配;通过后返回 Access Token,本例的 OIDC 成功响应还含 ID Token(身份令牌)。是否返回用于续期的 Refresh Token(刷新令牌)取决于策略。
- 分别使用结果。 客户端验证 ID Token 和本次
nonce后使用身份结果;向资源服务器发请求时使用 Access Token。资源服务器验证令牌,再检查 API 权限和具体照片的访问权。
这解释了“为什么不直接在回调 URL 发 Access Token”:浏览器回调只运输短期的一次性授权码,真正令牌通过独立的令牌端点取得,减少访问令牌进入地址、历史记录和相关泄漏路径的机会。RFC 9700 §2.1.1–2.1.2
3.3 PKCE 的计算与检查点¶
code_challenge = BASE64URL(SHA256(ASCII(code_verifier)))
这里 SHA-256 是摘要算法;Base64URL 是适合放入 URL 的编码,结果不带末尾的 = 填充。code_verifier 应由密码学安全随机数生成器产生,长度为 43~128 个允许字符;一种标准做法是生成 32 字节随机数后转成不带填充的 Base64URL。RFC 7636 §4.1–4.2
可以把 challenge 想成“先登记证明的摘要”,verifier 是“兑换时出示原件”。它不是把授权码加密,也不是把 Client Secret 变得可公开。
使用授权码流程的公开客户端必须使用 PKCE,机密客户端也推荐使用;选择 S256。服务端必须真正校验 verifier,并防止攻击者通过删除 challenge 降级绕过 PKCE。只在页面上看见这些参数,不代表防护已经生效。RFC 9700 §2.1.1
3.4 state、nonce 和 PKCE 的区别¶
| 机制 | 谁在哪里检查 | 保护什么 |
|---|---|---|
state |
客户端处理授权回调时检查 | 关联请求与回调;与当前用户代理安全绑定的一次性随机值可防回调 CSRF |
nonce |
OIDC 客户端验证 ID Token 时检查 | 把身份结果绑定到这次认证请求,检测重放;满足协议条件时也可防回调 CSRF、授权码注入 |
| PKCE | 授权服务器在令牌端点验证 code_verifier |
将授权码兑换绑定到本次发起事务;确认服务端正确支持并执行时,也可承担回调 CSRF 防护 |
state 的基本往返关系如下,尖括号均为每次动态生成或收到的值,不是可复制的常量:
授权请求:...?state=<本次随机值>
授权回调:...?code=<一次性授权码>&state=<原来的随机值>
客户端不能只检查 state“非空”或“在全局表里存在”,还要检查它属于当前会话或授权事务、未过期且未使用;验证后使其失效。错误回调也应与原请求关联。可以在客户端保存 state → 允许的登录后页面 的映射,但不要把密码、令牌等秘密塞进会经过浏览器的 state。
三者不是机械叠加的固定公式。客户端必须防回调 CSRF:确认 PKCE 防护可靠时可以依赖 PKCE;OIDC 可使用正确绑定并校验的 nonce;没有这些可靠防护时,必须使用与当前用户代理绑定的一次性 state。发送了 nonce 就要检查 ID Token 中的对应值,不能换到令牌就提前建立登录状态。RFC 9700 §2.1、§4.7、OIDC Core §3.1.3.7
PKCE、state、nonce 都不替代 HTTPS,也无法让已经被恶意脚本控制的应用变安全。
3.5 原生 App 的约束¶
原生 App 使用 OAuth2/OIDC 时,必须通过外部用户代理完成授权,例如系统浏览器或满足隔离要求的系统认证会话;不要在 App 可读取页面、密码或 Cookie 的内嵌 WebView 中完成授权。界面显示在 App 内,不一定就是 WebView,关键是 App 能否读取或改写认证环境。RFC 8252 §4、§8.12
回调可按平台能力使用已验证归属的 HTTPS 链接、基于受控域名反写的私有 URI scheme,或本机 loopback IP(回环地址)。公网通信使用 HTTPS;HTTP 回调的例外仅适用于原生客户端的本机回环地址,不适用于公网网站。RFC 8252 §7、RFC 9700 §2.6
3.6 不采用的 Grant¶
Grant 是协议规定的授权方式,不是“所有登录接口”的统称。现行安全基线要求:
- Resource Owner Password Credentials Grant(Password Grant,资源所有者密码凭据授权)不得使用;它把用户密码交给客户端,也难以适配多因素认证等交互。
- 不应使用 Implicit Grant(隐式授权)在授权响应中签发 Access Token。浏览器和原生 App 应采用授权码 + PKCE 主线。
这些约束来自 RFC 9700 §2.4 和 §2.1.2。第一方应用的自有密码登录接口,不因接收密码就自动成为 OAuth2 Password Grant,也不因改名就天然安全;其密码暴露面、限流、多因素认证和会话管理仍需独立设计。Java 的密码核验职责见标准认证链。
4. OIDC 怎样补上“用户是谁”¶
OpenID Connect(OIDC)是在 OAuth2 之上的身份层。OAuth2 Access Token 表示访问授权,不能单凭“拿到了 Access Token”就认定客户端获得了标准的用户登录证明。
客户端在授权请求的 scope 中加入 openid,即发起 OIDC 认证。沿用授权码流程时,成功的令牌响应包含 ID Token(身份令牌),由客户端验证后了解本次认证结果。OIDC 中,提供认证结果的一方称为 OpenID Provider(OP,身份提供方),依赖这个结果的客户端称为 Relying Party(RP,依赖方)。OIDC Core §1、§3.1
还会遇到两个入口:Discovery(发现元数据)告诉客户端可信提供方的端点和能力;UserInfo 是用 Access Token 获取已授权用户资料的端点,不是接收 ID Token 的普通业务接口。取得 UserInfo 后,其中的用户主体标识必须与已验证 ID Token 的主体一致,不能只按昵称或邮箱拼接身份。OIDC Core §5.3.2
ID Token 的用途、接收者和本次请求绑定必须同时正确;“能 Base64 解码”不等于“身份可信”。字段含义与校验步骤统一见 JWT 与 JWKS:验证流程,使用成熟 OIDC 客户端库完成验证,不自己拼装一套只检查签名的登录协议。
5. 授权码与三种 Token,分别交给谁¶
| 凭据 | 接收者 | 用途 | 可以当业务 API 访问令牌吗 |
|---|---|---|---|
| Authorization Code | 授权服务器的令牌端点 | 一次性兑换令牌 | 不可以 |
| Access Token | 预期的资源服务器 | 在授权范围内调用 API | 可以,仍须通过权限检查 |
| ID Token | 发起 OIDC 认证的客户端 | 验证本次用户身份结果 | 不可以 |
| Refresh Token | 授权服务器的刷新入口 | 换取新的 Access Token | 不可以 |
Access Token 不一定是 JWT,OAuth2 不强制其编码格式;ID Token 是 OIDC 定义的 JWT。前者可以是不透明随机值,由资源服务器按部署协议核验。Cookie 所维持的浏览器会话也不等于这些令牌,见Session 与 Cookie。
5.1 Refresh Token 为什么需要额外保护¶
Access Token 通常有效期较短。Refresh Token 让客户端在获准的会话期限内取得新的 Access Token,避免每次过期都打断用户重新登录;是否签发它由服务端按风险决定,不是所有流程都会返回。
Refresh Token 泄漏可能让攻击者持续取到新令牌,因此必须保护传输和存储。对公开客户端,授权服务器必须采用发送方约束或轮换机制检测重放:
- 发送方约束:除令牌外,还要求出示与该客户端实例绑定的密码学证明;不是只比较一个容易伪造的设备名称。
- 轮换:每次刷新发出新 Refresh Token、使旧值失效并保留关联关系。再次使用旧值说明可能泄漏,服务端撤销关联的活动 Refresh Token,要求重新取得授权,不能只返回错误后让攻击者继续刷新。
这是 RFC 9700 §4.14 的重放防护要求。客户端也应协调并发刷新,避免多个请求同时拿旧值刷新而被判为重放。
浏览器公开客户端若获发 Refresh Token,还应设置最大总寿命或闲置超时;如果首枚令牌预设了到期时间,轮换不能把后续令牌延长到该时刻以后。RFC 10017 §6.3.2.3
不透明随机 Refresh Token 在服务端只存不可逆摘要,是降低数据库泄漏后直接盗用风险的一种实现策略,不是 OAuth2 规定的唯一格式。具体存储、撤销粒度和到期策略应在实现文档中明确,不把某个项目的固定天数当作协议要求。
6. Client Credentials:服务代表自己调用¶
Client Credentials(客户端凭据授权)适合没有终端用户参与的机器间调用,例如定时任务读取被授权的统计数据。它只适用于机密客户端,不能把 Secret 分发到浏览器或原生 App 中。RFC 6749 §4.4
流程只有三步:
- 服务向令牌端点提交
grant_type=client_credentials和所需scope,按注册方式证明自身身份。 - 授权服务器核对客户端凭据与允许的能力,返回 Access Token。
- 服务携带 Access Token 调用目标 API;资源服务器按服务身份、权限范围和业务规则授权。
没有用户登录、浏览器回调、授权码或 PKCE;也不应签发 Refresh Token,服务需要新令牌时重新认证取令牌。RFC 6749 §4.4.3
服务令牌不能冒充普通用户身份或偷偷继承其角色。Client Secret 只留在可信后端;条件允许时可采用证书或私钥签名等非对称客户端认证方式,避免共享秘密带来的泄漏风险。RFC 9700 §2.5、§4.15
7. scope、role、authority、audience 不是同一件事¶
| 概念 | 主要回答什么 | 示例含义 |
|---|---|---|
| Scope,权限范围 | 此次授权允许客户端做什么 | photos:read 允许读取照片能力 |
| Role,业务角色 | 主体在业务中处于什么角色 | 普通用户、内容管理员;具体模型由应用定义 |
| Authority,框架权限项 | 安全框架最终用什么权限值做判断 | Spring Security 可将 scope 或角色映射为权限字符串 |
| Audience,预期接收方 | 这个令牌应交给谁消费 | Access Token 面向某资源服务器,ID Token 面向某客户端 |
Scope 不等于角色,角色也不自动意味着拥有所有业务对象;Audience 只限定接收方,不表示“可读”“可写”。aud 等 JWT 字段的完整解释见字段说明,这里关注其授权含义。
例如请求删除某张照片,资源服务器应依次判断:令牌可信且有效吗?是发给本站的吗?有删除能力吗?这张照片属于当前用户,或者满足管理员规则吗?任一步失败都不能继续。OAuth2 提供授权框架,但不会替应用自动完成对象所有权判断。RFC 9700 §2.3
8. CSRF:区分普通业务请求与授权回调¶
CSRF 是 Cross-Site Request Forgery(跨站请求伪造)。关键不是“请求从哪一页发出”这么简单,而是攻击者借用了浏览器会自动携带的认证凭据。
8.1 Cookie 会话下的业务 CSRF¶
典型过程是:用户登录可信站点后打开攻击者页面;攻击者诱导浏览器向可信站点提交修改操作;浏览器可能自动附带该站点 Cookie。如果服务端只看 Cookie 就执行,用户的登录身份被用于非本人意图的请求。
常见防护是 CSRF Token:让可信页面取得不可预测的校验值,修改状态的请求必须额外提交它,服务端核验后才执行。SameSite Cookie、Origin/Referer 来源检查可以组成额外防线,但不能跳过具体浏览器与部署条件分析。需要 Cookie 基础时回看浏览器会话。Spring Security:CSRF
若 API 仅接受由客户端主动设置的 Authorization 请求头,且不接受浏览器自动附加的其他认证凭据,传统 Cookie 型 CSRF 风险通常较低。是否叫“REST API”或是否无状态不是豁免依据;如果把认证令牌放入自动发送的 Cookie,仍需分析 CSRF,不能仅凭“用了 JWT”全局关闭防护。
8.2 OAuth2 回调 CSRF¶
攻击者也可能把自己的授权回调送到受害者浏览器,让客户端把受害者错误地关联到攻击者账号。防护重点是“返回结果确实属于本次授权事务”,对应 state、nonce、PKCE,不能照搬普通业务表单的处理方式后就假定安全。
授权服务器自己的登录表单、授权确认页面以及采用 Cookie 会话的 BFF,仍有各自的业务 CSRF 边界。回调有 state,不意味着整个站点都不再需要 CSRF Token。
8.3 不能相互替代的机制¶
HTTPS 保护传输;CSRF 防护确认请求意图与会话或事务的绑定;XSS(Cross-Site Scripting,跨站脚本)防护阻止恶意脚本在可信页面执行。恶意脚本如果已控制页面,可能盗取可读令牌,或直接替用户操作。
CORS 的定义和配置统一见网关:CORS;它不替代身份认证、CSRF 防护或业务权限校验。不要把“配置了跨域”当成上述安全机制的总开关。
9. OIDC 与单点登录¶
Single Sign-On(SSO,单点登录)指用户完成一次认证后,访问多个受信任应用时可以复用认证状态,而不必每个应用重复输入凭据。OIDC 可提供标准协议基础,但“接入 OIDC”不等于“完整 SSO 自动完成”。
一个典型过程是:
- 用户访问应用 A,被送往身份提供方;完成登录后回到 A。
- 用户访问应用 B,B 也发起自己的 OIDC 授权请求。
- 浏览器访问同一身份提供方时,提供方识别已有会话,按策略完成本次认证与授权,再把 B 自己的授权码送回 B。
- A、B 各自验证本次结果并维护自己的应用会话;不是让 B 直接拿走 A 的 ID Token 或共享 A 的所有 Cookie。
能否无交互完成还取决于提供方会话是否有效、是否需要重新认证或授权确认,以及浏览器 Cookie、域名和客户端配置。OIDC 定义了 prompt、max_age 等控制交互和认证时效的参数,客户端不能假定已有会话就永远免登录。OIDC Core §3.1.2.1
“退出当前应用”“退出身份提供方”“撤销已签发令牌”是不同动作。单点退出需要明确的会话与协议设计;只删除某个页面的本地令牌,不保证其他应用退出,也不保证尚未到期的 Access Token 立即失效。
10. 浏览器应用:令牌放在哪一层¶
RFC 10017 按安全性由高到低介绍三种架构。差别不在页面用什么前端框架,而在 OAuth2 职责与令牌暴露给哪一层:
| 架构 | 令牌保存在哪里 | 浏览器怎样访问业务 API | 主要代价与风险 |
|---|---|---|---|
| Backend for Frontend(BFF,面向前端的专用后端) | Access/Refresh Token 都留在受控后端 | 浏览器带会话 Cookie 请求 BFF,由 BFF 携带令牌代理 API | 后端需维护会话、CSRF 防护和严格代理白名单 |
| Token-mediating Backend(令牌中介后端) | 后端保管 Refresh Token,Access Token 交给浏览器 | 浏览器携带 Access Token 直调 API | 前端恶意脚本仍可能取得 Access Token |
| Browser-based OAuth Client(纯浏览器 OAuth 客户端) | OAuth 流程与令牌由浏览器侧处理 | 浏览器自己换令牌并调用 API | 公开客户端必须使用 PKCE;脚本攻击暴露面更大 |
对业务系统、敏感应用和处理个人数据的应用,RFC 10017 强烈推荐 BFF。BFF 不是“加了一个网关就自动完成”:它作为机密客户端处理授权和令牌,且不能让前端指定任意代理目的地,否则可能把令牌转发给攻击者。RFC 10017 §6.1
BFF 减少令牌暴露给浏览器脚本的机会,但不消灭 XSS;恶意脚本仍可能通过已登录浏览器调用 BFF。选择架构后仍需落实输入输出安全、会话保护、CSRF 和业务授权,不能只选一个名称就认定系统安全。
11. 自测:能否顺着凭据找到接收者¶
- 相册打印例子里,谁是客户端,谁持有用户密码,谁决定能否读取某张照片?
- 为什么原生 App 中的 Client Secret 不能作为可靠的共享秘密?
- 授权码为什么不能直接调用 API?拿到授权码的人为什么不一定能换令牌?
- state、nonce 和 PKCE 分别在哪里校验?它们是否所有流程都必须一起出现?
- Access Token 和 ID Token 的接收者有什么区别?Refresh Token 能否当 Bearer Token 调普通 API?
- 为什么通过
aud检查并拥有photos:read,仍不代表能读取所有用户的照片? - 机器间调用为什么不应伪装成某个普通用户登录?
- 为什么 BFF 仍需 CSRF、XSS 防护,退出应用 A 也不一定让应用 B 退出?
核对要点:客户端是打印应用,用户在授权服务器认证,资源服务器落实对象权限;分发到不受控设备的共享秘密可以被提取;授权码只是一次性中间凭据,PKCE 绑定本次兑换;三种绑定机制校验位置和适用条件不同;API 用 Access Token,客户端用 ID Token,刷新只用 Refresh Token;令牌接收方、能力范围、业务对象权限是不同层次;服务身份与用户身份必须区分;BFF 与 SSO 都仍依赖明确的浏览器和会话安全设计。
12. 资料与后续阅读¶
协议结论以正式规范和官方文档为准:
- RFC 6749:OAuth 2.0 授权框架
- RFC 9700:OAuth 2.0 安全最佳实践
- RFC 7636:PKCE
- RFC 8252:原生 App 的 OAuth2
- RFC 10017:浏览器应用的 OAuth2
- OpenID Connect Core 1.0(Errata Set 2)
视频用于建立直观认识,不作为安全结论的规范来源;本文也不是字幕整理:
基础专题按职责继续读:认证与标准密码链、JWT/JWKS 验证、网关与过滤链。
若同时克隆了示例工程,可在项目仓库打开 hhjava/docs/AUTHENTICATION_GUIDE.md。项目接口、配置和启动步骤以该实战指南及源码为准,不属于本文的通用协议约定;知识库网页不提供依赖本机目录布局的跨仓库链接。