Node中cookie/session/token的使用

2020-12-02  本文已影响0人  哆啦_

项目中是使用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。可以设置expires或者max-age来设置过期时间:

cookie的作用域

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

设置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的方式有很多缺点:

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

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

JWT实现Token机制

JWT实现Token

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

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

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

我们可以使用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()的,也就是当前项目启动的目录

上一篇下一篇

猜你喜欢

热点阅读