node.js express mongoose用户建模、权限校验

目录

userModel.js

依赖引入

数据建模

中间件

模型方法

创建user model并导出

catchAsync.js

authController.js

依赖引入

token生成

注册

登录

密码修改

userRoutes.js

路由设计

protect中间件

角色中间件

app.js


userModel.js

依赖引入

const mongoose = require('mongoose');  
const validator = require('validator'); // 数据校验
const bcrypt = require('bcryptjs');  // 密码加密

数据建模

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, '请输入用户名!']
  },
  email: {
    type: String,
    required: [true, '请提供邮箱'],
    unique: true,
    lowercase: true,
    validate: [validator.isEmail, '请提供一个有效的邮箱']
  },
  photo: String,
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  },
  password: {
    type: String,
    required: [true, '请提供密码'],
    minlength: 8,
    select: false
  },
  passwordConfirm: {
    type: String,
    required: [true, '请提供确认密码'],
    validate: {
      validator: function(el) {
        return el === this.password;
      },
      message: '密码不一致!'
    }
  },
  passwordChangedAt: Date,
  active: {
    type: Boolean,
    default: true,
    select: false
  }
});

中间件

userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next(); 
  // 每次修改密码保存文档之前,包括文档第一次入库,执行这个中间件。
  // this指向当前文档,isModified为文档的内置方法,用于判断被修改过的password字段是否和数据库里的一致


  this.password = await bcrypt.hash(this.password, 12);  // 使用bcrypt库对密码加密
  this.passwordConfirm = undefined;  // 不存储确认密码的value
  next();
});


//除第一次,每次修改密码执行这个,记录密码修改的时间戳
userSchema.pre('save', function(next) {
  if (!this.isModified('password') || this.isNew) return next();
  this.passwordChangedAt = Date.now();
  next();
});


 // 当对这个model进行query搜索的时候过滤掉active为false的文档
userSchema.pre(/^find/, function(next) {
  this.find({ active: { $ne: false } }); 
  next();
});

pre中间件允许在mongoose执行对mongoDb的操作(例如保存、查询等)之前执行一些逻辑,可以匹配正则。根据定义的中间件代码顺序执行。

模型方法

//在登录的时候被model调用,用于对比传入的密码是否匹配存入数据库加密后的密码。
userSchema.methods.correctPassword = async function(
  candidatePassword,
  userPassword
) {
  return await bcrypt.compare(candidatePassword, userPassword);
};


userSchema.methods.changedPasswordAfter = function(JWTTimestamp) {// 下面会解释
  if (this.passwordChangedAt) {
    const changedTimestamp = parseInt(
      this.passwordChangedAt.getTime() / 1000,
      10
    );

    return JWTTimestamp < changedTimestamp;
  }
  return false;
};

定义的模型方法可以被model所调用

创建user model并导出

const User = mongoose.model('User', userSchema);

module.exports = User;

catchAsync.js

module.exports = fn => {
  return (req, res, next) => {
    fn(req, res, next).catch(next);
  };
};

所有的中间件都被这个错误捕获中间件所包裹,原因是简化以下的操作。比如mongoose进行mongoDb的操作,await等待执行结果的时候如果出错,或者其他非mongoose的操作,只要是中间件执行出错,导致fn的状态变更为rejected,就会走catch,捕获error然后next(error)将error传递给全局错误处理中间件,被err参数所接收。

async (req, res, next) => {
 try {
  await model.method(param)
 } catch (error) {
  next(error)
 }
}

如果所有的controller都try...catch势必会造成代码冗余,所以创建一个统一的错误捕获中间件。


authController.js

依赖引入

const { promisify } = require('util');
const jwt = require('jsonwebtoken');
const User = require('./../models/userModel');
const catchAsync = require('./../utils/catchAsync');  // 一个统一捕获错误的中间件生成函数
const AppError = require('./../utils/appError');   // 错误处理

token生成

const createSendToken = (user, statusCode, res) => {
  const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, {
    expiresIn: process.env.JWT_EXPIRES_IN
  });
  user.password = undefined;
  res.status(statusCode).json({
    status: 'success',
    token,
    data: {
      user
    }
  });
};

通过jwt第三方库传入唯一id生成token

注册

exports.signup = catchAsync(async (req, res, next) => {
  const newUser = await User.create({
    name: req.body.name,
    email: req.body.email,
    password: req.body.password,
    passwordConfirm: req.body.passwordConfirm
  });

  createSendToken(newUser, 201, res);
});

登录

exports.login = catchAsync(async (req, res, next) => {
  const { email, password } = req.body;
  // 通过app.use(express.json());注册中间件可以读到request body

  if (!email || !password) {
    return next(new AppError('请提供账号或密码!', 400));
  }
  const user = await User.findOne({ email }).select('+password'); //mongoose查询

  if (!user || !(await user.correctPassword(password, user.password))) {
    return next(new AppError('邮箱或密码不正确', 401));
  }

  createSendToken(user, 200, res);
});

密码修改

