用户认证系统 #
一、认证系统概述 #
1.1 功能需求 #
- 用户注册
- 用户登录
- 密码重置
- Token刷新
- 权限控制
1.2 技术选型 #
| 功能 | 技术 |
|---|---|
| 密码加密 | bcryptjs |
| Token生成 | jsonwebtoken |
| 验证码 | nodemailer |
二、用户模型 #
2.1 models/User.js #
javascript
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, '请输入用户名'],
trim: true,
maxlength: [50, '用户名不能超过50个字符']
},
email: {
type: String,
required: [true, '请输入邮箱'],
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, '请输入有效的邮箱地址']
},
password: {
type: String,
required: [true, '请输入密码'],
minlength: [6, '密码至少6个字符'],
select: false
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
avatar: String,
isActive: {
type: Boolean,
default: true
},
emailVerified: {
type: Boolean,
default: false
},
verificationToken: String,
verificationTokenExpires: Date,
resetPasswordToken: String,
resetPasswordExpires: Date,
lastLoginAt: Date,
loginAttempts: {
type: Number,
default: 0
},
lockUntil: Date
}, {
timestamps: true
});
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
});
userSchema.methods.comparePassword = async function(password) {
return await bcrypt.compare(password, this.password);
};
userSchema.methods.createVerificationToken = function() {
const token = crypto.randomBytes(32).toString('hex');
this.verificationToken = crypto
.createHash('sha256')
.update(token)
.digest('hex');
this.verificationTokenExpires = Date.now() + 24 * 60 * 60 * 1000;
return token;
};
userSchema.methods.createPasswordResetToken = function() {
const token = crypto.randomBytes(32).toString('hex');
this.resetPasswordToken = crypto
.createHash('sha256')
.update(token)
.digest('hex');
this.resetPasswordExpires = Date.now() + 10 * 60 * 1000;
return token;
};
userSchema.methods.isLocked = function() {
return this.lockUntil && this.lockUntil > Date.now();
};
userSchema.methods.incLoginAttempts = function() {
if (this.lockUntil && this.lockUntil < Date.now()) {
return this.updateOne({
$set: { loginAttempts: 1 },
$unset: { lockUntil: 1 }
});
}
const updates = { $inc: { loginAttempts: 1 } };
if (this.loginAttempts + 1 >= 5) {
updates.$set = { lockUntil: Date.now() + 30 * 60 * 1000 };
}
return this.updateOne(updates);
};
userSchema.methods.resetLoginAttempts = function() {
return this.updateOne({
$set: { loginAttempts: 0 },
$unset: { lockUntil: 1 }
});
};
module.exports = mongoose.model('User', userSchema);
三、认证服务 #
3.1 services/authService.js #
javascript
const User = require('../models/User');
const jwt = require('jsonwebtoken');
const Email = require('../utils/email');
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
});
};
const generateRefreshToken = (id) => {
return jwt.sign({ id }, process.env.JWT_REFRESH_SECRET, {
expiresIn: '30d'
});
};
const createSendToken = (user, statusCode, res) => {
const token = generateToken(user._id);
const refreshToken = generateRefreshToken(user._id);
const cookieOptions = {
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
};
res.cookie('token', token, cookieOptions);
res.cookie('refreshToken', refreshToken, {
...cookieOptions,
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
});
user.password = undefined;
res.status(statusCode).json({
success: true,
data: { user, token, refreshToken }
});
};
const authService = {
async register(userData) {
const existingUser = await User.findOne({ email: userData.email });
if (existingUser) {
throw new Error('邮箱已被注册');
}
const user = await User.create(userData);
const verificationToken = user.createVerificationToken();
await user.save();
try {
await new Email(user, verificationToken).sendVerification();
} catch (error) {
console.error('发送验证邮件失败:', error);
}
return user;
},
async login(email, password) {
const user = await User.findOne({ email }).select('+password');
if (!user) {
throw new Error('邮箱或密码错误');
}
if (user.isLocked()) {
throw new Error('账户已被锁定,请30分钟后再试');
}
const isMatch = await user.comparePassword(password);
if (!isMatch) {
await user.incLoginAttempts();
throw new Error('邮箱或密码错误');
}
await user.resetLoginAttempts();
user.lastLoginAt = Date.now();
await user.save();
return user;
},
async verifyEmail(token) {
const hashedToken = crypto
.createHash('sha256')
.update(token)
.digest('hex');
const user = await User.findOne({
verificationToken: hashedToken,
verificationTokenExpires: { $gt: Date.now() }
});
if (!user) {
throw new Error('无效或过期的验证链接');
}
user.emailVerified = true;
user.verificationToken = undefined;
user.verificationTokenExpires = undefined;
await user.save();
return user;
},
async forgotPassword(email) {
const user = await User.findOne({ email });
if (!user) {
throw new Error('该邮箱未注册');
}
const resetToken = user.createPasswordResetToken();
await user.save();
try {
await new Email(user, resetToken).sendPasswordReset();
return true;
} catch (error) {
user.resetPasswordToken = undefined;
user.resetPasswordExpires = undefined;
await user.save();
throw new Error('发送重置邮件失败');
}
},
async resetPassword(token, newPassword) {
const hashedToken = crypto
.createHash('sha256')
.update(token)
.digest('hex');
const user = await User.findOne({
resetPasswordToken: hashedToken,
resetPasswordExpires: { $gt: Date.now() }
});
if (!user) {
throw new Error('无效或过期的重置链接');
}
user.password = newPassword;
user.resetPasswordToken = undefined;
user.resetPasswordExpires = undefined;
user.loginAttempts = 0;
user.lockUntil = undefined;
await user.save();
return user;
},
async changePassword(userId, currentPassword, newPassword) {
const user = await User.findById(userId).select('+password');
const isMatch = await user.comparePassword(currentPassword);
if (!isMatch) {
throw new Error('当前密码错误');
}
user.password = newPassword;
await user.save();
return user;
},
async refreshToken(refreshToken) {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const user = await User.findById(decoded.id);
if (!user) {
throw new Error('无效的refresh token');
}
return generateToken(user._id);
}
};
module.exports = authService;
四、认证控制器 #
4.1 controllers/authController.js #
javascript
const authService = require('../services/authService');
const asyncHandler = require('../utils/asyncHandler');
exports.register = asyncHandler(async (req, res) => {
const user = await authService.register(req.body);
res.status(201).json({
success: true,
message: '注册成功,请查收验证邮件',
data: { user }
});
});
exports.login = asyncHandler(async (req, res) => {
const { email, password } = req.body;
const user = await authService.login(email, password);
const token = generateToken(user._id);
res.json({
success: true,
data: { user, token }
});
});
exports.verifyEmail = asyncHandler(async (req, res) => {
const user = await authService.verifyEmail(req.params.token);
res.json({
success: true,
message: '邮箱验证成功'
});
});
exports.forgotPassword = asyncHandler(async (req, res) => {
await authService.forgotPassword(req.body.email);
res.json({
success: true,
message: '重置邮件已发送'
});
});
exports.resetPassword = asyncHandler(async (req, res) => {
const user = await authService.resetPassword(req.params.token, req.body.password);
const token = generateToken(user._id);
res.json({
success: true,
message: '密码重置成功',
data: { token }
});
});
exports.changePassword = asyncHandler(async (req, res) => {
const { currentPassword, newPassword } = req.body;
await authService.changePassword(req.user.id, currentPassword, newPassword);
res.json({
success: true,
message: '密码修改成功'
});
});
exports.refreshToken = asyncHandler(async (req, res) => {
const { refreshToken } = req.body;
const token = await authService.refreshToken(refreshToken);
res.json({
success: true,
data: { token }
});
});
exports.getMe = asyncHandler(async (req, res) => {
res.json({
success: true,
data: req.user
});
});
exports.logout = asyncHandler(async (req, res) => {
res.cookie('token', 'none', {
expires: new Date(Date.now() + 10 * 1000),
httpOnly: true
});
res.json({
success: true,
message: '退出成功'
});
});
五、邮件工具 #
5.1 utils/email.js #
javascript
const nodemailer = require('nodemailer');
class Email {
constructor(user, token) {
this.to = user.email;
this.name = user.name;
this.token = token;
}
createTransport() {
return nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: process.env.EMAIL_PORT,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD
}
});
}
async send(template, subject) {
const transport = this.createTransport();
const mailOptions = {
from: process.env.EMAIL_FROM,
to: this.to,
subject,
html: template
};
await transport.sendMail(mailOptions);
}
async sendVerification() {
const url = `${process.env.FRONTEND_URL}/verify-email/${this.token}`;
await this.send(
`<p>请点击以下链接验证邮箱:</p>
<a href="${url}">${url}</a>`,
'邮箱验证'
);
}
async sendPasswordReset() {
const url = `${process.env.FRONTEND_URL}/reset-password/${this.token}`;
await this.send(
`<p>请点击以下链接重置密码:</p>
<a href="${url}">${url}</a>
<p>链接10分钟后过期</p>`,
'密码重置'
);
}
}
module.exports = Email;
六、认证中间件 #
6.1 middlewares/auth.js #
javascript
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const asyncHandler = require('../utils/asyncHandler');
exports.auth = asyncHandler(async (req, res, next) => {
let token;
if (req.headers.authorization?.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
} else if (req.cookies.token) {
token = req.cookies.token;
}
if (!token) {
return res.status(401).json({ error: '请先登录' });
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.id);
if (!user) {
return res.status(401).json({ error: '用户不存在' });
}
if (!user.isActive) {
return res.status(401).json({ error: '账户已被禁用' });
}
req.user = user;
next();
});
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: '没有权限' });
}
next();
};
};
exports.optionalAuth = asyncHandler(async (req, res, next) => {
let token;
if (req.headers.authorization?.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (token) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id);
} catch (error) {
req.user = null;
}
}
next();
});
七、路由定义 #
7.1 routes/authRoutes.js #
javascript
const express = require('express');
const router = express.Router();
const { body } = require('express-validator');
const authController = require('../controllers/authController');
const { auth } = require('../middlewares/auth');
const { validate } = require('../middlewares/validate');
router.post('/register', [
body('name').notEmpty().trim(),
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 6 }),
validate
], authController.register);
router.post('/login', [
body('email').isEmail(),
body('password').notEmpty(),
validate
], authController.login);
router.get('/verify-email/:token', authController.verifyEmail);
router.post('/forgot-password', [
body('email').isEmail(),
validate
], authController.forgotPassword);
router.post('/reset-password/:token', [
body('password').isLength({ min: 6 }),
validate
], authController.resetPassword);
router.post('/change-password', auth, [
body('currentPassword').notEmpty(),
body('newPassword').isLength({ min: 6 }),
validate
], authController.changePassword);
router.post('/refresh-token', authController.refreshToken);
router.get('/me', auth, authController.getMe);
router.post('/logout', authController.logout);
module.exports = router;
八、总结 #
用户认证系统要点:
| 功能 | 说明 |
|---|---|
| 注册 | 创建用户、发送验证邮件 |
| 登录 | 验证凭据、生成Token |
| 密码重置 | 发送重置链接、更新密码 |
| Token刷新 | 延长会话时间 |
| 权限控制 | 基于角色的访问控制 |
下一步,让我们学习博客系统!
最后更新:2026-03-28