Cookie 详解

2,131 阅读6分钟

这是我参与更文挑战的第1天,活动详情查看:更文挑战

cookie 简介

HTTP 是无状态的协议,协议本身不保留之前的请求或相应报文的信息,这是为了减少服务器的资源消耗,更快地处理大量事务,确保协议的可伸缩性,使协议能够适用在更多场景。

但是在很多场景下服务端都需要明确请求是来自于哪个用户,期望客户端能够将用户的登录状态等信息告知服务端,于是 HTTP/1.1 引入了 Cookie,实现用户识别跟状态信息的管理。

不同浏览器对 Cookie 数量和大小的限制,是不一样的。一般来说,单个域名设置的 Cookie 不应超过30个,每个 Cookie 的大小不能超过4KB。超过限制以后,Cookie 将被忽略,不会被设置。

Cookie 主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

为 Cookie 服务的 HTTP 首部字段:

  • HTTP 响应,通过 Set-Cookie 字段进行Cookie配置

    HTTP/1.1 200 OK
    ...
    Set-Cookie: name=LvLin;domain=github.com;path=/blog
    Set-Cookie: id=test123;
    
  • 浏览器向服务器发送 HTTP 请求时,会自动带上满足条件的 Cookie(具体条件见下文)

    GET /sample_page.html HTTP/1.1
    Host: www.github.com
    Cookie: name=value; name2=value2; name3=value3
    

可以通过 document.cookie 返回当前网页的 Cookie,也可以进行新 Cookie 的创建,但是 document.cookie 无法创建和访问具有 HttpOnly 标记的 Cookie。

document.cookie // "name=LvLin;id=test123"

document.cookie = "a=1" // 等号两边不能有空格
document.cookie // "a=1; name=LvLin; id=test123"

如果想改变一个已存在的 Cookie(通过 Set-Cookie 或 document.cookie ),需要keydomainpathsecure都匹配,否则会出现两个同名的 Cookie,在发起请求的时候如果满足条件则都会被带上,后端无法区分。

cookie 的属性介绍

name、value

Cookie 的内容信息,以 name=value 的形式进行存储,区分大小写。

domain

domain 属性指定浏览器发出 HTTP 请求时,哪些域名要附带这个 Cookie。

可以设置 domain 为当前服务器域名或者父域名。如果未设置,则该 Cookie 值只能在发向该域名的请求时被携带。如果设置了 domain,则发向该域以及子域的请求都会带上该 Cookie。

由于历史原因,存在前缀点的 domain,有前缀点表示允许子域请求携带该cookie,无前缀点则不允许。目前规范规定忽略前缀点。

path

path 指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie。如果配置的时候设置了path,那么在该path下的子路径都允许携带该 cookie。

Expires、Max-Age

这两个字段用于定义 Cookie 的生命周期。Expires 是一个绝对时间,如果没有设置这个属性或者设置为 null,则表示这是一个Session Cookie。需要注意的是,浏览器是根据本地时间与 Expires 对比判断是否过期,而本地时间是可能变动的,所以无法保证 Cookie 一定会在服务器指定的时间过期。

Max-Age 是一个相对时间,是在 Cookie 失效之前需要经过的秒数。秒数为 0 或 -1 将会使 Cookie 直接过期。

HTTP/1.1 200 OK
...
Set-Cookie: id=test123; Expires=Tue, 01 Jun 2021 11:45:54 GMT;

如果同时指定了 Expires 和 Max-Age ,那么 Max-Age 的值将优先生效。如果都没有指定,那这个 Cookie 就是 Session Cookie,一旦用户关闭浏览器,这个 Cookie 就会被删除。有些浏览器提供了会话恢复功能,这种情况下即使关闭了浏览器,Session Cookie 也会被保留下来,就好像浏览器从来没有关闭一样,这会导致 Cookie 的生命周期无限期延长。

SameSite

SameSite Cookie 允许服务器要求某个 cookie 在跨站请求时不会被发送,从而可以阻止跨站请求伪造攻击(CSRF)。总共有三个值:Strict,Lax,None。原默认为 None,先规定默认为 Lax,为 None 时必须具有 Secure 属性。

三种规则对应的 Cookie 发送情况如下所示:

请求类型实例StrictLaxNone
链接<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

Secure,HttpOnly

Secure 属性限制该 Cookie 只有在 HTTPS 协议下,才会被发送到服务器。若当前协议是 HTTP,则浏览器会忽略服务器发来的 Secure 属性。

HttpOnly 属性指定该 Cookie 无法通过 JavaScript 脚本获取到,防止恶意脚本的攻击。

Cookie 与跨域、安全

在发送跨域 AJAX 请求时,Cookie 默认不会被发送。如果需要允许跨域请求携带 Cookie ,需要在发起请求的时候设置 withCredentials 请求头,服务端需要在请求响应中配置 CORS 相关头部(Allow-cross-with-credentials:true)。

一个比较特殊的情况,如果跨域 AJAX 请求配置了 withCredentials,即使响应没有配置 CORS 相关头部而被浏览器拦截,响应中的 set-cookie 也会生效,可以成功种上 Cookie。感兴趣的同学可以看看小伙伴的经历,详细规定可以参见规范

但是在 img 标签、script 标签的 src 属性发起跨域请求时,请求会携带 Cookie。表单提交跨域请求也会带上 Cookie。CSRF 攻击就是通过这些方式实现的。通过配置 Cookie 的 SameSite 属性,可以防范 CSRF 攻击。而避免 XSS 攻击造成损失,敏感的 Cookie 要设置 secure、httpOnly属性。

推荐两篇前端安全相关的文章:

最后,梳理一下请求会携带 Cookie 的条件

  • domain 跟 path 属性与请求的域名路径匹配,Cookie 才会被携带
  • 如果有 Secure 属性,请求协议必须是 HTTPS
  • 跨域 AJAX 请求不会携带 Cookie,如果需要携带需要配置 withCredentials 请求头以及 CORS 响应头
  • 如果请求跨站了,还要判断 SameSite 属性是否满足发送条件

参考

浏览器系列之 Cookie 和 SameSite 属性, by 冴羽

HTTP cookies, by MDN

Cookie, by 阮一峰