Node中cookie/session/token的使用

项目中是使用Koa的搭建服务器。
端口配置放在.env文件中,上传时应该忽略该文件,因为每个设备有自己的端口号。
这里使用dotenv来加载env文件。

登录凭证

web开发中,使用最多的就是http协议,但http是一个无状态的协议。也就是每一次http请求都是一个独立的请求,并不知道之前的状态。比如我们登录成功之后,再去调用其他接口获取信息时会发送一个新的请求,而这次的请求并不知道用户是否已经登录过。

所以登录成功之后服务器需要返回给客户端一个登录凭证,下次请求时拿着登录凭证证明该用户已经登录。

常见的登录凭证:

  1. cookie+session

  2. Token令牌

Cookie

cookie类型为小型文本文件,某些网站为了辨别用户身份而存储在用户本地终端的数据。浏览器会在特定情况下携带上cookie来发送网络请求。

cookie总是抱存在客户端,按照在客户端的存储位置可以分为内存coolie和硬盘cookie

  • 内存cookie由浏览器维护,保存在内存中,浏览器关闭时cookie会消失,存在时间是短暂的

  • 硬盘cookie保存在硬盘中,有一个过期时间,手动清除或者到了过期时间才会消失

如何判断一个cookie是内存cookie还是硬盘cookie?

  • 没有设置过期时间,默认是内存cookie

  • 有设置过期时间,并且过期时间不为0或者负数的cookie是硬盘cookie,需要手动或者到期时才会删除

cookie的生命周期

默认情况下cookie是内存cookie,也称之为会话cookie。可以设置expires或者max-age来设置过期时间:

  • expires:设置的是Date.toUTCString(),设置格式是:expires=date-in-GMTString-format

  • max-age:设置过期的时间:max-age=max-age-in-seconds(例如一年为60 * 60 * 24 * 365)

cookie的作用域

cookie的作用域也就是允许cookie发送给哪些URL

  • Domain:指定哪些主机可以接收cookie

    • 如果不指定,默认为origin,不包括子域名

    • 如果指定Domain,则包含子域名。比如,如果设置Domain=mozilla.org, 则cookie也包含在子域名中(比如develop.mozilla.org)

  • Path:指定主机下哪些路径可以接收cookie

    • 比如设置Path=/docs,则/docs、/docs/web/、/docs/web/http都会匹配到
设置cookie

cookie可以由客户端设置,也可以由服务端设置。一般都是由服务器设置cookie,客户端来删除cookie

谷歌浏览器查看cookie的方法:F12进入控制台 => Application => Storage => Cookies

客户端设置cookie

      // document.cookie= 'name=coderwy';
      // 5s之后删除cookie
      document.cookie = 'age=18;max-age=5'

服务端设置cookie

testRouter.get('/test', (ctx, next) => {
  // 设置cookie
  ctx.cookies.set('name','Lucy',{
    maxAge:5 * 1000 // 单位是毫秒
  })
  ctx.body = "test";
})

服务端获取cookie

// domain默认为origin 所以同域名下可以获取到cookie
testRouter.get('/demo', (ctx, next) => {
  // 获取cookie
  const value = ctx.cookies.get('name')
  ctx.body = '你的cookie是:' + value
})

浏览器在发送请求时,会自动将cookie添加到请求头中


image.png

Session

session是基于cookie来实现的,在koa项目中可以借助koa-session来实现session认证

const session = Session({
  key:'sessionid',
  maxAge:10 * 1000,
  signed:false // 暂时设置为不需要签名
}, app)
app.use(session)

// 登录接口
testRouter.get('/test', (ctx, next) => {
  // 假设用户通过name和password登录 登录成功之后从数据库查询
  // 得到id和name
  const id = 110
  const name = 'Lucy'
  // 设置session
  // 因为有app.use(session)操作 所有有session这个属性
  ctx.session.user = {id, name}
  
  ctx.body = "test";
})
image.png

