【案例】HTTP Cookie 的运行机制

问:你知道 cookie ?

答:很好吃的饼干🍪,我很喜欢。

pexels-sara-santos-1020585.jpg

呀呀呀~ 此 cookie 非彼 cookie

accept_cookie.png

cookie 是什么

HTTP(Hypertext Transfer protocol,超文本传输协议) 有一个很重要的特点:

无状态性:这也就是说每个请求都是独立的,服务器不会记住之前的请求状态。随着互联网的发展,交互式 Web 兴起,而 HTTP 无状态的特点严重影响其发展。

交互式 Web:客户端与服务器可以交互,比如用户登陆,购物,论坛等

网景公司(Netscape) 当时一名员工 Lou Montulli(卢-蒙特利),在1994年将 cookies 的概念应用于网络通信,用来解决用户网上购物的购物车历史记录问题。到目前为止,所有浏览器都支持 Cookie

Cookies_Founder-300x200.png

这里的 cookie,指的就是 HTTP Cookie(也叫做 Web Cookie 或者浏览器 Cookie)。Cookie服务端发送用户浏览器保存在浏览器本地的一小块数据

浏览器会存储 Cookie 并在下一次向同一个服务器再发起请求时携带并发送到服务器上。 Cookie 通过用户的浏览器在服务器和浏览器之间传递。

Cookie 通常包含了一些键值对,用于标识用户和存储相关的信息。

are_you_visit_one_site.png

Cookie 的作用是在用户访问同一网站或者相关网站时,用于认证用户、追踪用户行为,存储用户偏好设置等。网站可以通过读取和写入 Cookie 来实现个性化的用户体验,如记住用户的登录状态、购物车内容、语言偏好等等。

cookie 的工作流程

cookie 原理图.png

上面已经提及了 HTTP 是无状态的。我们在浏览平常的新闻的时候,无需认证,但是,我们在新闻下评论,那就需要认证了。上图给出了简单的 cookie 运行机制的介绍。简单归总如下:

  1. 浏览器发起一个 HTTP 请求,比如用户账号/密码登陆
  2. 服务器端,对用户账号密码进行验证,验证用户通过后,将用户的信息封装成 cookie,比如:ctx.cookies.set('userId', '123456')。然后把设置的 cookie 信息通过 HTTP 响应返回给浏览器
  3. 浏览器接收到返回的 cookie 信息,并将其保存在内存或者硬盘中。然后之后的每次 HTTP 请求都会带上用户的 cookie 信息,比如 userId=123456
  4. 服务端获取到 cookie 信息,解析了 cookie ,获取到用户的信息,这里指 userId=123456,然后返回相关的用户信息

一般来说,具有过期时间的 cookie 存储在硬盘中,方便浏览器关闭后仍然保存;而会话 cookie 存储在内存中,随着浏览器关闭而被删除。

演示

下面,我们来演示如何设置 cookie

案例的演示环境: macOS Monterey - Apple M1 node version - v14.18.1 Visual Studio Code 及其 Live Server 插件

我们已经了解了 cookie 的工作流程,下面会分同源和跨源来展示案例。

首先,我们添加个 hostname, 方便测试,当然你可以直接使用 ip 地址测试。

通过 sudo vim /etc/hosts 添加 127.0.0.1 a.example.com 的映射:

hosts添加映射.png
同源案例

这里我们使用了 Koa 框架开发服务端,为了方便管理路由,我们引入 koa-router 库,代码如下:

代码语言:javascript
复制
// index.js
const Koa = require('koa');
const app = new Koa();

const Router = require('koa-router');
const router = new Router();

// 模拟登陆
router.get('/api/same_origin_request', async (ctx, next) => {
ctx.cookies.set('username', 'same-origin-jimmy');
ctx.response.body = {
message: 'Hello! Jimmy.'
}
});
// 模拟登陆后的请求
router.get('/api/same_origin_another_request', async (ctx, next) => {
ctx.response.body = {
message: 'Hello! Ivy.'
}
});

app.use(router.routes());
app.listen(3000, () => {
console.log("Server is running on port 3000");
})

