序言
本文主要是针对cookie,session,以及jwt怎么生成的,有什么区别。以及cookie的一些常用的属性进行讨论,同样会总结各种方式的不足之处,如果有不足的地方希望大家可以指正,好了不废话了,我们开始吧!!!!!!!
目的
这里会抛出一个话题,为什么会出现cookie,session呢??? 想要知道这个答案的话,必须联想到http的特性:无状态特征。简单举个例子:就是用户第一次登录一个门户网站,登录成功。第二次进入的时候还需要重新登录,这肯定是不行的。因为http是无状态的,所以我们就需要有个地方记录用户的信息。所以cookie等出现了
案例说明
本案例中我们会粘贴出代码,但是具体的代码还是参照源码。本案例中使用的技术是koa + @koa/router 来给大家做演示
cookie说明
1. 代码案例
route.get('/write', (ctx) => {
ctx.cookies.set('age', 20, {
// 设置domain 表示子域a.lihh/ b.lihh 可以共享父域的cookie
domain: 'lihh.com',
// 如果设置了httpOnly为true的话 客户端通过document.cookie是获取不到的
httpOnly: true,
signed: true
})
ctx.body = '设置成功'
})
- 上述的代码是cookie相关所有的代码,接下来我们会针对domain/ httpOnly/ signed 挨个进行演示,至于的其余的属性我们就不做过多的演示了,因为用的机会其实并不多
domain相关的设置
1. domain 不设置情况下
route.get('/write', (ctx) => {
ctx.cookies.set('age', 20)
ctx.body = '设置成功'
})
- 首先说明一点,我们此时有两个代理站点:
a.lihh.com
以及b.lihh.com
,主要是为了模拟不同域名:- 此时运行在域名
a.lihh.com:4000/write
下 - 可以看到默认的domian就是当前的地址
- 接下来我们看下地址
b.lihh.com:4000/read
下,其实我们会发现什么都没有。说明当domain设置为当前地址的时候,两个域名间cookie是无法共享的,但是真的不共享吗???请继续往下看
- 此时运行在域名
2. 设置共享domain
route.get('/write', (ctx) => {
ctx.cookies.set('age', 20, {
domain: 'lihh.com',
})
ctx.body = '设置成功'
})
- 通过上述代码我们会发现,我们设置了代码
domain: 'lihh.com'
. 我们把上述代码的案例运行下看下结果:- 我们同样执行域名
a.lihh.com:4000/write
- 我们会发现domain中变为了
.lihh.com
了,接下来我们看下域名b.lihh.com
的执行结果 - 当我们运行域名
http://b.lihh.com:4000/read
的时候,发现cookie已经存在了。而且domain也是.lihh..com
。说明他们做到了cookie共享
- 我们同样执行域名
3. 结论
- cookie在父子域名之间可以共享,也就是兄弟域名之间是可以共享的
- 但是两个完全不相干的域名之间是无法做到共享的
httpOnly的一点小知识
1. httpOnly的设置小知识
route.get('/write', (ctx) => {
ctx.cookies.set('age', 20, {
domain: 'lihh.com',
httpOnly: true
})
ctx.body = '设置成功'
})
- 上述的代码中我们添加了配置
httpOnly: true
,表示 cookie 只能通过 HTTP 协议发送. - 还有一旦设置了这个属性,通过API document.cookie 是无法获取该属性的,但是在浏览器的Application面板中还是可以修改的(个人理解:意义不是很大)
- 接下来让我们看下添加配置项后有什么不同
- 其实没有什么特别的不同的地方,就是在属性HttpOnly中标记了√
签名 安全
- 其实通过上述的演示我们都能看到所有的信息其实都是明文保存的,一点安全性没有。如果数据被人篡改了,当我们接口请求后端的时候,后端其实使用的是错误的数据。
- 其实从前端来保存数据本身就是不安全的,但是我们的目的不是破罐子破摔,而是尽量让它变得安全起来
- 基于上述的特点我们的签名就来了,其实我们需要给cookie进行签名,一定程度上来提高数据的安全性
app.keys = ['lihh-test']
route.get('/write', (ctx) => {
ctx.cookies.set('age', 20, {
domain: 'lihh.com',
httpOnly: true,
signed: true
})
ctx.body = '设置成功'
})
- 我们先看来下在koa中如果设置了签名,出来的效果是什么,还是老样子我们来运行下
http://a.lihh.com:4000/write
试试- 通过上述的截图中可以看到koa为我们的属性[age]设置的签名。那签名如何形成的呢
- 接下来分析下签名如何生成的,到底签名能不能进行修改
1. 签名解析
const crypto = require('crypto')
const values = ['age', 20]
const secret = 'lihh-test'
const toBase64URL = (str) => {
// 针对转换base64位 如果是= 直接替换位空 如果是+号 直接替换为- 如果是/ 直接替换为_
return str.replace(/=/gi, '').replace(/+/g, '-').replace(///g, '_')
}
const content = toBase64URL(crypto.createHmac('sha1', secret).update(values.join('=')).digest('base64'))
console.log(content === '1KCzh4f24Jd8gbS2NwB9W7znAo8') // true
- 这个是上述我们来模拟koa的签名出来的,你会发现其实跟koa的结果是保持一致的。接下来我们来分析下:
- 需要的包:
crypto
我们需要这个包来进行加密算法 - 上述的代码
const values = ['age', 20]
这个是表示需要加密的内容 - 也许有人会问加密算法可以使用MD5加密吗???首先MD5是公开算法,其次MD5算法是可逆的,显然是不安全的。既然是加密,我们就需要密钥,这里变量
secret
就表示加密的密钥,这里的密钥是存放到服务器上的,所以是相对安全的 - 这里我们采用了
sha1
进行加密(sha1 不可逆)。 请看上述的代码。 - 那如何验证是对的呢??? 从上述截图中我们可以看到会出现两个key[age], [age.sig]. 我们就是拿到了原始的key/value 根据sha1重新进行加密。拿加密后的新值跟coookie中的[age.sig]的值进行判断,是否一致
- 需要的包:
其余属性
expires
设置cookie的时效性path
设置cookie是否针对某个路径进行cookie设置, 意义不大- ...
session说明
session演示的代码可以参照上述具体的地址,这里不做截图演示。但是会说出大致原理,其实session很简单
// 密钥
const secret = 'zfsecret'
app.keys = [secret]
const router = new Router()
let cardName = 'lihh-test' // 店铺名字
// 变量内存 正式环境一般都是redis
let session = {}
router.get('/wash', async function (ctx) {
// 也是存储到cookie 加盐算法 保护id
const hasVisit = ctx.cookies.get(cardName, { signed: true })
// 判断cookie中是否存在
if (hasVisit && session[hasVisit]) {
// 必须保证你的卡是我的店的
session[hasVisit].mny -= 100
ctx.body = '恭喜你消费了 ' + session[hasVisit].mny
} else {
const id = uuid.v4() //冲500
session[id] = { mny: 500 }
// 给cookie中设置内容
ctx.cookies.set(cardName, id, { signed: true })
ctx.body = '恭喜你已经是本店会员了 有500元'
}
})
- session的实现过程请求看下面的截图:
-
如果能看懂上述的截图就不用看下面的文字描述了
- 如果用户第一次登录的话,用户访问后端,后端会根据查询出来的用户信息,保存到一个地方,而且以一个唯一的值作为对象key。其实就是身份凭证,就是后来我们所说的sessionId
- 服务器端进行响应,客户端将sessionId进行保存
- 往后的请求浏览器都会携带sessionId来验证,表明了用户身份
-
其实session的实现原理还是很简单,用一句大白话来说就是:
给每个人分配一个唯一的id(这个id在用户表中可以查询到用户信息),表示身份凭证,以后每次来请求带携带着id就够了
-
其实我们可以做的还有几个事情:
- session的携带有个方面:第一个是可以放到cookie中。第二个可以放到请求header中
- 为了更加的安全性,我们可以给sessionId进行签名。一定程度上保障数据的安全性
-
接下来我们来看下关于sessionId的运行结果:
-
从上述截图中我们其实是使用了uuid作为sessionId,同时对uuid进行签名
jwt 说明
其实我们都知道,一种新鲜技术的出现往往是为了弥补另一种技术的不足。jwt也是如此。关于cookie/ session/ jwt横向对比后面我们会做结论。但是jwt的出现确实为了弥补sesson不足
1. 什么是jwt
- jwt全称是Json Web Token. 是用户认证的一种方式。
2. token详解
const head = this.toBase64({ typ: 'JWT', alg: 'HS256' })
const content = this.toBase64(info)
const sign = this.sign([head, '.', content].join(''), secret)
// 加密类型(sha256) + 加密内容 + 通过盐值来生成签名
return head + '.' + content + '.' + sign
- 其实token的实现内容有很多,上述只是粘贴出了一部分代码,具体的代码请参照上述gitHub地址
- 其实关于jwt并不是很好演示,这里我们就先说下大致的实现过程再结合token如何生成的来详细讲解下
- 个人觉得token 以及session在一定程度上有点类似。session中有sessionId以及用户信息。但是token中存在所谓的“令牌”,同样,“令牌”是唯一的,表示的身份信息
- 这里用大白话来解释下:
使用用户id通过sha256加密,结合一些别的内容得到的值就是身份令牌
- 下面来演示下得到的token的结果:
- 其实token分为三部分(下面我们回举例说下):
- 加密类型
- 加密内容(我们一般可以使用id)
- 以及签名
1. 加密类型
- 我们token采用的加密类型是sha256. 最后通过base64位进行转换,例如如下代码:
Buffer.from(JSON.stringify({ typ: 'JWT', alg:'HS256'})).toString('base64')
- 其实这里的对象不变的,就是转换为了base64位
2. 加密内容
- 加密内容就更加简单了,就是把上述类型直接替换位内容就行。为了具有唯一性,可查询性我们一般都是用id
toBase64({id: 'XXXXX'})
3. 签名
- 其实签名跟上述cookie等签名的原理大致是一致的,但是这里只不过使用了sha256来进行签名认证
crypto.createHmac('sha256', secret).update(content).digest('base64')
// secret 相当于密钥
// content 内容其实就是[加密类型, 加密内容].join("") 的值
4. 最后
- 所以看到上述的截图的时候其实token分为三部分,分别是:加密类型 + 加密内容 + 签名,用点进行相连。
- 关于token的验证:其实就是用.分割字符串。拿前两个值再次进行签名。判断是否跟第三个值是否一致
大总结
- cookie优点:
- 使用灵活,简单,方便
- 传输方便,不需要我们操作
- cookie缺点:
- 保存前端明文
- 无法进行跨域
- session优点:
- 相对于JWT,可以主动清除session
- session保存在服务器,相对安全
- 结合cookie进行传输,相对容易点
- session缺点:
- cookie + session在跨域场景表现不好
- 如果是分布式部署,需要做多机共享
- 基于cookie的机制很容易被攻击
- 需要服务端支持保存sessionId
- token优点:
- 可以基于多种方式(cookie/ header)来进行传输
- 在分布式部署的时候,只需要获取到加密内容就行
- 不需要服务端额外的保存数据
- token缺点:
- 如果放置到header中,加密的内容越多,请求头服务越重
赠送
通过上述的分析,大家应该都知道什么叫cookie/ session/ token了。接下来赠送几篇文章登录相关机制 大家可以看下
结束
说了这么多了,如果不明白的,或是讲解不对的地方希望大家多多指正。还是老样子接下来我们坐下自我介绍