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