2023年11月25日发(作者:)
【浏览器】Cookie详解
Cookie是由服务器端⽣成,发送给User-Agent,浏览器会将Cookie的key/value保存到某个⽬录下的⽂本⽂件内,下次请求同⼀⽹
站时就发送该Cookie给服务器。
Cookie的诞⽣
由于HTTP协议是⽆状态的,⽽服务器端的业务必须是要有状态的。Cookie诞⽣的最初⽬的是为了存储web中的状态信息,以⽅便服务器端
使⽤。⽐如判断⽤户是否是第⼀次访问⽹站。⽬前最新的规范是RFC 6265,它是⼀个由浏览器服务器共同协作实现的规范。
Cookie的处理分为:
服务器向客户端发送cookie
浏览器将cookie保存
之后每次http请求浏览器都会将cookie发送给服务器端
1、服务器端的发送与解析
1.1、发送cookie
服务器端像客户端发送Cookie是通过HTTP响应报⽂实现的,在Set-Cookie中设置需要像客户端发送的cookie,cookie格式如下:
Set-Cookie: "name=value;domain=.;path=/;expires=Sat, 11 Jun 2016 11:29:42 GMT;HttpOnly;secure"
其中name=value是必选项,其它都是可选项。
Cookie的主要构成如下:
name:⼀个唯⼀确定的cookie名称。通常来讲cookie的名称是不区分⼤⼩写的。
value:存储在cookie中的字符串值。最好为cookie的name和value进⾏url编码
domain:cookie对于哪个域是有效的。所有向该域发送的请求中都会包含这个cookie信息。这个值可以包含⼦域(如:),
也可以不包含它(如:.,则对于的所有⼦域都有效).
path: 表⽰这个cookie影响到的路径,浏览器跟会根据这项配置,像指定域中匹配的路径发送cookie。
expires:失效时间,表⽰cookie何时应该被删除的时间戳(也就是,何时应该停⽌向服务器发送这个cookie)。如果不设置这个时间戳,
浏览器会在页⾯关闭时即将删除所有cookie;不过也可以⾃⼰设置删除时间。这个值是GMT时间格式,如果客户端和服务器端时间不
⼀致,使⽤expires就会存在偏差。
max-age: 与expires作⽤相同,⽤来告诉浏览器此cookie多久过期(单位是秒),⽽不是⼀个固定的时间点。正常情况下,max-age的
优先级⾼于expires。
HttpOnly: 告知浏览器不允许通过脚本去更改这个值,同样这个值在中也不可见。但在http请求中仍
然会携带这个cookie。注意这个值虽然在脚本中不可获取,但仍然在浏览器安装⽬录中以⽂件形式存在。这项设置通常在服务器端设
置。
secure: 安全标志,指定后,只有在使⽤SSL链接时候才能发送到服务器,如果是http链接则不会传递该信息。就算设置了secure 属性
也并不代表他⼈不能看到你机器本地保存的 cookie 信息,所以不要把重要信息放cookie就对了,服务器端设置
cookie⽰例如下:
var http = require('http');
var fs = require('fs');
Server(function(req, res) {
der('status', '200 OK');
der('Set-Cookie', 'isVisit=true;domain=.;path=/;max-age=1000');
('Hello World');
();
}).listen(8888);
('running localhost:8888')
直接设置Set-Cookie过于原始,我们可以对cookie的设置过程做如下封装:
var serilize = function(name, val, options) {
if (!name) {
throw new Error("coolie must have name");
}
var enc = encodeURIComponent;
var parts = [];
val = (val !== null && val !== undefined) ? ng() : "";
options = options || {};
(enc(name) + "=" + enc(val));
// domain中必须包含两个点号
if () {
("domain=" + );
}
if () {
("path=" + );
}
// 如果不设置expires和max-age浏览器会在页⾯关闭时清空cookie
if (s) {
("expires=" + tring());
}
if ( && typeof === "number") {
("max-age=" + );
}
if (ly) {
("HTTPOnly");
}
if () {
("secure");
}
return (";");
}
需要注意的是,如果给cookie设置⼀个过去的时间,浏览器会⽴即删除该cookie;此外domain项必须有两个点,因此不能设置为localhost。
1.2、服务器端解析cookie
cookie可以设置不同的域与路径,所以对于同⼀个name value,在不同域不同路径下是可以重复的,浏览器会按照与当前请求url或页⾯地址
最佳匹配的顺序来排定先后顺序
所以当前端传递到服务器端的cookie有多个重复name value时,我们只需要最匹配的那个,也就是第⼀个。服务器端解析代码如下:
var parse = function(cstr) {
if (!cstr) {
return null;
}
var dec = decodeURIComponent;
var cookies = {};
var parts = (/s*;s*/g);
h(function(p){
var pos = f('=');
// name 与value存⼊cookie之前,必须经过编码
var name = pos > -1 ? dec((0, pos)) : p;
var val = pos > -1 ? dec((pos + 1)) : null;
//只需要拿到最匹配的那个
if (!Property(name)) {
cookies[name] = val;
}/* else if (!cookies[name] instanceof Array) {
cookies[name] = [cookies[name]].push(val);
} else {
cookies[name].push(val);
}*/
});
return cookies;
}
2、客户端的存取
浏览器将后台传递过来的cookie进⾏管理,并且允许开发者在JavaScript中使⽤来存取cookie。但是这个接⼝使⽤起来⾮常
蹩脚。它会因为使⽤它的⽅式不同⽽表现出不同的⾏为。
当⽤来获取属性值时,返回当前页⾯可⽤的(根据cookie的域、路径、失效时间和安全设置)所有的字符串,字符串的格
式如下:
"name1=value1;name2=value2;name3=value3"
当⽤来设置值的时候,属性可设置为⼀个新的cookie字符串。这个字符串会被解释并添加到现有的cookie集合中。如:
= "_fa=aaaffffasdsf;domain=.;path=/"
设置并不会覆盖cookie,除⾮设置的name value domain path都与⼀个已存在cookie重复。
由于cookie的读写⾮常不⽅便,我们可以⾃⼰封装⼀些函数来处理cookie,主要是针对cookie的添加、修改、删除。
var cookieUtils = {
get: function(name){
var cookieName=encodeURIComponent(name) + "=";
//只取得最匹配的name,value
var cookieStart = f(cookieName);
var cookieValue = null;
if (cookieStart > -1) {
// 从cookieStart算起
var cookieEnd = f(';', cookieStart);
//从=后⾯开始
if (cookieEnd > -1) {
cookieValue = decodeURIComponent(ing(cookieStart + , cookieEnd));
} else {
cookieValue = decodeURIComponent(ing(cookieStart + , ));
}
}
return cookieValue;
},
set: function(name, val, options) {
if (!name) {
throw new Error("coolie must have name");
}
var enc = encodeURIComponent;
var parts = [];
val = (val !== null && val !== undefined) ? ng() : "";
options = options || {};
(enc(name) + "=" + enc(val));
// domain中必须包含两个点号
if () {
("domain=" + );
}
if () {
("path=" + );
}
// 如果不设置expires和max-age浏览器会在页⾯关闭时清空cookie
if (s) {
("expires=" + tring());
}
if ( && typeof === "number") {
("max-age=" + );
}
if (ly) {
("HTTPOnly");
}
if () {
("secure");
}
= (";");
},
delete: function(name, options) {
s = new Date(0);// 设置为过去⽇期
this.set(name, null, options);
}
}
缓存优点
通常所说的Web缓存指的是可以⾃动保存常见http请求副本的http设备。对于前端开发者来说,浏览器充当了重要⾓⾊。除此外常见的还有
中的Cache⽂件夹和Media Cache⽂件夹。
公有缓存
公有缓存是特殊的共享代理服务器,被称为缓存代理服务器或代理缓存(反向代理的⼀种⽤途)。公有缓存会接受来⾃多个⽤户的访问,所
以通过它能够更好的减少冗余流量。
下图中每个客户端都会重复的向服务器访问⼀个资源(此时还不在私有缓存中),这样它会多次访问服务器,增加服务器压⼒。⽽使⽤共享
的公有缓存时,缓存只需要从服务器取⼀次,以后不⽤再经过服务器,能够显著减轻服务器压⼒。
事实上在实际应⽤中通常采⽤层次化的公有缓存,基本思想是在靠近客户端的地⽅使⽤⼩型廉价缓存,⽽更⾼层次中,则逐步采⽤更⼤、功
能更强的缓存在装载多⽤户共享的资源。
缓存处理流程
⽽对于前端开发者来说,我们主要跟浏览器中的缓存打交道,所以上图流程简化为:
下⾯这张图展⽰了某⼀⽹站,对不同资源的请求结果,其中可以看到有的资源直接从缓存中读取,有的资源跟服务器进⾏了再验证,有的资
源重新从服务器端获取。
注意,我们讨论的所有关于缓存资源的问题,都仅仅针对GET请求。⽽对于POST, DELETE, PUT这类⾏为性操作通常不做任何缓存
新鲜度限值
HTTP通过缓存将服务器资源的副本保留⼀段时间,这段时间称为新鲜度限值。这在⼀段时间内请求相同资源不会再通过服务器。HTTP协
议中Cache-Control和 Expires可以⽤来设置新鲜度的限值,前者是HTTP1.1中新增的响应头,后者是HTTP1.0中的响应头。⼆者所做的事时
都是相同的,但由于Cache-Control使⽤的是相对时间,⽽Expires可能存在客户端与服务器端时间不⼀样的问题,所以我们更倾向于选择
Cache-Control。
Cache-Control
下⾯我们来看看Cache-Control都可以设置哪些属性值:
max-age(单位为s)指定设置缓存最⼤的有效时间,定义的是时间长短。当浏览器向服务器发送请求后,在max-age这段时间⾥浏览器就不
会再向服务器发送请求了。
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta http-equiv="X-UA-Compatible" content="IE=EDGE" />
<title>Web Cachetitle>
<link rel="shortcut icon" href="./">
<script>
script>
head>
<body class="claro">
<img src="./">
body>
html>
var http = require('http');
var fs = require('fs');
Server(function(req, res) {
if ( === '/' || === '' || === '/') {
le('./', function(err, file) {
()
//对主⽂档设置缓存,⽆效果
der('Cache-Control', "no-cache, max-age=" + 5);
der('Content-Type', 'text/html');
ead('200', "OK");
(file);
});
}
if ( === '/') {
le('./', function(err, file) {
der('Cache-Control', "max-age=" + 5);//缓存五秒
der('Content-Type', 'images/png');
ead('200', "Not Modified");
(file);
});
}
}).listen(8888)
当在5秒内第⼆次访问页⾯时,浏览器会直接从缓存中取得资源
public 指定响应可以在代理缓存中被缓存,于是可以被多⽤户共享。如果没有明确指定private,则默认为public。
private 响应只能在私有缓存中被缓存,不能放在代理缓存上。对⼀些⽤户信息敏感的资源,通常需要设置为private。
no-cache 表⽰必须先与服务器确认资源是否被更改过(依靠If-None-Match和Etag),然后再决定是否使⽤本地缓存。
如果上⽂中关于的处理改成下⾯这样,则每次访问页⾯,浏览器都需要先去服务器端验证资源有没有被更改。
le('./', function(err, file) {
(s);
()
if (!s['if-none-match']) {
der('Cache-Control', "no-cache, max-age=" + 5);
der('Content-Type', 'images/png');
der('Etag', "ffff");
ead('200', "Not Modified");
(file);
} else {
if (s['if-none-match'] === 'ffff') {
ead('304', "Not Modified");
();
客户端的新鲜度限值
Cache-Control不仅仅可以在响应头中设置,还可以在请求头中设置。浏览器通过请求头中设置Cache-Control可以决定是否从缓存中读取资
源。这也是为什么有时候点击浏览器刷新按钮和在地址栏回车,在NetWork模块中看到完全不同的结果
Expires
不推荐使⽤Expires,它指定的是具体的过期⽇期⽽不是秒数。因为很多服务器跟客户端存在时钟不⼀致的情况,所以最好还是使⽤Cache-
Control.
服务器再验证
浏览器或代理缓存中缓存的资源过期了,并不意味着它和原始服务器上的资源有实际的差异,仅仅意味着到了要进⾏核对的时间了。这种情
况被称为服务器再验证。
如果资源发⽣变化,则需要取得新的资源,并在缓存中替换旧资源。
如果资源没有发⽣变化,缓存只需要获取新的响应头,和⼀个新的过期时间,对缓存中的资源过期时间进⾏更新即可。
HTTP1.1推荐使⽤的验证⽅式是If-None-Match/Etag,在HTTP1.0中则使⽤If-Modified-Since/Last-Modified。
Etag与If-None-Match
根据实体内容⽣成⼀段hash字符串,标识资源的状态,由服务端产⽣。浏览器会将这串字符串传回服务器,验证资源是否已经修改,如果没
有修改,过程如下(图⽚来⾃浅谈Web缓存):
上⽂的demo中我们见到过服务器端如何验证Etag:
由于Etag有服务器构造,所以在集群环境中⼀定要保证Etag的唯⼀性
If-Modified-Since与Last-Modified
这两个是HTTP1.0中⽤来验证资源是否过期的请求/响应头,这两个头部都是⽇期,验证过程与Etag类似,这⾥不详细介绍。使⽤这两个头
部来验证资源是否更新时,存在以下问题:
有些⽂档资源周期性的被重写,但实际内容没有改变。此时⽂件元数据中会显⽰⽂件最近的修改⽇期与If-Modified-Since不相同,导致不必
要的响应。
有些⽂档资源被修改了,但修改内容并不重要,不需要所有的缓存都更新(⽐如代码注释)
关于缓存的更新问题,请⼤家看看这⾥张云龙的回答,本⽂就不详细展开了。
本⽂demo代码如下:
DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta http-equiv="X-UA-Compatible" content="IE=EDGE" /> <title>Web Cachetitle> <link rel="shortcut icon" href="./"> <script> script> head> <body class="claro"> <img src="./"> body> html> var http = require('http'); var fs = require('fs'); Server(function(req, res) { if ( === '/' || === '' || === '/') { le('./', function(err, file) { () //对主⽂档设置缓存,⽆效果 der('Cache-Control', "no-cache, max-age=" + 5); der('Content-Type', 'text/html'); ead('200', "OK"); (file); }); } if ( === '/') { le('./', function(err, file) { () der('Content-Type', 'images/png'); ead('200', "OK"); (file); }) } if ( === '/') { le('./', function(err, file) { (s); () if (!s['if-none-match']) { der('Cache-Control', "max-age=" + 5); der('Content-Type', 'images/png'); der('Etag', "ffff"); } else {
发布评论