在基于浏览器的应用程序中,JWT应该保存在哪里以及如何使用它?

77

我正在尝试在我的身份验证系统中实现JWT,有几个问题。为了存储令牌,我可以使用cookies,但也可以使用 localStoragesessionStorage

哪种方式是最好的选择?

我已经了解到JWT保护网站免受CSRF攻击,但是如果我将JWT令牌保存在cookie存储中,我无法想象它如何起到保护作用。

那么,它如何能够保护免受CSRF攻击呢?

更新1
我看到了一些用法示例,如下:

curl -v -X POST -H "Authorization: Basic VE01enNFem9FZG9NRERjVEJjbXRBcWJGdTBFYTpYUU9URExINlBBOHJvUHJfSktrTHhUSTNseGNh"

当我从浏览器向服务器发出请求时,我该如何实现?我还看到有些人在URL中实现了令牌:

http://exmple.com?jwt=token

如果我通过AJAX请求,我可以设置一个头部信息 jwt: [token] 然后我就可以从头部信息中读取 token。

更新2

我安装了 Google Chrome 扩展程序 "Advanced REST Client",并成功将 Token 作为自定义标题传递。在向服务器发出 GET 请求时,是否可能通过 JavaScript 设置此标题数据?

5个回答

173
选择存储方式更多是在权衡利弊,而不是试图找到一个明确的最佳选择。让我们来看几个选项:

选项1 - Web存储 (localStoragesessionStorage)

优点

  • 浏览器不会自动将Web存储中的任何内容包含到HTTP请求中,使其容易受到CSRF攻击
  • 只能由在创建数据的完全相同域中运行的Javascript访问
  • 允许使用最语义化正确的方法在HTTP中传递令牌身份验证凭据(Authorization头和Bearer模式)
  • 很容易挑选应该包含身份验证的请求

缺点

  • 不能被在创建数据的子域中运行的Javascript访问(由example.com写入的值无法被sub.example.com读取)
  • ⚠️易受XSS攻击
  • 为了执行经过身份验证的请求,只能使用浏览器/库API允许你自定义请求(在Authorization头中传递令牌)

用法

您可以利用浏览器的localStoragesessionStorage API在执行请求时存储和检索令牌。

localStorage.setItem('token', 'asY-x34SfYPk'); // write
console.log(localStorage.getItem('token')); // read

选项2 - 仅HTTP的Cookie

优点

  • 它不容易受到XSS攻击
  • 浏览器会自动包含满足Cookie规范(域,路径和生命周期)的任何请求中的标记
  • Cookie可以在顶级域中创建并用于由子域执行的请求

缺点

  • ⚠️ 容易受到CSRF攻击
  • 您需要注意并始终考虑子域中的Cookie可能的使用方式
  • 挑选应该包括Cookie的请求是可行的,但会有些混乱
  • 您可能会遇到一些小差异的问题,因为浏览器处理Cookie的方式不同
  • 如果不小心,则可能会实现一个容易受到XSS攻击的CSRF缓解策略
  • 服务器端需要验证Cookie以进行身份验证,而不是更合适的Authorization

用法

不需要在客户端执行任何操作,因为浏览器会自动处理所有事情。

选项3 - Javascript可以访问的Cookie 被服务器端忽略

优点

  • 不容易受到CSRF攻击(因为服务器端忽略它)
  • Cookie可以在顶级域中创建并用于由子域执行的请求
  • 允许使用最语义正确的方法通过HTTP传递令牌身份验证凭据(使用带有Bearer方案的Authorization头)
  • 较易挑选应该包含身份验证的请求

缺点

  • ⚠️ 容易受到XSS攻击
  • 如果设置Cookie的路径不当,则浏览器会自动在请求中包含Cookie,从而增加不必要的开销
  • 为了执行身份验证请求,只能使用允许您自定义请求的浏览器/库API(在Authorization头中传递令牌)

用法

您可以利用浏览器的document.cookie API存储和检索令牌以便执行请求。这个API没有Web存储那么细粒度(您获取所有Cookie),因此需要额外的工作来解析所需的信息。

document.cookie = "token=asY-x34SfYPk"; // write
console.log(document.cookie); // read

补充说明

这可能看起来是一个奇怪的选项,但它有一个好处——你可以将存储空间提供给顶级域名和所有子域名,这是Web存储无法提供的。然而,实现更加复杂。


结论 - 最终说明

对于大多数常见情况,我建议选择选项1,主要原因如下:

  • 如果你创建了一个Web应用程序,你需要处理XSS;无论你在哪里存储令牌,都需要处理
  • 如果你不使用基于cookie的身份验证,CSRF甚至不会出现在你的视野中,所以这是少了一件需要担心的事情

另请注意,基于cookie的选项也有很大差异,对于选项3,cookie纯粹用作存储机制,因此它几乎就像是客户端的实现细节。然而,选项2意味着更传统的处理身份验证的方式;如果您想进一步了解cookie与token的区别,您可能会发现本文有趣:Cookies vs Tokens: The Definitive Guide

