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

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 对比

特性SessionJWT
存储位置服务端客户端
扩展性需要共享存储天然无状态
撤销容易(删除 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、实时通信和房间管理。