Cookie那些事儿

面试别人的时候,经常碰到自诩精通HTTP协议的人,我一般会丢个问题“HTTP协议是否有状态?”,来试探是否真的“了解”HTTP协议,很多人听到问题就懵逼了,这TM还是我认识的HTTP协议么。偶尔碰到一个说是无状态的,我继续追问“既然HTTP是无状态的,那么我打开一个网站,然后刷新一下,服务器能否知道这次的访问者和上次的访问者是否是同一个客户端?”,然后继续懵逼,转而把答案改为“那应该是有状态的吧?”

所以啊,我们很多时候学技术性东西,还是要追求甚解,知其然,知其所以然。

HTTP协议当然是无状态的,我们在实际场景中是如何区分客户的呢?传统的做法就是利用Cookie来标记用户,用户第一次访问服务器,服务器便在Response Header中添加Set-Cookie,写入标记该用户的唯一标识符,用户二次访问时,将该标识符带回,服务器即可区分该用户是否是之前的用户了。

说到我们本文的主角Cookie,当然不是那么简简单单的事情(事实上是我自己一直以来将其看得过于简单,后来脸被打肿)。

两种方式设置cookie:

  • Set-Cookie(http方式)
    服务端通过在Response Header中添加Set-Cookie,告知客户端如何存储cookie,一般格式:
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>;Max-Age=<non-zero-digit>;Expires=<date>;Path=<path-value>;SameSite=<Strict|Lax>; Secure; HttpOnly
  • document.cookie(代码方式)
    客户端(通常指浏览器)通过javascript直接给document.cookie赋值设置cookie
document.cookie="<cookie-name>=<cookie-value>; Domain=<domain-value>;Max-Age=<non-zero-digit>;Expires=<date>;Path=<path-value>;SameSite=<Strict|Lax>; Secure;"

很明显,除了客户端无法设置HttpOnly外,其他与服务端基本一致。

另外,需要注意的是,书写顺序问题:<cookie-name>=<cookie-value>;必须写在最前面,后面的其他项顺序则可以随意了。这样也就限制了一句设置代码只能设置一个cookie,服务端需要在一次http响应中添加多个set-cookie header来一次性写入多个cookie,客户端则需要调用多次document.cookie赋值才能设置多个cookie。

cookie-name=cookie-value

根据RFC6265,二者可取值情况如下:
– cookie-name 区分大小写,字母、数字以及!#$%&’*+-.^_|`~。
– cookie-value 是可选的。支持字母、数字及!#$%&'()*+-./:<=>?@[]^_{|}`~。关于编码:许多应用会对 cookie 值按照URL编码(URL encoding)规则进行编码,但是按照 RFC 规范,这不是必须的。不过满足规范中对于 cookie-value 所允许使用的字符的要求是有用的。

尽管目前实验了Chrome及Firefox支持更多的字符串,甚至中文,但由于各浏览器、各服务端解析可能存在不一致的情况,还是推荐按标准形式,可以减少古怪问题出现的概率。

  • __Secure-前缀,以 __Secure- 为前缀的 cookie(其中连接符是前缀的一部分),必须与 secure 属性一同设置,同时必须应用于安全页面(即使用 HTTPS 访问的页面)。
  • __Host-前缀,以 __Host- 为前缀的 cookie,必须与 secure 属性一同设置,必须应用于安全页面(即使用 HTTPS 访问的页面),必须不能设置 domain 属性 (也就不会发送给子域),同时 path 属性的值必须为“/”。

__Secure-前缀为例讲解其如何弥补后面即将提到的Secure的不足之处:
由于发往服务端的Cookie只有key-value对,并不包含domain、secure之类的其它信息。当我们用token作为用户标识时,设置domain为www.a.comcookie,并带上了Secure设置,尽管www.a.com为https,但攻击者(中间人)完全可以构造一个非https的网站xxx.a.com诱导用户访问(被劫持的网站,不需要真实存在),然后设置domain为.a.com的cookie,当服务端拿到这两个token时并没法分别哪个是安全的token。
但是我们若不以token作为cookie name,而是使用__Secure-token,攻击者就无计可施了,因为设置__Secure-前缀的cookie必须同时设置Secure,而Secure的设置只能在https链接中,攻击者又无法通过中间人的方式攻击https。

Domain=domain-value

用于设置cookie生效的范围。若不设置该项,浏览器默认将其标记为HostOnly,也就是只有在完全匹配的hostname(注意是hostname,而不是host,也就是说cookie的domain不区分端口)下面方可读取cookie。比如,我在a.com下面设置cookie时没有指定domain,那么我在sub.a.com下面就无法读取到该cookie。

若设置domain,domain的取值只能是当前hostname或父级域,比如,在sub.a.com下面,可选的domain只能是sub.a.com或者a.com,其它取值将会被浏览器忽略。

