Node.js 认证与授权
章节概述
认证与授权是 Web 应用安全的核心组成部分。认证(Authentication)是验证用户身份的过程,而授权(Authorization)是确定用户是否有权限访问特定资源的过程。在 Node.js 应用中,我们通常使用 Session、JWT(JSON Web Token)或 OAuth 等机制来实现认证,使用基于角色的访问控制(RBAC)等方法来实现授权。本集将详细介绍这些认证与授权机制的原理和实现方法,并通过一个完整的用户登录系统案例展示它们的实际应用。
核心知识点讲解
1. 认证与授权基础
1.1 认证(Authentication)
认证是验证用户身份的过程,确保用户是其声称的身份。常见的认证方式包括:
- 用户名/密码认证:最常见的认证方式,用户输入用户名和密码进行验证
- 短信/邮箱验证码:通过手机短信或邮箱发送验证码进行认证
- 生物识别认证:使用指纹、面部识别等生物特征进行认证
- 多因素认证(MFA):结合多种认证方式,提高安全性
- 单点登录(SSO):用户只需登录一次,即可访问多个相关系统
1.2 授权(Authorization)
授权是确定用户是否有权限访问特定资源的过程,确保用户只能访问其被允许的资源。常见的授权方式包括:
- 基于角色的访问控制(RBAC):根据用户角色分配权限
- 基于属性的访问控制(ABAC):根据用户属性、资源属性和环境条件进行授权
- 基于规则的访问控制:根据预设规则进行授权
- 基于策略的访问控制:根据组织策略进行授权
1.3 认证与授权的区别
- 认证:回答「你是谁?」的问题
- 授权:回答「你能做什么?」的问题
2. Session 认证
Session 认证是一种传统的认证方式,它通过在服务器端存储用户会话信息来实现认证。
2.1 Session 认证流程
- 用户输入用户名和密码,发送登录请求
- 服务器验证用户名和密码,验证通过后创建会话
- 服务器将会话 ID 存储在 Cookie 中,返回给客户端
- 客户端后续请求会携带包含会话 ID 的 Cookie
- 服务器通过会话 ID 查找会话信息,验证用户身份
- 用户登出时,服务器销毁会话
2.2 Session 认证实现
const express = require('express');
const session = require('express-session');
const bcrypt = require('bcrypt');
const app = express();
// 配置 Session
app.use(session({
secret: 'your-secret-key', // 用于签名会话 ID 的密钥
resave: false, // 强制会话保存,即使未修改
saveUninitialized: false, // 强制保存未初始化的会话
cookie: {
secure: false, // 生产环境中应设置为 true,使用 HTTPS
httpOnly: true, // 防止客户端 JavaScript 访问 Cookie
maxAge: 3600000 // Cookie 过期时间(毫秒)
}
}));
// 解析请求体
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 模拟用户数据库
const users = [
{ id: 1, username: 'admin', password: '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', role: 'admin' }, // 密码: admin123
{ id: 2, username: 'user', password: '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', role: 'user' } // 密码: user123
];
// 登录路由
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// 查找用户
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({ success: false, message: '用户名或密码错误' });
}
// 验证密码
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({ success: false, message: '用户名或密码错误' });
}
// 创建会话
req.session.user = {
id: user.id,
username: user.username,
role: user.role
};
res.json({ success: true, message: '登录成功', user: req.session.user });
});
// 登出路由
app.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
return res.status(500).json({ success: false, message: '登出失败' });
}
res.json({ success: true, message: '登出成功' });
});
});
// 受保护的路由
app.get('/protected', (req, res) => {
if (!req.session.user) {
return res.status(401).json({ success: false, message: '未登录' });
}
res.json({ success: true, message: '访问成功', user: req.session.user });
});
// 启动服务器
const port = 3000;
app.listen(port, () => {
console.log(`服务器已启动,监听端口 ${port}`);
});2.3 Session 认证的优缺点
优点:
- 实现简单,易于理解
- 服务器端存储会话信息,安全性较高
- 支持会话过期和销毁机制
缺点:
- 服务器端存储会话信息,增加服务器负担
- 不利于水平扩展,需要会话共享机制(如 Redis)
- 依赖 Cookie,可能被 CSRF 攻击
3. JWT 认证
JWT(JSON Web Token)是一种基于令牌的认证机制,它将用户信息编码为 JSON 对象,使用密钥进行签名,然后作为令牌发送给客户端。
3.1 JWT 结构
JWT 由三部分组成,用点(.)分隔:
- Header(头部):包含令牌类型和签名算法
- Payload(载荷):包含声明(claims),如用户 ID、用户名、过期时间等
- Signature(签名):使用头部指定的算法对头部和载荷进行签名
示例:
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE2MjAwMDAwMDAsImV4cCI6MTYyMDAwMzYwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c3.2 JWT 认证流程
- 用户输入用户名和密码,发送登录请求
- 服务器验证用户名和密码,验证通过后生成 JWT
- 服务器将 JWT 返回给客户端
- 客户端存储 JWT(通常存储在 localStorage 或 Cookie 中)
- 客户端后续请求携带 JWT(通常在 Authorization 头部)
- 服务器验证 JWT 的有效性,验证通过后处理请求
3.3 JWT 认证实现
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const app = express();
// 解析请求体
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// JWT 密钥
const JWT_SECRET = 'your-secret-key';
// 模拟用户数据库
const users = [
{ id: 1, username: 'admin', password: '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', role: 'admin' }, // 密码: admin123
{ id: 2, username: 'user', password: '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', role: 'user' } // 密码: user123
];
// 生成 JWT
function generateToken(user) {
return jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
JWT_SECRET,
{ expiresIn: '1h' } // 令牌过期时间
);
}
// 验证 JWT 中间件
function authenticateToken(req, res, next) {
// 从请求头获取令牌
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ success: false, message: '未提供令牌' });
}
// 验证令牌
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ success: false, message: '令牌无效' });
}
req.user = user;
next();
});
}
// 登录路由
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// 查找用户
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({ success: false, message: '用户名或密码错误' });
}
// 验证密码
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({ success: false, message: '用户名或密码错误' });
}
// 生成 JWT
const token = generateToken(user);
res.json({ success: true, message: '登录成功', token, user: { id: user.id, username: user.username, role: user.role } });
});
// 受保护的路由
app.get('/protected', authenticateToken, (req, res) => {
res.json({ success: true, message: '访问成功', user: req.user });
});
// 启动服务器
const port = 3000;
app.listen(port, () => {
console.log(`服务器已启动,监听端口 ${port}`);
});3.4 JWT 认证的优缺点
优点:
- 无状态,服务器端不需要存储会话信息
- 便于水平扩展,适合分布式系统
- 自包含,令牌中包含用户信息,减少数据库查询
- 支持跨域认证,适合前后端分离架构
缺点:
- 令牌一旦生成,无法在服务器端撤销(除非使用黑名单)
- 令牌可能被窃取,需要使用 HTTPS 传输
- 令牌大小可能较大,增加网络传输负担
- 签名验证需要计算,可能影响性能
4. OAuth 认证
OAuth 是一种开放标准的授权协议,允许用户授权第三方应用访问其在另一个服务上的资源,而不需要共享密码。
4.1 OAuth 角色
- 资源所有者:用户,拥有受保护的资源
- 客户端:第三方应用,请求访问用户资源
- 授权服务器:验证用户身份并颁发访问令牌
- 资源服务器:存储用户资源,验证访问令牌并提供资源
4.2 OAuth 流程
OAuth 2.0 支持多种授权流程,包括:
- 授权码流程:最安全的流程,适用于有后端的应用
- 隐式流程:适用于无后端的单页应用
- 密码流程:直接使用用户名和密码获取令牌,仅适用于受信任的应用
- 客户端凭证流程:适用于服务器间通信
- 刷新令牌流程:使用刷新令牌获取新的访问令牌
4.3 OAuth 实现(使用 Passport.js)
const express = require('express');
const passport = require('passport');
const OAuth2Strategy = require('passport-google-oauth20').Strategy;
const session = require('express-session');
const app = express();
// 配置 Session
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false
}));
// 初始化 Passport
app.use(passport.initialize());
app.use(passport.session());
// 配置 Google OAuth 策略
passport.use(new OAuth2Strategy({
clientID: 'your-google-client-id',
clientSecret: 'your-google-client-secret',
callbackURL: 'http://localhost:3000/auth/google/callback'
}, (accessToken, refreshToken, profile, done) => {
// 这里可以根据 profile 创建或更新用户
return done(null, profile);
}));
// 序列化用户到会话
passport.serializeUser((user, done) => {
done(null, user);
});
// 从会话反序列化用户
passport.deserializeUser((user, done) => {
done(null, user);
});
// 登录路由
app.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
// 回调路由
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
// 登录成功,重定向到首页
res.redirect('/');
}
);
// 登出路由
app.get('/logout', (req, res) => {
req.logout();
res.redirect('/');
});
// 首页路由
app.get('/', (req, res) => {
if (req.isAuthenticated()) {
res.send(`<h1>欢迎,${req.user.displayName}</h1><a href="/logout">登出</a>`);
} else {
res.send('<h1>请登录</h1><a href="/auth/google">使用 Google 登录</a>');
}
});
// 启动服务器
const port = 3000;
app.listen(port, () => {
console.log(`服务器已启动,监听端口 ${port}`);
});4.4 OAuth 的优缺点
优点:
- 用户不需要共享密码给第三方应用
- 支持细粒度的权限控制
- 便于集成第三方服务
- 提高用户体验,减少注册步骤
缺点:
- 实现复杂度较高
- 依赖第三方服务的可用性
- 可能涉及隐私问题
- 需要额外的网络请求,影响性能
5. 权限控制
5.1 基于角色的访问控制(RBAC)
RBAC 是一种常见的权限控制方法,根据用户角色分配权限。
// 角色权限映射
const rolePermissions = {
admin: ['read', 'write', 'delete', 'admin'],
user: ['read', 'write'],
guest: ['read']
};
// 权限检查中间件
function checkPermission(requiredPermission) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ success: false, message: '未登录' });
}
const userRole = req.user.role;
const permissions = rolePermissions[userRole] || [];
if (!permissions.includes(requiredPermission)) {
return res.status(403).json({ success: false, message: '权限不足' });
}
next();
};
}
// 受保护的路由
app.get('/admin', authenticateToken, checkPermission('admin'), (req, res) => {
res.json({ success: true, message: '管理员页面' });
});
app.get('/user', authenticateToken, checkPermission('read'), (req, res) => {
res.json({ success: true, message: '用户页面' });
});5.2 基于属性的访问控制(ABAC)
ABAC 是一种更灵活的权限控制方法,根据用户属性、资源属性和环境条件进行授权。
// 权限检查函数
function checkAccess(user, resource, action) {
// 管理员可以访问所有资源
if (user.role === 'admin') {
return true;
}
// 用户只能访问自己的资源
if (resource.type === 'user' && resource.ownerId === user.id) {
return true;
}
// 其他规则...
return false;
}
// 权限检查中间件
function checkAccessMiddleware(resourceType, action) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ success: false, message: '未登录' });
}
const resource = {
type: resourceType,
ownerId: req.params.id, // 假设资源 ID 是用户 ID
// 其他资源属性...
};
if (!checkAccess(req.user, resource, action)) {
return res.status(403).json({ success: false, message: '权限不足' });
}
next();
};
}
// 受保护的路由
app.get('/users/:id', authenticateToken, checkAccessMiddleware('user', 'read'), (req, res) => {
res.json({ success: true, message: '用户信息' });
});实用案例分析
案例:完整的用户登录系统
下面我们将使用 Express、JWT 和 MongoDB 实现一个完整的用户登录系统,支持用户注册、登录、密码重置和权限控制。
项目结构
user-auth-system/
├── app.js
├── config/
│ ├── db.js
│ └── jwt.js
├── middleware/
│ ├── auth.js
│ └── permissions.js
├── models/
│ └── User.js
├── routes/
│ ├── auth.js
│ ├── user.js
│ └── admin.js
├── controllers/
│ ├── authController.js
│ ├── userController.js
│ └── adminController.js
├── utils/
│ ├── password.js
│ └── email.js
├── package.json
└── .env安装依赖
npm install express mongoose jsonwebtoken bcrypt dotenv nodemailer配置文件(.env)
# MongoDB 配置
MONGODB_URI=mongodb://localhost:27017/user-auth-system
# JWT 配置
JWT_SECRET=your-secret-key
JWT_EXPIRES_IN=1h
# 服务器端口
PORT=3000
# 邮件配置
EMAIL_SERVICE=gmail
EMAIL_USER=your-email@gmail.com
EMAIL_PASS=your-email-password数据库配置(config/db.js)
const mongoose = require('mongoose');
require('dotenv').config();
// 连接 MongoDB
async function connectDB() {
try {
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log('成功连接到 MongoDB');
} catch (error) {
console.error('连接 MongoDB 失败:', error);
process.exit(1);
}
}
module.exports = connectDB;JWT 配置(config/jwt.js)
const jwt = require('jsonwebtoken');
require('dotenv').config();
// 生成 JWT
function generateToken(user) {
return jwt.sign(
{ userId: user._id, username: user.username, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN }
);
}
// 验证 JWT
function verifyToken(token) {
return jwt.verify(token, process.env.JWT_SECRET);
}
module.exports = {
generateToken,
verifyToken
};密码工具(utils/password.js)
const bcrypt = require('bcrypt');
// 哈希密码
async function hashPassword(password) {
const saltRounds = 10;
return await bcrypt.hash(password, saltRounds);
}
// 验证密码
async function verifyPassword(password, hashedPassword) {
return await bcrypt.compare(password, hashedPassword);
}
module.exports = {
hashPassword,
verifyPassword
};邮件工具(utils/email.js)
const nodemailer = require('nodemailer');
require('dotenv').config();
// 创建邮件传输器
const transporter = nodemailer.createTransporter({
service: process.env.EMAIL_SERVICE,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});
// 发送邮件
async function sendEmail(to, subject, text, html) {
try {
await transporter.sendMail({
from: process.env.EMAIL_USER,
to,
subject,
text,
html
});
console.log('邮件发送成功');
} catch (error) {
console.error('邮件发送失败:', error);
}
}
module.exports = sendEmail;用户模型(models/User.js)
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
role: {
type: String,
enum: ['admin', 'user'],
default: 'user'
},
resetPasswordToken: {
type: String
},
resetPasswordExpires: {
type: Date
},
createdAt: {
type: Date,
default: Date.now
}
});
const User = mongoose.model('User', userSchema);
module.exports = User;认证中间件(middleware/auth.js)
const { verifyToken } = require('../config/jwt');
// 认证中间件
function authenticateToken(req, res, next) {
// 从请求头获取令牌
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ success: false, message: '未提供令牌' });
}
try {
// 验证令牌
const user = verifyToken(token);
req.user = user;
next();
} catch (error) {
return res.status(403).json({ success: false, message: '令牌无效' });
}
}
module.exports = authenticateToken;权限中间件(middleware/permissions.js)
// 角色权限映射
const rolePermissions = {
admin: ['read', 'write', 'delete', 'admin'],
user: ['read', 'write']
};
// 权限检查中间件
function checkPermission(requiredPermission) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ success: false, message: '未登录' });
}
const userRole = req.user.role;
const permissions = rolePermissions[userRole] || [];
if (!permissions.includes(requiredPermission)) {
return res.status(403).json({ success: false, message: '权限不足' });
}
next();
};
}
module.exports = checkPermission;认证控制器(controllers/authController.js)
const User = require('../models/User');
const { hashPassword, verifyPassword } = require('../utils/password');
const { generateToken } = require('../config/jwt');
const sendEmail = require('../utils/email');
const crypto = require('crypto');
// 用户注册
async function register(req, res) {
try {
const { username, email, password } = req.body;
// 检查用户名是否已存在
const existingUser = await User.findOne({ $or: [{ username }, { email }] });
if (existingUser) {
return res.status(400).json({ success: false, message: '用户名或邮箱已存在' });
}
// 哈希密码
const hashedPassword = await hashPassword(password);
// 创建用户
const user = new User({
username,
email,
password: hashedPassword
});
await user.save();
// 生成令牌
const token = generateToken(user);
res.status(201).json({
success: true,
message: '注册成功',
token,
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
}
});
} catch (error) {
console.error('注册失败:', error);
res.status(500).json({ success: false, message: '注册失败', error: error.message });
}
}
// 用户登录
async function login(req, res) {
try {
const { email, password } = req.body;
// 查找用户
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ success: false, message: '邮箱或密码错误' });
}
// 验证密码
const isValidPassword = await verifyPassword(password, user.password);
if (!isValidPassword) {
return res.status(401).json({ success: false, message: '邮箱或密码错误' });
}
// 生成令牌
const token = generateToken(user);
res.json({
success: true,
message: '登录成功',
token,
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
}
});
} catch (error) {
console.error('登录失败:', error);
res.status(500).json({ success: false, message: '登录失败', error: error.message });
}
}
// 忘记密码
async function forgotPassword(req, res) {
try {
const { email } = req.body;
// 查找用户
const user = await User.findOne({ email });
if (!user) {
return res.status(404).json({ success: false, message: '用户不存在' });
}
// 生成重置密码令牌
const resetToken = crypto.randomBytes(32).toString('hex');
const resetPasswordToken = crypto.createHash('sha256').update(resetToken).digest('hex');
const resetPasswordExpires = Date.now() + 3600000; // 1小时后过期
// 更新用户
user.resetPasswordToken = resetPasswordToken;
user.resetPasswordExpires = resetPasswordExpires;
await user.save();
// 发送重置密码邮件
const resetUrl = `http://localhost:3000/reset-password/${resetToken}`;
const message = `请点击以下链接重置密码:<a href="${resetUrl}">${resetUrl}</a>`;
await sendEmail(user.email, '重置密码', message, message);
res.json({ success: true, message: '重置密码邮件已发送' });
} catch (error) {
console.error('忘记密码失败:', error);
res.status(500).json({ success: false, message: '忘记密码失败', error: error.message });
}
}
// 重置密码
async function resetPassword(req, res) {
try {
const { token, password } = req.body;
// 哈希令牌
const resetPasswordToken = crypto.createHash('sha256').update(token).digest('hex');
// 查找用户
const user = await User.findOne({
resetPasswordToken,
resetPasswordExpires: { $gt: Date.now() }
});
if (!user) {
return res.status(400).json({ success: false, message: '令牌无效或已过期' });
}
// 哈希新密码
const hashedPassword = await hashPassword(password);
// 更新用户
user.password = hashedPassword;
user.resetPasswordToken = undefined;
user.resetPasswordExpires = undefined;
await user.save();
res.json({ success: true, message: '密码重置成功' });
} catch (error) {
console.error('重置密码失败:', error);
res.status(500).json({ success: false, message: '重置密码失败', error: error.message });
}
}
// 获取当前用户信息
async function getCurrentUser(req, res) {
try {
const user = await User.findById(req.user.userId).select('-password');
if (!user) {
return res.status(404).json({ success: false, message: '用户不存在' });
}
res.json({ success: true, user });
} catch (error) {
console.error('获取用户信息失败:', error);
res.status(500).json({ success: false, message: '获取用户信息失败', error: error.message });
}
}
module.exports = {
register,
login,
forgotPassword,
resetPassword,
getCurrentUser
};用户控制器(controllers/userController.js)
const User = require('../models/User');
// 获取用户列表
async function getAllUsers(req, res) {
try {
const users = await User.find().select('-password');
res.json({ success: true, users });
} catch (error) {
console.error('获取用户列表失败:', error);
res.status(500).json({ success: false, message: '获取用户列表失败', error: error.message });
}
}
// 获取用户详情
async function getUserById(req, res) {
try {
const user = await User.findById(req.params.id).select('-password');
if (!user) {
return res.status(404).json({ success: false, message: '用户不存在' });
}
res.json({ success: true, user });
} catch (error) {
console.error('获取用户详情失败:', error);
res.status(500).json({ success: false, message: '获取用户详情失败', error: error.message });
}
}
// 更新用户信息
async function updateUser(req, res) {
try {
const { username, email } = req.body;
// 检查用户名和邮箱是否已被使用
const existingUser = await User.findOne({
$or: [{ username }, { email }],
_id: { $ne: req.user.userId }
});
if (existingUser) {
return res.status(400).json({ success: false, message: '用户名或邮箱已被使用' });
}
// 更新用户
const user = await User.findByIdAndUpdate(
req.user.userId,
{ username, email },
{ new: true }
).select('-password');
if (!user) {
return res.status(404).json({ success: false, message: '用户不存在' });
}
res.json({ success: true, message: '用户信息更新成功', user });
} catch (error) {
console.error('更新用户信息失败:', error);
res.status(500).json({ success: false, message: '更新用户信息失败', error: error.message });
}
}
// 删除用户
async function deleteUser(req, res) {
try {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({ success: false, message: '用户不存在' });
}
res.json({ success: true, message: '用户删除成功' });
} catch (error) {
console.error('删除用户失败:', error);
res.status(500).json({ success: false, message: '删除用户失败', error: error.message });
}
}
module.exports = {
getAllUsers,
getUserById,
updateUser,
deleteUser
};管理员控制器(controllers/adminController.js)
const User = require('../models/User');
const { hashPassword } = require('../utils/password');
// 创建用户
async function createUser(req, res) {
try {
const { username, email, password, role } = req.body;
// 检查用户名和邮箱是否已存在
const existingUser = await User.findOne({ $or: [{ username }, { email }] });
if (existingUser) {
return res.status(400).json({ success: false, message: '用户名或邮箱已存在' });
}
// 哈希密码
const hashedPassword = await hashPassword(password);
// 创建用户
const user = new User({
username,
email,
password: hashedPassword,
role
});
await user.save();
res.status(201).json({
success: true,
message: '用户创建成功',
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
}
});
} catch (error) {
console.error('创建用户失败:', error);
res.status(500).json({ success: false, message: '创建用户失败', error: error.message });
}
}
// 更新用户角色
async function updateUserRole(req, res) {
try {
const { role } = req.body;
// 更新用户角色
const user = await User.findByIdAndUpdate(
req.params.id,
{ role },
{ new: true }
).select('-password');
if (!user) {
return res.status(404).json({ success: false, message: '用户不存在' });
}
res.json({ success: true, message: '用户角色更新成功', user });
} catch (error) {
console.error('更新用户角色失败:', error);
res.status(500).json({ success: false, message: '更新用户角色失败', error: error.message });
}
}
module.exports = {
createUser,
updateUserRole
};认证路由(routes/auth.js)
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
const authenticateToken = require('../middleware/auth');
// 注册
router.post('/register', authController.register);
// 登录
router.post('/login', authController.login);
// 忘记密码
router.post('/forgot-password', authController.forgotPassword);
// 重置密码
router.post('/reset-password', authController.resetPassword);
// 获取当前用户信息
router.get('/me', authenticateToken, authController.getCurrentUser);
module.exports = router;用户路由(routes/user.js)
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const authenticateToken = require('../middleware/auth');
const checkPermission = require('../middleware/permissions');
// 获取用户列表(需要管理员权限)
router.get('/', authenticateToken, checkPermission('admin'), userController.getAllUsers);
// 获取用户详情
router.get('/:id', authenticateToken, userController.getUserById);
// 更新用户信息
router.put('/update', authenticateToken, userController.updateUser);
// 删除用户(需要管理员权限)
router.delete('/:id', authenticateToken, checkPermission('admin'), userController.deleteUser);
module.exports = router;管理员路由(routes/admin.js)
const express = require('express');
const router = express.Router();
const adminController = require('../controllers/adminController');
const authenticateToken = require('../middleware/auth');
const checkPermission = require('../middleware/permissions');
// 创建用户
router.post('/users', authenticateToken, checkPermission('admin'), adminController.createUser);
// 更新用户角色
router.put('/users/:id/role', authenticateToken, checkPermission('admin'), adminController.updateUserRole);
module.exports = router;主应用文件(app.js)
const express = require('express');
const dotenv = require('dotenv');
const connectDB = require('./config/db');
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/user');
const adminRoutes = require('./routes/admin');
// 加载环境变量
dotenv.config();
// 连接数据库
connectDB();
// 创建 Express 应用
const app = express();
// 中间件
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 路由
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
app.use('/api/admin', adminRoutes);
// 健康检查
app.get('/health', (req, res) => {
res.json({ status: 'ok', message: '服务运行正常' });
});
// 启动服务器
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`服务器已启动,监听端口 ${port}`);
});代码解析:
项目结构:使用了模块化的项目结构,将代码分为配置、中间件、模型、路由和控制器等部分
认证机制:实现了基于 JWT 的认证,包括用户注册、登录、忘记密码和重置密码功能
授权机制:实现了基于角色的访问控制(RBAC),限制不同角色的访问权限
数据安全:使用 bcrypt 对密码进行哈希处理,使用 HTTPS 传输令牌
错误处理:对各种操作可能出现的错误进行了处理,返回友好的错误信息
中间件:使用了认证中间件和权限检查中间件,自动处理认证和授权
数据库操作:使用 Mongoose ODM 操作 MongoDB,实现了用户的 CRUD 操作
邮件服务:集成了 nodemailer 发送重置密码邮件
运行方法:
- 安装依赖:
npm install express mongoose jsonwebtoken bcrypt dotenv nodemailer passport-google-oauth20 - 创建项目结构,将上述代码保存到对应文件
- 创建
.env文件,配置 MongoDB 连接信息、JWT 密钥和邮件服务信息 - 启动 MongoDB 服务
- 启动应用:
node app.js - 使用 API 测试工具(如 Postman)测试各个接口
学习目标
通过本集的学习,你应该能够:
- 理解认证与授权的基本概念和区别
- 掌握 Session、JWT 和 OAuth 等认证机制的原理和实现方法
- 学会使用基于角色的访问控制(RBAC)实现授权
- 理解密码哈希、令牌验证等安全技术
- 实现一个完整的用户登录系统,包括注册、登录、密码重置和权限控制
- 掌握 Passport.js 等认证库的使用方法
- 理解 OAuth 2.0 授权流程和应用场景
小结
认证与授权是 Web 应用安全的核心组成部分。本集介绍了几种常见的认证机制,包括 Session、JWT 和 OAuth,以及基于角色的访问控制(RBAC)授权方法。通过一个完整的用户登录系统案例,我们展示了如何在 Node.js 应用中实现这些认证与授权机制,包括用户注册、登录、密码重置和权限控制等功能。
在实际应用中,我们需要根据具体的业务场景选择合适的认证与授权机制。例如,对于前后端分离架构,JWT 是一种常见的选择;对于需要集成第三方服务的应用,OAuth 是一种理想的选择;对于传统的 Web 应用,Session 仍然是一种简单有效的选择。
无论选择哪种认证与授权机制,我们都需要注意安全性,例如使用 HTTPS 传输敏感信息,对密码进行哈希处理,设置合理的令牌过期时间,以及实施适当的权限控制等。
在下一集中,我们将学习 Node.js RESTful API 设计,了解如何设计和实现符合 REST 原则的 API,包括 API 设计规范、错误处理和版本控制等。