Skip to content

安全机制对照:state / 防伪 token / PKCE ​

这三个参数经常被放在一起问"到底防什么",但它们保护的目标、校验的时机、校验的人完全不同。 把三者并排放在本项目这条登录链路上:

机制出现在哪谁校验一句话职责
state① 授权请求 → 回调客户端自己请求-响应配对 + 传递应用状态;防 CSRF 角色已弱化
防伪 token(__RequestVerificationToken)② 登录表单 POST(同意 / 登出同款)OP防"伪造登录 → 在受害者浏览器里种下攻击者会话"
PKCE(code_challenge / code_verifier)① 授权请求 + ④ 换 tokenOP兜底:换 code 的人 == 发起 authorize 的人

1. state:客户端侧的"请求-响应配对 + 传递应用状态" ​

① 请求-响应配对。 并发发起多次登录时,回调必须和"当初那次请求"一一对应。 客户端拿回调里的 state 反查自己保存的请求上下文(PKCE code_verifier、returnUrl、nonce 等),查不到就丢弃。 本项目 demo 客户端用 oidc-client-ts:UserManager 封装了 state 管理,回调按 state 取回中间态。

② 传递应用状态。 /connect/authorize 与回调是两次独立请求,中间隔着登录页 / 同意页。 客户端想记住的东西(如"登录完回哪个页面")靠 state 这一程带过去、再带回来。

③ 防 CSRF 已弱化。 state 最初还负责防"伪造回调":攻击者把自己的授权码塞进受害者的回调, 诱导受害者客户端拿它换 token → 受害者以攻击者账号登录(一种登录 CSRF)。 在纯授权码流 + 强制 PKCE 下,这个攻击面被 PKCE 覆盖——攻击者没有受害者客户端的 code_verifier,code 换不出 token。 所以 state 的防 CSRF 职责不再重要,OAuth 2.0 的现行建议也把它的定位回到"传递应用状态"。

谁校验:客户端自己。OP 只把 state 原样回传(见 ① 授权请求 的实测 302),不检查、不解释。

先完整走一遍攻击(登录 CSRF):

  1. 攻击者在恶意页面放一个自动提交的表单(受害者一访问该页就 submit()):action 指向 POST /Account/Login,字段填攻击者自己的用户名密码;
  2. 受害者的浏览器把这个表单跨站提交过去,凭据校验通过 → 在受害者浏览器里种下攻击者的会话 cookie;
  3. 攻击在这一刻已经成功——不需要受害者再点任何"登录"按钮,也不走 OAuth 前端通道;
  4. 受害者之后继续用这个浏览器(下单、填资料……),请求带的全是攻击者的 cookie,所有操作落在攻击者账号下。

防伪 token 堵住的正是第 2 步:服务端要求表单携带只有同源页面才拿得到的令牌 (隐藏字段 + HttpOnly cookie 配对,见 ② 登录),跨站表单拿不到 → 请求被拒。

谁校验:OP。本项目在 AccountController 的表单 POST 端点上标注 [ValidateAntiForgeryToken], 失败是 400 空响应体([ValidateAntiForgeryToken],与 Identity UI 一致);同意 / 登出端点同样用该 attribute。

它不属于 OAuth 规范

防伪 token 是"表单 POST 端点"自身的 CSRF 防御——保护的是**"种 cookie"这个动作**,而不是授权码流程本身。

3. PKCE:OP 在换 token 时兜底"换 code 的人 == 发起 authorize 的人" ​

① 兜底。 在纯授权码流 + 强制 PKCE 场景下,它挡住"code 被偷(或被塞)后拿去换 token", 也覆盖了 state 原本的防 CSRF 功能(见上一节)。

② OP 校验。 校验发生在 POST /connect/token,由 OP(本项目是 OpenIddict)完成,客户端不做判断。

③ 核心机制。 authorize 时客户端只给出哈希 code_challenge;换 token 时才给出原文 code_verifier; OP 比对 BASE64URL(SHA256(code_verifier)) == code_challenge。 关键点:code_verifier 从不经过 redirect_uri——攻击者能偷到 code(它走 URL、浏览器历史和 Referer), 但偷不到 verifier(它只出现在"客户端 → token 端点"的 POST 里)→ 偷来的 code 换不出 token。

谁校验:OP。本项目注册客户端时用 Requirements.Features.ProofKeyForCodeExchange 强制 PKCE; 失败见 坑速查:ID2054(没带 PKCE)/ ID2052(verifier 与 challenge 不匹配)。

校验方速查 ​

机制校验方校验时机失败表现
state客户端回调返回后客户端丢弃该次回调(配对失败,重新登录)
防伪 tokenOP处理表单 POST 时400 空响应体([ValidateAntiForgeryToken];登录页表单不再回 302 error=)
PKCEOPPOST /connect/tokeninvalid_grant(ID2054 / ID2052)

常见理解偏差(本页的由来) ​

#容易这样理解更准确的说法
1state"保证并发一致性"拆成两件事:请求-响应配对 + 传递应用状态;防 CSRF 已被 PKCE 弱化
2防伪 token 的攻击要在"诱导用户点击登录"之后才完成不需要后续点击——种下 cookie 的那一刻攻击已经成功,且不走 OAuth
3PKCE 只是"兜底"保留"兜底"定位,但要具体:兜的是 code 被偷换 token,并覆盖 state 的防 CSRF;code_verifier 从不经过 redirect_uri

OpenIddict + ASP.NET Core Identity 授权服务器,React 登录页,逐步拆解登录链路