掌握现代Web应用的安全基石,保护用户数据和系统资源🔐

学习目标

  • ✅ 理解认证与授权的核心区别
  • ✅ 掌握JWT的工作原理和完整实现
  • ✅ 学会使用bcrypt加密用户密码
  • ✅ 实现基于角色的访问控制(RBAC)

📖📖 教程概览

用户认证与授权是Web应用安全的核心。本教程将带你从零开始实现完整的用户认证系统,涵盖密码加密、JWT令牌、权限控制等安全机制。

💻💻 第一步:认证与授权基础

1.1 认证 vs 授权

认证(Authentication):验证用户身份,回答"你是谁?"

  • 用户名密码登录
  • 手机验证码登录
  • 第三方登录

授权(Authorization):验证用户权限,回答"你能做什么?"

  • 角色权限控制
  • 资源访问控制
  • 操作权限验证

1.2 安全威胁与防护

  • 密码泄露:使用bcrypt加密存储
  • 会话劫持:JWT签名验证
  • CSRF攻击:CSRF Token防护
  • XSS攻击:输入验证和转义

🛠🛠 第二步:用户模型与密码加密

2.1 安装必要依赖

npm install bcryptjs jsonwebtoken
npm install -D @types/bcryptjs @types/jsonwebtoken

2.2 用户数据模型

// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
    username: {
        type: String,
        required: [true, '用户名不能为空'],
        unique: true,
        trim: true,
        minlength: [3, '用户名至少3个字符'],
        maxlength: [30, '用户名最多30个字符']
    },
    email: {
        type: String,
        required: [true, '邮箱不能为空'],
        unique: true,
        lowercase: true,
        match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, '请输入有效的邮箱地址']
    },
    password: {
        type: String,
        required: [true, '密码不能为空'],
        minlength: [6, '密码至少6个字符']
    },
    role: {
        type: String,
        enum: ['user', 'admin', 'moderator'],
        default: 'user'
    },
    isActive: {
        type: Boolean,
        default: true
    }
}, {
    timestamps: true
});

// 密码加密中间件
userSchema.pre('save', async function(next) {
    if (!this.isModified('password')) return next();
    
    try {
        const salt = await bcrypt.genSalt(12);
        this.password = await bcrypt.hash(this.password, salt);
        next();
    } catch (error) {
        next(error);
    }
});

// 密码验证方法
userSchema.methods.comparePassword = async function(candidatePassword) {
    return await bcrypt.compare(candidatePassword, this.password);
};

// 转换为JSON时隐藏密码
userSchema.methods.toJSON = function() {
    const user = this.toObject();
    delete user.password;
    return user;
};

module.exports = mongoose.model('User', userSchema);

🌟🌟 第三步:JWT认证实现

3.1 JWT工具类封装

// utils/jwt.js
const jwt = require('jsonwebtoken');

const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-key-here';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';

class JWTUtils {
    // 生成访问令牌
    static generateAccessToken(payload) {
        return jwt.sign(payload, JWT_SECRET, {
            expiresIn: JWT_EXPIRES_IN,
            issuer: 'your-app-name',
            audience: 'your-app-users'
        });
    }
    
    // 生成刷新令牌
    static generateRefreshToken(payload) {
        return jwt.sign(payload, JWT_SECRET, {
            expiresIn: '30d',
            issuer: 'your-app-name',
            audience: 'your-app-users'
        });
    }
    
    // 验证令牌
    static verifyToken(token) {
        try {
            return jwt.verify(token, JWT_SECRET);
        } catch (error) {
            throw new Error('令牌验证失败: ' + error.message);
        }
    }
    
    // 从请求头提取令牌
    static extractTokenFromHeader(req) {
        const authHeader = req.headers.authorization;
        
        if (!authHeader || !authHeader.startsWith('Bearer ')) {
            return null;
        }
        
        return authHeader.substring(7);
    }
    
    // 解码令牌(不验证)
    static decodeToken(token) {
        return jwt.decode(token);
    }
}

module.exports = JWTUtils;

3.2 认证中间件

// middleware/auth.js
const JWTUtils = require('../utils/jwt');
const User = require('../models/User');

const authenticate = async (req, res, next) => {
    try {
        const token = JWTUtils.extractTokenFromHeader(req);
        
        if (!token) {
            return res.status(401).json({
                success: false,
                error: '访问令牌不存在,请先登录'
            });
        }
        
        const decoded = JWTUtils.verifyToken(token);
        const user = await User.findById(decoded.id);
        
        if (!user) {
            return res.status(401).json({
                success: false,
                error: '用户不存在或令牌已失效'
            });
        }
        
        if (!user.isActive) {
            return res.status(401).json({
                success: false,
                error: '账户已被禁用,请联系管理员'
            });
        }
        
        req.user = user;
        next();
    } catch (error) {
        return res.status(401).json({
            success: false,
            error: '令牌验证失败: ' + error.message
        });
    }
};

