4. OIDC授权码流程拆解
OIDC 的授权码流程(Authorization Code Flow)是企业身份平台集成中最常用、也最值得深入理解的一条主链路。它之所以成为默认推荐,核心原因在于:令牌不经过浏览器前端,而是由后端服务器通过受保护的后端通道换取。要真正掌握它,需要把整条链路拆成前端通道、后端通道、授权码交换和 PKCE 四个部分来看,并理解每一环的安全职责。
一、整体链路:两条通道,一次交换
授权码流程可以概括为“前端拿码、后端换令牌”。用户浏览器只负责携带授权码(code)回到应用,而真正敏感的 ID Token、Access Token、Refresh Token 全部通过应用后端与身份平台之间的直接 HTTPS 请求获取。这样即使浏览器历史、Referer 或前端脚本被窥探,也拿不到令牌本身。
典型参与者有四个:用户浏览器(User Agent)、应用前端(RP 前端)、应用后端(RP 后端)、身份平台(OP,即 OpenID Provider)。下面这张图展示了授权码在两条通道之间的流转关系,以及 PKCE 参数在其中的位置。
二、前端通道:只做“拿码”这件事
前端通道的起点是应用前端构造授权请求,把浏览器重定向到身份平台的 /authorize 端点。一个典型的授权请求参数如下(示例参数,非真实环境):
GET https://idp.example.com/authorize?
response_type=code
&client_id=web-portal-001
&redirect_uri=https://app.example.com/callback
&scope=openid profile email
&state=8f3a1c9e2b
&nonce=n-0S6_WzA2Mj
&code_challenge=QYPAZ5NU8yvtlQ9erXrUYRDSOWAQYweHy
&code_challenge_method=S256
这里每个参数都有明确职责。response_type=code 表示走授权码流程;scope 必须包含 openid,否则就退化成 OAuth 2.0 而非 OIDC;state 用于防 CSRF,回调时必须原样校验;nonce 会被写入 ID Token,用于防重放;code_challenge 与 code_challenge_method 则是 PKCE 的入口。
用户在身份平台完成认证(可能还包含 MFA、同意授权)后,身份平台把浏览器重定向回 redirect_uri,并在查询串中带上 code 和 state。此时前端要做的第一件事是校验 state 是否与发起时一致,不一致必须直接终止流程。授权码本身是短时效、一次性的,通常有效期只有几十秒到几分钟。
三、后端通道:授权码交换
前端拿到 code 后,不应自行兑换令牌,而是把 code 交给应用后端。后端使用 /token 端点发起一次服务器到服务器的 POST 请求,完成授权码交换。请求体典型形式如下:
POST /token HTTP/1.1
Host: idp.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https://app.example.com/callback
&client_id=web-portal-001
&client_secret=***(机密客户端)
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
身份平台收到后依次校验:client 身份是否合法、redirect_uri 是否与授权请求一致、code 是否有效且未被使用、PKCE 的 verifier 与 challenge 是否匹配。全部通过后,返回包含 id_token、access_token,通常还有 refresh_token 的 JSON 响应。
后端拿到 ID Token 后,必须完成验证才能信任其中的身份声明:验证签名(通过 JWKS 获取公钥)、校验 iss、aud、exp、iat,并核对 nonce 是否与授权请求一致。只有验证通过,才把用户身份写入会话。
四、PKCE:为公开客户端补上关键一环
PKCE(Proof Key for Code Exchange)最初为移动端和 SPA 这类无法安全保存 client_secret 的公开客户端设计,如今已成为授权码流程的通用最佳实践。它的核心思路是:授权请求时提交 challenge,换令牌时提交 verifier,由身份平台验证两者是否对应。
具体做法是:前端生成一个高熵随机串作为 code_verifier,对其做 SHA-256 后 Base64URL 编码得到 code_challenge。授权请求只发送 challenge,verifier 保存在前端会话中;换令牌时后端携带 verifier。攻击者即使截获了授权码,没有 verifier 也无法完成交换。
| 安全要点 | 防护目标 | 实现关键 |
|---|---|---|
| state 校验 | CSRF 攻击 | 回调时与发起值严格比对 |
| nonce 校验 | ID Token 重放 | 写入授权请求,验证 ID Token 中 nonce |
| PKCE | 授权码截获兑换 | S256 challenge,换令牌带 verifier |
| redirect_uri 白名单 | 开放重定向窃码 | 身份平台精确匹配注册值 |
| ID Token 签名验证 | 伪造身份声明 | JWKS 公钥验签,校验 iss/aud/exp |
| 授权码一次性 | 重放兑换 | OP 侧使用后立即失效 |
五、落地时的判断依据
集成完成后,判断链路是否正确,可以观察几个信号:浏览器地址栏回调中是否只出现 code 和 state,而不出现任何 token;后端日志中 /token 请求是否返回 200 且响应含 id_token;ID Token 解码后 aud 是否等于本应用 client_id、nonce 是否与请求一致。若 /token 返回 invalid_grant,优先排查 code 是否过期、是否被重复使用、PKCE verifier 是否与 challenge 匹配、redirect_uri 是否完全一致。把这些环节逐一核对,授权码流程就能在企业身份平台集成中稳定、安全地运转。