Passport.js 安装与配置 #

环境准备 #

系统要求 #

在开始之前,请确保你的系统满足以下要求:

要求 版本
Node.js >= 14.0.0(推荐 LTS 版本)
npm >= 6.0.0
Express >= 4.0.0

检查环境 #

bash
# 检查 Node.js 版本
node --version

# 检查 npm 版本
npm --version

项目初始化 #

创建项目 #

bash
# 创建项目目录
mkdir my-auth-app
cd my-auth-app

# 初始化 package.json
npm init -y

# 安装 Express
npm install express

# 安装 Passport
npm install passport

项目结构 #

text
my-auth-app/
├── config/
│   └── passport.js      # Passport 配置
├── models/
│   └── User.js          # 用户模型
├── routes/
│   ├── auth.js          # 认证路由
│   └── index.js         # 主路由
├── middleware/
│   └── auth.js          # 认证中间件
├── app.js               # 应用入口
└── package.json

安装依赖 #

核心依赖 #

bash
# Express 框架
npm install express

# Passport 核心
npm install passport

# 会话支持
npm install express-session

# 本地策略
npm install passport-local

# 密码加密
npm install bcryptjs

可选依赖 #

bash
# MongoDB 用户模型
npm install mongoose

# 环境变量
npm install dotenv

# 请求体解析
npm install body-parser

# 闪存消息
npm install connect-flash

基础配置 #

创建 Express 应用 #

javascript
// app.js
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const flash = require('connect-flash');

const app = express();

// 解析请求体
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

// 会话配置
app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: false, // 开发环境设为 false,生产环境设为 true
    maxAge: 24 * 60 * 60 * 1000 // 24 小时
  }
}));

// Passport 初始化
app.use(passport.initialize());
app.use(passport.session());

// 闪存消息
app.use(flash());

// 路由
app.use('/auth', require('./routes/auth'));
app.use('/', require('./routes/index'));

// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Passport 配置文件 #

javascript
// config/passport.js
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User = require('../models/User');

// 配置本地策略
passport.use(new LocalStrategy(
  {
    usernameField: 'username',
    passwordField: 'password'
  },
  async (username, password, done) => {
    try {
      const user = await User.findOne({ username });
      
      if (!user) {
        return done(null, false, { message: '用户不存在' });
      }
      
      const isMatch = await user.comparePassword(password);
      
      if (!isMatch) {
        return done(null, false, { message: '密码错误' });
      }
      
      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

// 序列化用户
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);
  }
});

module.exports = passport;

用户模型 #

javascript
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    unique: true,
    trim: true
  },
  email: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    lowercase: true
  },
  password: {
    type: String,
    required: true
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// 密码加密中间件
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) {
    return next();
  }
  
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

// 验证密码方法
userSchema.methods.comparePassword = async function(candidatePassword) {
  return await bcrypt.compare(candidatePassword, this.password);
};

module.exports = mongoose.model('User', userSchema);

认证路由 #

注册路由 #

javascript
// routes/auth.js
const express = require('express');
const router = express.Router();
const User = require('../models/User');

// 注册页面
router.get('/register', (req, res) => {
  res.render('register', { message: req.flash('error') });
});

// 注册处理
router.post('/register', async (req, res) => {
  try {
    const { username, email, password, confirmPassword } = req.body;
    
    // 验证
    if (password !== confirmPassword) {
      req.flash('error', '两次密码不一致');
      return res.redirect('/auth/register');
    }
    
    // 检查用户是否存在
    const existingUser = await User.findOne({ 
      $or: [{ username }, { email }] 
    });
    
    if (existingUser) {
      req.flash('error', '用户名或邮箱已存在');
      return res.redirect('/auth/register');
    }
    
    // 创建用户
    const user = new User({ username, email, password });
    await user.save();
    
    req.flash('success', '注册成功,请登录');
    res.redirect('/auth/login');
  } catch (error) {
    console.error(error);
    req.flash('error', '注册失败');
    res.redirect('/auth/register');
  }
});

module.exports = router;

登录路由 #

javascript
// routes/auth.js (续)
const passport = require('passport');

// 登录页面
router.get('/login', (req, res) => {
  res.render('login', { 
    message: req.flash('error'),
    success: req.flash('success')
  });
});

// 登录处理
router.post('/login', (req, res, next) => {
  passport.authenticate('local', {
    successRedirect: '/profile',
    failureRedirect: '/auth/login',
    failureFlash: true
  })(req, res, next);
});

// 登出
router.get('/logout', (req, res, next) => {
  req.logout((err) => {
    if (err) return next(err);
    res.redirect('/');
  });
});

