目录
参考:https://www.zhihu.com/question/353373715/answer/974583248
作者官网:编程理想国
一、session
session的中文翻译是“会话”,当用户打开某个web应用时,便与web服务器产生一次session。服务器使用session把用户的信息临时保存在了服务器上,用户离开网站后session会被销毁。这种用户信息存储方式相对cookie来说更安全,可是session有一个缺陷:如果web服务器做了负载均衡,那么下一个操作请求到了另一台服务器的时候session会丢失。
认识 Session
Session 机制准确来说,也是通过 K-V 数据格式来保存状态。其中:
- Key:也称 SessionID,保存在客户端浏览器。
- Value:也称“Session”,保存在服务端。
可以看到,客户端只需要存储 SessionID。具体映射的数据结构放在了服务端,因此跳出了浏览器 cookie 只可以存储 string 类型的限制。
而客户端存储 SessionID,还是需要借助 cookie 来实现。
整体流程
假设/login
接口登陆成功后,服务器可以生成 sessionId 和 session。其中,session 中保存了过期时间,一些冗余信息等。代码如下:
router.get("/login", async (ctx, next) => {
const { user, pwd } = querystring.parse(ctx.request.search.slice(1));
// mock数据,模拟一下登陆过程
if (user === "test" && pwd === "123456") {
// 生成客户端存储的sessinId
const sessionId = crypto
.createHash("md5")
.update(user + pwd)
.digest("hex");
// 生成服务端存储的session
const session = {
expire: Date.now() + 1000 * 60 * 60 * 24, // 过期时间
info: {
// 保存的信息
name: user
}
};
sessions.set(sessionId, session);
ctx.cookies.set("sessionId", sessionId);
ctx.response.body = "登陆成功";
} else {
ctx.response.body = "登陆失败";
ctx.response.status = 401;
}
});
然后客户端在 cookies 中携带 sessionId,访问/userInfo
接口,获得用户信息。服务端检查 sessionId 合法性,以及是否过期。代码如下:
router.get("/userInfo", async (ctx, next) => {
const sessionId = ctx.cookies.get("sessionId");
const session = sessions.get(sessionId);
// 如果sessionId不存在
if (!session) {
ctx.response.body = "无法识别身份";
ctx.response.status = 401;
return;
}
// session过期
if (session.expire <= Date.now()) {
ctx.response.body = "session过期,请重新登陆";
ctx.response.status = 401;
return;
}
ctx.response.body = session.info;
});
打开 chrome dev tools,我们可以看到,http 请求自动携带了 cookie 中的 sessionId。并且通过后端检验,拿到了用户信息。
Jsessionid只是tomcat对sessionid的叫法,其实就是sessionid;其他容器也许就不叫jsessionid了。
session的生命周期
web容器生成一个session_id来标识用户,session还在有效期就不会重新生成。客户端清空cookie,session_id就会丢失。
当你第一次访问一个网站的时候,服务器会向浏览器发送一个sessionid,浏览器得到这个sessionid会将他放在进程内存里(不保存在硬盘cookie中),当关闭浏览器时,进程关闭,浏览器的sessionid 消失。
浏览器的清除缓存机制是只清除硬盘内Cookie文件,而不会清除内存中的Cookie。
总结:比较 cookie 与 session
session 传输数据少,数据结构灵活:相较于 cookie 来说,session 存储在服务端,客户端仅保留换取 session 的用户凭证(session_id)。因此传输数据量小,速度快。
session 更安全:检验、生成、验证都是在服务端按照指定规则完成,而 cookie 可能被客户端通过 js 代码篡改。
session 的不足:服务器是有状态的。多台后端服务器(做负载均衡时会部署多台服务器)无法共享 session。解决方法是,专门准备一台 session 服务器,关于 session 的所有操作都交给它来调用。而服务器之间的调用,可以走内网 ip,走 RPC 调用(不走 http)。
二、cookie
cookie是保存在本地终端的数据。cookie由服务器生成,发送给浏览器,浏览器把cookie以kv形式保存到某个目录下的文本文件内,下一次请求同一网站时会把该cookie发送给服务器。
cookie的组成有:名称(key)、值(value)、有效域(domain)、路径(域的路径,一般设置为全局:"\")、失效时间、安全标志(指定后,cookie只有在使用SSL连接时才发送到服务器(https))。
认识 Cookie
cookie 是以 K-V 形式,存储在浏览器中一种数据。它可以在服务端设置,也可以在浏览器端用 js 代码设置。它拥有 maxAge、domain、path 等属性,借助这些属性,可以实现父子域名之间的数据传递。
Cookie大小和个数
cookie的数据4k左右
一、浏览器允许每个域名所包含的cookie数:
Microsoft指出InternetExplorer8增加cookie限制为每个域名50个,但IE7似乎也允许每个域名50个cookie。
Firefox每个域名cookie限制为50个。
Opera每个域名cookie限制为30个。
Safari/WebKit貌似没有cookie限制。但是如果cookie很多,则会使header大小超过服务器的处理的限制,会导致错误发生。
注:“每个域名cookie限制为20个”将不再正确!
二、当很多的cookie被设置,浏览器如何去响应。
除Safari(可以设置全部cookie,不管数量多少),有两个方法:
最少最近使用(leastrecentlyused(LRU))的方法:当Cookie已达到限额,自动踢除最老的Cookie,以使给最新的Cookie一些空间。Internet Explorer和Opera使用此方法。
Firefox很独特:虽然最后的设置的Cookie始终保留,但似乎随机决定哪些cookie被保留。似乎没有任何计划(建议:在Firefox中不要超过Cookie限制)。
三、不同浏览器间cookie总大小也不同:
Firefox和Safari允许cookie多达4097个字节,包括名(name)、值(value)和等号。
Opera允许cookie多达4096个字节,包括:名(name)、值(value)和等号。
Internet Explorer允许cookie多达4095个字节,包括:名(name)、值(value)和等号。
注:多字节字符计算为两个字节。在所有浏览器中,任何cookie大小超过限制都被忽略,且永远不会被设置。
题外话:localStorage中一般浏览器支持的是5M大小,在不同的浏览器中localStorage会有所不同。
设置 Cookie
虽然 cookie 是 K-V 形式存储的,但是在设置 cookie 的值的时候,是直接给定形如key1=value1; key2=value2
的字符串。它在服务器/浏览器端均可以设置:
- 浏览器端:通过 js 代码来设置,例如
document.cookie = "firstName=dongyuanxin; path=/
- 服务器端:通过给 Http Response Headers 中的
Set-Cookie
字段赋值,来设置 cookie。客户端接收到Set-Cookie
字段后,将其存储在浏览器中。
在服务端,以 koajs 为例,设置 key 为id
,value 为xxoo521.com
的 cookie。代码如下:
const Koa = require("koa");
const Router = require("koa-router");
const serve = require("koa-static");
const port = 3333;
const app = new Koa();
const router = new Router();
router.get("/api", async (ctx, next) => {
const cookie = ctx.cookies.get("id");
if (!cookie) {
// 设置 id = xxoo521.com
ctx.cookies.set("id", "xxoo521.com");
}
ctx.response.body = "原文地址:xxoo521.com";
});
app.use(serve("."))
.use(router.routes())
.listen(port, () => {
console.log("listen port:", port);
});
代码调试
在启动上面代码,并且请求/api
接口后。在 Chrome Dev Tools 中,能看到服务器返回的 Headers 的信息,如下图:
按照协议,浏览器应该成功保存了 cookies 的值。此时,找到Application => Storage => Cookies => 当前域名
,即可验证 cookie 是否设置成功,如下图:
整体流程
以用户购买商品为例,整体流程如下:
- 监测到浏览器客户端没有标识用户的 cookie,跳转到登陆界面
- 用户账号密码登陆,后端验证,成功后,给 Http Response Headers 中的
Set-cookie
设置标识用户的 cookie - 用户登陆成功,浏览器保存用户标识的 cookie
- 购买商品,自动携带用户身份的 cookie,后端验证无误后,购买成功
cookie传输
服务器通过响应头的方式将SessionID返回给浏览器写入到Cookie中,浏览器下次请求就会将SessiondID以Cookie形式传递给服务器端。
当客户端再次访问时,会默认携带cookie中的sessionId来实现会话机制。
参考:https://www.cnblogs.com/belongs-to-qinghua/p/11353228.html
总结
由此可见,单纯的使用 cookie,需要将用户的身份信息保存在客户端,并不安全。除此之外,cookie 还有大小限制,以及只能使用字符串类型作为 value 值。
三、token
token的意思是“令牌”,是用户身份的验证方式,一般通过MD5、SHA算法将密钥、公钥、时间戳等元素加密产生的加密字符串,由服务器生成,服务器无需存储,仅存储在客户端。通信时,客户端请求时带上token,服务器通过摘要算法来校验token。解决Session默认机制下不适合分布式部署的问题。最简单的token组成:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,由token的前几位+盐以哈希算法压缩成一定长的十六进制字符串,可以防止恶意第三方拼接token请求服务器)。还可以把不变的参数也放进token,避免多次查库。
参考:《深入了解 Token 认证的来龙去脉》 深入了解 Token 认证的来龙去脉
token的认证
Token的作用
- Token 完全由应用管理,所以它可以避开同源策略
- Token 可以避免 CSRF 攻击(http://dwz.cn/7joLzx)
- Token 可以是无状态的,可以在多个服务间共享
Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位。如果这个 Token 在服务端持久化(比如存入数据库),那它就是一个永久的身份令牌。
Token 的有效期
登录密码一般要求定期改变密码,以防止泄漏,所以密码是有有效期的;SSL 安全证书都有有效期,目的是为了解决吊销的问题(http://dwz.cn/7joMhq)。所以无论是从安全的角度考虑,还是从吊销的角度考虑,Token 都需要设有效期。根据系统的安全需要,有效期应尽可能的短。
用户在正常操作的过程中,Token 过期失效了的两种解决方法(不需要用户重新登录):
一种方案是在服务器端保存 Token 状态,用户每次操作都会自动刷新(推迟) Token 的过期时间——Session 就是采用这种策略来保持用户登录状态的。然而仍然存在这样一个问题,在前后端分离、单页 App 这些情况下,每秒种可能发起很多次请求,每次都去刷新过期时间会产生非常大的代价。如果 Token 的过期时间被持久化到数据库或文件,代价就更大了。所以通常为了提升效率,减少消耗,会把 Token 的过期时间保存在缓存或者内存中。
另一种方案是使用 Refresh Token,它可以避免频繁的读写操作。这种方案中,服务端不需要刷新 Token 的过期时间,一旦 Token 过期,就反馈给前端,前端使用 Refresh Token 申请一个全新 Token 继续使用。这种方案中,服务端只需要在客户端请求更新 Token 的时候对 Refresh Token 的有效性进行一次检查,大大减少了更新有效期的操作,也就避免了频繁读写。当然 Refresh Token 也是有有效期的,但是这个有效期就可以长一点了,比如,以天为单位的时间。
时序图表示
使用 Token 和 Refresh Token 的时序图如下:
1)登录
2)业务请求
3)Token 过期,刷新 Token
上面的时序图中并未提到 Refresh Token 过期怎么办。不过很显然,Refresh Token 既然已经过期,就该要求用户重新登录了。
当然还可以把这个机制设计得更复杂一些,比如,Refresh Token 每次使用的时候,都更新它的过期时间,直到与它的创建时间相比,已经超过了非常长的一段时间(比如三个月),这等于是在相当长一段时间内允许 Refresh Token 自动续期。
以上说的Token 都是有状态的,即在服务端需要保存并记录相关属性。
无状态 Token
如果我们把所有状态信息都附加在 Token 上,服务器就可以不保存。但是服务端仍然需要认证 Token 有效。不过只要服务端能确认是自己签发的 Token,而且其信息未被改动过,那就可以认为 Token 有效——“签名”可以作此保证。平时常说的签名都存在一方签发,另一方验证的情况,所以要使用非对称加密算法。但是在这里,签发和验证都是同一方,所以对称加密算法就能达到要求,而对称算法比非对称算法要快得多(可达数十倍差距)。
更进一步思考,对称加密算法除了加密,还带有还原加密内容的功能,而这一功能在对 Token 签名时并无必要——既然不需要解密,摘要(散列)算法就会更快。可以指定密码的散列算法,自然是 HMAC。
在使用无状态 Token 的时候,有两点需要注意:
-
Refresh Token 有效时间较长,所以它应该在服务器端有状态,以增强安全性,确保用户注销时可控
-
应该考虑使用二次认证来增强敏感操作的安全性
上面说的只是认证服务和业务服务集成在一起的情况。
分离认证服务
当 Token 无状态之后,单点登录就变得容易了。前端拿到一个有效的 Token,它就可以在任何同一体系的服务上认证通过——只要它们使用同样的密钥和算法来认证 Token 的有效性。如下图:
当然,如果 Token 过期了,前端仍然需要去认证服务更新 Token:
可见,虽然认证和业务分离了,实际即并没产生多大的差异。当然,这是建立在认证服务器信任业务服务器的前提下,因为认证服务器产生 Token 的密钥和业务服务器认证 Token 的密钥和算法相同。换句话说,业务服务器同样可以创建有效的 Token。
如果业务服务器不能被信任,该怎么办?这里就不深入描述“不受信的业务服务器”的情形了。
token传输
浏览器请求时不会默认携带token,需要在请求头中添加认证字段Authorization携带token信息。
四、关系的理解
客户第一次发送请求给服务器,此时服务器产生一个唯一的sessionID,并返回给客户端(通过cookie),保存于客户端的内存中,并与一个浏览器窗口对应着,由于HTTP协议的特性,这一次连接就断开了。
以后此客户端再发送请求给服务器的时候,就会在请求request中携带cookie,由于cookie中有sessionID,所以服务器就知道这是刚才那个客户端。
例子:
举个简单例子就像人们去超市购物,去存包,第一个去的时候(客户第一次发送请求给服务器),超市会给你一个号码牌(此时服务器产生一个唯一的sessionID,并返回给客户端(通过cookie)),你可以在你自己的柜子里存东西(在服务器属于此客户的内存区域存数据),下次你再去的时候,拿着这个号码牌(请求request中携带cookie),超市就知道哪些东西是你的,然后给你取出来,如果你几天都没去取(session失效了,在服务器端配置),你再去的时候东西就拿不到了
如果你把这个号码牌丢了(刚才的cookie失效了,比如你重启电脑,刚才存于内存中sessionID也就丢了),再去拿东西,当然无法定位了,也就拿不到东西了
如果是新打开一个浏览器的情况,那就像是又一个人去超市存东西一样,你的东西跟他的东西是两码事,互不影响,他有他自己的sessionID,你有你自己的。