Passport.js 最佳实践 #

安全最佳实践 #

1. 密码安全 #

使用强哈希算法 #

javascript
// 使用 bcryptjs
const bcrypt = require('bcryptjs');

// 注册时加密密码
const saltRounds = 12; // 推荐值 10-12
const hashedPassword = await bcrypt.hash(password, saltRounds);

// 登录时验证密码
const isMatch = await bcrypt.compare(password, hashedPassword);

密码强度验证 #

javascript
// utils/passwordValidator.js
function validatePassword(password) {
  const errors = [];
  
  if (password.length < 8) {
    errors.push('密码至少8个字符');
  }
  
  if (!/[A-Z]/.test(password)) {
    errors.push('密码需要包含大写字母');
  }
  
  if (!/[a-z]/.test(password)) {
    errors.push('密码需要包含小写字母');
  }
  
  if (!/[0-9]/.test(password)) {
    errors.push('密码需要包含数字');
  }
  
  if (!/[!@#$%^&*]/.test(password)) {
    errors.push('密码需要包含特殊字符');
  }
  
  return {
    isValid: errors.length === 0,
    errors
  };
}

module.exports = { validatePassword };

密码泄露检测 #

javascript
// 使用 haveibeenpwned API 检测密码泄露
const crypto = require('crypto');

async function isPasswordBreached(password) {
  const sha1 = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
  const prefix = sha1.substring(0, 5);
  const suffix = sha1.substring(5);
  
  const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
  const data = await response.text();
  
  return data.includes(suffix);
}

// 注册时检查
router.post('/register', async (req, res) => {
  const { password } = req.body;
  
  if (await isPasswordBreached(password)) {
    return res.status(400).json({
      success: false,
      message: '该密码已被泄露,请使用其他密码'
    });
  }
  
  // 继续注册流程
});

2. Session 安全 #

安全的 Session 配置 #

javascript
// 生产环境 Session 配置
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  name: 'sessionId', // 不使用默认名称
  resave: false,
  saveUninitialized: false,
  rolling: true, // 每次请求刷新 Cookie
  cookie: {
    secure: true, // 仅 HTTPS
    httpOnly: true, // 防止 XSS
    sameSite: 'strict', // 防止 CSRF
    maxAge: 24 * 60 * 60 * 1000, // 24 小时
    domain: process.env.COOKIE_DOMAIN
  }
}));

Session 固定攻击防护 #

javascript
// 登录成功后重新生成 Session
router.post('/login', (req, res, next) => {
  passport.authenticate('local', (err, user, info) => {
    if (err) return next(err);
    if (!user) return res.status(401).json(info);
    
    // 重新生成 Session
    req.session.regenerate((err) => {
      if (err) return next(err);
      
      req.logIn(user, (err) => {
        if (err) return next(err);
        
        // 设置登录时间
        req.session.loginTime = Date.now();
        
        res.json({ success: true, user });
      });
    });
  })(req, res, next);
});

3. JWT 安全 #

安全的 JWT 配置 #

javascript
// 生成安全的 JWT
const jwt = require('jsonwebtoken');

function generateTokens(user) {
  // 访问令牌:短有效期
  const accessToken = jwt.sign(
    {
      sub: user._id,
      role: user.role,
      iat: Math.floor(Date.now() / 1000)
    },
    process.env.JWT_SECRET,
    {
      expiresIn: '15m', // 15 分钟
      issuer: 'your-app',
      audience: 'your-app',
      jwtid: crypto.randomBytes(16).toString('hex') // 唯一 ID
    }
  );
  
  // 刷新令牌:长有效期
  const refreshToken = jwt.sign(
    {
      sub: user._id,
      type: 'refresh'
    },
    process.env.JWT_REFRESH_SECRET,
    {
      expiresIn: '7d',
      jwtid: crypto.randomBytes(16).toString('hex')
    }
  );
  
  return { accessToken, refreshToken };
}

Token 存储安全 #

javascript
// 使用 HttpOnly Cookie 存储 Token
router.post('/login', async (req, res) => {
  const { accessToken, refreshToken } = generateTokens(user);
  
  // 设置 HttpOnly Cookie
  res.cookie('accessToken', accessToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 15 * 60 * 1000 // 15 分钟
  });
  
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000 // 7 天
  });
  
  res.json({ success: true, user });
});

