Passport.js JWT 认证 #

什么是 JWT? #

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为 JSON 对象。JWT 是无状态的,服务器不需要存储会话信息,非常适合 RESTful API。

JWT 结构 #

JWT 由三部分组成,用点(.)分隔:

text
┌─────────────────────────────────────────────────────────────┐
│                        JWT 结构                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Header.Payload.Signature                                  │
│                                                             │
│   ┌─────────────┐  ┌─────────────┐  ┌─────────────┐        │
│   │   Header    │  │   Payload   │  │  Signature  │        │
│   ├─────────────┤  ├─────────────┤  ├─────────────┤        │
│   │ {           │  │ {           │  │ HMACSHA256( │        │
│   │  "alg":     │  │  "sub":     │  │  base64Url  │        │
│   │  "HS256",   │  │  "123456",  │  │  (header)+  │        │
│   │  "typ":     │  │  "name":    │  │  "."+       │        │
│   │  "JWT"      │  │  "John",    │  │  base64Url  │        │
│   │ }           │  │  "iat":     │  │  (payload), │        │
│   └─────────────┘  │  1234567890 │  │  secret)    │        │
│                    │ }           │  └─────────────┘        │
│                    └─────────────┘                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Session vs JWT #

特性 Session JWT
存储位置 服务器 客户端
状态 有状态 无状态
扩展性 需要 Session Store 天然支持分布式
跨域 需要配置 天然支持
注销 立即生效 需要额外处理
安全性 Cookie 安全 Token 安全

安装依赖 #

bash
# JWT 策略
npm install passport-jwt

# JWT 工具库
npm install jsonwebtoken

JWT 策略配置 #

基本配置 #

javascript
// config/passport.js
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const User = require('../models/User');

const options = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: process.env.JWT_SECRET,
  issuer: 'your-app-name',
  audience: 'your-app-name'
};

passport.use(new JwtStrategy(options, async (payload, done) => {
  try {
    const user = await User.findById(payload.sub);
    
    if (user) {
      return done(null, user);
    }
    
    return done(null, false);
  } catch (error) {
    return done(error, false);
  }
}));

module.exports = passport;

提取 Token 的方式 #

javascript
const ExtractJwt = require('passport-jwt').ExtractJwt;

// 从 Authorization Header 提取(Bearer Token)
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken()

// 从 URL 参数提取
jwtFromRequest: ExtractJwt.fromUrlQueryParameter('token')

// 从 Cookie 提取
jwtFromRequest: ExtractJwt.fromCookie()

// 自定义提取方式
jwtFromRequest: (req) => {
  let token = null;
  if (req && req.cookies) {
    token = req.cookies['jwt'];
  }
  return token;
}

// 组合多种方式
jwtFromRequest: (req) => {
  return ExtractJwt.fromAuthHeaderAsBearerToken()(req) ||
         ExtractJwt.fromUrlQueryParameter('token')(req);
}

Token 生成与验证 #

Token 生成工具 #

javascript
// utils/jwt.js
const jwt = require('jsonwebtoken');

const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
const JWT_REFRESH_EXPIRES_IN = process.env.JWT_REFRESH_EXPIRES_IN || '30d';

// 生成访问令牌
function generateAccessToken(user) {
  const payload = {
    sub: user._id || user.id,
    username: user.username,
    email: user.email,
    role: user.role
  };
  
  return jwt.sign(payload, JWT_SECRET, {
    expiresIn: JWT_EXPIRES_IN,
    issuer: 'your-app-name',
    audience: 'your-app-name'
  });
}

// 生成刷新令牌
function generateRefreshToken(user) {
  const payload = {
    sub: user._id || user.id,
    type: 'refresh'
  };
  
  return jwt.sign(payload, JWT_SECRET, {
    expiresIn: JWT_REFRESH_EXPIRES_IN,
    issuer: 'your-app-name'
  });
}

// 验证令牌
function verifyToken(token) {
  try {
    return jwt.verify(token, JWT_SECRET, {
      issuer: 'your-app-name',
      audience: 'your-app-name'
    });
  } catch (error) {
    return null;
  }
}

// 解码令牌(不验证)
function decodeToken(token) {
  return jwt.decode(token);
}

module.exports = {
  generateAccessToken,
  generateRefreshToken,
  verifyToken,
  decodeToken
};

登录接口 #

javascript
// routes/auth.js
const express = require('express');
const router = express.Router();
const passport = require('passport');
const { generateAccessToken, generateRefreshToken } = require('../utils/jwt');
const User = require('../models/User');