上面,我们编写了两个路由,路由 /api/same_origin_request 模拟我们登陆,假设验证了用户/密码,然后设定 usernamecookie 信息,并返回信息;路由 /api/same_origin_another_request 模拟登陆后,获取指定用户的资源信息(验证是否带上了 cookie 信息发送到服务端)。

我们通过执行 node index.js 运行程序。

jimmy_node_index.png

通过浏览器,我们访问链接 http://a.example.com/api/same_origin_request

simulate_login_cookie.png

此时 cookie 信息会自动写入 username=same_origin_jimmy。我们通过面板 Application -> Cookies -> http://a.example.com 查看到相关 cookie 信息:

cookie_infomation.png

我们可以看到地址 http://a.example.com 下面保存的 cookie 信息,它们有很多字段,是什么意思呢?

字段

含义

Name

cookie 的名称

Value

储存在 cookie 中的数据值

Domain

cookie 在哪个域名下创建的,默认是同一 host,如果指定了域名,则包含子域名,比如 Domain=example.com,则 cookie 也包含在子域名中(比如:a.example.com)

Path

指定哪些路径下的请求才会发送相应的 cookie。举例:以 / 为路径分隔符,其子路径也会被匹配

Expires / Max-Age

cookie 的过期时间,Expires 是绝对时间,Max-Age 是相对时间,两者的参照时间点是客户端的时间。

Size

表示 Cookie 的大小。见下

HttpOnly

限制客户端脚本对 cookie 访问。提高安全性。

Secure

标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端。它永远不会使用不安全的 HTTP 发送(本地主机除外)。

SameSite

允许服务器指定是否/何时通过跨站点请求发送。可能的值有:Strict - cookie 仅发送到它来源的站点; Lax - 与 Strict 相似,只是在用户导航到 cookie 的源站点时发送 cookie,Lax 是默认值;None 指定浏览器会在同源请求和跨域请求下继续发送 cookie,但仅在安全的上下文(即,如果 SameSite=None,且必须设置 Secure 属性)

Partition Key

用于将一个网站的 cookie 划分为多个分区。

Priority

Chrome 独有,与 cookie 的删除策略有关

Size 的支持数据来源网络

浏览器

Cookie最大条数

Cookie最大长度/单位:字节

IE

50

4095

Chrome

150

4096

FireFox

50

4097

Opera

30

4096

Safari

无限

4097

好了,我们简单了解了 cookie 的相关参数说明。我们现在通过浏览器打开同源网站的另一个 url 请求 - http://a.example.com:3000/api/same_origin_another_request。这个时候,应该在 Request Headers 中带上 cookie 属性才对。验证如下图:

同源——另一个接口.png
跨域案例

OK!我们参考上篇文章 - 【案例】同源策略 - CORS 处理 处理里跨域问题。

我们设置简单网页代码:

代码语言:javascript
复制
<!-- demo/index.html -->
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>cookie 信息</title>
</head>
<body>
  <button id="trigger">请求接口</button>

<script>
(function() {
document.getElementById("trigger").addEventListener("click", function() {
fetch('http://a.example.com', {
method: 'GET',
credentials: 'include', // 指示浏览器在跨域请求中包含凭证
})
})
})()
</script>
</body>
</html>

上面我们添加了按钮 请求接口,点击该接口,触发请求。该 fetch 请求中,需要留意 credentials: 'include:它指示浏览器在跨域请求中包含凭证,例如 cookie 信息。

credentials 有值如下:

含义

same-origin

只在同源请求中包含凭证信息,为默认值。

include

在跨域请求中包含凭证信息。需要确保目标服务器明确允许跨域请求的凭证信息。

omit

忽略凭证信息。无论是同源请求还是跨域请求,在请求中都不包含凭证信息。

使用 credentials: 'include' 选项时,要确保在发送跨域请求时的源(Origin)不是通配符(*),而是明确指定的域名。这是出于安全性考虑,以防止凭证信息泄露给不受信任的域名。

服务端的代码设置如下:

代码语言:javascript
复制
// index.js
const Koa = require('koa');
const app = new Koa();

const Router = require('koa-router');
const router = new Router();