// 可选认证(不强制要求登录)
const optionalAuthenticate = async (req, res, next) => {
    try {
        const token = JWTUtils.extractTokenFromHeader(req);
        
        if (token) {
            const decoded = JWTUtils.verifyToken(token);
            const user = await User.findById(decoded.id);
            if (user && user.isActive) {
                req.user = user;
            }
        }
        
        next();
    } catch (error) {
        // 忽略可选认证的错误
        next();
    }
};

module.exports = {
    authenticate,
    optionalAuthenticate
};

🎮🎮 第四步:权限控制实现

4.1 角色权限中间件

// middleware/authorize.js
const authorize = (...allowedRoles) => {
    return (req, res, next) => {
        if (!req.user) {
            return res.status(401).json({
                success: false,
                error: '请先登录系统'
            });
        }
        
        if (!allowedRoles.includes(req.user.role)) {
            return res.status(403).json({
                success: false,
                error: '权限不足,无法访问该资源'
            });
        }
        
        next();
    };
};

// 权限等级控制
const permissionLevels = {
    user: 1,
    moderator: 2,
    admin: 3
};

const requireMinLevel = (minLevel) => {
    return (req, res, next) => {
        if (!req.user) {
            return res.status(401).json({
                success: false,
                error: '请先登录系统'
            });
        }
        
        const userLevel = permissionLevels[req.user.role] || 0;
        
        if (userLevel < minLevel) {
            return res.status(403).json({
                success: false,
                error: '权限等级不足,无法执行此操作'
            });
        }
        
        next();
    };
};

module.exports = {
    authorize,
    requireMinLevel,
    permissionLevels
};

4.2 资源所有权验证

// middleware/ownership.js
const Post = require('../models/Post'); // 假设有Post模型

const checkPostOwnership = async (req, res, next) => {
    try {
        const post = await Post.findById(req.params.id);
        
        if (!post) {
            return res.status(404).json({
                success: false,
                error: '文章不存在'
            });
        }
        
        // 管理员可以操作所有文章
        if (req.user.role === 'admin') {
            return next();
        }
        
        // 检查是否是文章作者
        if (post.author.toString() !== req.user._id.toString()) {
            return res.status(403).json({
                success: false,
                error: '无权操作他人的文章'
            });
        }
        
        next();
    } catch (error) {
        res.status(500).json({
            success: false,
            error: '服务器错误: ' + error.message
        });
    }
};

module.exports = {
    checkPostOwnership
};

🚀🚀 第五步:完整认证API实现

5.1 认证控制器

// controllers/authController.js
const User = require('../models/User');
const JWTUtils = require('../utils/jwt');

class AuthController {
    // 用户注册
    async register(req, res) {
        try {
            const { username, email, password } = req.body;
            
            // 检查用户是否已存在
            const existingUser = await User.findOne({
                $or: [{ email }, { username }]
            });
            
            if (existingUser) {
                return res.status(400).json({
                    success: false,
                    error: '用户名或邮箱已被注册'
                });
            }
            
            // 创建新用户
            const user = new User({
                username,
                email,
                password
            });
            
            await user.save();
            
            // 生成令牌
            const tokenPayload = {
                id: user._id,
                username: user.username,
                role: user.role
            };
            
            const accessToken = JWTUtils.generateAccessToken(tokenPayload);
            const refreshToken = JWTUtils.generateRefreshToken(tokenPayload);
            
            res.status(201).json({
                success: true,
                message: '注册成功',
                data: {
                    user: user.toJSON(),
                    tokens: {
                        accessToken,
                        refreshToken
                    }
                }
            });
            
        } catch (error) {
            res.status(500).json({
                success: false,
                error: '注册失败: ' + error.message
            });
        }
    }
    
    // 用户登录
    async login(req, res) {
        try {
            const { login, password } = req.body; // login可以是用户名或邮箱
            
            if (!login || !password) {
                return res.status(400).json({
                    success: false,
                    error: '用户名/邮箱和密码为必填项'
                });
            }
            
            // 查找用户
            const user = await User.findOne({
                $or: [
                    { email: login },
                    { username: login }
                ]
            });
            
            if (!user) {
                return res.status(401).json({
                    success: false,
                    error: '用户名或密码错误'
                });
            }
            
            // 验证密码
            const isPasswordValid = await user.comparePassword(password);
            if (!isPasswordValid) {
                return res.status(401).json({
                    success: false,
                    error: '用户名或密码错误'
                });
            }
            
            if (!user.isActive) {
                return res.status(401).json({
                    success: false,
                    error: '账户已被禁用,请联系管理员'
                });
            }
            
            // 生成令牌
            const tokenPayload = {
                id: user._id,
                username: user.username,
                role: user.role
            };
            
            const accessToken = JWTUtils.generateAccessToken(tokenPayload);
            const refreshToken = JWTUtils.generateRefreshToken(tokenPayload);
            
            res.json({
                success: true,
                message: '登录成功',
                data: {
                    user: user.toJSON(),
                    tokens: {
                        accessToken,
                        refreshToken
                    }
                }
            });
            
        } catch (error) {
            res.status(500).json({
                success: false,
                error: '登录失败: ' + error.message
            });
        }
    }
    