4. CSRF 防护 #

bash
npm install csurf
javascript
// CSRF 防护
const csrf = require('csurf');

const csrfProtection = csrf({
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict'
  }
});

// 应用到路由
app.use(csrfProtection);

// 提供 CSRF Token
app.get('/api/csrf-token', (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

// 表单提交需要 CSRF Token
app.post('/form', (req, res) => {
  // 自动验证 CSRF Token
});

5. 速率限制 #

bash
npm install express-rate-limit
javascript
// 登录速率限制
const rateLimit = require('express-rate-limit');

// 通用限制
const generalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 分钟
  max: 100, // 每个 IP 最多 100 次请求
  message: {
    success: false,
    message: '请求过于频繁,请稍后再试'
  }
});

// 登录限制(更严格)
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 分钟
  max: 5, // 每个 IP 最多 5 次登录尝试
  message: {
    success: false,
    message: '登录尝试过多,请15分钟后再试'
  },
  skipSuccessfulRequests: true // 成功的请求不计入限制
});

// 应用限制
app.use('/api/', generalLimiter);
app.use('/auth/login', loginLimiter);

6. 安全头 #

bash
npm install helmet
javascript
// 使用 Helmet 设置安全头
const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "trusted-cdn.com"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "api.your-app.com"],
      fontSrc: ["'self'", "fonts.gstatic.com"],
      objectSrc: ["'none'"],
      frameSrc: ["'none'"],
      upgradeInsecureRequests: []
    }
  },
  crossOriginEmbedderPolicy: true,
  crossOriginOpenerPolicy: true,
  crossOriginResourcePolicy: { policy: "same-origin" },
  dnsPrefetchControl: { allow: false },
  frameguard: { action: 'deny' },
  hidePoweredBy: true,
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  },
  ieNoOpen: true,
  noSniff: true,
  originAgentCluster: true,
  permittedCrossDomainPolicies: { permittedPolicies: "none" },
  referrerPolicy: { policy: "strict-origin-when-cross-origin" },
  xssFilter: true
}));

常见问题解决 #

1. 认证状态丢失 #

javascript
// 问题:刷新页面后用户状态丢失

// 原因排查
app.use((req, res, next) => {
  console.log('Session ID:', req.sessionID);
  console.log('Session:', req.session);
  console.log('User:', req.user);
  next();
});

// 解决方案
// 1. 确保 session 中间件在 passport 之前
app.use(session({...}));
app.use(passport.initialize());
app.use(passport.session());

// 2. 确保 serializeUser 和 deserializeUser 正确配置
passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
  try {
    const user = await User.findById(id);
    done(null, user);
  } catch (error) {
    done(error, null);
  }
});

// 3. 检查 Cookie 设置
app.use(session({
  cookie: {
    secure: false, // 开发环境设为 false
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000
  }
}));

2. CORS 问题 #

javascript
// 问题:跨域请求失败

// 解决方案
const cors = require('cors');

app.use(cors({
  origin: process.env.CLIENT_URL, // 允许的前端域名
  credentials: true, // 允许携带 Cookie
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// 前端配置
// fetch 请求需要设置 credentials
fetch('http://api.example.com/data', {
  credentials: 'include' // 或 'same-origin'
});

3. 重定向循环 #

javascript
// 问题:登录后无限重定向

// 原因:认证检查逻辑错误

// 错误示例
function ensureAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return res.redirect('/dashboard'); // 错误:已登录还重定向
  }
  next();
}

// 正确示例
function ensureAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next(); // 已登录,继续
  }
  res.redirect('/login'); // 未登录,重定向到登录页
}

// 避免登录页面重定向循环
function ensureNotAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return res.redirect('/dashboard');
  }
  next();
}

router.get('/login', ensureNotAuthenticated, (req, res) => {
  res.render('login');
});

4. OAuth 回调失败 #

javascript
// 问题:OAuth 回调返回错误

