端午吃粽子不如彻底吃下token鉴权

1,005 阅读9分钟

我正在参加「初夏创意投稿大赛」详情请看:初夏创意投稿大赛

本文代码基于本人上篇文章:夏天到了,让我手摸手教你快速入门 nodeJS 吧! - 掘金 (juejin.cn) 内容。

为什么需要做权限控制?

设想这样一个场景,我们做了一个实际的购物网站。用户登陆后,想查看 ‘我的 - 历史浏览记录’。

那么,我们从浏览器发起请求的时候,是不是得告诉服务器,是哪个用户发起的请求呢?

是不是登录以后,用户的每个请求,都得让服务器知道,是谁在进行操作?

所以,想要搭建一个真正的服务器,第一步就是要吃下鉴权。

权限控制方式简介

cookie - session

最直接的想法,是不是登录之后,把用户的唯一标识存起来 (假设为 ID),无论发送什么请求,都把这个标识带上,服务器就知道是谁在进行操作了呢?

存在哪里呢?

一般来说,我们存在客户端的 cookie 上。页面跳转时,我们从 cookie上获取用户信息,发送给服务器。

Cookie,是一个保存在客户端的简单文本文件。使用 name/value 存值,使用 js 可以对 cookie 进行基本操作。


通常,ID 是不会轻易改变的。如果使用明文传输,有个人用了你的电脑,看到你的唯一标识并记录了下来,然后在自己的电脑上登录,那个人信息就泄露了。

为了解决这个问题,一般来说,服务器验证完用户的登录信息后,会创建一个 session 和用户 ID 对应起来。然后会根据一定的规则,产生一个 sessionID 和 session 对应起来。

userid -> session - > sessionID

你可能会问,为什么要增加一个 sessionid 呢?因为 session 是不会改变的,而 sessionID 是会改变的,它会跟 session 对应起来。我们可以设置 sessionID 的过期时间,过期了再登录,服务器就会给我们发一个新的 sessionID。所以这个时候,记住你的 sessionID 用处也不大了。

那为什么又要去创建 userid 和 session 的对应关系,而不是直接使用 userid 创建和 sessionid 的对应关系呢?这是因为,session 并不是一个字符串,你可以把它理解成对象。它除了能够有唯一的 session 标识,还可以存一些其他的东西。比如用户名,用户配置等等。早期的时候,应用功能没有现在这么复杂,所以配置文件比较少。

这样,当 sessionID 匹配上后,我们可以直接从 session 里拿一些全局配置,比较方便,不需要去查数据表了,能够加快响应速度。

但是这样做带来的直接问题就是,服务器得为每个用户创建一个 session ,用户一多,服务器的压力就会变大。有的时候,为了缓解服务器压力,或者说某个服务器出问题了,会把请求分散到其他服务器,而其他服务器并没有存对应的 session ,就带来了各种各样的问题。

所以出现了一种鉴权方式,token。

token

token 略掉了 session 这个中间商,它觉得,只要不为每个用户创建一个 session , 就不存在 session 相关问题了。可是直接使用 userid 和 sessionID 匹配关系的话,服务器的存储的文件是变少了,可是换了服务器,这个匹配关系依然跟不上啊?

能不能不要存匹配关系就能鉴定用户身份呢?

在 session-cookie 鉴权中,服务端给客户端一个 sessionid,客户端将 sessionid 返回。

是不是只要确定,服务端发出去的,和客户返回的,是同一个字符串,就能证明,这个客户不是假的,是真正登录成功了,拿到了服务端的独门通行证?就好像各门各派都会有门派令牌,拿着这个令牌,就能确定,这个人确实是从我门派出去的。

确定客户不是伪造的,我还需要拿到客户的 id ,来做后面的一系列处理。

顺着这个思路,token 鉴权的大致思路就出来了。

每次校验完用户信息后,服务端就设定一个特殊的字符串(私钥),把这个特殊的字符串和 一些数据一起,比如 {userid:'1'},一起加密,然后得到一串加密字符串,我们把它叫做 签名。

服务端:数据 + 私钥 -> 签名

然后我们把这个签名和这个数据一起发给客户端。

1.服务端:数据 + 私钥 -> 签名

  1. 签名 + 数据 传送到客户端。

当客户端再次请求的时候,它会带着 数据 + 签名。

由于私钥不变且外部不可知,这时候,我们把 数据+私钥,再加密一次。

注意,根据现有的加密算法,对同一字符串加密得到的结果是一样的。这时候,我们把它和签名作比较,如果一样,就说明这个 token 是服务器发出去的那一个,这用户合法!我们拿到数据里的 userid , 就可以取数据了!

1.用户验证后,服务端:数据 + 私钥 -> 签名

2.服务端将 签名 + 数据 ,作为 token ,传送到客户端。

3.客户端再次请求时 , 服务端获取 token 中的 数据,和私钥一起加密,再和 token 中的 签名 对比。

一般来说,鉴权合法后,我们才会进行下一步的接口处理,才能确定返回数据。

实践

用户登录验证

一般来说,我们输入用户名和密码登陆后,服务器会验证用户名和密码是否正确。

验证成功后,将 用户id 作为 token 中的数据部分。

先完成第一步:用户登录成功后,拿到用户 id 。

先注册一个用户,即我们上文中提到的,添加一条数据。不同的是,发起请求的时候只会输入用户名和密码,后端自动生成 id 。

先删除 data.json 中原有的废数据。在 routes/users.js 修改 addUser 如下:

router.post('/addUser',function *(next){
  // 获取用户数据
  const _this = this;
  const requestData = this.request.body;// 拿到请求的数据
  
  fs.readFile(filepath, 'utf-8', (err, data) => {// data 是从文件中读取到的数据
    if(err){
        console.log(err);
        console.log('ERROR!Failed to read file!')
    }

    const params = data && JSON.parse(data) || [];// 拿到文件中原来存在的数据
    const id = params.length + 1;// 设置 id 从 1 开始,依次递增
    const newParams = [...params, { id, ...requestData }];
    fs.writeFile(filepath,JSON.stringify(newParams),(err)=>{
      if(err) _this.body = '写入文件失败!';
    })
})
})

然后在 win+R cmd 终端中输入如下命令:

curl -X POST -H "content-type: application/json; charset=utf-8" http://127.0.0.1:8003/users/addUser -d {\"username\":\"admin\",\"password\":\"123456\"}

curl -X POST -H "content-type: application/json; charset=utf-8" http://127.0.0.1:8003/users/addUser -d {\"username\":\"Candy\",\"password\":\"123456\"}


得到 data/data.json 文件中如下内容:

[{"id":1,"username":"admin","password":"123456"},{"id":2,"username":"Candy","password":"123456"}]

可以看到,我们添加用户(注册用户)成功了。

接下来,我们做用户登录验证。先设定,当用户名和密码分别是 'admin'、‘123456’ 的时候才登录成功,返回 登录成功,(然后进行鉴权,鉴权暂时使用伪代码)。否则,回复,登录失败。

我们在 routes/users.js 添加如下路由处理:

router.post('/login', function* (next) {
  // 获取用户数据
  const _this = this;
  const requestData = this.request.body;
  if (requestData.username === 'admin' && requestData.password === '123456') {
    // 鉴权成功,添加鉴权成功和处理。
    _this.body = '登录成功!'
  }else{
    _this.body = '登录失败!'
  }
})

然后在终端分别发起请求:


curl -X POST -H "content-type: application/json; charset=utf-8" http://127.0.0.1:8003/users/login -d {\"username\":\"Candy\",\"password\":\"123456\"}

curl -X POST -H "content-type: application/json; charset=utf-8" http://127.0.0.1:8003/users/login -d {\"username\":\"admin\",\"password\":\"123456\"}