其中Value是我们设置的session.user的base64的编码。可以看到session本质上还是一个cookie

获取session:

// 获取session
testRouter.get('/demo', (ctx, next) => {
  console.log(ctx.session.user); // { id: 110, name: 'lucy' }
  ctx.body = 'demo'
})

由于浏览器会自定在请求时带上cookie,也就是上面的sessionid和其value,服务器这边会对其进行解析,最终获得登录凭证(id: 110, name: 'lucy')

现在来看下使用签名的情况:

const session = Session({
  key:'sessionid',
  maxAge:30 * 1000,
  signed:true // 使用加密签名
}, app)
app.keys = ['aaaaa']
app.use(session)

此时设置的session内容为:


image.png

在我们获取session时能正常获取,而当我们修改了sessionid对应的value时,就不会获取到对应的信息


image.png

这里是随便修改的value,但我们知道value是根据id和name的内容转成的base64,如果别人也通过其他的id来生成base64来修改value的话,服务端也是能获取到对应的登录凭证的,所以需要进行加密签名

此时获取session时:

// 获取session
testRouter.get('/demo', (ctx, next) => {
  console.log(ctx.session.user); 
  // 判断session内容
  // ...
  ctx.body = 'demo'
})

此时打印:

undefined

token

cookie和session的方式有很多缺点:

  • cookie会附加在每个http请求中,无形中增加了流量
  • cookie是明文传输的,所以存在安全性问题
  • cookie的大小限制为4kb,对于复杂的需求是不够的
  • 对于浏览器其他的客户端(比如iOS,Android)需要手动设置cookie
  • 对于分布式系统和服务器集群中如何可以保证其他系统也可以正确的解析session?

所以目前的前后端分离的开发过程中,经常使用token进行身份验证:

token也就是令牌,在验证了用户账号密码正确的情况下,给用户颁发一个令牌,这个令牌作为用户访问其他接口或者资源的凭证。

JWT实现Token机制

JWT实现Token

JWT(Json Web Token)生成的Token由三部分组成:

  • header
    • alg:采用的加密算法。默认是HMAC SHA256(也就是HS256,一种对称加密算法),采用同一个密钥进行加密解密
    • typ:JWT。固定值,通常写成JWT即可
    • 会通过base64Url算法对上面两部分进行编码,生成一串字符串(也就是header部分了);
  • payload
    • 携带的数据,比如可以讲用户的id和name放到payload中
    • 默认会携带iat(issued at),令牌签发时间
    • 也可以设置过期时间:exp(expiration time)
    • 通过base64Url算法对携带的数据进行编码
  • signature:因为通过进行base64Url编码的结果很容易被反编码的。所以除了header和payload之外还需要签名
    • 设置一个secretKey,然后将前两个的结果和secretKey进行HS256算法的加密:
    • HS256(baseUrl(header) + . + baseUrl(payload), secretKey);
    • 所以secretKey暴露是一件非常危险的事情,因为之后就可以模拟颁发token,也可以解密token。
image.png
const Koa = require('koa')
const Router = require('koa-router')
const jwt = require('jsonwebtoken')

const app = new Koa()

const testRouter = new Router()

const SECRET_KEY = 'secretkey'

// 登录接口
testRouter.post('/test', (ctx, next) => {
  const payload = {id:119, name:'lwy'}
  // 生成token
  const token = jwt.sign(payload,SECRET_KEY,{
    expiresIn:10 
  })
  // 返回token
  ctx.body = token
})

app.use(testRouter.routes())
app.use(testRouter.allowedMethods())

app.listen(8080, () => {
  console.log('服务器启动成功');
})

使用postman进行请求可以看到返回的token为:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTE5LCJuYW1lIjoibHd5IiwiaWF0IjoxNjA2NzI0MTg1LCJleHAiOjE2MDY3MjQxOTV9.D8q8-8zIZ5CamXi-sSZdWnPoN_yQ-A4Y8zzIYJW77yQ

