用户认证与权限管理
用户认证与权限管理的基本概念
用户认证与权限管理是任何现代应用的核心功能之一。认证解决的是"你是谁"的问题,而权限管理则决定"你能做什么"。在Mongoose中,我们可以通过Schema设计、中间件和插件等方式实现这些功能。
Mongoose中的用户模型设计
用户模型是认证系统的基础。一个典型的用户Schema可能包含以下字段:
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true,
minlength: 3
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
validate: [validator.isEmail, 'Invalid email']
},
password: {
type: String,
required: true,
minlength: 8,
select: false
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
isActive: {
type: Boolean,
default: true
},
lastLogin: Date,
passwordChangedAt: Date,
passwordResetToken: String,
passwordResetExpires: Date
}, {
timestamps: true
});
密码加密与安全存储
明文存储密码是极其危险的。我们应该使用bcrypt等库对密码进行哈希处理:
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
this.passwordChangedAt = Date.now() - 1000;
next();
});
认证流程实现
实现基本的登录认证流程:
userSchema.methods.correctPassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
userSchema.methods.createAuthToken = function() {
return jwt.sign(
{ id: this._id, role: this.role },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN }
);
};
// 登录控制器示例
exports.login = async (req, res) => {
const { email, password } = req.body;
// 1) 检查邮箱和密码是否存在
if (!email || !password) {
return res.status(400).json({
status: 'fail',
message: '请提供邮箱和密码'
});
}
// 2) 检查用户是否存在且密码正确
const user = await User.findOne({ email }).select('+password');
if (!user || !(await user.correctPassword(password))) {
return res.status(401).json({
status: 'fail',
message: '邮箱或密码不正确'
});
}
// 3) 生成token
const token = user.createAuthToken();
// 4) 发送响应
res.status(200).json({
status: 'success',
token,
data: {
user
}
});
};
权限控制与角色管理
基于角色的访问控制(RBAC)是常见的权限管理方式:
// 角色权限映射
const rolePermissions = {
user: ['read:own_profile', 'update:own_profile'],
moderator: ['read:any_profile', 'update:any_profile', 'delete:comments'],
admin: ['read:any_profile', 'update:any_profile', 'delete:any_profile', 'manage:users']
};
// 权限检查中间件
exports.restrictTo = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
status: 'fail',
message: '您没有执行此操作的权限'
});
}
next();
};
};
// 使用示例 - 保护路由
router.get(
'/admin/users',
authController.protect,
authController.restrictTo('admin'),
userController.getAllUsers
);
高级权限控制
对于更复杂的权限需求,可以使用基于属性的访问控制(ABAC):
// 检查用户是否拥有资源的权限
userSchema.methods.canAccessResource = function(resource) {
if (this.role === 'admin') return true;
// 用户只能访问自己的资源
if (resource.user && resource.user.equals(this._id)) return true;
// 特定条件下的访问权限
if (this.role === 'moderator' && resource.type === 'comment') return true;
return false;
};
// 使用示例
exports.getUser = async (req, res) => {
const user = await User.findById(req.params.id);
if (!req.user.canAccessResource(user)) {
return res.status(403).json({
status: 'fail',
message: '无权访问此资源'
});
}
res.status(200).json({
status: 'success',
data: {
user
}
});
};
密码重置与账户安全
实现安全的密码重置流程:
userSchema.methods.createPasswordResetToken = function() {
const resetToken = crypto.randomBytes(32).toString('hex');
this.passwordResetToken = crypto
.createHash('sha256')
.update(resetToken)
.digest('hex');
this.passwordResetExpires = Date.now() + 10 * 60 * 1000; // 10分钟
return resetToken;
};
// 密码重置控制器
exports.forgotPassword = async (req, res) => {
// 1) 获取用户
const user = await User.findOne({ email: req.body.email });
if (!user) {
return res.status(404).json({
status: 'fail',
message: '该邮箱未注册'
});
}
// 2) 生成重置token
const resetToken = user.createPasswordResetToken();
await user.save({ validateBeforeSave: false });
// 3) 发送邮件
const resetURL = `${req.protocol}://${req.get('host')}/api/v1/users/resetPassword/${resetToken}`;
try {
await sendEmail({
email: user.email,
subject: '您的密码重置token(10分钟内有效)',
message: `请访问以下链接重置密码: ${resetURL}`
});
res.status(200).json({
status: 'success',
message: '重置链接已发送至邮箱'
});
} catch (err) {
user.passwordResetToken = undefined;
user.passwordResetExpires = undefined;
await user.save({ validateBeforeSave: false });
return res.status(500).json({
status: 'error',
message: '发送邮件时出错,请稍后重试'
});
}
};
会话管理与JWT刷新
实现安全的JWT刷新机制:
// 生成刷新token
userSchema.methods.createRefreshToken = function() {
return jwt.sign(
{ id: this._id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN }
);
};
// 刷新token端点
exports.refreshToken = async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({
status: 'fail',
message: '未提供刷新token'
});
}
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const user = await User.findById(decoded.id);
if (!user) {
return res.status(401).json({
status: 'fail',
message: '用户不存在'
});
}
const newAccessToken = user.createAuthToken();
res.status(200).json({
status: 'success',
accessToken: newAccessToken
});
} catch (err) {
return res.status(403).json({
status: 'fail',
message: '无效的刷新token'
});
}
};
多因素认证实现
增加额外的安全层:
userSchema.add({
twoFactorEnabled: {
type: Boolean,
default: false
},
twoFactorSecret: String
});
// 启用双因素认证
exports.enableTwoFactor = async (req, res) => {
const secret = speakeasy.generateSecret({ length: 20 });
const user = await User.findByIdAndUpdate(
req.user.id,
{
twoFactorEnabled: true,
twoFactorSecret: secret.base32
},
{ new: true }
);
res.status(200).json({
status: 'success',
data: {
otpauthUrl: secret.otpauth_url,
secret: secret.base32
}
});
};
// 验证双因素代码
exports.verifyTwoFactor = async (req, res) => {
const { token } = req.body;
const user = await User.findById(req.user.id);
const verified = speakeasy.totp.verify({
secret: user.twoFactorSecret,
encoding: 'base32',
token
});
if (!verified) {
return res.status(400).json({
status: 'fail',
message: '验证码无效'
});
}
// 标记为已验证,可以继续登录流程
req.session.twoFactorVerified = true;
res.status(200).json({
status: 'success',
message: '双因素认证成功'
});
};
审计日志与安全监控
记录关键操作以便审计:
const auditSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.ObjectId,
ref: 'User'
},
action: String,
entityType: String,
entityId: mongoose.Schema.Types.Mixed,
metadata: Object,
ipAddress: String,
userAgent: String,
timestamp: {
type: Date,
default: Date.now
}
});
// 审计日志中间件
userSchema.post('save', function(doc) {
if (doc.isNew) {
AuditLog.create({
user: doc._id,
action: 'user_created',
entityType: 'User',
entityId: doc._id,
metadata: {
email: doc.email,
role: doc.role
}
});
}
});
// 删除操作审计
userSchema.pre('remove', function(next) {
AuditLog.create({
user: this._id,
action: 'user_deleted',
entityType: 'User',
entityId: this._id
});
next();
});
性能优化与最佳实践
优化认证系统的性能:
// 使用索引加速查询
userSchema.index({ email: 1 });
userSchema.index({ passwordResetToken: 1 }, { expireAfterSeconds: 0 });
// 批量操作时的优化
exports.deactivateInactiveUsers = async () => {
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const result = await User.updateMany(
{
lastLogin: { $lt: thirtyDaysAgo },
isActive: true
},
{
$set: { isActive: false }
}
);
return result.nModified;
};
// 使用lean()提高查询性能
exports.getUserProfile = async (userId) => {
return User.findById(userId)
.select('-password -passwordResetToken -passwordResetExpires')
.lean();
};
测试与调试
编写测试用例确保认证系统可靠:
describe('用户认证', () => {
beforeEach(async () => {
await User.deleteMany();
});
test('成功注册新用户', async () => {
const res = await request(app)
.post('/api/v1/auth/signup')
.send({
username: 'testuser',
email: 'test@example.com',
password: 'password123',
passwordConfirm: 'password123'
});
expect(res.statusCode).toBe(201);
expect(res.body.token).toBeDefined();
const user = await User.findOne({ email: 'test@example.com' });
expect(user).toBeTruthy();
expect(user.password).not.toBe('password123');
});
test('登录失败-密码错误', async () => {
await User.create({
username: 'testuser',
email: 'test@example.com',
password: await bcrypt.hash('correctpassword', 12)
});
const res = await request(app)
.post('/api/v1/auth/login')
.send({
email: 'test@example.com',
password: 'wrongpassword'
});
expect(res.statusCode).toBe(401);
expect(res.body.message).toMatch(/邮箱或密码不正确/);
});
});
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:与Express.js的集成
下一篇:日志与审计功能实现