cookie小知识
http请求携带cookie必须满足下面几个条件
拿一个Http POST请求来说 http://aaa.www.com/xxxxx/list
- 浏览器端某个Cookie的domain字段等于http://aaa.www.com或者http://www.com (在同一个域内)
- 都是http或者https,或者不同的情况下Secure属性为false
- 要发送请求的路径,即上面的xxxxx跟浏览器端Cookie的path属性必须一致,或者是浏览器端Cookie的path的子目录,比如浏览器端Cookie的path为/test,那么xxxxxxx必须为/test或者/test/xxxx等子目录(如果cookie设置在http://www.com/list中,那么请求http://aaa.www.com/xxxxx/list可以携带)
SameSite小知识
如果你最近有关注过chrome的控制台,可能会发现经常报一些warning:
A cookie associated with a cross-site resource at http://baidu.com/ was set without theSameSite
attribute. A future release of Chrome will only deliver cookies with cross-site requests if they are set withSameSite=None
andSecure
.
出现这个警告的原因是:chrome在80版本之后,更新了cookies的携带机制,把原来Cookie的SameSite
属性值,由None
改成了Lax
,这就会导致一些需要第三方cookie的应用产生了异常。
在介绍SameSite
属性之前,我们先来复习一下cookie的基础知识
Cookie基础
Cookie常见的属性:
Name: cookie名。
Value: cookie值。
Domain: cookie的域。如果设成.deepred.com
,那么a.deepred.com
和b.deepred.com
域名下,都可以使用.deepred.com
的cookie。
Path: cookie的路径。请求资源的路径一定要包含这个path才能携带cookie。一般设置成/
即可。
Expires/Max-Age: cookie过期时间。默认不设置,则是Session
会话,关闭页面后,该cookie立即失效。
HttpOnly: 设成true
后,JS使用document.cookie
则访问不到。常用于避免XSS攻击。
Secure: 标记为Secure的cookie只应通过被HTTPS协议加密过的请求发送给服务端。
SameSite: 用来限制第三方Cookie。
最后一个属性非常重要,也就是我们即将要说的SameSite
了。
Cookie携带的场景
我们假设有一个名字为sessionId
的cookie
,domain
设置成了.demo.com
。
1.在a.demo.com
域名下,ajax在该域名下的所有请求,都会自动带上sessionId
ajax.get('/api/data') // 自动带上sessionId
2.在b.demo.com
域名下,ajax在该域名下的所有请求,都会自动带上sessionId
ajax.post('/api2/data2') // 自动带上sessionId
3.在b.demo.com
域名下,ajax请求a.demo.com
的api,需要设置withCredentials
才能带上sessionId
ajax.get('https://a.demo.com/api/data') // 不能自动带上sessionId
ajax.get('https://a.demo.com/api/data', {withCredentials: true}) // 自动带上sessionId
注意一下: https://a.demo.com/api/data
需要支持cors跨域,并且Access-Control-Allow-Origin
不能设成*
,要设置成https://b.demo.com
,只有这样,withCredentials
才有用
router.get('/api/data', (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', ctx.headers.origin);
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , myheader');
ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
ctx.set('Access-Control-Allow-Credentials', 'true');
};
4.在b.demo.com
域名下,使用iframe
加载a.demo.com
,会自动带上sessionId
a.demo.com
和b.demo.com
同属一个域名下的子域名(同站)
5.在a.demo2.com
域名下,ajax请求a.demo.com
的api,需要设置withCredentials
才能带上sessionId
ajax.get('https://a.demo.com/api/data') // 不能自动带上sessionId
ajax.get('https://a.demo.com/api/data', {withCredentials: true}) // 自动带上sessionId
6.在a.demo2.com
域名下,使用iframe
加载a.demo.com
,会自动带上sessionId
a.demo.com
和a.demo2.com
属于完全不相干的两个网站(跨站)
目前为止,都是我们所熟知的cookie携带场景。
然而,在chrome 80版本之后,谷歌把cookie的SameSite
属性,从None
改成了Lax
。这时候,会导致第5和第6种场景sessionId
因为跨站而导致丢失!
跨站解释
http://a.demo.com和http://b.demo.com属于同站,http://a.demo.com和http://a.demo2.com属于跨站
注意和跨域做比较: http://a.demo.com和http://b.demo.com属于跨域
SameSite
cookie的SameSite
属性用来限制第三方Cookie,从而减少安全风险(防止CSRF)
SameSite
可以有下面三种值:
Strict
仅允许一方请求携带Cookie,即浏览器将只发送相同站点请求的Cookie,即当前网页URL与请求目标URL完全一致。Lax
允许部分第三方请求携带CookieNone
无论是否跨站都会发送Cookie
请求类型 | 示例 | 正常情况 | 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 | 不发送 |
从上图可以看出,SameSite
从None
改成了Lax
后,Form
,Iframe
,Ajax
和Image
中跨站的请求受到的影响最大。
解决方法
解决方法也很简单粗暴:强行把SameSite
设置成None
。不过需要特别注意几点:
1.SameSite
设置成None
后,Cookie就必须同时加上Secure
属性
ctx.cookies.set('sessionId', {
maxAge: 1000 * 60 * 60,
secure: true,
sameSite: 'none',
});
这也意味着,你的网站需要支持https
!(Lax
和Strict
不需要支持https)
如果线上的网站同时支持http
和https
,你可能需要让运维将http
强制重定向到https
(建议使用307状态码而不是302状态码)
2.部分浏览器不能加SameSite=none
,比如IOS 12的Safari,以及一些老版本的chrome浏览器,它们会错误的把SameSite=none
识别成SameSite=strict
。
具体不兼容的浏览器可以见这里
因此后端要根据UA
来判断是否加上SameSite=none
同站和同源
同源
具有相同协议,主机名和端口的组合的网站被视为 相同来源 。其他所有内容均视为 跨域
站
像 .com 和 .org 这样的顶级域名( tld )会在根区域 数据库 中被列出。在上面的示例中, site 是 TLD 和它前面的部分域的组合。例如,给定一个URL https://www.example.com:443/foo , site 就是 example.com 。
然而,对于 .co.jp 或 .github 这样的域名。仅仅使用 .jp 或 .io 的 TLD 是不够细粒度的。而且也没有办法通过算法确定特定 TLD 的可注册域名级别。这就是创建“有效顶级域名”列表的原因。它们在公共后缀列表中定义。 etld 列表在 publicsuffix.org/list 上维护。
整个站点命名为 eTLD + 1 。例如,假定 URL 为 https://my-project.github.io ,则 eTLD 为 .github.io ,而 eTLD + 1 为 my-project.github.io ,这被视为 site 。换句话说, eTLD+1 是有效的 TLD 紧接其之前的域的一部分。
同站(same-site) 和 跨站(cross-site)
具有相同 eTLD+1 的网站被视为 “同站”。具有不同 eTLD+1 的网站是 “跨站”。
schemeful same-site
尽管 “同站” 忽略了协议(“无协议的同站”),但在某些情况下,必须严格区分协议,以防止 HTTP 被用作弱通道。在这些情况下,一些文档将 “同站” 更明确地称为 schemeful same-site 。在这种情况下, http://www.example.com 和 https://www.example.com 被认为是跨站点的,因为协议不匹配。
如何检查请求是否为 “同站”,“同源”,或“跨站”
Chrome
发送请求时会附带一个 Sec-Fetch-Site HTTP Header
。截至2020年4月,还没有其他浏览器支持 Sec-Fetch-Site
,这个 HTTP Header
将有以下值之一:
-
cross-site
-
same-site
-
same-origin
-
none
通过检查 Sec-Fetch-Site 的值,您可以确定请求是 “同站”,“同源” 还是 “跨站”。