module.exports = router;

受保护路由 #

javascript
// middleware/auth.js
function ensureAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  req.flash('error', '请先登录');
  res.redirect('/auth/login');
}

function ensureNotAuthenticated(req, res, next) {
  if (!req.isAuthenticated()) {
    return next();
  }
  res.redirect('/profile');
}

module.exports = {
  ensureAuthenticated,
  ensureNotAuthenticated
};
javascript
// routes/index.js
const express = require('express');
const router = express.Router();
const { ensureAuthenticated } = require('../middleware/auth');

// 首页
router.get('/', (req, res) => {
  res.render('index', { user: req.user });
});

// 个人资料页(需要登录)
router.get('/profile', ensureAuthenticated, (req, res) => {
  res.render('profile', { user: req.user });
});

module.exports = router;

环境变量配置 #

创建 .env 文件 #

bash
# .env
NODE_ENV=development
PORT=3000

# Session
SESSION_SECRET=your-super-secret-key-change-in-production

# MongoDB
MONGODB_URI=mongodb://localhost:27017/my-auth-app

# 其他配置
BCRYPT_SALT_ROUNDS=10

加载环境变量 #

javascript
// app.js
require('dotenv').config();

const express = require('express');
const mongoose = require('mongoose');
const session = require('express-session');
const passport = require('passport');

// 连接数据库
mongoose.connect(process.env.MONGODB_URI)
  .then(() => console.log('MongoDB connected'))
  .catch(err => console.error('MongoDB connection error:', err));

const app = express();

// 会话配置
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false
}));

// Passport 配置
require('./config/passport')(passport);
app.use(passport.initialize());
app.use(passport.session());

// ... 其他配置

完整示例 #

完整的 app.js #

javascript
// app.js
require('dotenv').config();

const express = require('express');
const mongoose = require('mongoose');
const session = require('express-session');
const passport = require('passport');
const flash = require('connect-flash');
const path = require('path');

// 连接数据库
mongoose.connect(process.env.MONGODB_URI)
  .then(() => console.log('MongoDB connected'))
  .catch(err => console.error('MongoDB connection error:', err));

const app = express();

// 视图引擎设置
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

// 静态文件
app.use(express.static(path.join(__dirname, 'public')));

// 请求体解析
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

// 会话配置
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    maxAge: 24 * 60 * 60 * 1000
  }
}));

// Passport 初始化
app.use(passport.initialize());
app.use(passport.session());

// 闪存消息
app.use(flash());

// 全局变量
app.use((req, res, next) => {
  res.locals.user = req.user || null;
  res.locals.success_msg = req.flash('success');
  res.locals.error_msg = req.flash('error');
  next();
});

// 路由
app.use('/auth', require('./routes/auth'));
app.use('/', require('./routes/index'));

// 错误处理
app.use((req, res) => {
  res.status(404).render('404');
});

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).render('500');
});

// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

验证安装 #

测试脚本 #

javascript
// test/test.js
const request = require('supertest');
const app = require('../app');

describe('Authentication', () => {
  it('should show login page', async () => {
    const response = await request(app).get('/auth/login');
    expect(response.status).toBe(200);
  });
  
  it('should show register page', async () => {
    const response = await request(app).get('/auth/register');
    expect(response.status).toBe(200);
  });
  
  it('should redirect to login for protected route', async () => {
    const response = await request(app).get('/profile');
    expect(response.status).toBe(302);
    expect(response.headers.location).toContain('/auth/login');
  });
});

运行测试 #

bash
# 安装测试依赖
npm install --save-dev jest supertest

# 运行测试
npx jest

常见问题 #

1. 会话不持久 #

javascript
// 确保会话配置正确
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false, // 设为 false 避免空会话
  cookie: {
    secure: process.env.NODE_ENV === 'production', // 生产环境启用
    maxAge: 24 * 60 * 60 * 1000
  }
}));

2. Passport 初始化顺序 #

javascript
// 正确的顺序
app.use(session({...}));        // 1. 先配置会话
app.use(passport.initialize()); // 2. 初始化 Passport
app.use(passport.session());    // 3. 启用 Passport 会话支持

3. 跨域问题 #

javascript
// 开发环境 CORS 配置
const cors = require('cors');

app.use(cors({
  origin: 'http://localhost:3000',
  credentials: true // 允许携带 Cookie
}));

下一步 #

现在你已经完成了 Passport.js 的安装和基础配置,接下来学习 本地策略,实现完整的用户名/密码认证!

最后更新:2026-03-28