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

2023-12-13 04:56:49

目录

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);  // 全局错误处理中间件

文章来源:https://blog.csdn.net/SupperSA/article/details/134944399
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。