背景
继续上篇
完成对云服务器的初步了解,以及nginx
的配置,以及MySQL
安装,建表操作以及使用Node.js框架Koa
进行简单的数据增删改查
功能。 但是针对一个完善一点网站,还需要一个权限认证(登入,登出,注册,api的身份验证),接下来就是Node写的jwt(jsonwebtoken)
搭建token身份验证模块。
目标功能
- 登录
- 注册
- 登出
- 所有API的身份验证
// login
const User = require('../controller/user');
router.post('/user/login', User.login);
router.post('/user/register', User.register);
router.get('/user/loginout', User.loginOut);
复制代码
基于Token
一个等同于用户名和密码的,能够进行身份验证的令牌
过程
```javascript
- 客户端使用用户名和密码请求登录
- 服务端收到请求后验证是否成功登录
- 成功:返回一个Token给客户端
- 失败:返回失败提示信息
- 客户端收到Token后存储Token
- 每次发起请求时将Token发给服务端
- 服务端收到请求后,验证Token的合法性
- 成功:返回客户端所需数据
- 失败:返回验证失败的信息
复制代码 ```流程图
CryptoJS前端加解密
crypto-js
是一个纯 javascript 写的加密算法类库 ,可以非常方便地在 javascript 进行 MD5、SHA1、SHA2、SHA3、RIPEMD-160 哈希散列,进行 AES、DES、Rabbit、RC4、Triple DES 加解密。
这里是将前端传过来的密码加密与数据库存的密码作对照。当然也可以客户端使用相同的公钥加密,然后服务端使用公钥解密然后做对照是否一致。
安装crypto-js
后,代码如下。 ```javascript const CryptoJS = require(‘crypto-js’); /**
- 加密 / function encrypt(word) { const key = CryptoJS.enc.Utf8.parse(‘yyq1234567890yyq’);//16位随机公钥 const srcs = CryptoJS.enc.Utf8.parse(word); const encrypted = CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); } /*
- 解密
*/
function decrypt(word) {
// 需要16位
const key = CryptoJS.enc.Utf8.parse(‘yyq1234567890yyq’);//16位随机公钥
const srcs = CryptoJS.enc.Utf8.stringify(word);
const decrypt = CryptoJS.AES.decrypt(srcs, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
console.log(decrypt);
return CryptoJS.enc.Utf8.stringify(decrypt).toString();
}
module.exports = {
encrypt,
decrypt
};
复制代码
openssl genrsa -out rsa_private_key.pem 1024 openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem 复制代码<a name="OvfV3"></a>
## JWT(Json Web Tokens)
生成Token的解决方案有许多,但是我这里使用的是`JWT`。`Json web token (JWT)`,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:`XXX.XXX.XXXX` ![image.svg](https://cdn.nlark.com/yuque/0/2020/svg/1170748/1605357761604-e047fcba-ea21-49d7-8b1c-32911c970f77.svg#align=left&display=inline&height=600&margin=%5Bobject%20Object%5D&name=image.svg&originHeight=600&originWidth=800&size=106&status=done&style=none&width=800)
<a name="osiCe"></a>
#### 利用OpenSSL生成私钥和公钥
划重点,后面生成令牌`toekn`的时候,使用私钥加密以及后面验证的时候使用公钥解密获取用户信息
<a name="11UGd"></a>
### 代码贴一下
```javascript
// 引入模块依赖
const fs = require('fs');
const path = require('path');
//jsonwebtoken需要安装一下
const jwt = require('jsonwebtoken');
// 创建 token 类
class Jwt {
constructor(data) {
//token需要带上的信息 例如:用户id
this.data = data;
}
// 生成token
generateToken() {
const data = this.data;
const created = Date.now();
//私钥 加密
const cert = fs.readFileSync(path.join(__dirname, '../pem/rsa_private_key.pem')); // 私钥 可以自己生成
const token = jwt.sign(
{
data,
exp: created + 60 * 30 * 1000
},
cert,
{ algorithm: 'RS256' }
);
return token;
}
// 校验token
verifyToken() {
const token = this.data;
const cert = fs.readFileSync(path.join(__dirname, '../pem/rsa_public_key.pem')); // 公钥 可以自己生成
let res;
try {
//公钥 解密
const result = jwt.verify(token, cert, { algorithms: ['RS256'] }) || {};
const { exp = 0 } = result;
const current = Date.now();
//验证时效性
if (current <= exp) {
res = result.data || {};
}
} catch (e) {
res = 'err';
}
return res;
}
}
module.exports = Jwt;
复制代码
所有API的身份验证
const passUrl = ['/user/login', '/user/register', '/404', '/user/loginout'];
app.use(async(ctx, next) => {
// 我这里知识把登陆和注册请求去掉了,其他的多有请求都需要进行token校验
if (!~passUrl.findIndex(item => ctx.request.url === item)) {
const token = ctx.headers.token;
if (!token) {
ctx.body = { status: 403, msg: 'token不能为空' };
}
const jwt = new JwtUtil(token);
const result = jwt.verifyToken();
// 如果考验通过就next,否则就返回登陆信息不正确
if (result == 'err' || !result) {
ctx.body = { status: 403, msg: '登录已过期,请重新登录' };
return false;
} else {
// 可解析出用户id
console.log(result);
// 查询Id 再验证token
const res = await User.findOne({
where: { id: Number(result) }
});
// Token不存在或者不一致
if (res.token !== token || !res.token) {
ctx.body = { status: 403, msg: '登录已过期,请重新登录' };
return false;
}
}
}
await next();
});
复制代码
登录API
判断用户名和密码是否正确,然后生成并保存token,再返回给客户端// 登录
const login = async ctx => {
const bodyData = ctx.request.body || {};
const userName = bodyData.userName;
const passWord = bodyData.passWord;
if (!userName || !passWord) {
ctx.body = {
code: 300,
msg: '用户名密码不能为空!'
};
return false;
}
try {
let result = await User.findAll({
where: {
userName: userName
}
});
if (result.length) {
result = result[0];
console.log(result);
// 利用aes 密码解密来判断密码是否正确
const aes = encryptionAndDecryption.encrypt(passWord);
if (result.passWord === aes) {
// 登陆成功,添加token验证
const _id = result.id.toString();
// 将用户id传入并生成token
const jwt = new JwtUtil(_id);
const token = jwt.generateToken();
console.log('login id:' + _id);
console.log('login token:' + token);
// 将token存入 后面可以改成redis缓存
const updateRes = await User.update(
{
token
},
{
where: {
id: _id
}
}
);
// 将 token 返回给客户端
ctx.body = { status: 200, msg: '登陆成功', token: token };
} else {
ctx.body = { status: 500, msg: '账号密码错误' };
return false;
}
} else {
ctx.body = { status: 500, msg: '账号密码错误' };
}
} catch (error) {
ctx.body = { status: 500, msg: error };
}
};
复制代码
注册API
简单的注册,判断用户名是否存在,然后生成并保存token,再返回给客户端// 注册
const register = async ctx => {
const bodyData = ctx.request.body || {};
const userName = bodyData.userName;
const passWord = bodyData.passWord;
if (!userName || !passWord) {
ctx.body = {
code: 300,
msg: '用户名密码不能为空!'
};
return false;
}
try {
const result = await User.findAll({
where: {
userName: userName
}
});
if (result.length) {
ctx.body = {
code: 300,
msg: '用户名已存在'
};
return false;
}
// 更新数据库
const res = await User.create({
userName,
passWord: encryptionAndDecryption.encrypt(passWord)
});
// 登陆成功,添加token验证
const _id = res.dataValues.id.toString();
// 将用户id传入并生成token
const jwt = new JwtUtil(_id);
const token = jwt.generateToken();
// 将token存入
await User.update(
{ token },
{
where: {
id: _id
}
}
);
ctx.body = {
code: 100,
data: '创建成功',
token: token
};
} catch (err) {
ctx.body = {
code: 300,
data: err
};
}
};
复制代码
登出API
清空服务端存储的token记录const loginOut = async ctx => {
const jwt = new JwtUtil(ctx.headers.token);
const result = jwt.verifyToken();
// 将token存入
const res = await User.update(
{ token: '' },
{
where: {
id: result
}
}
);
console.log(res);
ctx.body = {
code: 100,
msg: '登出成功'
};
};
复制代码
数据库User表结构
const sequelize = require('../utils/sequelize');
const Sequelize = require('sequelize');
const moment = require('moment');
// 定义表结构
const user = sequelize.define(
'user',
{
id: {
type: Sequelize.INTEGER(11), // 设置字段类型
primaryKey: true, // 设置为主键
autoIncrement: true // 自增
},
userName: {
type: Sequelize.STRING
},
passWord: {
type: Sequelize.STRING
},
token: {
type: Sequelize.TEXT,
allowNull: true
},
createdAt: {
type: Sequelize.DATE,
defaultValue: Sequelize.NOW,
get() {
// this.getDataValue 获取当前字段value
return moment(this.getDataValue('createdAt')).format('YYYY-MM-DD HH:mm');
}
},
updatedAt: {
type: Sequelize.DATE,
defaultValue: Sequelize.NOW,
get() {
return moment(this.getDataValue('updatedAt')).format('YYYY-MM-DD HH:mm');
}
}
},
{
// sequelize会自动使用传入的模型名(define的第一个参数)的复数做为表名 设置true取消默认设置
freezeTableName: true
}
);
module.exports = user;
复制代码
多想一下
其实这里还可以加上菜单权限
,在访问登录接口成功,生成token返回的同时,查询到权限列表返回给客户端。另外再编写一个\auth
路由来获取权限列表。当然这个过程还是通过Token
获取用户信息。//前后端约定一套权限码
//服务端返回用户具有的权限CODE
// 例如:
[
'USER', //用户菜单
'MY', // 我的菜单
]
//客户端获取之后通过服务端返回的权限码进行判断渲染对应的内容
复制代码
关于这个项目
- 服务端收到请求后验证是否成功登录
在编写调试Node.js项目,修改代码后,需要频繁的手动close掉,然后再重新启动,非常不方便。所以推荐使用
Node自动重启工具 nodemon
//全局安装一下
npm install -g nodemon
//使用方法
//app.js入口文件 8181
//端号,也可以不带
nodemon app.js 8181
复制代码
还有就是ES6语法和代码格式化,这里不再赘述。安装
babel
和eslint
即可。最后
这里主要记录代码过程,有些概念没有细说,参考资料如下:JWT , crypto-js
附上代码地址[https://gitee.com/wisdom_QQ/koa](https://gitee.com/wisdom_QQ/koa)
看完了,请点赞!!!
看完了,请点赞!!!
看完了,请点赞!!!