核心思想是:避免将长期有效、高权限的 API Token 直接暴露在容易被 XSS 攻击窃取的存储(如 localStorage 或非 HttpOnly Cookie)中,同时又要让客户端 JS 能够获得某种凭证来调用 API。 它通过引入一些额外的步骤或架构层来实现更高的安全性。
以下是几种常见的实现模式:
模式一:使用安全的会话 Cookie + 后端签发短效 API Token
-
登录流程:
- 用户登录成功。
- 服务器创建一个会话(可以是服务器端 session,也可以是包含用户标识的内部凭证),并将会话 ID 或一个安全的内部凭证存储在一个
HttpOnly,Secure,SameSite=Lax/Strict的 Cookie 中(我们称之为“会话 Cookie”)。这个 Cookie JS 无法读取。 - 服务器不直接返回长期有效的 API Token (JWT) 给前端 JS。
-
API 调用流程:
- 当客户端 JS 需要调用某个受保护的 API (例如
/api/resource) 时,它不直接调用该 API。 - JS 首先向自己的后端(或 BFF - Backend for Frontend)发起一个专门用于获取临时 API Token 的请求 (例如
/api/get-temporary-token)。 - 浏览器自动携带那个安全的会话 Cookie 发送这个请求。
- 后端收到
/api/get-temporary-token请求,验证会话 Cookie 的有效性,确认用户身份。 - 如果验证通过,后端动态生成一个短生命周期(例如 5-15 分钟)的 API Token (JWT),这个 Token 包含了调用目标 API 所需的权限。
- 后端将这个短效 Token 返回给前端 JS(在响应体中)。
- 前端 JS 拿到这个短效 Token,将其设置到
Authorization: Bearer <short-lived-token>请求头中,然后再去调用实际的目标 API (/api/resource)。 - 目标 API (
/api/resource) 验证这个短效 Token 的有效性并处理请求。
- 当客户端 JS 需要调用某个受保护的 API (例如
-
优点:
- 提高了安全性: 长期有效的凭证(会话 Cookie)是
HttpOnly的,不易被 XSS 窃取。暴露给 JS 的只是短效 Token,即使被 XSS 窃取,其有效期也很短,攻击窗口大大缩小。 - 解耦: 前端不需要管理复杂的 Token 刷新逻辑。
- 提高了安全性: 长期有效的凭证(会话 Cookie)是
-
缺点:
- 增加了复杂性: 需要一个额外的后端端点来签发临时 Token。
- 增加了网络请求: 每次需要调用 API(或者每隔一段时间)都需要先请求一次临时 Token,增加了延迟。
- 短效 Token 仍可能被窃取: 在其短暂的生命周期内,如果存在 XSS 漏洞,这个短效 Token 仍然可以被窃取并用于发起请求。
模式二:Backend for Frontend (BFF) 代理模式
-
登录流程:
- 同模式一,用户登录后获得一个安全的会话 Cookie (
HttpOnly,Secure,SameSite)。
- 同模式一,用户登录后获得一个安全的会话 Cookie (
-
API 调用流程:
- 当客户端 JS 需要调用某个 API 时,它不直接调用目标资源服务器的 API。
- JS 向自己的 BFF 发起请求 (例如
/bff/get-user-data)。 - 浏览器自动携带安全的会话 Cookie 发送这个请求。
- BFF 收到请求,验证会话 Cookie。
- 如果验证通过,BFF 负责:
- 查找或生成调用实际下游 API 所需的长期 API Token(这个 Token 可能安全地存储在 BFF 配置中、从内部服务获取,或者基于会话信息生成)。
- 使用这个长期 API Token 向实际的资源服务器 API (
https://real-api.com/users/data) 发起请求。 - 接收来自资源服务器的响应。
- 将响应数据(可能经过处理或裁剪)返回给前端 JS。
- 前端 JS 收到 BFF 返回的数据,它完全不知道也不需要处理实际的 API Token。
-
优点:
- 极高的安全性: 真正的 API Token 从未暴露给浏览器或客户端 JS,XSS 漏洞无法窃取它。
- 简化前端: 前端只需与 BFF 交互,不需要处理认证头、Token 刷新等。
- API 聚合/裁剪: BFF 可以整合多个下游 API 调用,或者根据前端需要裁剪数据。
-
缺点:
- 需要 BFF 层: 增加了架构复杂性、部署和维护成本。
- BFF 成为瓶颈/单点: 所有 API 请求都经过 BFF,可能影响性能和可用性。
- BFF 本身的安全性很重要。
模式三:Refresh Token 存储在 HttpOnly Cookie 中(常见 OAuth2/JWT 实践)
-
登录流程:
- 用户登录成功。
- 服务器生成两种 Token:
- Access Token (访问令牌): 生命周期较短(如 15 分钟 - 1 小时),权限可能受限,用于访问受保护资源。
- Refresh Token (刷新令牌): 生命周期很长(如几天、几周甚至几个月),权限通常只能用来获取新的 Access Token。
- 服务器将 Refresh Token 存储在一个
HttpOnly,Secure,SameSite=Lax/Strict的 Cookie 中。 - 服务器将 Access Token 返回给前端 JS(通常在响应体中)。
-
API 调用流程:
- 前端 JS 将 Access Token 存储在内存(例如 JavaScript 变量)或 Session Storage 中(比 Local Storage 稍好,因为关闭标签页就清除,但仍易受 XSS 攻击)。不推荐 Local Storage。
- 当 JS 需要调用 API 时,从内存/Session Storage 中读取 Access Token,设置到
Authorization: Bearer <access-token>请求头中。 - API 服务器验证 Access Token。
-
Token 刷新流程:
- 当 Access Token 过期时,API 调用会失败(通常返回 401 Unauthorized)。
- 前端 JS 检测到 401 错误后,向服务器特定的刷新端点 (例如
/api/refresh_token) 发起请求。 - 浏览器自动携带那个包含 Refresh Token 的
HttpOnlyCookie 发送这个刷新请求。 - 服务器的刷新端点验证 Refresh Token 的有效性。
- 重要: 刷新端点需要有 CSRF 防护,因为它会因
HttpOnlyCookie 而受到 CSRF 威胁。
- 重要: 刷新端点需要有 CSRF 防护,因为它会因
- 如果 Refresh Token 有效,服务器会生成新的 Access Token(可能还有新的 Refresh Token),将新的 Refresh Token 通过
Set-Cookie更新到HttpOnlyCookie 中,并将新的 Access Token 返回给前端 JS(在响应体中)。 - 前端 JS 用新的 Access Token 更新内存/Session Storage 中的值,并重新尝试之前失败的 API 调用。
-
优点:
- 保护了最重要的凭证: 长生命周期的 Refresh Token 是
HttpOnly的,不易被 XSS 窃取。 - 减少了密码暴露: 用户不需要频繁重新输入密码。
- 符合 OAuth2 标准实践: 是一种成熟且广泛使用的模式。
- 保护了最重要的凭证: 长生命周期的 Refresh Token 是
-
缺点:
- Access Token 仍暴露给 JS: 在其有效期内,Access Token 存储在 JS 可访问的地方,仍然是 XSS 攻击的目标。
- 实现复杂度较高: 前端需要处理 Token 过期、自动刷新、重新请求等逻辑。后端需要实现安全的 Token 刷新端点(包括 CSRF 防护)。
总结:
API令牌的各种模式都比直接将长期 Token 存在 Local Storage 或非 HttpOnly Cookie 中更安全,但它们都增加了实现的复杂性。
- 模式一(短效 Token) 相对简单,但安全提升有限且有额外开销。
- 模式二(BFF 代理) 安全性最高,但需要引入 BFF 架构。
- 模式三(Refresh Token Cookie) 是目前 Web 应用(尤其是 SPA)中使用 JWT 进行身份验证的非常流行和推荐的方式,它在安全性和用户体验之间取得了较好的平衡,但需要前后端都实现相应的逻辑。
选择哪种模式取决于你的应用架构、安全需求、开发资源以及愿意承担的复杂性程度。核心思想都是尽量减少高权限、长时效凭证直接暴露给客户端 JavaScript 的风险。