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 认证流程

  1. 用户输入用户名和密码,发送登录请求
  2. 服务器验证用户名和密码,验证通过后创建会话
  3. 服务器将会话 ID 存储在 Cookie 中,返回给客户端
  4. 客户端后续请求会携带包含会话 ID 的 Cookie
  5. 服务器通过会话 ID 查找会话信息,验证用户身份
  6. 用户登出时,服务器销毁会话

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_adQssw5c

3.2 JWT 认证流程

  1. 用户输入用户名和密码,发送登录请求
  2. 服务器验证用户名和密码,验证通过后生成 JWT
  3. 服务器将 JWT 返回给客户端
  4. 客户端存储 JWT(通常存储在 localStorage 或 Cookie 中)
  5. 客户端后续请求携带 JWT(通常在 Authorization 头部)
  6. 服务器验证 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 支持多种授权流程,包括:

  1. 授权码流程:最安全的流程,适用于有后端的应用
  2. 隐式流程:适用于无后端的单页应用
  3. 密码流程:直接使用用户名和密码获取令牌,仅适用于受信任的应用
  4. 客户端凭证流程:适用于服务器间通信
  5. 刷新令牌流程:使用刷新令牌获取新的访问令牌

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}`);
});

代码解析:

  1. 项目结构:使用了模块化的项目结构,将代码分为配置、中间件、模型、路由和控制器等部分

  2. 认证机制:实现了基于 JWT 的认证,包括用户注册、登录、忘记密码和重置密码功能

  3. 授权机制:实现了基于角色的访问控制(RBAC),限制不同角色的访问权限

  4. 数据安全:使用 bcrypt 对密码进行哈希处理,使用 HTTPS 传输令牌

  5. 错误处理:对各种操作可能出现的错误进行了处理,返回友好的错误信息

  6. 中间件:使用了认证中间件和权限检查中间件,自动处理认证和授权

  7. 数据库操作:使用 Mongoose ODM 操作 MongoDB,实现了用户的 CRUD 操作

  8. 邮件服务:集成了 nodemailer 发送重置密码邮件

运行方法:

  1. 安装依赖:npm install express mongoose jsonwebtoken bcrypt dotenv nodemailer passport-google-oauth20
  2. 创建项目结构,将上述代码保存到对应文件
  3. 创建 .env 文件,配置 MongoDB 连接信息、JWT 密钥和邮件服务信息
  4. 启动 MongoDB 服务
  5. 启动应用:node app.js
  6. 使用 API 测试工具(如 Postman)测试各个接口

学习目标

通过本集的学习,你应该能够:

  1. 理解认证与授权的基本概念和区别
  2. 掌握 Session、JWT 和 OAuth 等认证机制的原理和实现方法
  3. 学会使用基于角色的访问控制(RBAC)实现授权
  4. 理解密码哈希、令牌验证等安全技术
  5. 实现一个完整的用户登录系统,包括注册、登录、密码重置和权限控制
  6. 掌握 Passport.js 等认证库的使用方法
  7. 理解 OAuth 2.0 授权流程和应用场景

小结

认证与授权是 Web 应用安全的核心组成部分。本集介绍了几种常见的认证机制,包括 Session、JWT 和 OAuth,以及基于角色的访问控制(RBAC)授权方法。通过一个完整的用户登录系统案例,我们展示了如何在 Node.js 应用中实现这些认证与授权机制,包括用户注册、登录、密码重置和权限控制等功能。

在实际应用中,我们需要根据具体的业务场景选择合适的认证与授权机制。例如,对于前后端分离架构,JWT 是一种常见的选择;对于需要集成第三方服务的应用,OAuth 是一种理想的选择;对于传统的 Web 应用,Session 仍然是一种简单有效的选择。

无论选择哪种认证与授权机制,我们都需要注意安全性,例如使用 HTTPS 传输敏感信息,对密码进行哈希处理,设置合理的令牌过期时间,以及实施适当的权限控制等。

在下一集中,我们将学习 Node.js RESTful API 设计,了解如何设计和实现符合 REST 原则的 API,包括 API 设计规范、错误处理和版本控制等。

« 上一篇 Node.js Redis 缓存 下一篇 » Node.js RESTful API 设计