题干与适用场景
你的产品需要代表用户读取第三方日历,但不能接触用户在日历服务的密码。请解释公开客户端使用 OAuth 2.0 授权码流程与 PKCE 的完整过程,并说明后端和授权服务器各自必须校验什么。
本题假设客户端是浏览器单页应用或原生应用,无法可靠保存一个所有安装实例共享的静态密钥,因此属于公开客户端。 授权服务器负责认证用户并取得授权,资源服务器提供日历 API,客户端在获得访问令牌后调用资源服务器。授权范围 先限定为只读日历;是否签发刷新令牌由授权服务器策略决定。
2026 年 3 月发布的公开后端面试指南直接要求候选人解释授权码模式、PKCE、公开与机密客户端以及 OAuth 与 OpenID Connect 的区别。当前安全基线还要以 OAuth 2.0 Security Best Current Practice 为准:公开客户端 必须使用 PKCE,机密客户端也建议使用,授权服务器必须防止 PKCE 降级,并对已注册回调地址执行精确匹配。
这是一道 backend 题,因为核心能力是设计和验证跨服务授权协议、令牌边界与安全控制。前端跳转只是协议载体。
面试官考察点
第一,候选人是否先分清四个角色:资源所有者是用户,客户端是日历聚合产品,授权服务器负责登录、同意和发令牌, 资源服务器持有日历数据。把“客户端”和“用户”混为一谈,后面的权限与令牌接收方就会全部说错。
第二,能否解释授权码为什么分成前后两段。浏览器只带回一个短期、一次性的授权码;访问令牌由客户端在令牌端点 交换取得,不放进浏览器跳转 URL。PKCE 再把授权请求生成的随机秘密与这次代码交换绑定,截获授权码的一方没有 code_verifier,仍然不能换取令牌。
第三,能否准确区分控制的职责:
| 控制 | 主要绑定关系 | 主要解决的问题 | |---|---|---| | state | 发起请求的客户端会话与回调 | 请求关联、登录 CSRF | | PKCE | 授权请求与令牌交换 | 授权码截获、代码注入 | | 客户端认证 | 机密客户端与授权服务器 | 证明执行令牌交换的客户端身份 | | 精确回调地址 | 注册客户端与允许的回调端点 | 授权码被送到攻击者控制的地址 | | OIDC nonce | 登录请求与 ID Token | ID Token 重放与登录会话关联 |
最后,强回答会主动说明 PKCE 的边界。它不防止同一页面中的 XSS 读取 verifier 或令牌,不替代 TLS、回调地址 校验、最小权限、令牌安全存储,也不把 OAuth 自动变成登录协议。
回答前需要澄清的问题
- 客户端是公开还是机密客户端? SPA 和原生应用不能靠静态 client secret 证明身份;有受控后端的 Web
应用可以成为机密客户端,在 PKCE 之外还要执行客户端认证。
- 目标是调用 API,还是让用户登录产品? 调用日历 API 使用 OAuth;需要用户身份时应使用 OIDC,并校验
ID Token,不能从 access token 猜用户是谁。
- 客户端只连接一个授权服务器,还是允许租户自带身份提供方? 多授权服务器场景还要校验 issuer 或为每个
issuer 使用不同回调地址,防止 mix-up 攻击。
- 需要离线访问吗? 不需要时不要申请刷新令牌;需要时必须定义刷新令牌轮换、重放检测、撤销和失效策略。
- 回调事务保存在哪里? 必须保存一组
state → code_verifier绑定。单个全局 verifier 会让并发标签页互相
覆盖;把 verifier 放进 URL 或日志又会破坏它的秘密性。
- 日历权限需要多大? 默认只申请当前功能需要的只读 scope;写权限和全账户权限要由具体功能证明必要性。
30 秒回答框架
“我会让客户端为每次授权生成不可预测的 state 和高熵 code_verifier,用 S256 得到 codechallenge。浏览器被重定向到授权端点,携带 responsetype=code、client ID、精确注册的回调地址、 最小 scope、state 和 challenge。用户只在授权服务器登录并同意,回调只返回短期一次性 code 和原样 state。 客户端先校验 state 并取回这次事务的 verifier,再向令牌端点提交 code、回调地址和 verifier。授权服务器验证 code、client、redirect URI 与 challenge 绑定后才发访问令牌。公开客户端不能把静态 secret 当凭据;机密 客户端还要做客户端认证。PKCE 防授权码被截获或注入,state 负责请求关联,OIDC 的 ID Token 才用于登录身份。”
分步骤深入解答
第一步:先建立一次性的授权事务。
客户端为每次点击“连接日历”创建两份相互独立的随机值:
state:不可猜测的事务 ID,绑定当前用户会话、预期授权服务器、回调地址和授权后的站内去向。code_verifier:使用密码学安全随机数生成。RFC 7636 要求它由未保留字符组成,长度为 43 到 128 个字符,
并建议至少提供 256 位熵。
客户端用下面的变换生成 challenge:
code_challenge = base64url_without_padding(
SHA256(ASCII(code_verifier))
)
code_challenge_method = S256每个授权事务分别保存 state → code_verifier。浏览器可以有两个同时进行的日历连接流程,因此不能只保存 “最后一个 verifier”。事务还要有短期过期时间,并在成功或失败后删除。verifier 不应进入跳转 URL、分析事件 或应用日志。
第二步:构造授权请求。
客户端通过用户代理访问授权端点:
GET /authorize
?response_type=code
&client_id=calendar-client
&redirect_uri=registered-callback
&scope=calendar.read
&state=random-transaction-id
&code_challenge=derived-challenge
&code_challenge_method=S256授权服务器验证 client ID、响应类型、scope 和回调地址。回调地址应与预注册值精确匹配,不能使用宽泛域名、 任意子路径或用户可控跳转参数。用户凭据只提交给授权服务器;客户端接触的是授权结果,不是用户密码。
第三步:处理回调,但暂时不要调用令牌端点。
用户同意后,授权服务器把用户代理重定向到已注册回调,并带回 code 与原样 state。客户端先执行本地事务 校验:
state是否存在、未过期、尚未消费。- 它是否绑定当前浏览器会话和预期授权服务器。
- 当前回调是否到达这次事务登记的回调端点。
- 如果响应是错误,是否仍能按 state 找到并安全结束对应事务。
state 不匹配时直接终止,不能“先换令牌看看是否成功”。授权码应短期且只能使用一次;授权服务器发现重复交换 必须拒绝,并在可能时撤销基于该 code 已签发的令牌。
第四步:用 verifier 交换令牌。
客户端向令牌端点发送:
POST /token
grant_type=authorization_code
code=returned-authorization-code
redirect_uri=registered-callback
client_id=calendar-client
code_verifier=stored-verifier授权服务器需要把当前请求与最初授权事务重新拼起来:code 属于哪个 client,使用了哪个 redirect URI,是否 过期或用过,授权请求是否包含 challenge,以及当前 verifier 经 S256 计算后是否等于已保存的 challenge。 如果原请求没有 challenge,却在令牌请求中收到 verifier,服务器不能悄悄降级为无 PKCE 流程。
公开客户端发送 client ID 用于标识,但不能靠公开代码中的静态 secret 证明身份。机密客户端在相同 code 与 PKCE 校验之外,还要使用约定的客户端认证方式;PKCE 不替代客户端认证。
第五步:用攻击路径解释每个控制为什么存在。
假设合法客户端生成 verifier V1 和 challenge C1。攻击者截获回调中的 code,却不知道 V1。攻击者向 令牌端点提交自己的 verifier,计算结果不等于 C1,交换失败。这是 PKCE 对授权码截获的核心保护。
再假设攻击者发起自己的授权事务,然后试图把自己的 code 注入受害者回调。受害者事务保存的是另一组 state 和 verifier;state 关联或 PKCE 绑定至少一处不匹配,客户端必须终止。授权服务器同时要记录“该 code 是否带 challenge”并强制校验,避免攻击者删掉 challenge 后触发降级。
如果攻击者把回调地址改成自己控制的端点,精确注册与精确字符串匹配应在授权请求阶段拒绝。若客户端支持多个 授权服务器,还必须比较响应的 issuer 与事务中保存的 issuer,或使用每个 issuer 独立的回调地址;只记住一个 授权端点 URL 不足以防 mix-up。
第六步:把令牌限制在真实用途内。
访问令牌代表授权,不应被客户端当作用户身份证明。资源服务器要校验令牌是否面向自己,并限制到所需资源和 动作。客户端只申请 calendar.read,访问令牌设置短有效期,并避免进入 URL、日志和可被无关脚本读取的持久 存储。
PKCE 在 code 换成 token 后已经完成职责。XSS 如果能读取 SPA 内存、事务存储或访问令牌,PKCE 无法挽回。 高风险浏览器应用可采用 BFF:令牌保存在受控后端,浏览器只持有 HttpOnly 会话 cookie。这个选择减少 JavaScript 直接接触令牌的机会,但会引入 cookie 会话、CSRF 防护、后端扩容和代理 API 的成本。
若公开客户端获得刷新令牌,授权服务器必须使用发送者约束或刷新令牌轮换来检测重放。轮换时每次刷新都发新 refresh token 并使旧值失效;旧值再次出现说明可能泄漏,应撤销这条授权链并要求重新授权。
第七步:分清 OAuth、OIDC 和其他授权方式。
OAuth 回答“客户端可否代表用户访问某个资源”。OIDC 在 OAuth 之上增加身份层,客户端通过 ID Token 验证 用户登录。OIDC 回调还要校验签名、issuer、audience、有效期和 nonce;access token 面向资源服务器,ID Token 面向客户端,两者不能互换。
没有用户参与、服务代表自身调用受控资源时,才考虑 client credentials。设备输入受限时可以使用设备授权流程。 隐式流程把访问令牌放进授权响应,增加 URL 泄漏和重放面;当前最佳实践建议使用返回 code 的流程。资源所有者 密码凭据模式会让客户端直接接触用户密码,当前安全基线明确禁止使用。
第八步:用失败测试验收协议,而不只跑通 happy path。
| 测试 | 预期结果 | |---|---| | 修改一个 verifier 字符 | 令牌交换失败 | | 同一 code 交换两次 | 第二次失败,并触发相应安全处理 | | 回调 state 不存在、过期或属于另一会话 | 客户端在调用令牌端点前拒绝 | | 授权请求缺少 challenge,令牌请求却带 verifier | 授权服务器拒绝降级 | | redirect URI 只有大小写、路径或尾斜杠不同 | 精确匹配失败 | | 两个标签页同时发起授权并乱序返回 | 各自按 state 找到正确 verifier | | 连接两个授权服务器后交换 issuer | 客户端拒绝 mix-up | | 日志与分析事件扫描 | 不出现 code、verifier、access token 或 refresh token | | XSS 读取浏览器可访问存储 | 明确认定 PKCE 不提供此项保护,并评估 BFF |
生产监控还要区分授权拒绝、state 校验失败、PKCE 校验失败、code 重用、redirect 不匹配和刷新令牌重放。只统计 “OAuth 失败”会掩盖攻击信号与客户端实现错误。
高质量示范回答
“我先把场景定为公开客户端访问第三方日历 API。客户端无法安全保存共享 secret,所以我不会把写在 SPA 包里的 secret 当成客户端认证。
每次授权开始时,我生成不可预测的 state 和至少 256 位熵的 code verifier,使用 S256 得到 challenge,并按 state 保存这次事务的 verifier、会话、issuer、回调地址和过期时间。授权请求携带 code 响应类型、client ID、 精确注册的回调地址、只读 scope、state 和 challenge。用户只在授权服务器登录与同意。
回调收到 code 和 state 后,我先验证 state 属于当前会话且未消费,再取回对应 verifier。随后向 token endpoint 发送 code、同一 redirect URI、client ID 和 verifier。授权服务器确认 code 短期未使用、属于该客户端和回调 地址,并验证 verifier 的 S256 结果与 challenge 相等,才签发访问令牌。攻击者即使截到 code,没有 verifier 也换不到令牌;如果删除 challenge 试图降级,授权服务器也必须拒绝。
state 负责请求关联,PKCE 绑定 code 交换,二者不能用一个含糊的‘防 CSRF 参数’概括。机密 Web 应用还要做 客户端认证;PKCE 仍然建议保留。访问令牌只用于日历资源服务器,若产品要把这个流程当登录,还应使用 OIDC, 并验证 ID Token 的签名、issuer、audience、有效期和 nonce。
最后我会测试错误 verifier、重复 code、state 串会话、精确回调不匹配、并发标签页、PKCE 降级和多 issuer 混淆,并确认日志里没有 code 或令牌。PKCE 不能防同源 XSS 偷走浏览器中的 verifier 或 token;风险高时我会 评估由 BFF 保存令牌,用 HttpOnly 会话 cookie 连接浏览器。”
常见错误
- 把 client ID 当成 secret → client ID 会出现在授权请求中,只用于标识客户端 → **公开客户端使用 PKCE,
机密客户端另做真实客户端认证。**
- 只说“code 比 token 安全” → 没有说明截获 code 后为何不能交换 → **画出 challenge 与 verifier 的绑定,
并说明令牌端点的比较。**
- 使用
plain或允许缺少 PKCE → verifier 可能在授权请求中暴露,或攻击者可触发降级 → **使用 S256,
并让授权服务器记录和强制执行 PKCE。**
- 生成 state 却不绑定会话 → 攻击者可以搬运合法 state,或并发流程互相覆盖 → **保存一次性的
state、会话、issuer 与 verifier 映射。**
- 允许模糊回调匹配 → 授权码可能被送往攻击者控制的子路径或跳转器 → 预注册并精确比较 redirect URI。
- 把 access token 解码后直接当登录身份 → token 的接收方和语义可能是资源服务器 → **登录使用 OIDC,
按规范校验 ID Token。**
- 声称 PKCE 可以防 XSS → XSS 可能直接读取 verifier、code 或已签发令牌 → **减少浏览器令牌暴露,
修复 XSS,并按风险评估 BFF。**
- 把 code、verifier 或 token 写入日志 → 诊断系统扩大秘密的可见范围 → **记录事务 ID、错误分类和哈希化
关联值,不记录可兑换凭据。**
追问及应对
追问 1:如果客户端改成有后端的传统 Web 应用,还需要 PKCE 吗?
需要。机密客户端在令牌端点执行客户端认证,证明交换者持有受保护凭据;PKCE 绑定的是本次授权请求和 code 交换,可进一步防止授权码注入和误用。两者保护不同关系。verifier 与 state 可以保存在服务端会话中,浏览器 只携带不可预测的会话标识。
追问 2:如果这个流程还承担“使用日历账号登录”,要增加什么?
使用 OIDC,而不是把 access token 或 UserInfo 响应随意当成登录证明。请求包含 openid scope 和 nonce, 客户端验证 ID Token 签名、issuer、audience、有效期及 nonce,并把 issuer 与 subject 组合成外部身份键。 state 继续关联浏览器事务,nonce 关联登录请求与 ID Token。
追问 3:如果 SaaS 允许每个租户配置不同授权服务器,最大的新风险是什么?
客户端可能把来自授权服务器 A 的响应送到授权服务器 B 的 token endpoint,形成 mix-up。事务中要保存预期 issuer,并校验响应 issuer;另一种办法是为每个 issuer 配置独立 redirect URI,并验证响应到达正确端点。 授权端点和令牌端点必须来自同一份可信 issuer 元数据。
追问 4:公开客户端必须长期后台同步日历,刷新令牌怎么保护?
只授予必要 scope,并使用刷新令牌轮换或发送者约束。轮换时保存 token family:每次刷新废弃旧值,旧 refresh token 再出现就撤销当前家族并要求重新授权。客户端还需要安全存储、撤销入口、最长寿命和长时间不活跃后的 过期策略。PKCE 只保护最初 code 交换,不保护之后被窃取的 refresh token。
追问 5:两个标签页同时连接不同日历账户,为什么会偶发失败?
常见原因是客户端只保存一个全局 state 或 verifier,第二个标签页覆盖第一个。回调必须按 state 查找独立事务, 事务中保存 verifier、预期账户或 issuer、回调地址和创建时间。消费后原子删除;未知、重复或过期 state 全部拒绝,不能退回“最近一次 verifier”。