// 登录
router.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  try {
    // 验证用户
    const user = await User.findOne({ username });
    
    if (!user) {
      return res.status(401).json({
        success: false,
        message: '用户名或密码错误'
      });
    }
    
    const isMatch = await user.comparePassword(password);
    
    if (!isMatch) {
      return res.status(401).json({
        success: false,
        message: '用户名或密码错误'
      });
    }
    
    // 生成令牌
    const accessToken = generateAccessToken(user);
    const refreshToken = generateRefreshToken(user);
    
    // 保存刷新令牌
    user.refreshToken = refreshToken;
    await user.save();
    
    res.json({
      success: true,
      accessToken,
      refreshToken,
      user: {
        id: user._id,
        username: user.username,
        email: user.email,
        role: user.role
      }
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({
      success: false,
      message: '服务器错误'
    });
  }
});

// 刷新令牌
router.post('/refresh', async (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(400).json({
      success: false,
      message: '缺少刷新令牌'
    });
  }
  
  try {
    // 验证刷新令牌
    const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET);
    
    if (decoded.type !== 'refresh') {
      return res.status(401).json({
        success: false,
        message: '无效的刷新令牌'
      });
    }
    
    // 查找用户并验证刷新令牌
    const user = await User.findOne({
      _id: decoded.sub,
      refreshToken
    });
    
    if (!user) {
      return res.status(401).json({
        success: false,
        message: '无效的刷新令牌'
      });
    }
    
    // 生成新的访问令牌
    const accessToken = generateAccessToken(user);
    
    res.json({
      success: true,
      accessToken
    });
  } catch (error) {
    return res.status(401).json({
      success: false,
      message: '无效或过期的刷新令牌'
    });
  }
});

// 登出
router.post('/logout', async (req, res) => {
  const { refreshToken } = req.body;
  
  if (refreshToken) {
    // 使刷新令牌失效
    await User.findOneAndUpdate(
      { refreshToken },
      { $unset: { refreshToken: 1 } }
    );
  }
  
  res.json({
    success: true,
    message: '登出成功'
  });
});

module.exports = router;

保护 API 路由 #

使用 Passport 中间件 #

javascript
// routes/api.js
const express = require('express');
const router = express.Router();
const passport = require('passport');

// 保护单个路由
router.get('/profile', 
  passport.authenticate('jwt', { session: false }), 
  (req, res) => {
    res.json({
      success: true,
      user: req.user
    });
  }
);

// 保护一组路由
router.use('/admin', 
  passport.authenticate('jwt', { session: false }),
  (req, res, next) => {
    if (req.user.role !== 'admin') {
      return res.status(403).json({
        success: false,
        message: '权限不足'
      });
    }
    next();
  }
);

router.get('/admin/users', (req, res) => {
  // 只有管理员可以访问
});

module.exports = router;

自定义认证中间件 #

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

// JWT 认证
function authenticateJWT(req, res, next) {
  passport.authenticate('jwt', { session: false }, (err, user, info) => {
    if (err) {
      return next(err);
    }
    
    if (!user) {
      return res.status(401).json({
        success: false,
        message: info?.message || '未授权'
      });
    }
    
    req.user = user;
    next();
  })(req, res, next);
}

// 角色检查
function checkRole(...roles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({
        success: false,
        message: '未授权'
      });
    }
    
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        success: false,
        message: '权限不足'
      });
    }
    
    next();
  };
}

// 可选认证(不强制登录)
function optionalAuth(req, res, next) {
  passport.authenticate('jwt', { session: false }, (err, user) => {
    if (err) {
      return next(err);
    }
    
    if (user) {
      req.user = user;
    }
    
    next();
  })(req, res, next);
}

module.exports = {
  authenticateJWT,
  checkRole,
  optionalAuth
};
javascript
// routes/api.js
const { authenticateJWT, checkRole, optionalAuth } = require('../middleware/auth');

// 需要登录
router.get('/profile', authenticateJWT, (req, res) => {
  res.json(req.user);
});

// 需要管理员权限
router.get('/admin/users', authenticateJWT, checkRole('admin'), (req, res) => {
  // ...
});

// 可选登录(公开内容,登录后显示更多)
router.get('/posts', optionalAuth, (req, res) => {
  if (req.user) {
    // 返回包含私有内容的文章
  } else {
    // 只返回公开文章
  }
});

Token 黑名单 #

实现令牌撤销 #

javascript
// models/TokenBlacklist.js
const mongoose = require('mongoose');

const tokenBlacklistSchema = new mongoose.Schema({
  token: {
    type: String,
    required: true,
    unique: true
  },
  userId: {
    type: mongoose.Schema.Types.ObjectId,
    required: true
  },
  reason: {
    type: String,
    enum: ['logout', 'password_change', 'security'],
    default: 'logout'
  },
  createdAt: {
    type: Date,
    default: Date.now,
    expires: 2592000 // 30 天后自动删除
  }
});