// 排查步骤
// 1. 检查回调 URL 配置
passport.use(new GoogleStrategy({
  clientID: process.env.GOOGLE_CLIENT_ID,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  callbackURL: process.env.GOOGLE_CALLBACK_URL // 确保与 OAuth 应用配置一致
}, verifyCallback));

// 2. 检查环境变量
console.log('Google Client ID:', process.env.GOOGLE_CLIENT_ID);
console.log('Google Callback URL:', process.env.GOOGLE_CALLBACK_URL);

// 3. 添加错误处理
router.get('/google/callback', 
  passport.authenticate('google', {
    failureRedirect: '/auth/login',
    failureFlash: true
  }),
  (req, res) => {
    res.redirect('/dashboard');
  }
);

// 4. 自定义错误处理
router.get('/google/callback', (req, res, next) => {
  passport.authenticate('google', (err, user, info) => {
    if (err) {
      console.error('OAuth error:', err);
      return res.redirect('/auth/login?error=oauth_error');
    }
    
    if (!user) {
      console.log('OAuth no user:', info);
      return res.redirect('/auth/login?error=no_user');
    }
    
    req.logIn(user, (err) => {
      if (err) return next(err);
      res.redirect('/dashboard');
    });
  })(req, res, next);
});

5. Token 过期处理 #

javascript
// 问题:Token 过期后用户体验差

// 解决方案:自动刷新 Token
// 前端拦截器
const axios = require('axios');

const api = axios.create({
  baseURL: '/api'
});

