Egg企业级实战(二)- 登录模块

2,195 阅读6分钟

本文按照完成的开发流程从实例的角度,一步步地搭建出一个开箱即用的Egg.js 应用,让我们开始吧!

完整项目地址:gitee.com/koukaile/eg…

本系列文章目录

01.登录模块

1.在controller中编写控制器

controller->新建user.js

'use strict';

/**
 * @Controller
**/

const Controller = require('egg').Controller;

class LoginController extends Controller {
  // 登录
  async login() {
    /**
    * @summary 登录
    * @description 登录接口
    * @router post /user/login
    * @request body login 配置请求携带参数
    * @Request header string token eg:write your token at here
    * @response 200 JsonResult 操作结果
    */
    const { ctx } = this;
    ctx.body = await this.service.login.login();
  }
}

module.exports = LoginController;

2.在service中编写服务层

'use strict';

const Service = require('egg').Service;

class UserService extends Service {
  //登录
  async login(){
    //your service
  }
}

module.exports = UserService;

3.前端非对称加密,后端Node.js解密

前端(vue)

在做项目中的登录功能时一般是通过form表单或者ajax方式将参数提交到服务器进行验证,在这个过程中,在前端对登录密码先进行一次加密的话,安全性肯定要优于直接提交的方式。

  • 安装:
    npm install encryptjs --save-dev
  • vue组件:
    import { JSEncrypt } from 'jsencrypt'
    踩坑备注:在vue3中发现报错,无法正常运行。 这里需要使用版本为3.0.0-rc1,新版会报错,其他版本也可尝试下,没有全部测试。
  • 方法内使用:
let encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey)//publicKey 是后台给的一个密钥
let rsaPassWord = encryptor.encrypt(password)//rsaPassWord 就是得出来的加密串

生成密匙:

这里直接使用在线生成的网站 web.chacuo.net/netrsakeypa…

后端(egg)

  • 安装依赖
    npm install --save node-jsencrypt
  • 配置
//config.default.js 配置私有密匙
//private key
config.private_key = `你的私有密匙`
  • 使用
'use strict';

const Service = require('egg').Service;
const JSEncrypt = require('node-jsencrypt')

class UserService extends Service {
  //登录
  async login(){
    const { ctx } = this
    // 设置私钥
    const prvKey = this.app.config.private_key
    let jsencrypt = new JSEncrypt()
    jsencrypt.setPrivateKey(prvKey)
    // 解密数据
    let paramsData = ctx.request.body.rsaParams
    let prvData = JSON.parse(jsencrypt.decrypt(paramsData));
    const user_number = prvData.user_number
    const user_password = prvData.user_password
    console.log(user_number,user_password)
  }
}

module.exports = UserService;

4.检测用户,并更新数据,生成token,存储用户信息

// 查找用户名
let user = await ctx.model.User.findOne({include:{as:'menu',model:ctx.model.UserLogin},where:{user_number:user_number}})
if(!user){
  return ctx.fail('账号或密码不存在')
}
//校验密码
let res = bcrypt.compareSync(user_password,user.dataValues.menu.dataValues.user_password)
if(res){
  //更新登录数据
  await ctx.model.User.update({login_num:user.dataValues.login_num+1,last_login_time:moment()},{where:{uuid:user.dataValues.uuid}});
  //生成token
  const token = app.jwt.sign({
    'uuid': user.dataValues.uuid, //需要存储的 token 数据
  }, app.config.jwt.secret, { expiresIn: '60m' }); // 60分钟token过期

  //存储session参数
  ctx.session.uid = user.dataValues.uuid;
  ctx.session.user_number = user_number;
  return ctx.success("登录成功!",{ token });
}else{
  return ctx.fail("用户名或密码错误!");
}

5.完整登录模块代码

//登录
  async login(){
    const { ctx,app } = this
    // 设置私钥
    const prvKey = this.app.config.private_key
    let jsencrypt = new JSEncrypt()
    jsencrypt.setPrivateKey(prvKey)
    // 解密数据
    let paramsData = ctx.request.body.rsaParams
    let prvData = JSON.parse(jsencrypt.decrypt(paramsData));
    const user_number = prvData.user_number
    const user_password = prvData.user_password
    // 查找用户名
    let user = await ctx.model.User.findOne({include:{as:'menu',model:ctx.model.UserLogin},where:{user_number:user_number}})
    if(!user){
      return ctx.fail('账号或密码不存在')
    }
    //校验密码
    let res = bcrypt.compareSync(user_password,user.dataValues.menu.dataValues.user_password)
    if(res){
      //更新登录数据
      await ctx.model.User.update({login_num:user.dataValues.login_num+1,last_login_time:moment()},{where:{uuid:user.dataValues.uuid}});
      //生成token
      const token = app.jwt.sign({
        'uuid': user.dataValues.uuid, //需要存储的 token 数据
      }, app.config.jwt.secret, { expiresIn: '5m' }); // 5分钟token过期

      //存储session参数
      ctx.session.uid=user.dataValues.uuid;
      ctx.session.user_number=user_number;
      return ctx.success("登录成功!",{ token });
    }else{
      return ctx.fail("用户名或密码错误!");
    }
  }

6.session存储获取不到

按官方文档测试时我们会发现,登录时设置了session,但是在其他service中获取不到,解决办法如下:

1、前端vue是用的axios发起的请求,则要如下设置:

import axios from 'axios'
axios.defaults.withCredentials=true

前台本地运行时使用127.0.0.1的形式与后台cors匹配

2、后端egg.js框架使用egg-cors插件来配置跨域

在config.default.js配置文件里按如下设置:
config.cors = {
    origin: 'http://127.0.0.1:8080',//一定要是域名端口
    credentials:true,//credentials设置为true,和前端保持一致
    allowMethods: 'GET,POST'
}

02.注册模块

1.在Controller中编写控制器

'use strict';

/**
 * @Controller
**/

const Controller = require('egg').Controller;

class LoginController extends Controller {
  // 注册
  async register() {
    /**
    * @summary 注册
    * @description 注册接口
    * @router post /user/register
    * @request body user_register 配置请求携带参数
    * @Request header string token eg:write your token at here
    * @response 200 JsonResult 操作结果
    */
    const { ctx } = this;
    ctx.body = await this.service.user.register();
  }
}

module.exports = LoginController;

2.在Service中编写业务代码

1.用户密码进行hash加salt加密处理

  • 安装bcrypt.js插件(bcryptjs是一个第三方加密库,用来实现在Node环境下的bcrypt加密) npm install bcryptjs -S
  • 引入插件
    let bcrypt = require('bcryptjs');
  • 用法示例
// 生成hash密码
const salt = bcrypt.genSaltSync(10);
const hash = bcrypt.hashSync("密码", salt);// Store hashin your password DB.
// 验证密码
bcrypt.compareSync("密码", hash); // true 
bcrypt.compareSync("密码", hash); // false 

2.注册模块完整代码

 //注册
  async register(){
    const { ctx,app } = this
    // 设置私钥
    const prvKey = this.app.config.private_key
    let jsencrypt = new JSEncrypt()
    jsencrypt.setPrivateKey(prvKey)
    // 解密数据
    let paramsData = ctx.request.body.rsaParams
    let prvData = JSON.parse(jsencrypt.decrypt(paramsData));
    //查找注册账号是否存在
    const is_user_numnber = await ctx.model.User.findOne({where:{user_number:prvData.user_number}})
    if(!prvData.user_number||prvData.user_number == ''){
      return ctx.warn('账号不能为空')
    }else if(!prvData.user_password||prvData.user_password == ''){
      return ctx.warn('密码不能为空')
    }else if(is_user_numnber!=null){
      return ctx.warn('账号已被注册')
    } else{
      //对密码进行hash加密,salt加盐
      const salt = bcrypt.genSaltSync(10)
      const user_password = bcrypt.hashSync(prvData.user_password, salt);
      let res = {};
      const params = {
        uuid:ctx.helper.uuidSet(),//此方法为扩展方法,在/extend/helper.js 中,用于生成uuid
        user_name:ctx.helper.makeName(),//此方法为扩展方法,在/extend/helper.js 中,用于生成随机昵称
        user_number:prvData.user_number,
        user_password:user_password,
        user_type:0,
        salt:salt,
        create_time:new Date()
      }
      try {
          await ctx.model.UserLogin.create(params)
          res = await ctx.model.User.create(params);
          ctx.logger.info(res)
          return ctx.success('注册成功');//此方法为扩展方法,在/extend/context.js 中,用于处理返回数据
      } catch (err) {
          ctx.logger.error(err);
          return ctx.fail(err)
      }
    }
  }

03.退出登录

1.在controller中编写控制器

'use strict';

/**
 * @Controller
**/

const Controller = require('egg').Controller;

class LoginController extends Controller {
  // 退出登录
  async logout() {
    /**
    * @summary 退出登录
    * @description 退出登录接口
    * @router post /user/logout
    * @Request header string token eg:write your token at here
    * @response 200 JsonResult 操作结果
    */
    const { ctx } = this;
    ctx.body = await this.service.user.logout();
  }
}

module.exports = LoginController;

2.在service中编写业务

'use strict';

const Service = require('egg').Service;

class UserService extends Service {
  //退出登录
  async logout(){
    const { ctx } = this
    const token = ctx.request.header.token
    let uuid =  ctx.helper.uuidGet(token) //自定义扩展件用来获取token并解密
    let user = await ctx.model.User.findOne({where:{uuid:uuid}})
    if(user){
      //清空登录session信息
      ctx.session.uuid = null
      ctx.session.user_number = null
      return ctx.success("登出成功!");
    }else{
      return ctx.fail("登出失败!");
    }
  }
}

