背景
继续上篇
- 【easy篇】前端到全栈从这里开始
- 【进阶篇】koa+Mysql的全栈之旅
完成对云服务器的初步了解,以及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
一个等同于用户名和密码的,能够进行身份验证的令牌
过程
1. 客户端使用用户名和密码请求登录
2. 服务端收到请求后验证是否成功登录
- 成功:返回一个Token给客户端
- 失败:返回失败提示信息
3. 客户端收到Token后存储Token
4. 每次发起请求时将Token发给服务端
5. 服务端收到请求后,验证Token的合法性
- 成功:返回客户端所需数据
- 失败:返回验证失败的信息
流程图
CryptoJS前端加解密
crypto-js
是一个纯 javascript 写的加密算法类库 ,可以非常方便地在 javascript 进行 MD5、SHA1、SHA2、SHA3、RIPEMD-160 哈希散列,进行 AES、DES、Rabbit、RC4、Triple DES 加解密。
这里是将前端传过来的密码加密与数据库存的密码作对照。当然也可以客户端使用相同的公钥加密,然后服务端使用公钥解密然后做对照是否一致。
安装crypto-js
后,代码如下。
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
};
JWT(Json Web Tokens)
生成Token的解决方案有许多,但是我这里使用的是JWT
。Json web token (JWT)
,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:XXX.XXX.XXXX
利用OpenSSL生成私钥和公钥
划重点,后面生成令牌toekn
的时候,使用私钥加密以及后面验证的时候使用公钥解密获取用户信息
openssl genrsa -out rsa_private_key.pem 1024
openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
代码贴一下
// 引入模块依赖
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
看完了,请点赞!!!
看完了,请点赞!!!
看完了,请点赞!!!