// 请求拦截器
api.interceptors.request.use(config => {
  const token = localStorage.getItem('accessToken');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 响应拦截器
api.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;
    
    // Token 过期
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      
      try {
        // 刷新 Token
        const response = await axios.post('/auth/refresh', {
          refreshToken: localStorage.getItem('refreshToken')
        });
        
        const { accessToken } = response.data;
        localStorage.setItem('accessToken', accessToken);
        
        // 重试原请求
        originalRequest.headers.Authorization = `Bearer ${accessToken}`;
        return api(originalRequest);
      } catch (refreshError) {
        // 刷新失败,跳转登录
        localStorage.removeItem('accessToken');
        localStorage.removeItem('refreshToken');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    
    return Promise.reject(error);
  }
);

性能优化 #

1. 数据库查询优化 #

javascript
// 反序列化时只查询必要字段
passport.deserializeUser(async (id, done) => {
  try {
    const user = await User.findById(id)
      .select('username email role avatar'); // 只选择必要字段
    done(null, user);
  } catch (error) {
    done(error, null);
  }
});

2. 缓存用户数据 #

javascript
// 使用 Redis 缓存用户数据
const redis = require('redis');
const client = redis.createClient();

passport.deserializeUser(async (id, done) => {
  try {
    // 先从缓存获取
    const cachedUser = await client.get(`user:${id}`);
    
    if (cachedUser) {
      return done(null, JSON.parse(cachedUser));
    }
    
    // 缓存未命中,查询数据库
    const user = await User.findById(id);
    
    if (user) {
      // 缓存用户数据,10分钟过期
      await client.setex(`user:${id}`, 600, JSON.stringify(user));
    }
    
    done(null, user);
  } catch (error) {
    done(error, null);
  }
});

// 用户信息更新时清除缓存
async function updateUser(userId, data) {
  const user = await User.findByIdAndUpdate(userId, data, { new: true });
  await client.del(`user:${userId}`);
  return user;
}

3. Session 存储优化 #

javascript
// 使用 Redis 存储 Session
const RedisStore = require('connect-redis').default;

app.use(session({
  store: new RedisStore({
    client: redisClient,
    prefix: 'sess:',
    ttl: 86400, // 24 小时
    disableTouch: false // 允许刷新
  }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  rolling: true // 自动刷新
}));

生产环境部署 #

1. 环境变量 #

bash
# .env.production
NODE_ENV=production

# Session
SESSION_SECRET=your-very-long-random-secret-key-at-least-32-chars
COOKIE_DOMAIN=.yourdomain.com

# JWT
JWT_SECRET=another-very-long-random-secret-key
JWT_REFRESH_SECRET=refresh-token-secret-key
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d

# OAuth
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_CALLBACK_URL=https://yourdomain.com/auth/google/callback

# Database
MONGODB_URI=mongodb://user:password@host:port/database
REDIS_URL=redis://user:password@host:port

2. 健康检查 #

javascript
// 健康检查端点
app.get('/health', async (req, res) => {
  const health = {
    status: 'ok',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    checks: {
      database: 'unknown',
      redis: 'unknown'
    }
  };
  
  // 检查数据库
  try {
    await mongoose.connection.db.admin().ping();
    health.checks.database = 'ok';
  } catch (error) {
    health.checks.database = 'error';
    health.status = 'degraded';
  }
  
  // 检查 Redis
  try {
    await redisClient.ping();
    health.checks.redis = 'ok';
  } catch (error) {
    health.checks.redis = 'error';
    health.status = 'degraded';
  }
  
  const statusCode = health.status === 'ok' ? 200 : 503;
  res.status(statusCode).json(health);
});

3. 日志记录 #

javascript
// 使用 Winston 记录日志
const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// 认证事件日志
app.use((req, res, next) => {
  if (req.path.startsWith('/auth')) {
    logger.info('Auth request', {
      method: req.method,
      path: req.path,
      ip: req.ip,
      userAgent: req.headers['user-agent']
    });
  }
  next();
});

// 登录成功日志
passport.serializeUser((user, done) => {
  logger.info('User logged in', {
    userId: user._id,
    username: user.username,
    ip: req?.ip
  });
  done(null, user.id);
});

4. 监控告警 #

javascript
// 登录失败监控
let failedLoginAttempts = {};
const MAX_FAILED_ATTEMPTS = 10;
const BLOCK_DURATION = 15 * 60 * 1000; // 15 分钟

function recordFailedLogin(ip) {
  if (!failedLoginAttempts[ip]) {
    failedLoginAttempts[ip] = {
      count: 0,
      firstAttempt: Date.now()
    };
  }
  
  failedLoginAttempts[ip].count++;
  failedLoginAttempts[ip].lastAttempt = Date.now();
  
  // 检测异常
  if (failedLoginAttempts[ip].count >= MAX_FAILED_ATTEMPTS) {
    logger.warn('Suspicious login activity detected', {
      ip,
      attempts: failedLoginAttempts[ip].count
    });
    // 发送告警
    sendAlert(`Suspicious login activity from IP: ${ip}`);
  }
}

// 定期清理
setInterval(() => {
  const now = Date.now();
  Object.keys(failedLoginAttempts).forEach(ip => {
    if (now - failedLoginAttempts[ip].lastAttempt > BLOCK_DURATION) {
      delete failedLoginAttempts[ip];
    }
  });
}, 60 * 1000); // 每分钟清理

检查清单 #

部署前检查 #

text
□ 环境变量已正确配置
□ Session Secret 足够复杂(至少32字符)
□ JWT Secret 足够复杂
□ 数据库连接已加密
□ Redis 连接已加密
□ HTTPS 已启用
□ Cookie 设置了 secure 和 httpOnly
□ CSRF 防护已启用
□ 速率限制已配置
□ 安全头已设置
□ 日志记录已配置
□ 健康检查端点已配置
□ 监控告警已配置
□ 错误处理已完善
□ 敏感信息不在日志中输出

安全检查 #

text
□ 密码使用强哈希算法
□ 密码强度验证已启用
□ 登录失败次数限制已配置
□ Session 固定攻击防护已启用
□ Token 有效期合理
□ Token 刷新机制已实现
□ Token 黑名单已实现(如需要)
□ CORS 配置正确
□ XSS 防护已启用
□ SQL 注入防护已启用
□ 输入验证已完善
□ 输出编码已完善

总结 #

Passport.js 是一个强大且灵活的认证中间件。遵循这些最佳实践,可以构建安全、可靠的认证系统:

  1. 安全第一:始终优先考虑安全性,使用强加密、安全配置
  2. 防御深度:多层防护,不依赖单一安全措施
  3. 最小权限:只授予必要的权限
  4. 监控告警:实时监控认证活动,及时发现异常
  5. 定期审计:定期检查和更新安全配置

继续学习和实践,不断提升认证系统的安全性和可靠性!

最后更新:2026-03-28