问:你知道 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
来实现个性化的用户体验,如记住用户的登录状态、购物车内容、语言偏好等等。
cookie 的工作流程
上面已经提及了 HTTP
是无状态的。我们在浏览平常的新闻的时候,无需认证,但是,我们在新闻下评论,那就需要认证了。上图给出了简单的 cookie
运行机制的介绍。简单归总如下:
- 浏览器发起一个
HTTP
请求,比如用户账号/密码登陆 - 服务器端,对用户账号密码进行验证,验证用户通过后,将用户的信息封装成
cookie
,比如:ctx.cookies.set('userId', '123456')
。然后把设置的cookie
信息通过HTTP
响应返回给浏览器 - 浏览器接收到返回的
cookie
信息,并将其保存在内存或者硬盘中。然后之后的每次HTTP
请求都会带上用户的cookie
信息,比如userId=123456
- 服务端获取到
cookie
信息,解析了cookie
,获取到用户的信息,这里指userId=123456
,然后返回相关的用户信息
一般来说,具有过期时间的
cookie
存储在硬盘中,方便浏览器关闭后仍然保存;而会话cookie
存储在内存中,随着浏览器关闭而被删除。
演示
下面,我们来演示如何设置 cookie
。
案例的演示环境:
macOS Monterey
- Apple M1node version
- v14.18.1Visual Studio Code
及其Live Server
插件
我们已经了解了 cookie
的工作流程,下面会分同源和跨源来展示案例。
首先,我们添加个 hostname
, 方便测试,当然你可以直接使用 ip
地址测试。
通过 sudo vim /etc/hosts
添加 127.0.0.1 a.example.com
的映射:
同源案例
这里我们使用了 Koa
框架开发服务端,为了方便管理路由,我们引入 koa-router
库,代码如下:
// 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
模拟我们登陆,假设验证了用户/密码,然后设定 username
的 cookie
信息,并返回信息;路由 /api/same_origin_another_request
模拟登陆后,获取指定用户的资源信息(验证是否带上了 cookie 信息发送到服务端)。
我们通过执行 node index.js
运行程序。
通过浏览器,我们访问链接 http://a.example.com/api/same_origin_request
:
此时 cookie
信息会自动写入 username=same_origin_jimmy
。我们通过面板 Application -> Cookies -> http://a.example.com
查看到相关 cookie
信息:
我们可以看到地址 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
属性才对。验证如下图:
跨域案例
OK
!我们参考上篇文章 - 【案例】同源策略 - CORS 处理 处理里跨域问题。
我们设置简单网页代码:
<!-- 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
)不是通配符(*
),而是明确指定的域名。这是出于安全性考虑,以防止凭证信息泄露给不受信任的域名。
服务端的代码设置如下:
// 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
,但是浏览器并不会存储它,如下图:
当在该模拟登陆的接口 /api/cross_origin_request
中添加了 credentials: 'include'
,则浏览器会保存 cookie
在内存或者硬盘中。 credentials: 'include'
指示浏览器在跨域请求中包含凭证。
上面服务端的代码中,我们还添加了一个模拟登陆后发起的请求 http://a.example.com/api/cross_origin_another_request
接口。
细心的读者会发现,两个请求的地址源不一样
http://a.example.com
和http://a.example.com
。这样做只是想验证下另外一个域名是否会存储cookie
而已。
另一个站点的代码如下:
<!-- 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
信息。如下图:
我们触发页面 另一个请求
按钮,发现请求头中,自动带上了 cookie
信息:
cookie 的缺点
上面,我们讲了很多 cookie
的好处,比如用户认证。那么,cookie
有什么缺点呢?
- 存储限制:
cookie
只能存储有限的数据量。如果一个站点设置了过多或者过大的cookie
,可能导致浏览器性能下降或者无法正常工作。主流浏览器对同一域名下的cookie
限制在几百到一千之间;对其大小通常在几KB
到几十KB
之间,见上表格。 - 隐私问题:
cookie
是明文存储在用户浏览器上。因此容易被直接恶意读取,尤其是敏感信息。 - 安全问题:因为
cookie
是在客户端浏览器上存储
,所以容易受到网络攻击。比如跨站脚本攻击(XSS
)和跨站请求伪造(CSRF
)。黑客可能利用这些漏洞来获取用户的Cookie
信息,冒充用户进行非法操作。关于XSS
和CSRF
后面会有一篇文章探讨。 - 用户操控 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 属性
- 图片来源网络,侵删