exports.updatePassword = catchAsync(async (req, res, next) => {
  const user = await User.findById(req.user.id).select('+password');

  if (!(await user.correctPassword(req.body.passwordCurrent, user.password))) {
    return next(new AppError('当前密码不正确.', 401));
  }

  user.password = req.body.password;
  user.passwordConfirm = req.body.passwordConfirm;
  await user.save();

  createSendToken(user, 200, res);
});

userRoutes.js

用户身份校验

每当用户注册或登录的时候会得到一个token,当用户携带token请求头访问用户资源的时候服务端会进行校验

路由设计

userRoutes.js

如果访问 xxxxx/users/getUser接口就得通过protect中间件,然后经过用户身份权限校验中间件,最后到达getUser

protect中间件

exports.protect = catchAsync(async (req, res, next) => {
  let token;
  if (     //解析请求头
    req.headers.authorization &&
    req.headers.authorization.startsWith('Bearer')
  ) {
    token = req.headers.authorization.split(' ')[1];
  }
  if (!token) { //如果token不存在会走错误处理
    return next(
      new AppError('您还没有注册或登录,无访问权限', 401)
    );
  }
  const decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET);
  //校验token,token过期或错误会走错误处理

  
  const currentUser = await User.findById(decoded.id);  // 解析出id
  if (!currentUser) {
     // 如果没有查出用户,说明用户被删除了,则进行错误处理
    return next(new AppError('用户已不存在', 401));
  }
 
  if (currentUser.changedPasswordAfter(decoded.iat)) {  
     // 用户没被删除,但是有过密码修改的操作,进行错误处理
     // changedPasswordAfter 是model自定义方法,传入jwt的生成时间,和修改密码的时间戳做判断
     // 如果 修改密码的时间戳 > jwt的生成日期iat(时间戳) 则返回true

    return next(new AppError('用户最近已修改过密码,请稍后登录.', 401));
  }

  req.user = currentUser;  // 验证通过,对req赋予user的信息,接下来的路由会继承req.user
  next();  // 执行下一个中间件
});

角色中间件

exports.restrictTo = (...roles) => {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return next(
        new AppError('您没有执行此操作的权限', 403)
      );
    }
    next();
  };
};

app.js

const userRouter = require('./routes/userRoutes');
//..... 引入的其他子路由或第三方中间件的库

const app = express();
app.use(express.json({ limit: '10kb' }));  // 限制body的传输大小为10kb
//.... 应用的其他中间件


app.use('/api/v1/users', userRouter); // 定义路由
//... 创建其他路由

app.use(globalErrorHandler);  // 全局错误处理中间件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/238750.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

解决canvas清晰度问题devicePixelRatio

视频教程 解决canvas清晰度的问题【渡一教育】_哔哩哔哩_bilibili 检测网页本身是否缩放 ,即缩放倍率 window.devicePixelRatio 为了获得清晰图像,需要遵循以下公式 原始尺寸样式尺寸*缩放倍率 在项目中,canvas里的原始尺寸一般与css中的样式尺寸一样,所以在写js代码时,涉…

纯电超跑SUV获得2024中国年度性能车大奖 路特斯ELETRE实力几何?

2023年12月5日&#xff0c;“中国年度车”颁奖盛典在北京圆满落幕。路特斯首款纯电超跑SUV ELETRE ( 参数询价 ) 斩获“2024中国年度性能车”大奖&#xff0c;成为首次获此殊荣的纯电SUV车型。一台纯电SUV能获得年度性能车奖项&#xff0c;注定是件非常有看点的事&#xff0c;那…

17.(vue3.x+vite)组件间通信方式之作用域插槽

前端技术社区总目录(订阅之前请先查看该博客) 示例效果 作用域插槽 父组件中的插槽内容是无法访问到子组件中的数据的,而作用域插槽就是解决获取子组件数据。 父组件代码 <template><div><div>父组件--Hello World!</div><Child>

【普中开发板】基于51单片机电子钟闹钟设计( proteus仿真+程序+设计报告+讲解视频)