module.exports = mongoose.model('TokenBlacklist', tokenBlacklistSchema);
javascript
// config/passport.js
const TokenBlacklist = require('../models/TokenBlacklist');

passport.use(new JwtStrategy(options, async (payload, done) => {
  try {
    // 检查令牌是否在黑名单中
    const isBlacklisted = await TokenBlacklist.findOne({
      token: payload.jti
    });
    
    if (isBlacklisted) {
      return done(null, false, { message: '令牌已失效' });
    }
    
    const user = await User.findById(payload.sub);
    
    if (user) {
      return done(null, user);
    }
    
    return done(null, false);
  } catch (error) {
    return done(error, false);
  }
}));
javascript
// utils/jwt.js
const crypto = require('crypto');

function generateAccessToken(user) {
  const jti = crypto.randomBytes(16).toString('hex');
  
  const payload = {
    sub: user._id || user.id,
    jti: jti, // JWT ID,用于黑名单
    username: user.username,
    role: user.role
  };
  
  return jwt.sign(payload, JWT_SECRET, {
    expiresIn: JWT_EXPIRES_IN,
    jwtid: jti
  });
}

// 登出时加入黑名单
async function revokeToken(token, userId, reason = 'logout') {
  const decoded = jwt.decode(token);
  
  await TokenBlacklist.create({
    token: decoded.jti,
    userId,
    reason
  });
}

完整示例 #

Express 应用配置 #

javascript
// app.js
const express = require('express');
const passport = require('passport');
const cors = require('cors');

const app = express();

// CORS 配置
app.use(cors({
  origin: process.env.CLIENT_URL,
  credentials: true
}));

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

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

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

// 错误处理
app.use((err, req, res, next) => {
  if (err.name === 'UnauthorizedError') {
    return res.status(401).json({
      success: false,
      message: '无效的令牌'
    });
  }
  
  console.error(err);
  res.status(500).json({
    success: false,
    message: '服务器错误'
  });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

客户端使用 #

javascript
// 存储 Token
function storeTokens(accessToken, refreshToken) {
  localStorage.setItem('accessToken', accessToken);
  localStorage.setItem('refreshToken', refreshToken);
}

// 获取 Token
function getAccessToken() {
  return localStorage.getItem('accessToken');
}

// API 请求
async function fetchWithAuth(url, options = {}) {
  const token = getAccessToken();
  
  const headers = {
    'Content-Type': 'application/json',
    ...options.headers
  };
  
  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }
  
  let response = await fetch(url, {
    ...options,
    headers
  });
  
  // Token 过期,尝试刷新
  if (response.status === 401) {
    const newToken = await refreshAccessToken();
    
    if (newToken) {
      headers['Authorization'] = `Bearer ${newToken}`;
      response = await fetch(url, {
        ...options,
        headers
      });
    }
  }
  
  return response;
}

// 刷新 Token
async function refreshAccessToken() {
  const refreshToken = localStorage.getItem('refreshToken');
  
  if (!refreshToken) {
    return null;
  }
  
  try {
    const response = await fetch('/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken })
    });
    
    const data = await response.json();
    
    if (data.success) {
      localStorage.setItem('accessToken', data.accessToken);
      return data.accessToken;
    }
    
    // 刷新失败,清除 Token
    localStorage.removeItem('accessToken');
    localStorage.removeItem('refreshToken');
    return null;
  } catch (error) {
    console.error('Token refresh failed:', error);
    return null;
  }
}

安全最佳实践 #

1. 使用强密钥 #

javascript
// 生成强密钥
const crypto = require('crypto');
const secret = crypto.randomBytes(64).toString('hex');
console.log('JWT_SECRET:', secret);

2. 设置合理的过期时间 #

javascript
// 访问令牌:短时间(15分钟 - 1小时)
const JWT_EXPIRES_IN = '15m';

// 刷新令牌:长时间(7天 - 30天)
const JWT_REFRESH_EXPIRES_IN = '7d';

3. HTTPS 传输 #

javascript
// 生产环境强制 HTTPS
if (process.env.NODE_ENV === 'production') {
  app.use((req, res, next) => {
    if (req.protocol !== 'https') {
      return res.redirect(`https://${req.headers.host}${req.url}`);
    }
    next();
  });
}

4. Token 存储安全 #

javascript
// 客户端存储建议
// 1. HttpOnly Cookie(最安全)
// 2. 内存存储(页面刷新后丢失)
// 3. localStorage(有 XSS 风险)
// 4. 避免 sessionStorage

// 使用 HttpOnly Cookie
router.post('/login', async (req, res) => {
  // ...
  
  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 });
});

下一步 #

现在你已经掌握了 JWT 认证,接下来学习 高级主题,探索自定义策略和多策略组合!

最后更新:2026-03-28