后端修炼篇④:用户认证与授权 - 构建安全的应用系统
掌握现代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/jsonwebtoken2.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)管理不同权限等级
练习任务:
- 实现完整的用户注册登录系统
- 添加邮箱验证功能
- 实现密码重置流程
- 创建多角色权限管理系统
- 添加登录失败次数限制
💡💡 常见问题
Q:JWT令牌如何实现安全退出?
A:JWT本身无状态,可以通过令牌黑名单或设置短期有效期实现安全退出
Q:bcrypt加密为什么比MD5更安全?
A:bcrypt使用盐值和多轮哈希,能有效抵抗彩虹表攻击,且计算速度可调
Q:如何防止JWT令牌被盗用?
A:使用HTTPS传输、设置合理有效期、存储令牌在HttpOnly Cookie中
Q:多角色权限系统如何设计?
A:使用RBAC模型,定义角色-权限关系,用户通过角色获得权限
👉👉 下一篇预告
在下一篇中,我们将学习文件上传与云存储,掌握多媒体文件处理、云存储集成和CDN加速等技术!
版权申明
本文系作者 @sgyyds 原创发布在孙哥博客站点。未经许可,禁止转载。
暂无评论数据