
JWT 认证安全
安全 JWT 实现
import jwt from 'jsonwebtoken'
import crypto from 'crypto'
const JWT_SECRET = process.env.JWT_SECRET // 至少 256 位
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET
const ACCESS_TOKEN_TTL = '15m'
const REFRESH_TOKEN_TTL = '7d'
function generateTokens(payload) {
const accessToken = jwt.sign(
{ ...payload, type: 'access' },
JWT_SECRET,
{
algorithm: 'HS256',
expiresIn: ACCESS_TOKEN_TTL,
issuer: 'myapp.com',
audience: 'myapp-users',
}
)
const refreshToken = jwt.sign(
{ userId: payload.userId, type: 'refresh', jti: crypto.randomUUID() },
JWT_REFRESH_SECRET,
{ expiresIn: REFRESH_TOKEN_TTL }
)
return { accessToken, refreshToken }
}
function verifyAccessToken(token) {
return jwt.verify(token, JWT_SECRET, {
algorithms: ['HS256'], // 始终指定算法——防止 alg:none 攻击
issuer: 'myapp.com',
audience: 'myapp-users',
})
}

常见 JWT 漏洞
// 漏洞 1:算法混淆(alg:none)
// 错误:未指定算法
jwt.verify(token, secret) // 易受 alg:none 攻击
// 正确:始终指定算法
jwt.verify(token, secret, { algorithms: ['HS256'] })
// 漏洞 2:弱密钥
// 错误:
const secret = 'secret123' // 太弱
// 正确:密码学随机
const secret = crypto.randomBytes(32).toString('hex') // 256 位
// 漏洞 3:将 JWT 存储在 localStorage
// 错误:易受 XSS 攻击
localStorage.setItem('token', jwt)
// 正确:httpOnly cookie——抵抗 XSS
res.cookie('access_token', token, {
httpOnly: true, // 无法通过 JS 访问
secure: true, // 仅 HTTPS
sameSite: 'strict',
maxAge: 15 * 60 * 1000,
})

使用 Redis 的刷新令牌轮换
import { createClient } from 'redis'
const redis = createClient({ url: process.env.REDIS_URL })
await redis.connect()
async function saveRefreshToken(jti, userId, expiresIn) {
await redis.setEx(
`refresh_token:${jti}`,
expiresIn,
JSON.stringify({ userId, used: false })
)
}
async function useRefreshToken(refreshToken) {
const payload = jwt.verify(refreshToken, JWT_REFRESH_SECRET, {
algorithms: ['HS256']
})
const stored = await redis.get(`refresh_token:${payload.jti}`)
if (!stored) throw new Error('无效的刷新令牌')
const data = JSON.parse(stored)
// 检测重用攻击
if (data.used) {
// 令牌重用——撤销所有用户令牌
await revokeAllUserTokens(data.userId)
throw new Error('检测到刷新令牌重用——所有会话已撤销')
}
// 标记为已使用
await redis.set(`refresh_token:${payload.jti}`,
JSON.stringify({ ...data, used: true }),
{ keepTTL: true }
)
// 颁发新令牌
const { accessToken, refreshToken: newRefreshToken } = generateTokens({
userId: data.userId
})
// 保存新刷新令牌
const newPayload = jwt.decode(newRefreshToken)
await saveRefreshToken(newPayload.jti, data.userId, 7 * 24 * 3600)
return { accessToken, refreshToken: newRefreshToken }
}

用于注销的 JWT 黑名单
async function logout(accessToken, refreshToken) {
// 将访问令牌加入黑名单直至过期
const decoded = jwt.decode(accessToken)
const ttl = decoded.exp - Math.floor(Date.now() / 1000)
if (ttl > 0) {
await redis.setEx(`blacklist:${decoded.jti}`, ttl, '1')
}
// 使刷新令牌失效
const refreshDecoded = jwt.decode(refreshToken)
await redis.del(`refresh_token:${refreshDecoded.jti}`)
}
// 检查黑名单的中间件
async function authMiddleware(req, res, next) {
const token = req.cookies.access_token || req.headers.authorization?.split(' ')[1]
if (!token) return res.status(401).json({ error: '无令牌' })
try {
const payload = verifyAccessToken(token)
// 检查黑名单
const blacklisted = await redis.get(`blacklist:${payload.jti}`)
if (blacklisted) return res.status(401).json({ error: '令牌已撤销' })
req.user = payload
next()
} catch (err) {
res.status(401).json({ error: '无效令牌' })
}
}
使用 RS256 的非对称 JWT
import { readFileSync } from 'fs'
// 生成密钥:openssl genrsa -out private.pem 2048
// openssl rsa -in private.pem -pubout -out public.pem
const privateKey = readFileSync('private.pem')
const publicKey = readFileSync('public.pem')
function signToken(payload) {
return jwt.sign(payload, privateKey, {
algorithm: 'RS256',
expiresIn: '15m',
keyid: 'key-v1', // 用于密钥轮换
})
}
function verifyToken(token) {
return jwt.verify(token, publicKey, {
algorithms: ['RS256'], // 仅 RS256——绝不使用 HS256
})
}
// RS256 优势:只有认证服务拥有私钥,
// 任何服务都可以使用公钥验证
安全总结
| 漏洞 |
预防措施 |
| 算法混淆 |
指定算法:['HS256'] |
| 令牌窃取 |
httpOnly cookie,短过期时间 |
| 弱密钥 |
256 位随机密钥 |
| 缺少验证 |
验证签发者、受众 |
| 无法撤销 |
Redis 黑名单 + 轮换 |