【案例】HTTP Cookie 的运行机制

问:你知道 cookie ?

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

呀呀呀~ 此 cookie 非彼 cookie

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

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

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

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

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

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

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

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

上面已经提及了 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 的映射:

同源案例

这里我们使用了 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 运行程序。

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

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

我们可以看到地址 http://a.example.com:3000 下面保存的 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 标记为 SecureCookie 只应通过被 HTTPS 协议加密过的请求发送给服务端。它永远不会使用不安全的 HTTP 发送(本地主机除外)。
SameSite 允许服务器指定是否/何时通过跨站点请求发送。可能的值有:Strict - cookie 仅发送到它来源的站点; Lax - 与 Strict 相似,只是在用户导航到 cookie 的源站点时发送 cookieLax 是默认值;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 属性才对。验证如下图:

跨域案例

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

我们设置简单网页代码:

html 复制代码
<!-- 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:3000', {
          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:5500',
  'http://a.example.com:5501'
];

// 测试跨源
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:5500/api/cross_origin_request 接口模拟了用户登陆并设置了允许跨域中携带凭证 Access-Control-Allow-Credentials,然后设置了返回的 cookie 信息。

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

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

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

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

另一个站点的代码如下:

html 复制代码
<!-- 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:3000/api/cross_origin_another_request', {
          method: 'GET',
          credentials: 'include'
        })
      })
      
    })()
  </script>
</body>
</html>

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

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

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

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

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

参考

相关推荐
uzong2 小时前
技术故障复盘模版
后端
GetcharZp2 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
加班是不可能的,除非双倍日工资2 小时前
css预编译器实现星空背景图
前端·css·vue3
桦说编程2 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研2 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi3 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip3 小时前
vite和webpack打包结构控制
前端·javascript
excel4 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国4 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼4 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin