本文按照完成的开发流程从实例的角度,一步步地搭建出一个开箱即用的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框架登录模块就已经搭建成功啦!快去尝试吧~
看完觉得不错的话,就给点个赞吧~