SPA里藏不住client secret:OAuth改用授权码+PKCE

|作者: QUASA 编辑团队|2 分钟阅读
SPA里藏不住client secret:OAuth改用授权码+PKCE

纯浏览器 SPA 应作为公开客户端使用授权码流程和 PKCE:前端不保存 client secret,登录回调接收授权码,而不再通过 Implicit Flow 直接接收访问令牌。IETF 的浏览器应用规范说明,交付给用户浏览器的代码无法保管预置密钥,并要求使用授权码的公开浏览器客户端实施 PKCE。

如果业务要求 OAuth 令牌始终留在服务端,就让 Backend for Frontend(BFF)负责授权码交换和 API 调用,浏览器通过会话 Cookie 与它交互。两种架构的选择取决于威胁模型:能在应用页面执行的恶意脚本,是否可能取得可带离浏览器使用的令牌,以及业务能否承受这种后果。

为什么打包进前端的密钥没有保密性

React、Vue、Angular 应用的 JavaScript 最终会下载到用户设备。构建时注入的环境变量、混淆字符串或写在请求里的固定凭据,都会随代码或网络请求暴露;任何拿到应用副本的人都可能提取同一份 client secret。它因此不能证明请求来自可信的应用实例。client_id 则只是客户端标识,公开出现在授权请求中是正常的。

纯 SPA 的注册和令牌端点配置应允许公开客户端使用授权码交换,无须提交共享密钥。如果身份平台要求浏览器提交固定 secret,先检查注册时是否选成了机密客户端;确需使用客户端凭据时,应让能保管凭据的服务端组件成为 OAuth 客户端。把密钥移入另一个前端文件、配置接口或浏览器 Cookie,并没有改变浏览器能够取得它的事实。

迁移 Implicit Flow 还要检查实际响应,而不只是改控制台选项。授权请求应使用 response_type=code,回调取得 code 后再向令牌端点换取令牌;旧流程若仍把 access token 放在授权响应的 URL 片段中,就仍保留了直接经浏览器前端通道交付令牌的路径。

PKCE 保护哪一段,登录请求怎样关联

PKCE 绑定的是一次授权请求与随后的授权码交换。客户端为每次登录生成新的随机 code_verifier,由它计算 code_challenge,并在前往授权端点时提交挑战值;换取令牌时再提交原验证值。授权服务器核对两者的关系,因此仅截获授权码的一方无法凭该授权码完成交换。支持 S256 时,应以验证值的 SHA-256 摘要计算挑战值,而非为所有用户复用一个常量。

  1. 发起登录时生成本次请求专用的 code_verifier、相应的 code_challenge,以及用于关联回调的随机 state。把它们与当前登录尝试关联,避免多个标签页或连续登录请求互相覆盖。
  2. 将浏览器导航到授权端点,发送 client_id、已注册的 redirect_uri、所需 scope、response_type=code、code_challenge 和 code_challenge_method=S256,并携带本次的 state。
  3. 回调时先处理错误并核对 state,再用收到的 code、本次的 code_verifier 和对应的 redirect_uri 请求令牌。交换结束后,清除只供本次请求使用的验证值,并从页面地址移除授权码。

PKCE 不会把公开客户端变成能够保管秘密的机密客户端。它针对授权码被截获后遭他人兑换的风险;如果恶意脚本已经在 SPA 的同源页面运行,脚本仍可能观察应用使用令牌的过程,或借当前用户身份发起请求。代码交换安全与令牌交付后的脚本风险,需要分别处理。

redirect URI 管回调,CORS 管跨源读取

redirect URI 决定授权响应送往何处,应预先注册并与授权请求精确匹配。生产环境宜使用固定的 HTTPS 回调地址,避免通配域名、开放跳转或由外部输入拼接的目标;协议、主机名、路径和尾部斜杠都值得逐项核对。收到 code 的页面还须确认它属于当前登录尝试,不能仅因为回调落在自家域名就继续换取令牌。

CORS 处理的是浏览器脚本能否读取跨源请求的响应。跳转到授权端点是页面导航;纯 SPA 从自身来源向身份平台的令牌端点提交 code,才会涉及令牌交换所需的跨源许可。CORS 许可不能替代 redirect URI 校验,也不能让前端持有的 client secret 获得保密性。

