题干与适用场景
你负责一个面向移动端和浏览器客户端的 OAuth API。现有资源服务器只校验 Bearer access token;一次日志泄露就可能让攻击者在令牌有效期内重放请求。请设计一套 DPoP(Demonstrating Proof-of-Possession)方案,说明授权服务器、客户端和资源服务器的边界,以及无法使用 DPoP 时的处理方式。
题目考察后端安全架构,不要求候选人记住某个厂商的配置界面。RFC 9449 把 access token 绑定到客户端公钥,并要求每次调用用私钥签署与方法和 URI 绑定的证明;RFC 9700 则把避免弱 OAuth 流程和使用 PKCE 等措施列为安全最佳实践。
面试官考察点
强回答会先指出 Bearer token 的根本问题:持有字符串即可使用,泄露后资源服务器无法分辨合法客户端与窃取者。随后给出端到端流程:客户端生成密钥、令牌端点验证 DPoP proof 并绑定公钥,资源服务器验证签名、请求方法、URI、令牌哈希和时间窗口。
面试官还会看你是否处理了 jti 重放、服务器 nonce、密钥丢失、代理改写 URI、刷新令牌、旧客户端兼容和观测告警。只说“给 JWT 加签名”不足以证明理解,因为普通 JWT 的签名并没有证明请求者仍持有签发时的私钥。
回答前需要澄清的问题
客户端和威胁模型
先确认客户端是浏览器 SPA、原生移动端还是服务端应用。公钥私钥能否放在硬件保护区、是否允许每台设备独立密钥,会影响密钥生命周期。还要问攻击者能否读取日志、代理、浏览器存储或设备文件,以及是否能实时拦截请求。
资源服务器边界
确认 API 是否经过网关、TLS 终止点和内部转发。DPoP 的 htu 必须与资源服务器验证的 URI 语义一致;如果网关重写路径,必须定义规范化和可信转发头,不能让客户端签一个地址、后端却验证另一个地址。
可用性与迁移约束
确认是否必须支持旧 Bearer 客户端、是否允许短暂的双栈策略、登录和刷新是否由同一授权服务器完成。若监管或高价值接口要求强 sender constraint,就应把 DPoP 设为必需;普通低风险接口可先以能力协商和分阶段监控迁移。
30 秒回答框架
“我会把 DPoP 当作三方协议来设计。客户端为每个安装生成私钥和公钥,在令牌请求中提交一次性 DPoP proof;授权服务器验证 proof 后把 access token 的 cnf.jkt 绑定到公钥。每次 API 请求都带新的 DPoP JWT,资源服务器检查签名、htm、htu、iat、唯一 jti、ath 以及 token 的 cnf.jkt 是否一致,并用 nonce 和短时间窗口降低重放风险。私钥丢失时撤销绑定令牌;旧客户端只在明确的受限范围内临时走 Bearer,逐步把高风险接口切到强制 DPoP。”
分步骤深入解答
第一步:建立密钥和授权绑定
客户端首次运行生成非对称密钥,私钥留在设备安全存储,公钥通过 DPoP proof 的 JWK 传给授权服务器。授权服务器验证 proof 的签名、类型和时间,再把 access token 的确认信息绑定到该公钥的 JWK thumbprint。若使用授权码流程,仍要启用 RFC 9700 推荐的 PKCE;DPoP 解决的是令牌持有者证明,不能替代授权码保护。
第二步:生成每次请求的 proof
客户端为每个请求生成新的 JWT,至少包含 jti、htm、htu 和 iat,并用绑定的私钥签名。访问令牌的哈希放在 ath,这样资源服务器能确认 proof 针对的是当前令牌,而不是同一把钥匙签出的另一请求。客户端将 proof 放入 DPoP 头,并在 Authorization 头使用 DPoP scheme,而不是 Bearer scheme。
第三步:按固定顺序验证
资源服务器先限制算法和 JWK 类型,再校验 JWT 签名、证书链或公钥格式、时间窗口和请求方法 URI。随后计算 access token 的哈希,与 ath 比较;读取 token 的 cnf.jkt,与 proof 公钥 thumbprint 比较。最后检查 jti 是否在当前窗口出现过,并按授权范围、受众和资源权限继续执行普通鉴权。
第四步:处理 nonce 与重放
服务端可以对高风险接口发出 nonce,客户端必须在下一次 proof 中携带该 nonce。jti 需要在窗口内做去重,单机缓存适用于小规模部署;多实例资源服务器应使用带过期时间的共享缓存,或接受短窗口下的重复风险并将其记录为安全事件。拒绝响应要区分过期、签名错误、thumbprint 不匹配和 nonce 缺失,便于告警而不泄露密钥细节。
第五步:设计轮换、撤销和降级
设备丢失、私钥泄露或检测到异常时,撤销与该 thumbprint 相关的 access 和 refresh token,并要求重新授权。密钥轮换不能只在客户端静默替换:新公钥必须重新完成授权绑定。迁移期可让资源服务器同时接受 Bearer 与 DPoP,但高价值写操作应单独配置强制 DPoP,避免“兼容”永远成为安全默认值。
第六步:比较 mTLS 和纯 Bearer
mTLS 把证明放在 TLS 客户端证书层,适合服务器到服务器且证书基础设施成熟的环境;DPoP 在应用层工作,更适合移动端和浏览器,但要维护 proof、nonce、jti 和密钥存储。纯 Bearer 的部署最简单,却无法抵抗令牌字符串被复制后的重放。选择应由客户端能力、代理拓扑、合规要求和运维成本决定,而不是把 DPoP 当作所有接口的无条件开关。
高质量示范回答
我会先把威胁模型定清楚:日志、代理或客户端存储可能泄露 access token,攻击者随后尝试在有效期内调用 API。单纯缩短过期时间只能缩短窗口,不能证明请求来自原客户端。
客户端为每个安装生成密钥并保护私钥。拿授权码换 token 时,我会同时要求 PKCE 和 DPoP proof;授权服务器验证 proof,把 access token 的 cnf.jkt 绑定到公钥。资源服务器收到请求后,按固定顺序验证 DPoP JWT 的签名、htm、htu、iat、jti、ath 和 nonce,再核对 proof 公钥与 token 的 thumbprint。任何一项不匹配都返回未授权,并把原因分类记录到安全指标。
jti 在多实例环境中放入带 TTL 的共享去重缓存。高风险写操作要求 nonce 和 DPoP,低风险读接口在迁移期可以双栈,但要按客户端版本和资源等级设置截止时间。私钥丢失时撤销关联 token 并重新走授权;不把 Bearer fallback 放进同一条高价值路径。最后用伪造 proof、重放旧 jti、改 URI、错 token 哈希、过期 nonce 和代理改写做集成测试,观察拒绝率、nonce 重试率和异常 thumbprint,确认协议真的在阻断重放。
常见错误
- 错误表现: 只给 access token 做 JWT 签名。→ 失败原因: 签名证明 token 来自授权服务器,却没有证明调用者持有客户端私钥。→ 修正方法: 绑定
cnf.jkt,并在每次请求验证新的 DPoP proof。 - 错误表现: 复用同一份 DPoP proof。→ 失败原因:
jti和时间窗口无法区分重放,攻击者可复制完整请求。→ 修正方法: 每次请求生成新的jti,服务端在窗口内做去重,必要时使用 nonce。 - 错误表现: 只在网关验证 DPoP,内部服务继续相信未绑定的用户头。→ 失败原因: 转发链路可能丢失方法、URI 或 token 绑定,后端重新接受伪造身份。→ 修正方法: 在可信边界传递规范化验证结果,并明确谁能设置该结果。
- 错误表现: 任何验证失败都自动回退 Bearer。→ 失败原因: 攻击者可以故意触发 DPoP 错误,降级到最弱路径。→ 修正方法: 只对明确登记的旧客户端和低风险资源开放限时 fallback。
追问及应对
追问一:多地域部署如何做 jti 去重?
按资源风险选择一致性。高价值写操作把 jti 去重放在跨地域可用的共享存储,并接受一次额外往返;低风险读操作可以使用地域缓存和短窗口,重复时记录异常但不牺牲整体可用性。无论选择哪种,都要把窗口、故障时行为和重复率写入 SLO。
追问二:浏览器中的私钥被 XSS 读走怎么办?
DPoP 不能修复同一执行环境已经被完全控制的客户端。优先使用不可导出密钥、严格内容安全策略、短生命周期 token 和刷新轮换;检测到 thumbprint 异常时撤销绑定。面试中应明确保护目标是“令牌被复制但私钥未被复制”的重放场景,而非承诺抵御所有端点接管。
追问三:代理会改变 URI,htu 怎么验证?
先定义外部规范 URI 和内部路由的映射,只有受信代理能提供经过认证的原始 scheme、host 和 path。资源服务器用同一规范化算法生成待比较 URI,拒绝客户端可控的未认证转发头。若无法建立可信映射,宁可在边界终止 DPoP 并签发内部短期身份,也不要猜测原始 URI。
追问四:旧客户端只能发 Bearer,能否永久兼容?
不能把永久兼容当成安全设计。按客户端版本、用户风险和资源类型设定迁移期限;先对高价值写接口强制 DPoP,向旧客户端返回可操作的升级错误,并监控剩余流量。只有风险评估证明必要且有额外的设备绑定或速率限制时,才保留受限 Bearer 路径。