正在加载,请稍候…

JWT 认证安全:常见漏洞与安全实现

在 Node.js 中实现安全的 JWT 认证。学习算法混淆攻击、令牌存储、刷新令牌模式、黑名单以及 JWT 安全最佳实践。

JWT 认证安全:常见漏洞与安全实现

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 认证安全:常见漏洞与安全实现插图

常见 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,
})

JWT 认证安全:常见漏洞与安全实现插图

使用 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 认证安全:常见漏洞与安全实现插图

用于注销的 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 黑名单 + 轮换