【普中开发板】基于51单片机电子钟闹钟数码管显示设计( proteus仿真程序设计报告讲解视频&#xff09; Proteus 仿真&#xff1a;Proteus 8.16 程序编译器&#xff1a;keil 4/keil 5 编程语言&#xff1a;C语言 设计编号&#xff1a;P03 51单片机电子钟闹钟 1. 主要功能&a…

RT-Thread Studio + STM32CubeMx 联合开发

RT-Thread Studio STM32CubeMx 联合开发 背景参考 背景 使用RTThreadNano开发工具&#xff1a; RTThread Studio&#xff1a;官方工具&#xff0c;对RTThread系统兼容最好&#xff0c;可以对不同的MCU平台进行开发&#xff0c;包括但不限于以下平台 STM32GD32IAR STM32CubeI…

mac 安装nvm以及切换node版本详细步骤

1、nvm介绍&#xff08;node版本管理工具&#xff09; nvm 可以让你通过命令行快速安装和使用不同版本的node 有时候项目太老&#xff0c;node版本太高,执行npm install命令会报错,可以借助nvm切换低版本的node。 2、安装nvm 在终端执行安装命令 curl -o- https://raw.gith…

Vue router深入学习

Vue router深入学习 一、单页应用程序介绍 1.概念 单页应用程序&#xff1a;SPA【Single Page Application】是指所有的功能都在一个html页面上实现 2.具体示例 单页应用网站&#xff1a; 网易云音乐 https://music.163.com/ 多页应用网站&#xff1a;京东 https://jd.co…

销售技巧培训之如何提升化妆品销售技巧

销售技巧培训之如何提升化妆品销售技巧 一、引言 在当今竞争激烈的化妆品市场&#xff0c;销售人员需要掌握一定的技巧才能吸引顾客&#xff0c;提高销售业绩。本文将通过实际案例&#xff0c;探讨化妆品销售的有效技巧&#xff0c;帮助销售人员提升业绩。 二、案例分析 案…

jstree组件的使用详细教程,部分案例( PHP / fastAdmin )

jstree 组件的使用。 简介&#xff1a;JsTree是一个jquery的插件&#xff0c;它提交一个非常友好并且强大的交互性的树&#xff0c;并且是完全免费或开源的&#xff08;MIT 许可&#xff09;。Jstree技持Html 或 json格式的的数据&#xff0c; 或者是ajax方式的动态请求加载数…

宁夏康源肛肠医院发声 有劳肠道了

肠道先生&#xff1a; 你好&#xff0c;我是宁夏康源肛肠医院&#xff0c;今日书信一封&#xff0c;旨在请先生出山&#xff0c;救万民健康于水火。当前&#xff0c;支原体肺炎肆虐&#xff0c;且尚无疫苗可预&#xff0c;值此危急之际&#xff0c;正是肠道先生免疫力发挥功力…

高速吹风筒方案中光耦的作用和原理--【其利天下技术】

我们都知道高速风筒方案中&#xff0c;采用了光耦作为隔离元器件&#xff0c;那么光耦是个什么特性的元件呢&#xff1f;它用来隔离什么东西呢&#xff1f;它又是如何做到隔离作用的呢&#xff1f;带着这样的疑问&#xff0c;我们对光耦做一番研究吧。 其利天下技术--光耦 &am…

AutoAnimate动画库,仅需一行代码

插件官网,支持react,vue AutoAnimate - Add motion to your apps with a single line of code 自动加动画原理 AutoAnimate 加动画的原理也很简单&#xff0c;监听绑定的 DOM 节点里 DOM 结构变化&#xff0c;自动添加对应的过渡动画&#xff1a; 增加子节点 > 渐入动画…

芳村金融茶最新进展——00后老板被带走调查

芳村 芳村茶叶的发展史可以追溯到1858年&#xff0c;当时芳村花地的翠林花园一角&#xff0c;已经有茶叶商行和制茶作坊。然而&#xff0c;芳村真正以花闻名&#xff0c;可以说是“先有花后有茶”&#xff0c;是花带动了茶的发展。在20世纪70年代左右&#xff0c;芳村和山村主要…

【论文阅读】Answering Label-Constraint Reachability in Large Graphs

Xu K, Zou L, Yu J X, et al. Answering label-constraint reachability in large graphs[C]//Proceedings of the 20th ACM international conference on Information and knowledge management. 2011: 1595-1600. Abstract 在本文中&#xff0c;我们研究了可达性查询的一种变…

光伏储能数据难题很棘手?架构升级很迷茫?来看三大真实案例

近年来&#xff0c;随着光伏储能装置的增加&#xff0c;设备数量和测点数量也在相应增加&#xff0c;数据采集频率也在不断提高&#xff0c;由此产生的时序数据量越来越庞大&#xff0c;对数据处理和实时分析的要求也越来越高。同时光伏储能系统需要长期保存大量的历史数据&…

IDEA小技巧

目录 1. IDEA自动添加注释 创建类的时候自动添加注释 创建函数、方法的注释 1. IDEA自动添加注释 参考文档&#xff1a;idea java 自动添加文件注释 idea新建类自动注释_mob6454cc73c728的技术博客_51CTO博客 【操作工具】IDEA创建类及已有类添加注释-详细操作_idea设置创建…

Python Selenium3 自动化测试实战:构建高效测试项目

&#x1f482; 个人网站:【 海拥】【神级代码资源网站】【办公神器】&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交流的小伙伴&#xff0c;请点击【全栈技术交流群】 在当今软件开发领域&#xff0c;自动化测试成为…

硅片制作工艺-详细图文版

首先将多晶硅和掺杂剂放入单晶炉内的石英坩埚中&#xff0c;将温度升高至1000多度&#xff0c;得到熔融状态的多晶硅。 硅锭生长是一个将多晶硅制成单晶硅的工序&#xff0c;将多晶硅加热成液体后&#xff0c;精密控制热环境&#xff0c;成长为高品质的单晶。 相关概念&#xf…

【JavaScript】JavaScript中的GC算法

1、内存管理 内存&#xff1a;由可读写单元组成&#xff0c;标识一片可操作的空间 管理&#xff1a; 认为的去操作一篇空间的申请、使用和释放 内存管理&#xff1a;开发者主动申请空间、使用空间、释放空间 管理流程&#xff1a; 申请-使用-释放 // 申请 let obj {} //使…