module.exports = UserService;

04.jwt校验用户登录

1.安装jwt

npm install egg-cors egg-jwt --save

2.配置jwt

//config/plugin.js
//jwt
jwt: {
  enable: true,
  package: 'egg-jwt',
},
//config/config.default.js
config.jwt = {
  secret: '123456',
};

3.编写jwtErr中间件

//middleware/jwtErr.js
module.exports = (options) => {
  return async function jwtErr(ctx, next) {
      const token = ctx.request.header.token;
      let decode = '';
      if (token) {
        try {
          // 解码token
          decode = ctx.app.jwt.verify(token, options.secret);
          await next();
        } catch (error) {
          ctx.status = 401;
          ctx.body = {
            massage: 'token已过期,请重新登录',
            code: -1,
          };
          return;
        }
      } else {
        ctx.status = 401;
        ctx.body = {
          message: 'token不存在',
          code: -1,
        };
      }
    };
}

4.使用

在需要检验用户登录才能访问的路由处添加中间件拦截

'use strict';

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
  const { router, controller , middleware } = app;
  // 重定向到swagger
  router.redirect('/', '/swagger-ui.html', 302);
  // middleware
  const jwtErr = middleware.jwtErr(app.config.jwt)
  // rotuer module
  router.post('/home/test',jwtErr,controller.home.test);
  /* user */
  //register
  router.post('/user/register',controller.user.register);
  // login
  router.post('/user/login', controller.user.login);
  // logout
  router.post('/user/logout', controller.user.logout);
};

5.vue前台处理

import axios from 'axios'
import DOMAIN_NAME from './config.js'
import router from '../router'
/*
* 请求拦截器
*/
axios.interceptors.request.use((config) => {
  // 预处理请求信息
  console.log('RequestMessage:',config,sessionStorage.getItem('token'))
  return config
}, (error) => {
  // 预处理请求异常时抛出error
  return Promise.reject(error)
})

/*
* 响应拦截器
*/
axios.interceptors.response.use((res) => {
  // 进行响应事件处理
  const status = parseInt(res.status)
  switch (status) {
    case 200 :return res;
    default:return Promise.reject({ code: status, msg: res.data.data.message });
  }
}, (error) => {
  const status = (error.toString().substring(error.toString().length-3,error.toString().length))
  switch(status){
    case '404':
     console.error('request地址不存在~')
     //router.push({path:'/error',query:{status:404}})
    break;
    case '401':
     console.error('用户校验失败,请重新登录~')
     router.push('/login')
    break;
    case '500':
     console.error('服务器内部错误~')
     //router.push({path:'/error',query:{status:500}})
    break;
  }
  return Promise.reject(error)
})

/**
 * 返回axios方法
 * @param url(如果传绝对地址则baseURL不会追加到url之前)
 * @param method
 * @param timeout
 * @param data
 * @param headers
 * @param dataType
 * @returns {AxiosPromise}
 */
export default function (url, {
  // 默认求情方式post
  method = 'post',
  // 超时
  timeout = 2000,
  // 请求主题
  data = {},
  // 请求头
  headers = {
    'Content-Type':DOMAIN_NAME.REQUEST_HEADER.application,
    'authorization':sessionStorage.getItem('token'),
    'token':sessionStorage.getItem('token')
    },
  // 文件类型
  dataType = 'json'
}) {
  const config = {
    method: method,
    timeout: timeout,
    url: url,
    baseURL: DOMAIN_NAME.URL_SERVER_MICRO,
    data: data,
    headers: headers,
    dataType: dataType,
    withCredentials:true
  }
  return axios(config)
}

05.全局错误处理

1.配置

//middleware/error_handler.js
/**
 * 统一错误处理
 * @returns {Function}
 */
module.exports = () => {
  return async function errorHandler(ctx) {
      //记录错误日志
      ctx.logger.error(ctx.response)

      const status = ctx.status || 500;
      // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
      const error = status === 500 && ctx.app.config.env === 'prod'? '服务器异常,请联系客服。': ctx.response.message;
      // 处理错误类型
      if (ctx.acceptJSON) {
        switch(status){
          case 404:
            ctx.status = 404
            ctx.body = {code:'-1', message: 'Not Found' };
          break;
          case 500: 
            ctx.status = 500
            ctx.body = {code:'-1', message: JSON.stringify(error) };
          break;
          default:
            ctx.status = status
            ctx.body = {code:'-1', message: ctx.error };
          break;
        }
      } else {
          await ctx.render('500',{msg:JSON.stringify(error)});
      }
  };
};

2.使用

在config.default.js中添加全局中间件

// add your middleware config here
config.middleware = ['errorHandler'];

06.总结

如果上面的都搭建完成了,那么node框架登录模块就已经搭建成功啦!快去尝试吧~
看完觉得不错的话,就给点个赞吧~