【全栈之旅】NodeJs登录流程以及Token身份验证

4,902

背景

继续上篇

  • 【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的解决方案有许多,但是我这里使用的是JWTJson 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语法和代码格式化,这里不再赘述。安装babeleslint即可。

最后

这里主要记录代码过程,有些概念没有细说,参考资料如下:JWT , crypto-js

 图片不见了。。。

附上代码地址https://gitee.com/wisdom_QQ/koa

看完了,请点赞!!!
看完了,请点赞!!!
看完了,请点赞!!!