用户认证系统 #

一、认证系统概述 #

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