背景

继续上篇

 完成对云服务器的初步了解,以及nginx的配置,以及MySQL安装,建表操作以及使用Node.js框架Koa进行简单的数据增删改查功能。 但是针对一个完善一点网站,还需要一个权限认证(登入,登出,注册,api的身份验证),接下来就是Node写的jwt(jsonwebtoken) 搭建token身份验证模块。

目标功能

  • 登录
  • 注册
  • 登出
  • 所有API的身份验证
    1. // login
    2. const User = require('../controller/user');
    3. router.post('/user/login', User.login);
    4. router.post('/user/register', User.register);
    5. router.get('/user/loginout', User.loginOut);
    6. 复制代码

    基于Token

     一个等同于用户名和密码的,能够进行身份验证的令牌

    过程

    ```javascript
  1. 客户端使用用户名和密码请求登录
    1. 服务端收到请求后验证是否成功登录
      • 成功:返回一个Token给客户端
      • 失败:返回失败提示信息
    2. 客户端收到Token后存储Token
    3. 每次发起请求时将Token发给服务端
    4. 服务端收到请求后,验证Token的合法性
      • 成功:返回客户端所需数据
      • 失败:返回验证失败的信息
        复制代码 ```

        流程图

        image.svg

        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 }; 复制代码
        1. <a name="OvfV3"></a>
        2. ## JWT(Json Web Tokens)
        3.  生成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)
        4. <a name="osiCe"></a>
        5. #### 利用OpenSSL生成私钥和公钥
        6.  划重点,后面生成令牌`toekn`的时候,使用私钥加密以及后面验证的时候使用公钥解密获取用户信息
        openssl genrsa -out rsa_private_key.pem 1024 openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem 复制代码
        1. <a name="11UGd"></a>
        2. ### 代码贴一下
        3. ```javascript
        4. // 引入模块依赖
        5. const fs = require('fs');
        6. const path = require('path');
        7. //jsonwebtoken需要安装一下
        8. const jwt = require('jsonwebtoken');
        9. // 创建 token 类
        10. class Jwt {
        11. constructor(data) {
        12. //token需要带上的信息 例如:用户id
        13. this.data = data;
        14. }
        15. // 生成token
        16. generateToken() {
        17. const data = this.data;
        18. const created = Date.now();
        19. //私钥 加密
        20. const cert = fs.readFileSync(path.join(__dirname, '../pem/rsa_private_key.pem')); // 私钥 可以自己生成
        21. const token = jwt.sign(
        22. {
        23. data,
        24. exp: created + 60 * 30 * 1000
        25. },
        26. cert,
        27. { algorithm: 'RS256' }
        28. );
        29. return token;
        30. }
        31. // 校验token
        32. verifyToken() {
        33. const token = this.data;
        34. const cert = fs.readFileSync(path.join(__dirname, '../pem/rsa_public_key.pem')); // 公钥 可以自己生成
        35. let res;
        36. try {
        37. //公钥 解密
        38. const result = jwt.verify(token, cert, { algorithms: ['RS256'] }) || {};
        39. const { exp = 0 } = result;
        40. const current = Date.now();
        41. //验证时效性
        42. if (current <= exp) {
        43. res = result.data || {};
        44. }
        45. } catch (e) {
        46. res = 'err';
        47. }
        48. return res;
        49. }
        50. }
        51. module.exports = Jwt;
        52. 复制代码

        所有API的身份验证

        1. const passUrl = ['/user/login', '/user/register', '/404', '/user/loginout'];
        2. app.use(async(ctx, next) => {
        3. // 我这里知识把登陆和注册请求去掉了,其他的多有请求都需要进行token校验
        4. if (!~passUrl.findIndex(item => ctx.request.url === item)) {
        5. const token = ctx.headers.token;
        6. if (!token) {
        7. ctx.body = { status: 403, msg: 'token不能为空' };
        8. }
        9. const jwt = new JwtUtil(token);
        10. const result = jwt.verifyToken();
        11. // 如果考验通过就next,否则就返回登陆信息不正确
        12. if (result == 'err' || !result) {
        13. ctx.body = { status: 403, msg: '登录已过期,请重新登录' };
        14. return false;
        15. } else {
        16. // 可解析出用户id
        17. console.log(result);
        18. // 查询Id 再验证token
        19. const res = await User.findOne({
        20. where: { id: Number(result) }
        21. });
        22. // Token不存在或者不一致
        23. if (res.token !== token || !res.token) {
        24. ctx.body = { status: 403, msg: '登录已过期,请重新登录' };
        25. return false;
        26. }
        27. }
        28. }
        29. await next();
        30. });
        31. 复制代码

        登录API

         判断用户名和密码是否正确,然后生成并保存token,再返回给客户端
        1. // 登录
        2. const login = async ctx => {
        3. const bodyData = ctx.request.body || {};
        4. const userName = bodyData.userName;
        5. const passWord = bodyData.passWord;
        6. if (!userName || !passWord) {
        7. ctx.body = {
        8. code: 300,
        9. msg: '用户名密码不能为空!'
        10. };
        11. return false;
        12. }
        13. try {
        14. let result = await User.findAll({
        15. where: {
        16. userName: userName
        17. }
        18. });
        19. if (result.length) {
        20. result = result[0];
        21. console.log(result);
        22. // 利用aes 密码解密来判断密码是否正确
        23. const aes = encryptionAndDecryption.encrypt(passWord);
        24. if (result.passWord === aes) {
        25. // 登陆成功,添加token验证
        26. const _id = result.id.toString();
        27. // 将用户id传入并生成token
        28. const jwt = new JwtUtil(_id);
        29. const token = jwt.generateToken();
        30. console.log('login id:' + _id);
        31. console.log('login token:' + token);
        32. // 将token存入 后面可以改成redis缓存
        33. const updateRes = await User.update(
        34. {
        35. token
        36. },
        37. {
        38. where: {
        39. id: _id
        40. }
        41. }
        42. );
        43. // 将 token 返回给客户端
        44. ctx.body = { status: 200, msg: '登陆成功', token: token };
        45. } else {
        46. ctx.body = { status: 500, msg: '账号密码错误' };
        47. return false;
        48. }
        49. } else {
        50. ctx.body = { status: 500, msg: '账号密码错误' };
        51. }
        52. } catch (error) {
        53. ctx.body = { status: 500, msg: error };
        54. }
        55. };
        56. 复制代码

        注册API

         简单的注册,判断用户名是否存在,然后生成并保存token,再返回给客户端
        1. // 注册
        2. const register = async ctx => {
        3. const bodyData = ctx.request.body || {};
        4. const userName = bodyData.userName;
        5. const passWord = bodyData.passWord;
        6. if (!userName || !passWord) {
        7. ctx.body = {
        8. code: 300,
        9. msg: '用户名密码不能为空!'
        10. };
        11. return false;
        12. }
        13. try {
        14. const result = await User.findAll({
        15. where: {
        16. userName: userName
        17. }
        18. });
        19. if (result.length) {
        20. ctx.body = {
        21. code: 300,
        22. msg: '用户名已存在'
        23. };
        24. return false;
        25. }
        26. // 更新数据库
        27. const res = await User.create({
        28. userName,
        29. passWord: encryptionAndDecryption.encrypt(passWord)
        30. });
        31. // 登陆成功,添加token验证
        32. const _id = res.dataValues.id.toString();
        33. // 将用户id传入并生成token
        34. const jwt = new JwtUtil(_id);
        35. const token = jwt.generateToken();
        36. // 将token存入
        37. await User.update(
        38. { token },
        39. {
        40. where: {
        41. id: _id
        42. }
        43. }
        44. );
        45. ctx.body = {
        46. code: 100,
        47. data: '创建成功',
        48. token: token
        49. };
        50. } catch (err) {
        51. ctx.body = {
        52. code: 300,
        53. data: err
        54. };
        55. }
        56. };
        57. 复制代码

        登出API

         清空服务端存储的token记录
        1. const loginOut = async ctx => {
        2. const jwt = new JwtUtil(ctx.headers.token);
        3. const result = jwt.verifyToken();
        4. // 将token存入
        5. const res = await User.update(
        6. { token: '' },
        7. {
        8. where: {
        9. id: result
        10. }
        11. }
        12. );
        13. console.log(res);
        14. ctx.body = {
        15. code: 100,
        16. msg: '登出成功'
        17. };
        18. };
        19. 复制代码

        数据库User表结构

        1. const sequelize = require('../utils/sequelize');
        2. const Sequelize = require('sequelize');
        3. const moment = require('moment');
        4. // 定义表结构
        5. const user = sequelize.define(
        6. 'user',
        7. {
        8. id: {
        9. type: Sequelize.INTEGER(11), // 设置字段类型
        10. primaryKey: true, // 设置为主键
        11. autoIncrement: true // 自增
        12. },
        13. userName: {
        14. type: Sequelize.STRING
        15. },
        16. passWord: {
        17. type: Sequelize.STRING
        18. },
        19. token: {
        20. type: Sequelize.TEXT,
        21. allowNull: true
        22. },
        23. createdAt: {
        24. type: Sequelize.DATE,
        25. defaultValue: Sequelize.NOW,
        26. get() {
        27. // this.getDataValue 获取当前字段value
        28. return moment(this.getDataValue('createdAt')).format('YYYY-MM-DD HH:mm');
        29. }
        30. },
        31. updatedAt: {
        32. type: Sequelize.DATE,
        33. defaultValue: Sequelize.NOW,
        34. get() {
        35. return moment(this.getDataValue('updatedAt')).format('YYYY-MM-DD HH:mm');
        36. }
        37. }
        38. },
        39. {
        40. // sequelize会自动使用传入的模型名(define的第一个参数)的复数做为表名 设置true取消默认设置
        41. freezeTableName: true
        42. }
        43. );
        44. module.exports = user;
        45. 复制代码

        多想一下

         其实这里还可以加上菜单权限,在访问登录接口成功,生成token返回的同时,查询到权限列表返回给客户端。另外再编写一个\auth路由来获取权限列表。当然这个过程还是通过Token获取用户信息。
        1. //前后端约定一套权限码
        2. //服务端返回用户具有的权限CODE
        3. // 例如:
        4. [
        5. 'USER', //用户菜单
        6. 'MY', // 我的菜单
        7. ]
        8. //客户端获取之后通过服务端返回的权限码进行判断渲染对应的内容
        9. 复制代码

        关于这个项目

  • 在编写调试Node.js项目,修改代码后,需要频繁的手动close掉,然后再重新启动,非常不方便。所以推荐使用Node自动重启工具 nodemon

    1. //全局安装一下
    2. npm install -g nodemon
    3. //使用方法
    4. //app.js入口文件 8181
    5. //端号,也可以不带
    6. nodemon app.js 8181
    7. 复制代码
  • 还有就是ES6语法和代码格式化,这里不再赘述。安装babeleslint即可。

    最后

    这里主要记录代码过程,有些概念没有细说,参考资料如下:JWT , crypto-js
    【全栈之旅】NodeJs登录流程以及Token身份验证 - 图2
    附上代码地址[https://gitee.com/wisdom_QQ/koa](https://gitee.com/wisdom_QQ/koa)
    看完了,请点赞!!!
    看完了,请点赞!!!
    看完了,请点赞!!!