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