另外,非常值得注意的是,一些顶级域本来就有两段或多段,比如:.com.cn.usa.gov.edu.cn等等,我们就无法在形如a.com.cn的网站中设置domain为.com.cn的cookie。

除了上面提到的多段顶级域名,一些提供网站服务的第三方平台,比如github.io、sina sae等,提供子域名给各个用户,也就是说w3c.github.ioalibaba.github.io不是同一个网站,那么在w3c.github.io下就不能写domain为.github.io的cookie。

由于上面提到的两种情况存在,Mozilla很早之前就为此建立了一个列表Public Suffix List,专门用于维护顶级域名及类似于github.io这样的第三方网站提供商的域名。据Public Suffix List官网介绍,目前使用该列表的软件包括Firefox、Chrome、IE等主流浏览器及其他一些对域名查询有需求的软件。
如果希望自己的网站添加进该列表,可以来这里提PR(被通过应该需要很多手续)。

Path=path-value

指定一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie 首部。字符 %x2F (“/”) 可以解释为文件目录分隔符,此目录的下级目录也满足匹配的条件(例如,如果 path=/docs,那么 “/docs”, “/docs/Web/” 或者 “/docs/Web/HTTP” 都满足匹配的条件)。
另外,由于domain与path是分开解析的,所以a=1;domain=.a.com;path=/x/y,也能在sub.a.com/x/y下被读取。

Expires=date

date应该是符合<day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT格式的字符串,用于标识cookie的过期时间。若不设置该项(下面的max-age也不设置)则cookie是会话级的,浏览器关闭则清除该cookie。

Max-Age=non-zero-digit

non-zero-digit是只在 cookie 失效之前需要经过的秒数。一位或多位非零(1-9)数字(ps:其实0也可以,只是没意义,浏览器可以实现为直接忽略)。一些老的浏览器(ie6、ie7 和 ie8)不支持这个属性。对于其他浏览器来说,假如二者 (指 Expires 和Max-Age) 均存在,那么 Max-Age 优先级更高。

SameSite=Strict|Lax|Unset(默认)

允许服务器设定一则 cookie 不随着跨域请求一起发送,这样可以在一定程度上防范跨站请求伪造攻击(CSRF)。
– Strict 任何时候都不跨域发送cookie,所以当你发现从a网站进入b网站,b网站的登陆状态总是失效的,然后刷新一下又正常了,不要悲伤、不要诧异,检查下你用于标记登陆状态的cookie是不是设置了samesite为strict
– Lax 点击a标签、form的get请求、通过location改变、通过window.open方式打开时会携带cookie,而ajax、script、link、img、iframe、form的post发起的跨域请求则不会携带cookie
– Unset 默认值,任何时候都会跨域携带cookie
samesite目前属于实验性属性,还未进入标准,目前(2018.10.08)兼容性不是太乐观can i use

Secure

一个带有安全属性的 cookie 只有在请求使用SSL和HTTPS协议的时候才会被发送到服务器,同时无法在非https的页面通过document.cookie读取,这可以有效防范SSl strip后cookie失窃。然而,保密或敏感信息永远不要在 HTTP cookie 中存储或传输,因为整个机制从本质上来说都是不安全的,比如前述协议并不意味着所有的信息都是经过加密的。
新版Chrome与Firefox已经不支持在非https的链接中设置Secure了。

HttpOnly

带此标识的cookie只能以http的方式设置于读取,也就是说,通过document.cookie的方式既不能写入也不能读取带HttpOnly标识的cookie,可有效防止xss的方式窃取cookie。事实上,一般后端web框架的sessionid一般都是设置了HttpOnly的。

第三方Cookie

跨域设置cookie的问题也顺带提一下,我已经在此处跌倒几次没长记性了:

首先现代浏览器,在a.com引入b.com的资源(link、img、script等),是可以正常写入与携带cookie的(ps:老版本的IE需要配置P3P),现在的广告追踪就是利用这个功能。

a.com下面通过ajax请求b.com,由于某些不可描述的原因,b.com会通过http的方式向浏览器写入cookie,未做任何处理的情况下,这种方式并不会正常写入cookie,而需要通过浏览器与服务端双方友好协商,你情我愿的情况下方可成功,流程就是:浏览器端发请求的时候告诉服务端,请同意我跨域带cookie给你,我也同意你跨域向我写cookie,如果这时候服务端应答:我愿意。

那么他们就愉快的牵手永远幸福的生活下去了。。

偏了偏了,差点写成言情小说了。

浏览器通过fetch请求时配置{credentials:'include'},通过XMLHttpRequest请求时配置xhr.withCredentialstrue,然后服务端Response Header里面添加 Access-Control-Allow-Credentials: true就OK了。 当然这也还要注意,在配置CORS相关的Response Header时,若Access-Control-Allow-Credentials的值为trueAccess-Control-Allow-Origin则不能设置为*,一来不安全,二来浏览器会报错。

参考链接: