Passport.js 高级主题 #

自定义策略 #

创建自定义策略 #

Passport.js 允许你创建自定义策略来满足特定的认证需求。

javascript
// strategies/ApiKeyStrategy.js
const passport = require('passport');
const util = require('util');

function ApiKeyStrategy(options, verify) {
  if (typeof options === 'function') {
    verify = options;
    options = {};
  }
  
  if (!verify) {
    throw new TypeError('ApiKeyStrategy requires a verify callback');
  }
  
  passport.Strategy.call(this);
  this.name = 'apikey';
  this._verify = verify;
  this._headerField = options.headerField || 'x-api-key';
  this._queryField = options.queryField || 'api_key';
}

util.inherits(ApiKeyStrategy, passport.Strategy);

ApiKeyStrategy.prototype.authenticate = function(req, options) {
  const self = this;
  
  // 从 Header 或 Query 获取 API Key
  const apiKey = req.headers[this._headerField] || 
                 req.query[this._queryField];
  
  if (!apiKey) {
    return this.fail({ message: 'Missing API key' }, 400);
  }
  
  function verified(err, user, info) {
    if (err) {
      return self.error(err);
    }
    if (!user) {
      return self.fail(info);
    }
    self.success(user, info);
  }
  
  try {
    this._verify(apiKey, verified);
  } catch (ex) {
    return self.error(ex);
  }
};

module.exports = ApiKeyStrategy;

使用自定义策略 #

javascript
// config/passport.js
const ApiKeyStrategy = require('../strategies/ApiKeyStrategy');