可以看到,其中是以 . 分隔成三部分的,对应的就是上面说的token的组成

接下来就可以通过postman来模拟把token传递给服务端。一种方式是把token放到body里,不过很少这样使用,一般都是将token放到header中。


image.png

可以看到有很多的认证方式,常用的是Bearer(送信人) Token方式

获取token:

testRouter.get('/demo', (ctx, next) => {
  // 获取token
  console.log(ctx.headers.authorization);
})

Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTE5LCJuYW1lIjoibHd5IiwiaWF0IjoxNjA2NzI0MTg1LCJleHAiOjE2MDY3MjQxOTV9.D8q8-8zIZ5CamXi-sSZdWnPoN_yQ-A4Y8zzIYJW77yQ

接下来可以获取到token:

testRouter.get('/demo', (ctx, next) => {
  // 获取token
  console.log(ctx.headers.authorization);
  const auth = ctx.header.authorization
  const token = auth.replace('Bearer ', '')
  try {
    // 验证token verify验证失败之后会抛出异常 
    const result = jwt.verify(token, SECRET_KEY)
    console.log(result);
    ctx.body = result
  } catch (error) {
    console.log('验证失败:', error);
    ctx.body = 'token 无效'
  }
})

token验证成功之后的返回:

{

​ "id": 119,

​ "name": "lwy",

​ "iat": 1606874354,

​ "exp": 1606884354

}

非对称加密

前面用到的HS256加密算法是一种对称加密(维基百科),一旦密钥暴露是一种很危险的事情。比如在分布式系统中,每一个子系统都需要获取到密钥,那么每个子系统既可以发布令牌,也可以验证令牌,但对一些资源服务器来说,只需要有验证令牌的功能即可。

这个时候就可以使用非对称加密(维基百科):

  • 私钥(private key):用于发布令牌

  • 公钥(public key):用于验证令牌

通常我们使用公钥加密,用私钥解密。而在数字签名中,我们使用私钥加密(相当于生成签名),公钥解密(相当于验证签名)

我们可以使用openssl来生成私钥,然后根据私钥生成对应的公钥。mac中自带的有openssl工具。

比如生成一个密钥到指定的目录中:

  1. cd 指定目录
  2. openssl:进入到ssl的命令行交互页面`
  3. genrsa -out private.key 1024: 生成私钥
    1. genrsa: gen是generate的缩写,rsa是常用的非对称加密算法
    2. -out:代表我们要导出
    3. private.key: 导出的文件名(代表我们生成的是私钥)
    4. 1024:生成的私钥的长度,也可以搞成其他长度
image.png

接下来还要生成用于验证签名的公钥:

rsa -in private.key -pubout -out public.key

  1. -in private.key:表示将刚刚生成的密钥作为输入
  2. -pubout:表示这次生成的是公钥
  3. -out:表示我们要导出
  4. public.key:导出的文件名
image.png

使用非对称加密生成token:

const PRIVATE_KEY = fs.readFileSync('./keys/private.key')
const PUBLIC_KEY = fs.readFileSync('./keys/public.key')

// 使用非对称加密
testRouter.post('/test', (ctx, next) => {
  const payload = {id:110, name:'lwy'}
  // 私钥用来生成签名
  const token = jwt.sign(payload, PRIVATE_KEY,{
    expiresIn:10 * 1000,
    algorithm:"RS256" // 指明用到的算法 
  })
  ctx.body = token
})
testRouter.get('/demo', (ctx, next) => {
  const auth = ctx.headers.authorization
  const token = auth.replace('Bearer ', '')
  try {
    const res = jwt.verify(token, PUBLIC_KEY,{
      algorithms:["RS256"]  
    })
    ctx.body = res 
  } catch (error) {
    console.log('token验证错误:', error);
    ctx.body = 'token失效'
  }
})

ps: 项目中相对路径是相对于process.cwd()的,也就是当前项目启动的目录

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容