Web 应用中 OAuth 安全的最佳实践

OAuth 2.0 已成为 API 身份验证的标准,但安全地实现它仍然具有挑战性。随着令牌盗窃、已弃用的流程以及浏览器特定漏洞每天都在威胁着 Web 应用程序,开发人员需要明确的指导来了解什么方法真正有效。本文涵盖了 2025 年您需要的基本 OAuth 2.0 安全最佳实践,从 RFC 9700 要求到 SPA 特定模式。
核心要点
- 永远不要在 localStorage 或 sessionStorage 中存储 OAuth 令牌——对于 SPA 使用内存存储
- 在 OAuth 2.1 下,所有客户端都必须使用带 PKCE 的授权码流程
- 实施刷新令牌轮换和短期访问令牌(15-30 分钟)
- 考虑使用前端后端(BFF)模式,将令牌完全排除在浏览器之外
为什么 OAuth 安全比以往更重要
OAuth 通过消除应用程序之间的密码共享来保护用户数据。OAuth 不是将您的凭据交给第三方应用(就像交出您的房门钥匙),而是颁发临时的、有作用域限制的令牌——更像是给代客泊车员一把只能启动汽车的钥匙。
但问题在于:令牌是有价值的攻击目标。被盗的访问令牌可以立即授予 API 访问权限。糟糕的实现选择——在 localStorage 中存储令牌、使用已弃用的流程或跳过 PKCE——会将小漏洞转变为重大安全漏洞。
核心安全风险及应对方法
令牌盗窃和存储
不要这样做: 在 localStorage 或 sessionStorage 中存储令牌。任何 XSS 攻击都可以读取这些值并将它们泄露到攻击者控制的服务器。
应该这样做: 对于 SPA,仅将令牌保存在内存中。对于服务器端应用,将它们存储在加密的服务器端会话中。使用短期访问令牌(15-30 分钟)来限制任何盗窃造成的损害。
应避免的已弃用流程
隐式流程已经过时。RFC 9700 和 OAuth 2.1 都禁止使用它。为什么?因为它直接在 URL 片段中发送令牌,将它们暴露给浏览器历史记录、引用头以及页面上的任何 JavaScript。
资源所有者密码凭据流程也应该避免——它通过要求应用直接处理用户密码来违背 OAuth 的整个目的。
始终使用: 对所有客户端(包括 SPA 和移动应用)使用带 PKCE 的授权码流程。
Discover how at OpenReplay.com.
现代标准:RFC 9700 和 OAuth 2.1
PKCE 现在是强制性的
PKCE(代码交换证明密钥)可防止授权码拦截攻击。其工作原理如下:
// Generate code verifier and challenge
const verifier = generateRandomString(128);
const challenge = base64url(sha256(verifier));
// Include challenge in authorization request
const authUrl = `https://auth.example.com/authorize?` +
`client_id=app123&` +
`code_challenge=${challenge}&` +
`code_challenge_method=S256`;
// Send verifier when exchanging code for token
const tokenResponse = await fetch('/token', {
method: 'POST',
body: JSON.stringify({
code: authorizationCode,
code_verifier: verifier
})
});
OAuth 2.1 使 PKCE 对所有授权码流程都是强制性的,而不仅仅是公共客户端。
使用 mTLS 或 DPoP 进行令牌绑定
令牌绑定确保被盗令牌无法被攻击者使用。两种主要方法:
- mTLS(双向 TLS): 将令牌绑定到客户端证书
- DPoP(持有证明演示): 使用加密证明而无需证书
两者都通过将令牌加密绑定到合法客户端来防止令牌重放攻击。
刷新令牌轮换
永远不要重复使用刷新令牌。每次刷新都应颁发新的刷新令牌并使旧令牌失效。这限制了被盗刷新令牌滥用的时间窗口:
// Server-side refresh token handling
async function refreshAccessToken(oldRefreshToken) {
// Validate and revoke old refresh token
await revokeToken(oldRefreshToken);
// Issue new token pair
return {
access_token: generateAccessToken(),
refresh_token: generateRefreshToken(), // New refresh token
expires_in: 1800
};
}
SPA OAuth 安全:特殊考虑
单页应用面临独特的挑战,因为所有代码都在浏览器中运行。浏览器是敌对环境——假设那里的任何数据都可能被泄露。
前端后端(BFF)模式
最安全的方法是将令牌完全排除在浏览器之外。轻量级后端代理处理 OAuth 流程并在服务器端维护令牌,使用安全的、httpOnly、sameSite cookie 来管理 SPA 会话。
令牌处理器模式
对于希望在没有后端复杂性的情况下获得 SPA 优势的团队,令牌处理器模式提供了一个折中方案。它使用专门的代理:
- 处理 OAuth 流程
- 安全存储令牌
- 向 SPA 颁发短期会话 cookie
- 将 cookie 认证请求转换为令牌认证的 API 调用
如果必须在浏览器中存储令牌
当 BFF 不可行时:
- 仅在内存中存储令牌,永远不要使用 localStorage
- 使用 service workers 隔离令牌访问
- 实施激进的令牌过期时间(5-15 分钟)
- 永远不要在浏览器中存储刷新令牌
实施检查清单
✓ 对所有客户端使用带 PKCE 的授权码流程
✓ 实施精确的重定向 URI 匹配
✓ 将令牌生命周期设置为最小可行持续时间
✓ 为公共客户端启用刷新令牌轮换
✓ 使用 state 参数进行 CSRF 保护
✓ 为高安全性应用实施令牌绑定(mTLS/DPoP)
✓ 对于 SPA:考虑 BFF 或令牌处理器模式
✓ 监控 OAuth 2.1 合规性要求
结论
在 Web 应用中保护 OAuth 需要理解协议的优势和面向您架构的特定威胁。从 RFC 9700 的要求开始:强制性 PKCE、禁止隐式流程以及正确的令牌处理。对于 SPA,认真考虑通过 BFF 或令牌处理器模式将令牌完全排除在浏览器之外。这些不仅仅是最佳实践——它们是 2025 年 OAuth 安全的最低标准。
常见问题
不可以。即使是没有敏感数据的应用也不应该使用 localStorage 存储令牌。被盗令牌可用于账户接管、API 滥用,并作为更严重攻击的跳板。始终将令牌存储在内存中或使用服务器端会话。
是的。OAuth 2.1 要求所有授权码流程都使用 PKCE,包括机密客户端。PKCE 增加了针对授权码拦截攻击的纵深防御,这是单独使用客户端密钥无法防止的,特别是在涉及受损网络或恶意浏览器扩展的场景中。
使用前端后端模式,由后端管理刷新令牌,并通过安全 cookie 向 SPA 提供短期访问令牌。或者,当访问令牌过期时,使用带 prompt=none 的静默认证来获取新令牌,无需用户交互。
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.