一、背景知识
我们知道,HTTP 是无状态的。也就是说,HTTP 请求方和响应方间无法维护状态,都是一次性的,它不知道前后的请求都发生了什么。
但有的场景下,我们需要维护状态。最典型的,一个用户登陆微博,发布、关注、评论,都应是在登录后的用户状态下的。
二、cookie 和 session 方案
后端维护session信息,比如将 { user: "wuyanbiao"} 存储,对应相应的sessionid为 31dcba****。客户端输入用户名、密码,调用相应登录接口,接口返回这个sessionid;以后客户端再请求业务相关信息,都需要携带这个sessionid,用于身份识别,这个就是我们熟悉的cookie了。
Cookie 是服务器保存在浏览器的一小段文本信息。
简单理解:
典型的session登录/ 验证流程:
对比认证方案:token和jwt的原理和使用:juejin.cn/post/728791…
三、cookie属性
-
domain 和path
cookie 是要限制::「空间范围」::的,通过 Domain(域)/ Path(路径)两级。
Domain属性指定浏览器发出 HTTP 请求时,哪些域名要附带这个 Cookie。如果没有指定该属性,浏览器会默认将其设为当前 URL 的一级域名,比如 www.example.com 会设为 example.com,而且以后如果访问example.com的任何子域名,HTTP 请求也会带上这个 Cookie。如果服务器在Set-Cookie字段指定的域名,不属于当前域名,浏览器会拒绝这个 Cookie。
Path属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie。只要浏览器发现,Path属性是 HTTP 请求路径的开头一部分,就会在头信息里面带上这个 Cookie。比如,PATH属性是/,那么请求/docs路径也会包含该 Cookie。当然,前提是域名必须一致。
path比较少用到。可以用于区分静态资源和ajax请求,节省带宽。
-
secure 和httpOnly
Secure属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。另一方面,如果当前协议是 HTTP,浏览器会自动忽略服务器发来的Secure属性。该属性只是一个开关,不需要指定值。如果通信是 HTTPS 协议,该开关自动打开。
HttpOnly属性指定该 Cookie 无法通过 JavaScript 脚本拿到,主要是document.cookie属性拿不到该属性。这样就防止了该 Cookie 被脚本读到,只有浏览器发出 HTTP 请求时,才会带上该 Cookie。
http only 的应用场景是防止 XSS (Cross-site scripting) 攻击: 例如我在评论区写了一段 hack JS,服务端因存在 XSS 漏洞,没有对 < 进行转义。导致这段 JS 在其他人打开此页面的时候也被执行了。 如果我上面的 hack JS 中写了获取所有 cookie,并发送到我的服务器上,这样用户的登录信息就泄漏了。 如果 cookie 开启了 http-only 之后,我的hack js 无法获取到用户的 cookie,它们的登录信息就无法泄漏了。
另外,对于xss攻击,一个有效的手段就是设置csp:Content Security Policy 入门教程 - 阮一峰的网络日志
github.comnkun/issues/1302
-
Expires和 Max-age
如果同时指定了Expires和Max-Age,那么Max-Age的值将优先生效。
备注:浏览器根据本地时间,决定 Cookie 是否过期,由于本地时间是不精确的,所以没有办法保证 Cookie 一定会在服务器指定的时间过期。
备注2:如果都没有设置,将变成session cookie,也就是关闭浏览器后会失效。
-
SameSite
SameSite主要用于防止 CSRF 攻击。 什么是CSRF( Cross-Site Request Forgery )攻击?可以参照下文:
Cookie 的 SameSite 属性 - 阮一峰的网络日志
(1)Strict
Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。
Set-Cookie: CookieName=CookieValue; SameSite=Strict;
这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。
(2)Lax
Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。
Set-Cookie: CookieName=CookieValue; SameSite=Lax;
导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。
设置了Strict或Lax以后,基本就杜绝了 CSRF 攻击。当然,前提是用户浏览器支持 SameSite 属性
(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
(一)服务端
- 生成(create)
服务端http的 response中Set-Cookie: foo=bar; domain=ai.sogou.com
(1)需要注意的是,domain的值只能设置为自身ai.sogou.com或者父域sogou.com ( 同理,客户端通过document.cookie设置域名的值也是这样受限)。如果指定的域名不属于当前域名,浏览器会拒绝这个 Cookie, 无论是服务器指定的还是客户端document.cookie指定的。(禁用三方cookie, 像chrome还未禁用,未来计划会禁用)
(2)Domain属性指定浏览器发出 HTTP 请求时,哪些域名要附带这个 Cookie。如果没有指定该属性,浏览器会默认将其设为当前域名,这时子域名将不会附带这个 Cookie。比如,example.com不设置 Cookie 的domain属性,那么sub.example.com将不会附带这个 Cookie。如果指定了domain属性,那么子域名也会附带这个 Cookie。
(3)是否需要在domain前面加dot 比如 .sogou.com
stackoverflow.com/questions/1…:
In RFC 2109, a domain without a leading dot meant that it could not be used on subdomains, and only a leading dot (.mydomain.com) would allow it to be used across multiple subdomains (but not the top-level domain, so what you ask was not possible in the older spec).
However, all modern browsers respect the newer specification RFC 6265, and will ignore any leading dot, meaning you can use the cookie on subdomains as well as the top-level domain.
- 接受读取(read)
浏览器发送到服务器,HTTP 头信息的Cookie字段获取
Cookie字段可以包含多个 Cookie,使用分号(;)分隔。
Cookie: name=value; name2=value2; name3=value3
服务器收到浏览器发来的 Cookie 时,有两点是无法知道的。
(1)Cookie 的各种属性,比如何时过期。
(2)哪个域名设置的 Cookie,到底是一级域名设的,还是某一个二级域名设的。
- 更新(update)
服务器想改变一个早先设置的 Cookie,必须同时满足四个条件:Cookie 的key、domain、path和secure都匹配
- 删除(delete)
设置expires为过去的某个时间
疑问:session认证方案中,是否可以将用户id存储在headers呢?
A: 也可以。但是必要性不大,因为session方案下,cookie id较小;也很小可能被篡改;
手写写入headers等方法更加繁琐。
(二)浏览器端
读与写,都是通过document.cookie。
注意本地服务器的document.cookie不生效 stackoverflow.com/questions/8…
- 生成(create)
各个属性的写入注意点如下。
-
path属性必须为绝对路径,默认为当前路径。
-
max-age属性的值为秒数。
-
expires属性的值为 UTC 格式,可以使用Date.prototype.toUTCString()进行日期格式转换。
- 接受读取read
document.cookie读取cookies,除了httpOnly为true的。
对于没有办法读取cookie属性的值(设置为httpOnly),开发调试人员可以使用开发者面板查看。
- 更新(update)
服务器想改变一个早先设置的 Cookie,必须同时满足四个条件:Cookie 的key、domain、path和secure都匹配
- 删除(delete)
设置expires为过去的某个时间
五、cookie的一些细节
- cookie 的不区分协议与端口。(同域hostname,而非同源)
Q: "浏览器的同源政策规定,两个网址只要域名相同和端口相同,就可以共享 Cookie(参见《同源政策》一章)。注意,这里不要求协议相同。"
A: 对于 cookie 共享,端口和协议都不需要区分。
- cookie的上限
browsercookielimits.squawky.net/
If you want to support most browsers, then don't exceed 50 cookies per domain, and don't exceed 4093 bytes per domain (i.e. total size of all cookies <= 4093 bytes)
超过限制以后,Cookie 将被忽略,不会被设置。
六、跨域cors和cookie
www.ruanyifeng.com/blog/2016/0…
(1)服务端Access-Control-Allow-Credentials ;
(2)request如果要携带cookie,需要特定参数指明。可能看到过这个参数为credentials或者withCredentials,什么时候用两者呢。主要跟请求的实现有关
- 客户端XHR(Ajax, axios等)的 withCredentials设置true或者不设置
- Fetch ,需要设置credentials
developer.mozilla.org/zh-CN/docs/…
注意,credentials 是Request接口的只读属性,但是可以通过传入参数改变
fetch('example.com:1234/users', {
credentials: 'include'
})
这与XHR的withCredentials 标志相似,不同的是有三个可选值(后者是两个)
-
omit: 从不发送cookies.
-
same-origin: 只有当URL与响应脚本同源才发送 cookies、 HTTP Basic authentication 等验证信息.(浏览器默认值,在旧版本浏览器,例如safari 11依旧是omit,safari 12已更改)
-
include: 不论是不是跨域的请求,总是发送请求资源域在本地的 cookies、 HTTP Basic authentication 等验证信息.
七、三方cookie
(1)使用场景
- 日志追踪
- 三方统一登录
淘宝、天猫都属于阿里旗下的产品,阿里为他们提供统一的登录服务,同时,你的登录信息也会存到这个统一登录服务的域下,所以存到这个域下的 Cookie 就成了三方 Cookie
- 行为和广告精准推送
(2)三方禁用后,怎么追踪用户
- 三方转一方,由使用cookie转化 使用请求参数
- 浏览器指纹
(3)一方 vs 三方
一方可以通过引入sdk这种方式,通过js document.cookie写入该站点。缺点:A、B站点分别引入SDK,同一个用户访问A、B站点,但是却无法识别为同一个用户。
三方一般是通过类似图片等方式,setcookie到第三方的域名下。优点:可以跨站点分析用户行为。缺点:用户隐私安全。
为了防止三方滥用cookie,目前可以使用SameSite(Strict、Lax、None),默认Lax,get请求允许,post等请求禁止。
八、session的存储方式
显然,服务端只是给 cookie 一个 sessionId,而 session 的具体内容(可能包含用户信息、session 状态等),要自己存一下。存储的方式有几种:
Redis(推荐):内存型数据库。以 key-value 的形式存,正合 sessionId-sessionData 的场景;且访问快。
内存:直接放到变量里。一旦服务重启就没了
数据库:普通数据库。性能不高。
「Session 的分布式问题」
通常服务端是集群,而用户请求过来会走一次负载均衡,不一定打到哪台机器上。那一旦用户后续接口请求到的机器和他登录请求的机器不一致,或者登录请求的机器宕机了,session 不就失效了吗?
这个问题现在有几种解决方式。
一是从「存储」角度,把 session 集中存储。如果我们用独立的 Redis 或普通数据库,就可以把 session 都存到一个库里。
二是从「分布」角度,让相同 IP 的请求在负载均衡时都打到同一台机器上。以 nginx 为例,可以配置 ip_hash 来实现。
但通常还是采用第一种方式,因为第二种相当于阉割了负载均衡,且仍没有解决「用户请求的机器宕机」的问题。
九、其他
App移动应用程序中的Cookie通常是由开发人员自己实现的,并且它们可能不同于Web应用程序中的Cookie。App移动应用程序中的Cookie也不会像Web应用程序中那样被浏览器自动管理和清除,而是需要开发人员自己处理。
参考
(1)鉴权必须了解的5个兄弟:cookie、session、token、jwt、单点登录
(2)headers vs cookie 传递参数 两种给 Http 添加状态的方式,都不完美-51CTO.COM