在浏览器中存储token的最佳实践
Web 应用程序不是静态站点,而是静态和动态内容的精心组合。更常见的是,Web 应用程序逻辑在浏览器中运行。该应用程序不是从服务器获取所有内容,而是在浏览器中运行 JavaScript,从后端 API 获取数据并相应地更新 Web 应用程序演示。
为了保护对数据的访问,我们应采用 OAuth 2.0。使用 OAuth 2.0,JavaScript 应用程序需要向 API 的每个请求添加访问 token 。
出于可用性原因,JavaScript 应用程序通常不会按需请求访问 token ,而是存储它。问题是,如何在 JavaScript 中获取这样的 token ?当我们获得 token 时,应用程序应该将 token 存储在哪里,以便在需要时将其添加到请求中?
本文将讨论浏览器中可用的不同存储解决方案,并重点介绍与每个选项相关的安全风险。
获取访问 token
在应用程序可以存储访问 token 之前,它需要获取一个。当前的最佳实践推荐一种获取访问 token 的方法:代码流。代码流程分为两步,首先从用户那里收集授权------授权代码。然后,应用程序在反向通道请求中用授权代码交换访问 token 。此请求称为 token 请求,如以下示例所示:
js
const accessToken = await fetch(OAuthServerTokenEndpoint, {
method: "POST",
// 获取token
body: new URLSearchParams({
client_id: "example-client",
grant_type: "authorization_code",
code: authorization_code,
code_verifier: pkce_code_verifier
})
})
.then (response => response.json())
.then (tokenResponse => {
// 从响应中获取token
if (tokenResponse.accessToken) {
return tokenResponse.accessToken;
}
})
请注意,任何人都可以检查浏览器加载的资源,包括任何JavaScript 代码。因此,任何用 JavaScript 实现的 OAuth 客户端都被视为公共客户端------无法保守秘密,因此无法在 token 请求期间进行身份验证。
浏览器威胁
跨站请求伪造 (CSRF)
在跨站点请求伪造 (CSRF)攻击中,恶意行为者会诱骗用户无意中通过浏览器执行恶意请求。例如,攻击者可能会在网站中嵌入精心设计的图像 src 字符串,从而触发浏览器运行 GET 请求,或者在恶意网站上添加表单来触发 POST 请求。在任何情况下,浏览器都可能会自动向此类请求添加 cookie,包括单点登录 (SSO) cookie。
CSRF 攻击通常利用用户经过身份验证的会话来执行恶意请求。因此,攻击者可以代表用户静默执行请求并调用用户可以调用的任何接口。不过,攻击者无法读取响应,因此他们通常会提出一次性状态更改请求,例如更新用户密码。
跨站脚本 (XSS)
跨站点脚本 (XSS)漏洞允许攻击者将恶意客户端代码注入到其他受信任的网站中。例如,漏洞可能发生在 Web 应用程序中用户输入生成未正确清理的输出的任何位置。浏览器会自动在受信任网站的上下文中运行恶意代码。
XSS 攻击可用于窃取访问和刷新 token 或执行 CSRF 攻击。不过,XSS 攻击有一个时间窗口,因为它们只能在有限的时间内运行,例如在 token 的生命周期内或只要存在漏洞的选项卡处于打开状态。
即使在 XSS 无法用于检索访问 token 的情况下,攻击者也可能利用 XSS 漏洞,使用 CSRF 将经过身份验证的请求发送到安全的 Web 端点。然后,攻击者可以冒充用户,调用用户可以调用的任何后端接口,并造成严重损害。
浏览器中的存储解决方案
当应用程序收到 token 时,它需要存储该 token 以便在 API 请求中使用它。有多种方法可以在用户的浏览器中保存数据。应用程序可以使用专用 API(例如 Web Storage API 或 IndexedDB)来存储 token 。应用程序还可以简单地将 token 保留在内存中或将它们放入 cookie 中。有些存储机制是持久性的,而另一些存储机制在一段时间后或在页面关闭或刷新时会被擦除。
本地存储
使用 JavaScript 中的全局对象通过 Web Storage API 访问本地存储localStorage。存储在本地存储中的数据可跨浏览器选项卡和会话使用,这意味着它不会过期,也不会在浏览器关闭时被删除。因此,通过 localStorage 存储的数据可以在应用程序的所有选项卡中访问。
js
// 保存数据
localStorage.setItem("token", accessToken);
// 加载数据
let accessToken = localStorage.getItem("token");
每当应用程序调用 API 时,它都会从存储中获取 token 并将其手动添加到请求中。然而,由于本地存储可通过 JavaScript 获得,这意味着该解决方案也容易受到跨站点脚本攻击 (XSS)。
如果我们使用 localStorage 来持久保存访问 token ,并且攻击者设法在我们的应用程序中运行外部 JavaScript 代码,则攻击者可以窃取任何 token 并直接调用 API。此外,XSS还允许攻击者操纵应用程序本地存储中的数据,这意味着攻击者可以更改 token 。
请注意,本地存储中的数据是永久存储的,这意味着存储在其中的任何 token 都驻留在用户设备(笔记本电脑、计算机、移动设备或其他设备)的文件系统上,即使在浏览器关闭后也可以被其他应用程序访问。因此,在使用 localStorage 时,请考虑安全性。考虑并防范浏览器外部的攻击媒介,例如恶意软件、被盗设备或磁盘。
根据上述讨论,请遵循以下建议:
- 不要在本地存储中存储 token 等敏感数据。
- 不要信任本地存储中的数据(尤其是用于身份验证和授权的数据)。
会话存储
会话存储是Web Storage API 提供的另一种存储机制。与本地存储不同,当选项卡或浏览器关闭时,使用sessionStorage对象存储的任何数据都会被擦除。此外,存储在会话存储中的数据无法在其他选项卡中访问。只有当前选项卡和源中的 JavaScript 代码可以使用相同的会话存储进行读写。
js
// 存储token
sessionStorage.setItem("token", accessToken);
// 获取token
let accessToken = sessionStorage.getItem("token");
会话存储可以被认为比本地存储更安全,因为浏览器会在窗口关闭时自动删除任何 token ,因此不会留下任何 token 。此外,由于会话存储不在选项卡之间共享,攻击者无法从另一个选项卡(或窗口)读取 token ,这减少了 XSS 攻击的影响。
实际上,使用 sessionStorage 存储 token 时的主要安全问题是 XSS。如果我们的应用程序容易受到 XSS 攻击,攻击者可以从存储中窃取 token 并在 API 调用中重放它。因此,会话存储不适合存储 token 等敏感数据。
indexedDB
IndexedDB 是一个用于在浏览器中异步存储大量数据的 API。但是,在存储 token 时,通常不需要浏览器 API 提供的功能。由于应用程序会在每次 API 调用时发送 token ,因此最好将其大小保持在最低限度。
与迄今为止讨论的其他客户端存储机制一样,对使用索引数据库 API 存储的数据的访问受到同源策略的限制。只有同源的资源和服务工作者才能访问数据。从安全角度来看,IndexedDB 与本地存储相当:
- token 可能会通过文件系统泄漏。
- token 可能会通过 XSS 攻击泄漏。
因此,请勿在 IndexedDB 中存储访问 token 或其他敏感数据。IndexedDB更适合应用程序离线工作所需的数据,例如图像。
存储在内存中
存储 token 的一种非常安全的方法是将其保存在内存中。与其他方法相比, token 不存储在文件系统中,从而降低了设备文件系统的风险。
最佳实践建议将 token 存储在内存中时将其保留在闭包中。例如,我们可以定义一个单独的方法来使用 token 调用 API。它不会向主应用程序(主线程)透露 token 。下面是如何使用 JavaScript 处理内存中的 token 的示例。
js
function protectedCalls(tokenResponse) {
const accessToken = tokenResponse.accessToken;
return {
// 使用内存中的token发送请求
getOrders: () => {
const req = new Request("https://server.example/orders");
req.headers.set("Authorization", accessToken);
return fetch(req)
}
}
}
const apiClient = protectedCalls(tokenResponse);
apiClient.getOrders();
请注意,攻击者在获取 token 后可能无法直接访问 token ,因此可能无法直接使用 token 调用 API。即便如此,他们也可以随时通过apiClient保存 token 引用的 API 调用 API。然而,任何此类攻击都仅限于选项卡打开的时间段以及界面提供的功能。
除了与潜在 XSS 漏洞相关的安全问题之外,将 token 保留在内存中对于用户体验也有很大的缺点,因为 token 会在页面重新加载时丢失。然后,应用程序必须获取新的 token ,这可能会触发新的用户身份验证。安全的设计应该考虑用户体验。
当使用 JavaScript 闭包或服务工作人员处理 token 和 API 请求时,XSS 攻击可能会针对 OAuth 流(例如回调或静默流)来获取 token 。他们可能会取消注册并绕过任何服务,或者使用原型污染通过覆盖诸如window.fetch. 因此,考虑 JavaScript 闭包是为了方便,而不是安全。
cookie
Cookie 是存储在浏览器中的数据片段。根据设计,浏览器会向服务器的每个请求添加 cookie。因此,应用程序必须谨慎使用 cookie。如果配置不仔细,浏览器可能会在跨站点请求中附加 cookie,并允许跨站点请求伪造 (CSRF) 攻击。
Cookie 具有控制其安全属性的属性。例如,SameSite 属性可以帮助降低 CSRF 攻击的风险。当 Cookie 的 SameSite 属性设置为 时Strict,浏览器会将其添加到源自 Cookie 来源站点且目标站点相同的请求中。当请求嵌入任何第三方网站(例如通过链接)时,浏览器不会添加 cookie。
我们可以通过 JavaScript 设置和检索 cookie。然而,当使用 JavaScript 读取 cookie 时,应用程序容易受到 XSS(除了 CSRF 之外)的攻击。因此,首选方案是使用一个后端组件来设置 cookie 并将其标记为HttpOnly. 该标志可以减少 XSS 攻击造成的数据泄露,因为它向浏览器表明 cookie 不能通过 JavaScript 获得。
为了防止 cookie 通过中间人攻击泄漏(这可能会导致会话劫持),cookie 只能通过加密连接 (HTTPS) 发送。要指示浏览器仅在 HTTPS 请求中发送 cookie,cookie 必须设置安全属性。
Set-Cookie:token=myvalue;SameSite=Strict;Secure;HttpOnly
与浏览器中的任何其他永久存储解决方案一样,即使浏览器关闭后,cookie 也可能驻留在文件系统上(例如,cookie 不必过期,或者浏览器可以保留会话 cookie 作为恢复会话功能的一部分)。为了降低从文件系统中窃取 token 的风险,请仅将加密 token 存储在 cookie 中。因此,后端组件必须仅在 Set-Cookie 标头中返回加密 token 。
带有 Cookie 的 OAuth 语义
Cookie 仍然是传输 token 和充当 API 凭证的最佳选择,因为即使攻击者成功利用 XSS 漏洞,也无法从 Cookie 中检索访问 token 。然而,要实现这一点,必须正确配置 cookie。
首先,将 cookie 标记为HttpOnly无法通过 JavaScript 访问,以解决 XSS 攻击的风险。另一个重要属性是secure ,可确保 cookie 仅通过 HTTPS 发送,以减轻中间人攻击。
其次,颁发仅在几分钟内有效的短期访问 token 。在最坏的情况下,具有最短生命周期的访问 token 只能在可接受的短时间内被滥用。通常认为 15 分钟的有效时间是合适的。让 cookie 和 token 大约在同一时间过期。
第三,将 token 视为敏感数据。仅在 cookie 中存储加密 token 。如果攻击者设法获得加密 token ,他们将无法从中解析任何数据。攻击者也无法将加密的 token 重放给任何其他 API,因为其他 API 无法解密该 token 。加密 token 只是限制了被盗 token 的影响。
第四,限制发送 API 凭证的时间。仅将 cookie 发送到需要 API 凭据的资源。这意味着确保浏览器仅将 cookie 添加到实际需要访问 token 的 API 调用中。为此,cookie 需要具有适当的设置,例如SameSite=Strict指向 API 端点的域和路径的域属性。
最后,当使用刷新 token 时,请确保将它们存储在自己的 cookie 中。无需在每个 API 请求中都发送它们,因此请确保情况并非如此。仅当刷新过期的访问 token 时才必须添加刷新 token 。这意味着持有刷新 token 的 cookie 与具有访问 token 的 cookie 的设置略有不同。
结论
使用 OAuth 和 token 可以最好地保护 API 访问。没有安全的解决方案可以在浏览器中存储 token 。所有可用的解决方案在某种程度上都容易受到 XSS 的攻击。因此,保护任何应用程序的首要任务应该是防止 XSS 漏洞。