得到如下结果:

Snipaste_2022-05-26_10-28-18.png 已经成功验证客户了,接下来,我们完成鉴权处理。

生成 token

下载相关插件。

npm i jsonwebtoken -D

这两个插件分别用于加密生成 token ,和 检查请求中所带的 token 是否合法。

我们使用 用户 id 作为数据, 然后生成一个 token 返回。正常来说,我们生成 token 后,需要给 客户端发送命令,让客户端 将 token 存在 cookie 中,每次请求的时候,都将 token 带上。

像这样:

Snipaste_2022-05-26_15-09-08.png

但是由于我们暂时还没有写客户端,因此,我们暂时设定流程如下:

用户登录成功后,返回 token 。下次发起请求时,手动将 token 写入。

修改 routes/user.js 内容如下。主要

...
const { sign } = require('jsonwebtoken');
const secret = 'learKoaToken';
...

router.post('/login', function* (next) {
  // 获取用户数据
  const _this = this;
  const requestData = this.request.body;
  if (requestData.username === 'admin' && requestData.password === '123456') {
    // 登录成功后,通过用户名和密码查询到用户id
    let id =  getUserId('admin','123456');// 设定一个查询用户id的方法
    const token = sign({id},secret,{expiresIn:'24h'});
    _this.body = {
      message:'获取 token 成功',
      code:1,
      token
    }
    
  }else{
    _this.body = '登录失败!'
  }
})
....

const getUserId = function (username,password){
    const data = fs.readFileSync(filepath, 'utf-8');
    const params = data && JSON.parse(data) || [];// 拿到文件中原来存在的数据
    let id = null;
    params.forEach(item=>{
      if(item.username === username && item.password === password){
        id = item.id;
      }
    })
    return id;
}

module.exports = router;

重启服务端后,发起如下请求:

curl -X POST -H "content-type: application/json; charset=utf-8" http://127.0.0.1:8003/users/login -d {\"username\":\"admin\",\"password\":\"123456\"}

得到如下结果:

Snipaste_2022-05-26_15-25-09.png

我们已经成功生成 token 了。接下来,就是我们需要通过 token 验证用户身份。

通过 token 验证用户身份

平时我们浏览网站,一般会有一个游客模式,和一个用户模式。

此时我们应该能理解,就是 带 token 和 不带 token 的区别。

现在,我们来实现这样一个功能,当我们 带 token 时,且 token 正确,我们拿到加密的数据。

var router = require('koa-router')();
const fs = require('fs');// 引入文件读写模块
const filepath = '../learn-koa/data/data.json';// 指定文件路径.这里要注意下路径,可以看下报错
const jwt = require('jsonwebtoken');
const secret = 'learKoaToken';

router.prefix('/users');

router.post('/testToken',function * (next) {
  const _this = this;
  const token = this.header.token;
  jwt.verify(token,secret,(err,decode)=>{// token 验证
    if(err){
        console.log(err)
    }else{
      _this.body=decode;
    }
})
});
....

module.exports = router;

然后我们在终端输入:

curl -X POST -H "token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNjUzNTQ1NjE1LCJleHAiOjE2NTM2MzIwMTV9.hWVTqHCwQTCi14RxIvQ8a0Mq7yXkpPleni-Iptev0Po" http://127.0.0.1:8003/users/testToken 

得到如下结果。可以看到我们鉴权成功啦。

Snipaste_2022-05-26_16-48-07.png

接下来,就是关于鉴权成功和失败的分别处理。

结语

鉴权是搭建一个后端的第一步,想要搭建一个可用的后端,还需要一些数据库的基本知识。

下一章,我们学习 MySql 数据库!

如果本文对你有帮助,记得给我点个赞噢~

本文代码地址为:https://gitee.com/is-wang-fugui-rich/learn-koa-auth.git