强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Node.js 开发指南 / 第 15 章 · 认证与授权

第 15 章 · 认证与授权

15.1 认证 vs 授权

概念 说明 问题
认证(Authentication) 你是谁? 验证用户身份
授权(Authorization) 你能做什么? 验证用户权限

15.2 Session 认证

npm install express-session connect-redis
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');

const app = express();

// Redis 存储 session(生产环境推荐)
const redisClient = createClient();
redisClient.connect();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET || 'your-secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',  // HTTPS
    httpOnly: true,    // 防止 XSS 访问
    maxAge: 24 * 60 * 60 * 1000, // 24 小时
    sameSite: 'lax',   // CSRF 防护
  },
}));

// 登录
app.post('/api/login', (req, res) => {
  const { username, password } = req.body;
  const user = authenticate(username, password);
  if (!user) {
    return res.status(401).json({ error: '用户名或密码错误' });
  }
  req.session.userId = user.id;
  req.session.role = user.role;
  res.json({ message: '登录成功' });
});

// 认证中间件
function requireAuth(req, res, next) {
  if (!req.session.userId) {
    return res.status(401).json({ error: '请先登录' });
  }
  next();
}

// 获取当前用户
app.get('/api/me', requireAuth, async (req, res) => {
  const user = await db.users.findById(req.session.userId);
  res.json({ user });
});

// 登出
app.post('/api/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) return res.status(500).json({ error: '登出失败' });
    res.clearCookie('connect.sid');
    res.json({ message: '已登出' });
  });
});

15.3 JWT 认证

npm install jsonwebtoken bcrypt
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const JWT_EXPIRES = '24h';
const REFRESH_EXPIRES = '7d';

// 生成令牌
function generateTokens(user) {
  const accessToken = jwt.sign(
    { id: user.id, role: user.role },
    JWT_SECRET,
    { expiresIn: JWT_EXPIRES }
  );
  
  const refreshToken = jwt.sign(
    { id: user.id },
    process.env.REFRESH_SECRET,
    { expiresIn: REFRESH_EXPIRES }
  );
  
  return { accessToken, refreshToken };
}

// 验证令牌
function verifyToken(token) {
  return jwt.verify(token, JWT_SECRET);
}

// 密码哈希
async function hashPassword(password) {
  return bcrypt.hash(password, 12);
}

async function comparePassword(password, hash) {
  return bcrypt.compare(password, hash);
}

// 登录路由
app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;
  
  const user = await db.users.findByEmail(email);
  if (!user || !(await comparePassword(password, user.passwordHash))) {
    return res.status(401).json({ error: '邮箱或密码错误' });
  }

  const tokens = generateTokens(user);
  res.json({
    user: { id: user.id, name: user.name, email: user.email, role: user.role },
    ...tokens,
  });
});

// 认证中间件
function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: '未提供认证令牌' });
  }

  try {
    const token = authHeader.split(' ')[1];
    req.user = verifyToken(token);
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: '令牌已过期', code: 'TOKEN_EXPIRED' });
    }
    res.status(401).json({ error: '令牌无效' });
  }
}

// 角色授权中间件
function authorize(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: '权限不足' });
    }
    next();
  };
}

// 刷新令牌
app.post('/api/refresh', async (req, res) => {
  const { refreshToken } = req.body;
  try {
    const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
    const user = await db.users.findById(payload.id);
    if (!user) return res.status(401).json({ error: '用户不存在' });
    
    const tokens = generateTokens(user);
    res.json(tokens);
  } catch {
    res.status(401).json({ error: '刷新令牌无效' });
  }
});

// 使用
app.get('/api/admin/users', authenticate, authorize('admin'), (req, res) => {
  res.json({ users: [] });
});

Session vs JWT 对比

特性 Session JWT
存储位置 服务端 客户端
扩展性 需要共享存储 天然无状态
撤销 容易(删除 session) 困难(需要黑名单)
跨域 Cookie 受域限制 Header 自由传递
大小 Cookie 只存 ID 可能较大
适用场景 传统 Web 应用 API / 微服务 / 移动端

15.4 OAuth2 认证

npm install passport passport-google-oauth20
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

passport.use(new GoogleStrategy({
  clientID: process.env.GOOGLE_CLIENT_ID,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  callbackURL: '/auth/google/callback',
}, async (accessToken, refreshToken, profile, done) => {
  try {
    // 查找或创建用户
    let user = await db.users.findByGoogleId(profile.id);
    if (!user) {
      user = await db.users.create({
        googleId: profile.id,
        name: profile.displayName,
        email: profile.emails[0].value,
        avatar: profile.photos[0]?.value,
      });
    }
    done(null, user);
  } catch (err) {
    done(err, null);
  }
}));

passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id, done) => {
  const user = await db.users.findById(id);
  done(null, user);
});

app.use(passport.initialize());
app.use(passport.session());

// 发起 OAuth 认证
app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

// OAuth 回调
app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login' }),
  (req, res) => {
    const tokens = generateTokens(req.user);
    res.redirect(`/auth/success?token=${tokens.accessToken}`);
  }
);

OAuth2 流程

用户 → 点击"使用 Google 登录"
        ↓
重定向到 Google 授权页面
        ↓
用户授权 → Google 回调到应用
        ↓
应用收到授权码 → 用授权码换取 Access Token
        ↓
用 Access Token 获取用户信息
        ↓
创建/更新本地用户 → 登录成功

15.5 RBAC 权限模型

// 基于角色的访问控制
const permissions = {
  admin: ['users:read', 'users:write', 'users:delete', 'posts:read', 'posts:write', 'posts:delete'],
  editor: ['posts:read', 'posts:write', 'users:read'],
  viewer: ['posts:read', 'users:read'],
};

// 权限检查中间件
function requirePermission(permission) {
  return (req, res, next) => {
    const userPermissions = permissions[req.user.role] || [];
    if (!userPermissions.includes(permission)) {
      return res.status(403).json({ error: `需要权限: ${permission}` });
    }
    next();
  };
}

// 使用
app.delete('/api/users/:id', authenticate, requirePermission('users:delete'), deleteUser);

注意事项

⚠️ JWT Secret 必须安全:使用足够长的随机字符串(至少 256 位),从环境变量读取。

⚠️ 永远不要在 JWT 中存储敏感信息:JWT 只是编码(Base64),不是加密,任何人都能解码。

⚠️ bcrypt 的 salt rounds:推荐 10-12 轮,更高会显著增加计算时间。

⚠️ HTTPS 必不可少:认证令牌在 HTTP 下传输等于明文暴露。

扩展阅读


上一章第 14 章 · 数据库 下一章第 16 章 · WebSocket 实时通信 — Socket.io、实时通信和房间管理。