    // 刷新令牌
    async refreshToken(req, res) {
        try {
            const { refreshToken } = req.body;
            
            if (!refreshToken) {
                return res.status(400).json({
                    success: false,
                    error: '刷新令牌不能为空'
                });
            }
            
            const decoded = JWTUtils.verifyToken(refreshToken);
            const user = await User.findById(decoded.id);
            
            if (!user || !user.isActive) {
                return res.status(401).json({
                    success: false,
                    error: '令牌刷新失败'
                });
            }
            
            const tokenPayload = {
                id: user._id,
                username: user.username,
                role: user.role
            };
            
            const newAccessToken = JWTUtils.generateAccessToken(tokenPayload);
            const newRefreshToken = JWTUtils.generateRefreshToken(tokenPayload);
            
            res.json({
                success: true,
                data: {
                    accessToken: newAccessToken,
                    refreshToken: newRefreshToken
                }
            });
            
        } catch (error) {
            res.status(401).json({
                success: false,
                error: '令牌刷新失败: ' + error.message
            });
        }
    }
    
    // 获取当前用户信息
    async getCurrentUser(req, res) {
        res.json({
            success: true,
            data: {
                user: req.user.toJSON()
            }
        });
    }
    
    // 用户登出
    async logout(req, res) {
        // JWT是无状态的,客户端删除令牌即可
        res.json({
            success: true,
            message: '登出成功'
        });
    }
}

module.exports = new AuthController();

5.2 路由配置

// routes/auth.js
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
const { authenticate } = require('../middleware/auth');

// 公开路由
router.post('/register', authController.register);
router.post('/login', authController.login);
router.post('/refresh-token', authController.refreshToken);

// 需要认证的路由
router.get('/me', authenticate, authController.getCurrentUser);
router.post('/logout', authenticate, authController.logout);

module.exports = router;

5.3 用户管理路由

// routes/users.js
const express = require('express');
const router = express.Router();
const User = require('../models/User');
const { authenticate, authorize } = require('../middleware/auth');

// 获取用户列表(仅管理员)
router.get('/', authenticate, authorize('admin'), async (req, res) => {
    try {
        const { page = 1, limit = 10, search } = req.query;
        
        const query = {};
        if (search) {
            query.$or = [
                { username: { $regex: search, $options: 'i' } },
                { email: { $regex: search, $options: 'i' } }
            ];
        }
        
        const users = await User.find(query)
            .select('-password')
            .limit(limit * 1)
            .skip((page - 1) * limit)
            .sort({ createdAt: -1 });
            
        const total = await User.countDocuments(query);
        
        res.json({
            success: true,
            data: {
                users,
                pagination: {
                    page: parseInt(page),
                    limit: parseInt(limit),
                    total,
                    pages: Math.ceil(total / limit)
                }
            }
        });
        
    } catch (error) {
        res.status(500).json({
            success: false,
            error: '获取用户列表失败: ' + error.message
        });
    }
});

// 更新用户信息
router.put('/profile', authenticate, async (req, res) => {
    try {
        const { username, email } = req.body;
        const updates = {};
        
        if (username) updates.username = username;
        if (email) updates.email = email;
        
        const user = await User.findByIdAndUpdate(
            req.user._id,
            updates,
            { new: true, runValidators: true }
        ).select('-password');
        
        res.json({
            success: true,
            message: '个人信息更新成功',
            data: { user }
        });
        
    } catch (error) {
        res.status(500).json({
            success: false,
            error: '更新失败: ' + error.message
        });
    }
});

module.exports = router;

📚📚 小结与练习

本章重点回顾:

  • 认证验证用户身份,授权控制用户权限
  • bcrypt加密保证密码安全,JWT实现无状态认证
  • 中间件链实现灵活的权限控制
  • 基于角色的访问控制(RBAC)管理不同权限等级

练习任务:

  1. 实现完整的用户注册登录系统
  2. 添加邮箱验证功能
  3. 实现密码重置流程
  4. 创建多角色权限管理系统
  5. 添加登录失败次数限制

💡💡 常见问题

Q:JWT令牌如何实现安全退出?
A:JWT本身无状态,可以通过令牌黑名单或设置短期有效期实现安全退出

Q:bcrypt加密为什么比MD5更安全?
A:bcrypt使用盐值和多轮哈希,能有效抵抗彩虹表攻击,且计算速度可调

Q:如何防止JWT令牌被盗用?
A:使用HTTPS传输、设置合理有效期、存储令牌在HttpOnly Cookie中

Q:多角色权限系统如何设计?
A:使用RBAC模型,定义角色-权限关系,用户通过角色获得权限

👉👉 下一篇预告

在下一篇中,我们将学习文件上传与云存储,掌握多媒体文件处理、云存储集成和CDN加速等技术!

分类: 「从零到上线」Web开发新手村 🔧后端修炼 标签: 用户认证JWT权限控制bcrypt后端安全

评论

暂无评论数据

暂无评论数据

目录