最后,虽然没有任何一个选项提到这一点,但使用HTTPS是强制性的,这意味着cookie应该被适当地创建以考虑到这一点。


3
非常好的解释!我也认为选项1至少对于我的企业内部网站是最好的选择,但公共网络应用可能会比较棘手。 - Anjan Biswas
来这里是为了选项2!对我来说似乎是最好的,尽管不是最语义正确的。 - Doug
3
从我所看到的情况来看,随着Set-Cookie头选项中SameSite属性的引入,选项2在可用性方面对CSRF攻击变得更加无懈可击。特别是在将Lax设置为默认值后。换句话说,如果不是现在,至少在不久的将来,选项2可能会更安全。而且,“如果你不小心,你可能会实施一个对XSS攻击易受攻击的CSRF缓解策略”这句话并不清楚你的意思。 - x-yuri

31

[编辑]本答案已被采纳,但来自João Angelo的回应更加详细,值得考虑。然而要注意,由于自2016年11月以来安全实践已发展,Option 2应该优先于Option 1实施。

查看此网站:https://auth0.com/blog/2014/01/07/angularjs-authentication-with-cookies-vs-token/

如果您想要存储它们,应该使用localStorage或sessionStorage(如果可用)或cookies。 您还应该使用Authorization头,但不应该使用基本方案,而应使用Bearer方案:

curl -v -X POST -H "Authorization: Bearer YOUR_JWT_HERE"

使用 JS,您可以使用以下代码:

<script type='text/javascript'>
// define vars
var url = 'https://...';

// ajax call
$.ajax({
    url: url,
    dataType : 'jsonp',
    beforeSend : function(xhr) {
      // set header if JWT is set
      if ($window.sessionStorage.token) {
          xhr.setRequestHeader("Authorization", "Bearer " +  $window.sessionStorage.token);
      }

    },
    error : function() {
      // error handler
    },
    success: function(data) {
        // success handler
    }
});
</script>

1
我想通过JWT替换cookie会话存储。现在,当我向服务器发出GET请求时,我该如何携带这个令牌?是的,我会将令牌保存在会话或本地存储中,但困难在于,我如何发送令牌以便每个请求(包括GET请求)都能到达服务器。我知道当我使用ajax请求时,我可以请求标题,但是当我不使用时,我如何将令牌发送到服务器? - softshipper
我的问题是,如果不用Ajax,我该怎么做呢?似乎这是不可能的,对吗? - softshipper
1
移动浏览器支持Cookies。与计算机上的浏览器一样,由于浏览器本身或浏览器的配置(例如可能拒绝第三方Cookie),可能存在一些限制。请参见https://dev59.com/q1LTa4cB1Zd3GeqPdcvb。 - Spomky-Labs
如果您在尝试访问 $window.sessionStorage.token 或 $(window).sessionStorage.token 时遇到“未定义”问题,那么只需执行以下操作:window.sessionStorage.token。 - Houdini Sutherland
1
你说的“不建议存储JWT”是什么意思?你如何在下一个请求中发送JWT令牌?你必须将令牌存储在localStorage、sessionStorage或cookie中。 - gabrielgiussi
显示剩余4条评论

22

截至2021年,随着现今大多数浏览器引入了SameSite:Lax / Strict选项来设置cookie,情况有所改变。

因此,为了阐述João Angelo的答案,我会说最安全的方式是:

使用以下选项将JWT存储在cookie中:

  • HttpOnly(仅限HTTP)
  • Secure(安全)
  • SameSite:Lax或Strict

这将同时避免XSS和CSRF攻击


4
似乎是2023年的完美答案。 - Prince Singh

13

那种特定的解决方案仍然存在应该注意的漏洞。发布的文章控制了针对该文章的 Disqus 评论。以下是更深入讨论的链接(不涉及政治公司议程)- https://disqus.com/home/discussion/stormpath/where_to_store_your_jwts_cookies_vs_html5_web_storage_stormpath_user_management_api/ - seasick
2
您提供的 Disqus 讨论似乎已经不存在了。您能否详细说明 StormPath 方法的缺点以及如何克服它们? - Darwin Airola

6

永远不要在存储外部存储JWT。

如果希望将JWT保留在长时间会话期间(例如令牌的到期时间仅为15分钟,但会话需要1小时),则每当令牌即将过期时,在后台静默地重新登录用户。

如果要跨会话保留JWT,则应使用“刷新令牌”(refresh token)。BTW,刷新令牌大多数情况下也用于上述目的。您应该将其存储在HttpOnly cookie中(更准确地说,由服务器通过Set-Cookie头设置,前端调用/refresh_token API终点)。

顺便说一下,刷新令牌是最小的罪恶;为了补充它,您还应确保遵循缓解XSS的最佳实践。

localStorage、sessionStorage和cookie都有漏洞。

这是我曾经读过的关于JWT的最佳指南: https://blog.hasura.io/best-practices-of-using-jwt-with-graphql/


那个指南非常珍贵。 - mouchin777
  • JWT 作为 id_token 就像您的用户凭证
  • JWT 作为 access_token 就像您的会话令牌
最安全的选项是在内存中。深入了解请查看此链接
- human

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接