我正在参加「初夏创意投稿大赛」详情请看:初夏创意投稿大赛
本文代码基于本人上篇文章:夏天到了,让我手摸手教你快速入门 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.服务端:数据 + 私钥 -> 签名
- 签名 + 数据 传送到客户端。
当客户端再次请求的时候,它会带着 数据 + 签名。
由于私钥不变且外部不可知,这时候,我们把 数据+私钥,再加密一次。
注意,根据现有的加密算法,对同一字符串加密得到的结果是一样的。这时候,我们把它和签名作比较,如果一样,就说明这个 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\"}
得到如下结果:
已经成功验证客户了,接下来,我们完成鉴权处理。
生成 token
下载相关插件。
npm i jsonwebtoken -D
这两个插件分别用于加密生成 token ,和 检查请求中所带的 token 是否合法。
我们使用 用户 id 作为数据, 然后生成一个 token 返回。正常来说,我们生成 token 后,需要给 客户端发送命令,让客户端 将 token 存在 cookie 中,每次请求的时候,都将 token 带上。
像这样:
但是由于我们暂时还没有写客户端,因此,我们暂时设定流程如下:
用户登录成功后,返回 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\"}
得到如下结果:
我们已经成功生成 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
得到如下结果。可以看到我们鉴权成功啦。
接下来,就是关于鉴权成功和失败的分别处理。
结语
鉴权是搭建一个后端的第一步,想要搭建一个可用的后端,还需要一些数据库的基本知识。
下一章,我们学习 MySql 数据库!
如果本文对你有帮助,记得给我点个赞噢~
本文代码地址为:https://gitee.com/is-wang-fugui-rich/learn-koa-auth.git