// 允许跨域白名单
const originArray = [
'http://a.example.com',
'http://a.example.com'
];

// 测试跨源
router.get('/api/cross_origin_request', async (ctx, next) => {
const { origin } = ctx.request.header;
ctx.set('Access-Control-Allow-Origin', originArray.includes(origin) ? origin : null);
// 允许发送凭证信息(如 cookie)
ctx.set('Access-Control-Allow-Credentials', 'true');
ctx.cookies.set('username', 'cross_origin_jimmy');
ctx.response.body = {
message: 'Hello! Jimmy.'
}
});
router.get('/api/cross_origin_another_request', async (ctx, next) => {
const { origin } = ctx.request.header;
ctx.set('Access-Control-Allow-Origin', originArray.includes(origin) ? origin : null);
ctx.set('Access-Control-Allow-Credentials', 'true');
ctx.response.body = {
message: 'Hello! Ivy.'
}
})

app.use(router.routes());

app.listen(3000, () => {
console.log("Server is running on port 3000");
})

上面代码中,我们通过 http://a.example.com/api/cross_origin_request 接口模拟了用户登陆并设置了允许跨域中携带凭证 Access-Control-Allow-Credentials,然后设置了返回的 cookie 信息。

demo/index.html 文件发起的模拟登陆请求中,缺少 credentials: 'include',在跨域中,虽然请求在 Response Headers 上返回的 cookie,但是浏览器并不会存储它,如下图:

缺少credentials_include.png

当在该模拟登陆的接口 /api/cross_origin_request 中添加了 credentials: 'include',则浏览器会保存 cookie 在内存或者硬盘中。 credentials: 'include' 指示浏览器在跨域请求中包含凭证。

上面服务端的代码中,我们还添加了一个模拟登陆后发起的请求 http://a.example.com/api/cross_origin_another_request 接口。

细心的读者会发现,两个请求的地址源不一样 http://a.example.comhttp://a.example.com。这样做只是想验证下另外一个域名是否会存储 cookie 而已。

另一个站点的代码如下:

代码语言:javascript
复制
<!-- another_demo/index.html -->

<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Another site for cookie</title>
</head>
<body>
<button id="trigger">另一个请求</button>
<script>
(function() {
document.getElementById("trigger").addEventListener("click", function() {
fetch('http://a.example.com/api/cross_origin_another_request', {
method: 'GET',
credentials: 'include'
})
})

})()

</script>
</body>
</html>

当我们自动打开该网页 http://a.example.com/,在 Application -> Cookies -> http://a.example.com 下看到写入的 cookie 信息。如下图:

another_demo_username_cookie.png

我们触发页面 另一个请求 按钮,发现请求头中,自动带上了 cookie 信息:

another_request_bring_cookie.png

cookie 的缺点

上面,我们讲了很多 cookie 的好处,比如用户认证。那么,cookie 有什么缺点呢?

  • 存储限制cookie 只能存储有限的数据量。如果一个站点设置了过多或者过大的 cookie,可能导致浏览器性能下降或者无法正常工作。主流浏览器对同一域名下的 cookie 限制在几百到一千之间;对其大小通常在几 KB 到几十 KB 之间,见上表格。
  • 隐私问题cookie 是明文存储在用户浏览器上。因此容易被直接恶意读取,尤其是敏感信息。
  • 安全问题:因为 cookie 是在客户端浏览器上存储,所以容易受到网络攻击。比如跨站脚本攻击(XSS)和跨站请求伪造(CSRF)。黑客可能利用这些漏洞来获取用户的 Cookie 信息,冒充用户进行非法操作。关于 XSSCSRF 后面会有一篇文章探讨。
  • 用户操控 cookie:虽然用户可以通过浏览器管理 cookie,但是他们可能没有意识到自己的行为会留下或者删掉 cookie

替代方案可有:session, localStorage 等,这里不展开探讨。

参考

  • HTTP Cookie
  • 一文带你超详细了解Cookie
  • Cookies - What are they and why you should care
  • What Are Cookies? And How They Work | Explained for Beginners!
  • Cookie 起源与发展
  • Cookie 的 SameSite 属性
  • 图片来源网络,侵删