跨域请求无法携带 Cookie 的问题排查
问题描述
生产环境有一个场景是,a.com 会加载 b.com 下的一个静态资源 b.com/b.js,并且需要验证登陆才能拿到。通过如下代码获取
addScript('b.com/b.js')
function addScript(src, callback) {
var script = document.createElement('script')
script.src = src
script.onload = script.onerror = function() {
if (script.parentNode) {
script.parentNode.removeChild(script)
}
if (callback) {
callback()
callback = null
}
}
document.head.appendChild(script)
}
也就是我们常用的 jsonp 的形式
我们通常会手动创建 script 标签并设置 src 属性,来实现 jsonp 跨域请求
之前一直没问题,突然有一天有同事发现这个资源加载不到了,并且只是个别人会有这个问题。
查看之后发现,在出问题的浏览器中addScript('b.com/b.js')
发出请求里没带 cookie,所以获取静态资源的请求直接 302 到了登陆页。经过一番查找,发现问题出在 cookie 的 SameSite 属性上。
在没有这个问题的浏览器中,通过手动设置 Chrome 的 chrome://flags/#same-site-by-default-cookies
为 Enabled 也能重现这个问题,至此问题确认。
下面对 SameSite 做简单介绍。
SameSite 属性(引用自阮一峰博客)
Chrome 51 开始,浏览器的 Cookie 新增加了一个SameSite
属性,用来防止 CSRF 攻击和用户追踪。
Cookie 的SameSite
属性用来限制第三方 Cookie,从而减少安全风险。
它可以设置三个值。
- Strict
- Lax
- None
2.1 Strict
Strict
最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。
Set-Cookie: CookieName=CookieValue; SameSite=Strict;
这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。
2.2 Lax
Lax
规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。
Set-Cookie: CookieName=CookieValue; SameSite=Lax;
导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。详见下表。
请求类型 | 示例 | 正常情况 | Lax |
---|---|---|---|
链接 | <a href="..."></a> | 发送 Cookie | 发送 Cookie |
预加载 | <link rel="prerender" href="..."/> | 发送 Cookie | 发送 Cookie |
GET 表单 | <form method="GET" action="..."> | 发送 Cookie | 发送 Cookie |
POST 表单 | <form method="POST" action="..."> | 发送 Cookie | 不发送 |
iframe | <iframe src="..."></iframe> | 发送 Cookie | 不发送 |
AJAX | $.get("...") | 发送 Cookie | 不发送 |
Image | <img src="..."> | 发送 Cookie | 不发送 |
设置了Strict
或Lax
以后,基本就杜绝了 CSRF 攻击。当然,前提是用户浏览器支持 SameSite 属性。
2.3 None
Chrome 计划将Lax
变为默认设置。这时,网站可以选择显式关闭SameSite
属性,将其设为None
。不过,前提是必须同时设置Secure
属性(Cookie 只能通过 HTTPS 协议发送),否则无效。
下面的设置无效。
Set-Cookie: widget_session=abc123; SameSite=None
下面的设置有效。
Set-Cookie: widget_session=abc123; SameSite=None; Secure
解决方案
解决方案就是显式设置 cookie 的 SameSite 属性为 None。一定是显式设置,response 中的 Set-Cookie 头里一定要包含 SameSite: None 字样。
参考文献
- 浏览器系列之 Cookie 和 SameSite 属性
- 谷歌浏览器新版本 Chrome 80 默认 SameSite 导致跨域登录状态失效的问题
- Cookie 的 SameSite 属性
- 当跨域遇到 Cookie 与 SameSite
备注:文中提到 jsonp 以及 scrip 标签的 src 属性,主要是为了能让搜这几个关键字的人有可能搜索到这篇文章