多种API 令牌模式(详细讲解)

106 阅读6分钟

核心思想是:避免将长期有效、高权限的 API Token 直接暴露在容易被 XSS 攻击窃取的存储(如 localStorage 或非 HttpOnly Cookie)中,同时又要让客户端 JS 能够获得某种凭证来调用 API。 它通过引入一些额外的步骤或架构层来实现更高的安全性。

以下是几种常见的实现模式:

模式一:使用安全的会话 Cookie + 后端签发短效 API Token

  1. 登录流程:

    • 用户登录成功。
    • 服务器创建一个会话(可以是服务器端 session,也可以是包含用户标识的内部凭证),并将会话 ID 或一个安全的内部凭证存储在一个 HttpOnly, Secure, SameSite=Lax/Strict 的 Cookie 中(我们称之为“会话 Cookie”)。这个 Cookie JS 无法读取。
    • 服务器不直接返回长期有效的 API Token (JWT) 给前端 JS。
  2. 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 的有效性并处理请求。
  3. 优点:

    • 提高了安全性: 长期有效的凭证(会话 Cookie)是 HttpOnly 的,不易被 XSS 窃取。暴露给 JS 的只是短效 Token,即使被 XSS 窃取,其有效期也很短,攻击窗口大大缩小。
    • 解耦: 前端不需要管理复杂的 Token 刷新逻辑。
  4. 缺点:

    • 增加了复杂性: 需要一个额外的后端端点来签发临时 Token。
    • 增加了网络请求: 每次需要调用 API(或者每隔一段时间)都需要先请求一次临时 Token,增加了延迟。
    • 短效 Token 仍可能被窃取: 在其短暂的生命周期内,如果存在 XSS 漏洞,这个短效 Token 仍然可以被窃取并用于发起请求。

模式二:Backend for Frontend (BFF) 代理模式

  1. 登录流程:

    • 同模式一,用户登录后获得一个安全的会话 Cookie (HttpOnly, Secure, SameSite)。
  2. 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。
  3. 优点:

    • 极高的安全性: 真正的 API Token 从未暴露给浏览器或客户端 JS,XSS 漏洞无法窃取它。
    • 简化前端: 前端只需与 BFF 交互,不需要处理认证头、Token 刷新等。
    • API 聚合/裁剪: BFF 可以整合多个下游 API 调用,或者根据前端需要裁剪数据。
  4. 缺点:

    • 需要 BFF 层: 增加了架构复杂性、部署和维护成本。
    • BFF 成为瓶颈/单点: 所有 API 请求都经过 BFF,可能影响性能和可用性。
    • BFF 本身的安全性很重要。

模式三:Refresh Token 存储在 HttpOnly Cookie 中(常见 OAuth2/JWT 实践)

  1. 登录流程:

    • 用户登录成功。
    • 服务器生成两种 Token:
      • Access Token (访问令牌): 生命周期较短(如 15 分钟 - 1 小时),权限可能受限,用于访问受保护资源。
      • Refresh Token (刷新令牌): 生命周期很长(如几天、几周甚至几个月),权限通常只能用来获取新的 Access Token。
    • 服务器将 Refresh Token 存储在一个 HttpOnly, Secure, SameSite=Lax/Strict 的 Cookie 中。
    • 服务器将 Access Token 返回给前端 JS(通常在响应体中)。
  2. 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。
  3. Token 刷新流程:

    • Access Token 过期时,API 调用会失败(通常返回 401 Unauthorized)。
    • 前端 JS 检测到 401 错误后,向服务器特定的刷新端点 (例如 /api/refresh_token) 发起请求。
    • 浏览器自动携带那个包含 Refresh TokenHttpOnly Cookie 发送这个刷新请求。
    • 服务器的刷新端点验证 Refresh Token 的有效性。
      • 重要: 刷新端点需要有 CSRF 防护,因为它会因 HttpOnly Cookie 而受到 CSRF 威胁。
    • 如果 Refresh Token 有效,服务器会生成新的 Access Token(可能还有新的 Refresh Token),将新的 Refresh Token 通过 Set-Cookie 更新到 HttpOnly Cookie 中,并将新的 Access Token 返回给前端 JS(在响应体中)。
    • 前端 JS 用新的 Access Token 更新内存/Session Storage 中的值,并重新尝试之前失败的 API 调用。
  4. 优点:

    • 保护了最重要的凭证: 长生命周期的 Refresh Token 是 HttpOnly 的,不易被 XSS 窃取。
    • 减少了密码暴露: 用户不需要频繁重新输入密码。
    • 符合 OAuth2 标准实践: 是一种成熟且广泛使用的模式。
  5. 缺点:

    • 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 的风险。