passport.use(new ApiKeyStrategy(
  async (apiKey, done) => {
    try {
      const user = await User.findOne({ apiKey });
      
      if (!user) {
        return done(null, false, { message: 'Invalid API key' });
      }
      
      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

// 路由中使用
router.get('/api/data', passport.authenticate('apikey', { session: false }), (req, res) => {
  res.json({ data: 'protected data' });
});

自定义 Bearer Token 策略 #

javascript
// strategies/CustomBearerStrategy.js
const passport = require('passport');
const util = require('util');

function CustomBearerStrategy(verify) {
  passport.Strategy.call(this);
  this.name = 'custom-bearer';
  this._verify = verify;
}

util.inherits(CustomBearerStrategy, passport.Strategy);

CustomBearerStrategy.prototype.authenticate = function(req) {
  const authHeader = req.headers['authorization'];
  
  if (!authHeader) {
    return this.fail({ message: 'No authorization header' });
  }
  
  const parts = authHeader.split(' ');
  
  if (parts.length !== 2 || parts[0].toLowerCase() !== 'bearer') {
    return this.fail({ message: 'Invalid authorization header format' });
  }
  
  const token = parts[1];
  
  this._verify(token, (err, user, info) => {
    if (err) return this.error(err);
    if (!user) return this.fail(info);
    this.success(user, info);
  });
};

module.exports = CustomBearerStrategy;

多策略组合 #

同时支持多种认证方式 #

javascript
// 同时支持 JWT 和 API Key
router.get('/api/data', 
  passport.authenticate(['jwt', 'apikey'], { session: false }),
  (req, res) => {
    res.json({ data: 'protected data' });
  }
);

条件认证中间件 #

javascript
// middleware/auth.js
function multiAuth(strategies, options = {}) {
  return (req, res, next) => {
    const authPromises = strategies.map(strategy => {
      return new Promise((resolve) => {
        passport.authenticate(strategy, { session: false }, (err, user, info) => {
          if (err || !user) {
            resolve({ success: false, strategy, info });
          } else {
            resolve({ success: true, user, strategy });
          }
        })(req, res, () => {});
      });
    });
    
    Promise.all(authPromises)
      .then(results => {
        const success = results.find(r => r.success);
        
        if (success) {
          req.user = success.user;
          req.authStrategy = success.strategy;
          next();
        } else {
          res.status(401).json({
            success: false,
            message: 'Authentication failed',
            details: results.map(r => ({
              strategy: r.strategy,
              message: r.info?.message
            }))
          });
        }
      })
      .catch(next);
  };
}

module.exports = { multiAuth };
javascript
// 使用多策略认证
const { multiAuth } = require('../middleware/auth');

router.get('/api/data', multiAuth(['jwt', 'apikey', 'basic']), (req, res) => {
  res.json({
    data: 'protected data',
    authenticatedVia: req.authStrategy
  });
});

优先级认证 #

javascript
// middleware/auth.js
function priorityAuth(strategies) {
  return async (req, res, next) => {
    for (const strategy of strategies) {
      try {
        const result = await new Promise((resolve) => {
          passport.authenticate(strategy, { session: false }, (err, user) => {
            resolve({ err, user });
          })(req, res, () => {});
        });
        
        if (result.user) {
          req.user = result.user;
          req.authStrategy = strategy;
          return next();
        }
      } catch (error) {
        console.error(`Strategy ${strategy} error:`, error);
      }
    }
    
    res.status(401).json({
      success: false,
      message: 'All authentication strategies failed'
    });
  };
}

// 使用优先级认证
router.get('/api/data', priorityAuth(['jwt', 'session', 'apikey']), (req, res) => {
  res.json({ data: 'protected' });
});

动态策略选择 #

根据请求选择策略 #

javascript
// middleware/auth.js
function dynamicAuth() {
  return (req, res, next) => {
    let strategy;
    
    // 根据请求特征选择策略
    if (req.headers['authorization']?.startsWith('Bearer ')) {
      strategy = 'jwt';
    } else if (req.headers['x-api-key']) {
      strategy = 'apikey';
    } else if (req.session?.passport?.user) {
      strategy = 'session';
    } else {
      return res.status(401).json({
        success: false,
        message: 'No valid authentication method found'
      });
    }
    
    passport.authenticate(strategy, { session: false })(req, res, next);
  };
}

// 使用动态策略
router.get('/api/data', dynamicAuth(), (req, res) => {
  res.json({ data: 'protected' });
});

根据路由选择策略 #

javascript
// config/auth-strategies.js
const routeStrategies = {
  '/api/public': null,
  '/api/user': ['jwt', 'session'],
  '/api/admin': ['jwt'],
  '/api/service': ['apikey']
};

function routeBasedAuth(req, res, next) {
  const path = req.path;
  let strategies = null;
  
  // 匹配路由
  for (const [pattern, strategy] of Object.entries(routeStrategies)) {
    if (path.startsWith(pattern)) {
      strategies = strategy;
      break;
    }
  }
  
  // 无需认证
  if (!strategies) {
    return next();
  }
  
  // 使用对应策略
  passport.authenticate(strategies, { session: false })(req, res, next);
}

app.use(routeBasedAuth);

匿名认证 #

可选认证 #

javascript
// middleware/auth.js
function optionalAuth(strategies) {
  return (req, res, next) => {
    passport.authenticate(strategies, { session: false }, (err, user) => {
      if (err) return next(err);
      
      if (user) {
        req.user = user;
      }
      
      next();
    })(req, res, next);
  };
}

// 使用可选认证
router.get('/posts', optionalAuth('jwt'), async (req, res) => {
  let posts;
  
  if (req.user) {
    // 已登录用户可以看到私有文章
    posts = await Post.find({ $or: [
      { isPublic: true },
      { author: req.user._id }
    ]});
  } else {
    // 未登录用户只能看到公开文章
    posts = await Post.find({ isPublic: true });
  }
  
  res.json(posts);
});

双因素认证 #

TOTP 策略 #

bash
npm install otplib
javascript
// strategies/TotpStrategy.js
const passport = require('passport');
const util = require('util');
const authenticator = require('otplib/authenticator');

function TotpStrategy(options, verify) {
  passport.Strategy.call(this);
  this.name = 'totp';
  this._verify = verify;
  this._window = options.window || 1;
}

util.inherits(TotpStrategy, passport.Strategy);

TotpStrategy.prototype.authenticate = function(req) {
  const token = req.body.token || req.query.token;
  const user = req.user;
  
  if (!user) {
    return this.fail({ message: 'User not authenticated' });
  }
  
  if (!token) {
    return this.fail({ message: 'Missing TOTP token' });
  }
  
  this._verify(user, token, (err, success, info) => {
    if (err) return this.error(err);
    if (!success) return this.fail(info);
    this.success(user, info);
  });
};

module.exports = TotpStrategy;
javascript
// config/passport.js
const TotpStrategy = require('../strategies/TotpStrategy');

passport.use('totp', new TotpStrategy(
  async (user, token, done) => {
    try {
      const isValid = authenticator.check(token, user.totpSecret);
      
      if (!isValid) {
        return done(null, false, { message: 'Invalid TOTP token' });
      }
      
      return done(null, true);
    } catch (error) {
      return done(error);
    }
  }
));

双因素认证流程 #

javascript
// routes/auth.js

// 启用双因素认证
router.post('/2fa/enable', ensureAuthenticated, async (req, res) => {
  const secret = authenticator.generateSecret();
  
  // 保存密钥(实际应用中应该加密存储)
  req.user.totpSecret = secret;
  req.user.totpEnabled = false; // 先不启用,等验证成功后再启用
  await req.user.save();
  
  // 生成二维码 URL
  const otpauth = authenticator.keyuri(
    req.user.username,
    'MyApp',
    secret
  );
  
  res.json({
    secret,
    otpauth,
    qrCodeUrl: `https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl=${encodeURIComponent(otpauth)}`
  });
});

// 验证并启用双因素认证
router.post('/2fa/verify', ensureAuthenticated, (req, res, next) => {
  passport.authenticate('totp', (err, success, info) => {
    if (err) return next(err);
    
    if (!success) {
      return res.status(400).json({
        success: false,
        message: info.message
      });
    }
    
    // 启用双因素认证
    req.user.totpEnabled = true;
    req.user.save();
    
    res.json({ success: true });
  })(req, res, next);
});

// 双因素认证登录
router.post('/login/2fa', ensureAuthenticated, (req, res, next) => {
  if (!req.user.totpEnabled) {
    return res.json({ success: true, user: req.user });
  }
  
  passport.authenticate('totp', (err, success, info) => {
    if (err) return next(err);
    
    if (!success) {
      return res.status(400).json({
        success: false,
        message: info.message
      });
    }
    
    res.json({ success: true, user: req.user });
  })(req, res, next);
});

认证状态管理 #

认证状态中间件 #

javascript
// middleware/auth.js
function authState(req, res, next) {
  req.authState = {
    isAuthenticated: req.isAuthenticated(),
    user: req.user || null,
    strategy: req.session?.passport?.strategy || null,
    timestamp: Date.now()
  };
  
  next();
}

// 使用认证状态
router.get('/auth/status', authState, (req, res) => {
  res.json(req.authState);
});

认证事件钩子 #

javascript
// hooks/authHooks.js
const authEvents = new EventEmitter();

function authHook(req, res, next) {
  const originalLogIn = req.logIn;
  const originalLogOut = req.logout;
  
  req.logIn = function(user, options, done) {
    return originalLogIn.call(this, user, options, (err) => {
      if (!err) {
        authEvents.emit('login', {
          user,
          ip: req.ip,
          userAgent: req.headers['user-agent'],
          timestamp: new Date()
        });
      }
      done?.(err);
    });
  };
  
  req.logout = function(done) {
    const user = req.user;
    return originalLogOut.call(this, (err) => {
      if (!err) {
        authEvents.emit('logout', {
          user,
          ip: req.ip,
          timestamp: new Date()
        });
      }
      done?.(err);
    });
  };
  
  next();
}

// 监听认证事件
authEvents.on('login', async (data) => {
  console.log('User logged in:', data.user.username);
  // 记录登录日志
  await LoginLog.create(data);
});

authEvents.on('logout', async (data) => {
  console.log('User logged out:', data.user.username);
});

module.exports = authHook;

权限控制 #

RBAC(基于角色的访问控制) #

javascript
// middleware/rbac.js
function checkPermission(resource, action) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({
        success: false,
        message: 'Unauthorized'
      });
    }
    
    const role = req.user.role;
    const permissions = {
      admin: {
        user: ['create', 'read', 'update', 'delete'],
        post: ['create', 'read', 'update', 'delete'],
        comment: ['create', 'read', 'update', 'delete']
      },
      editor: {
        user: ['read'],
        post: ['create', 'read', 'update'],
        comment: ['create', 'read', 'update']
      },
      user: {
        user: ['read'],
        post: ['create', 'read'],
        comment: ['create', 'read']
      }
    };
    
    const rolePermissions = permissions[role];
    
    if (!rolePermissions) {
      return res.status(403).json({
        success: false,
        message: 'Invalid role'
      });
    }
    
    const resourcePermissions = rolePermissions[resource];
    
    if (!resourcePermissions || !resourcePermissions.includes(action)) {
      return res.status(403).json({
        success: false,
        message: 'Permission denied'
      });
    }
    
    next();
  };
}

module.exports = { checkPermission };
javascript
// 使用权限控制
const { checkPermission } = require('../middleware/rbac');

router.post('/users', 
  authenticateJWT,
  checkPermission('user', 'create'),
  (req, res) => {
    // 创建用户
  }
);

router.delete('/posts/:id',
  authenticateJWT,
  checkPermission('post', 'delete'),
  (req, res) => {
    // 删除文章
  }
);

资源所有权检查 #

javascript
// middleware/ownership.js
function checkOwnership(model, param = 'id', userField = 'author') {
  return async (req, res, next) => {
    try {
      const resourceId = req.params[param];
      const resource = await model.findById(resourceId);
      
      if (!resource) {
        return res.status(404).json({
          success: false,
          message: 'Resource not found'
        });
      }
      
      // 管理员可以访问所有资源
      if (req.user.role === 'admin') {
        req.resource = resource;
        return next();
      }
      
      // 检查所有权
      const ownerId = resource[userField]._id || resource[userField];
      
      if (ownerId.toString() !== req.user._id.toString()) {
        return res.status(403).json({
          success: false,
          message: 'You do not have permission to access this resource'
        });
      }
      
      req.resource = resource;
      next();
    } catch (error) {
      next(error);
    }
  };
}

module.exports = { checkOwnership };
javascript
// 使用所有权检查
const { checkOwnership } = require('../middleware/ownership');

router.put('/posts/:id',
  authenticateJWT,
  checkOwnership(Post, 'id', 'author'),
  (req, res) => {
    // req.resource 已经包含文章
    // 可以安全地更新
  }
);

下一步 #

现在你已经掌握了高级主题,接下来学习 最佳实践,了解生产环境的安全建议和常见问题解决!

最后更新:2026-03-28