在 Microsoft identity platform 中,微软的 SPA 回调配置说明要求将使用授权码与 PKCE 的 redirect URI 设为 spa 类型,以支持相应的 CORS 令牌交换。若登录跳转成功、换令牌却在控制台报跨源错误,应先检查应用注册中的回调类型与实际 redirect_uri;给前端请求自行添加响应头无法代替身份平台的配置。

采用 BFF 时,令牌交换由服务端发起,浏览器不必跨源调用身份平台的令牌端点。浏览器与 BFF 或业务 API 之间若跨源,仍要单独配置相应接口的 CORS,并结合 Cookie 的发送条件处理;一个端点允许跨源访问,不意味着其他端点也应开放。

纯 SPA 的令牌放在哪里

如果 SPA 必须直接调用受保护的 API,访问令牌就需要在浏览器运行环境中供应用使用。Auth0 的令牌存储指南建议没有相应后端的 SPA 将令牌保存在内存中而不做持久化;页面刷新或标签页关闭后,应用需通过可用的登录或续期机制重新取得令牌。这减少了令牌留在设备上的时间,但会影响刷新后的会话体验。

localStorage 能跨页面刷新和标签页保留令牌,同源恶意脚本也能读取它。sessionStorage 的范围限于标签页,但挡不住在该页面执行的恶意脚本。普通、可由 JavaScript 读取的 Cookie 同样无法把 bearer token 与页面脚本隔开。即使令牌只在内存中,脚本仍可能在应用发起请求时截获它;内存存储是降低持续暴露的选择,不是 XSS 防护的替身。

若授权服务器向公开浏览器客户端签发刷新令牌,还应确认轮换或发送方约束、有效期以及失效后的重新登录路径。刷新令牌能持续换取新的访问令牌,泄露后的影响与一次短期访问令牌不同。退出登录时清除应用持有的令牌和本地状态,并按身份平台能力结束会话或撤销令牌;仅清空界面状态,不能推定已经签发的令牌立即失效。

何时让 BFF 接管令牌

当业务权限较高、浏览器不应接触刷新令牌,或需要把访问令牌也隔离在服务端时,BFF 更符合目标。BFF 作为机密客户端发起授权码流程、完成交换并管理令牌;浏览器向 BFF 的业务接口发送会话 Cookie,由 BFF 附加访问令牌调用指定 API,再把业务响应交还前端。页面脚本由此不需要拿到供其自行转发的 OAuth 令牌。

BFF 的会话 Cookie 应设置 HttpOnly 和 Secure,并按实际站点关系配置 SameSite;有状态变更的请求仍需防范 CSRF。代理接口也应限制可访问的目标主机、路径及操作,不能让前端指定任意 URL 供服务端附带令牌访问。否则,令牌虽未直接交给浏览器,服务端仍可能按恶意请求把它送往错误的目标。

BFF 隔离令牌并不等于消除页面脚本的所有风险。同源恶意脚本仍可能借用户当前会话向 BFF 发起操作,因此服务端必须逐项校验权限,前端也仍需防范脚本注入。退出登录时应销毁 BFF 会话、清除 Cookie,并处理服务端保存的令牌;身份平台的登录会话是否一并结束,则取决于所接平台提供的退出机制。

上线前核对登录与退出路径

  • 纯 SPA 注册为公开客户端,构建产物和浏览器请求中没有 client secret;BFF 架构中的凭据与 OAuth 令牌由服务端管理。
  • 登录请求使用授权码和每次重新生成的 PKCE 验证值;回调与发起请求正确关联,旧的 Implicit Flow 入口不再交付访问令牌。
  • 授权请求使用已注册的精确 redirect URI;纯 SPA 的令牌端点允许所需的浏览器跨源交换,平台要求的 SPA 回调类型已配置。
  • 明确访问令牌、刷新令牌与会话 Cookie 各存在哪里,并分别核对页面刷新、令牌过期和退出后再次进入应用的行为。
分享:

订阅我们的新闻通讯

将最新 Web3、AI 和加密货币新闻直接发送到您的邮箱。

0