Skip to content
On this page

jsonwebtoken

简介

jsonwebtoken 是一个用于生成和验证 JSON Web Tokens (JWT) 的 Node.js 库。JWT 是一种紧凑的、URL 安全的方式,用于在各方之间安全地传输信息。

主要特性

  • 支持多种签名算法(HMAC、RSA、ECDSA)
  • 可自定义 Token 有效期
  • 支持标准 JWT 声明(iss、sub、aud、exp 等)
  • 提供同步和异步 API
  • 支持自定义 Header 和 Payload
  • 完整的错误处理机制

安装

bash
npm install jsonwebtoken

基本使用

1. 生成 Token

javascript
const jwt = require('jsonwebtoken')

// 创建 token
const token = jwt.sign(
  { userId: 123, role: 'admin' }, // payload
  'your-secret-key', // 密钥
  { expiresIn: '1h' }, // 配置选项
)

2. 验证 Token

javascript
try {
  const decoded = jwt.verify(token, 'your-secret-key')
  console.log(decoded) // { userId: 123, role: 'admin', iat: 1516239022 }
} catch (err) {
  console.error('Token 验证失败:', err.message)
}

高级特性

1. 自定义声明

javascript
const token = jwt.sign(
  {
    sub: '1234567890',
    name: 'John Doe',
    admin: true,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 60 * 60,
  },
  'your-secret-key',
)

2. 异步操作

javascript
// 异步生成 token
jwt.sign({ data: 'payload' }, 'secret', { expiresIn: '1h' }, (err, token) => {
  if (err) throw err
  console.log(token)
})

// 异步验证 token
jwt.verify(token, 'secret', (err, decoded) => {
  if (err) throw err
  console.log(decoded)
})

3. 使用算法选项

javascript
const token = jwt.sign({ data: 'payload' }, 'secret', {
  algorithm: 'HS256',
  expiresIn: '1h',
  issuer: 'auth0',
})

最佳实践

  1. 安全密钥管理

    javascript
    const secretKey = process.env.JWT_SECRET_KEY
    if (!secretKey) {
      throw new Error('JWT secret key is not defined')
    }
  2. 错误处理

    javascript
    function verifyToken(token) {
      try {
        return jwt.verify(token, secretKey)
      } catch (err) {
        if (err.name === 'TokenExpiredError') {
          throw new Error('Token 已过期')
        }
        if (err.name === 'JsonWebTokenError') {
          throw new Error('无效的 Token')
        }
        throw err
      }
    }
  3. 刷新 Token

    javascript
    function refreshToken(oldToken) {
      const decoded = jwt.verify(oldToken, secretKey, {
        ignoreExpiration: true,
      })
      delete decoded.exp
      delete decoded.iat
    
      return jwt.sign(decoded, secretKey, { expiresIn: '1h' })
    }

应用场景

1. 用户认证系统

javascript
// 登录接口
app.post('/login', (req, res) => {
  const { username, password } = req.body

  // 验证用户凭据
  User.findOne({ username }, (err, user) => {
    if (err || !user) return res.status(401).json({ message: '用户名或密码错误' })

    user.comparePassword(password, (err, isMatch) => {
      if (err || !isMatch) return res.status(401).json({ message: '用户名或密码错误' })

      // 生成访问令牌和刷新令牌
      const accessToken = jwt.sign({ userId: user._id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '15m' })

      const refreshToken = jwt.sign({ userId: user._id }, process.env.JWT_REFRESH_SECRET, { expiresIn: '7d' })

      // 保存刷新令牌
      user.refreshToken = refreshToken
      user.save()

      res.json({ accessToken, refreshToken })
    })
  })
})

// 令牌刷新接口
app.post('/refresh-token', (req, res) => {
  const { refreshToken } = req.body

  if (!refreshToken) return res.status(401).json({ message: '刷新令牌不存在' })

  try {
    // 验证刷新令牌
    const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET)

    // 检查数据库中的刷新令牌
    User.findById(decoded.userId, (err, user) => {
      if (err || !user || user.refreshToken !== refreshToken) {
        return res.status(401).json({ message: '无效的刷新令牌' })
      }

      // 生成新的访问令牌
      const accessToken = jwt.sign({ userId: user._id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '15m' })

      res.json({ accessToken })
    })
  } catch (err) {
    return res.status(401).json({ message: '刷新令牌已过期或无效' })
  }
})

2. API 授权中间件

javascript
// 验证中间件
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization']
  const token = authHeader && authHeader.split(' ')[1] // Bearer TOKEN

  if (!token) return res.status(401).json({ message: '未提供访问令牌' })

  jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
    if (err) {
      if (err.name === 'TokenExpiredError') {
        return res.status(401).json({ message: '令牌已过期' })
      }
      return res.status(403).json({ message: '无效的令牌' })
    }

    req.user = decoded
    next()
  })
}

// 角色授权中间件
function authorizeRole(roles) {
  return (req, res, next) => {
    if (!req.user) return res.status(401).json({ message: '未认证' })

    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ message: '没有权限访问此资源' })
    }

    next()
  }
}

// 使用中间件保护路由
app.get('/api/profile', authenticateToken, (req, res) => {
  res.json({ user: req.user })
})

app.get('/api/admin', authenticateToken, authorizeRole(['admin']), (req, res) => {
  res.json({ message: '管理员面板' })
})

常见问题与解决方案

1. 令牌泄露问题

问题: JWT 令牌被窃取后,攻击者可以访问受保护资源。

解决方案:

javascript
// 在令牌中添加指纹信息
const token = jwt.sign(
  {
    userId: user._id,
    fingerprint: createHash(req.headers['user-agent']),
  },
  secretKey,
  { expiresIn: '1h' },
)

// 验证时检查指纹
function verifyTokenWithFingerprint(token, req) {
  const decoded = jwt.verify(token, secretKey)
  const currentFingerprint = createHash(req.headers['user-agent'])

  if (decoded.fingerprint !== currentFingerprint) {
    throw new Error('令牌指纹不匹配')
  }

  return decoded
}

2. 令牌撤销问题

问题: JWT 令牌一旦签发,在过期前无法撤销。

解决方案:

javascript
// 使用令牌黑名单
const tokenBlacklist = new Set()

// 注销接口
app.post('/logout', authenticateToken, (req, res) => {
  const token = req.headers.authorization.split(' ')[1]

  // 将令牌添加到黑名单,可以使用 Redis 存储
  tokenBlacklist.add(token)

  // 设置过期时间,与令牌过期时间一致
  setTimeout(() => {
    tokenBlacklist.delete(token)
  }, getTokenRemainingTime(token) * 1000)

  res.json({ message: '注销成功' })
})

// 验证中间件中检查黑名单
function authenticateToken(req, res, next) {
  const token = req.headers.authorization.split(' ')[1]

  if (tokenBlacklist.has(token)) {
    return res.status(401).json({ message: '令牌已被撤销' })
  }

  // 继续验证...
}

// 获取令牌剩余有效时间(秒)
function getTokenRemainingTime(token) {
  const decoded = jwt.decode(token)
  if (!decoded.exp) return 0

  const now = Math.floor(Date.now() / 1000)
  return Math.max(0, decoded.exp - now)
}

3. 令牌大小问题

问题: JWT 令牌包含过多信息导致大小过大。

解决方案:

javascript
// 只在令牌中存储必要信息
const token = jwt.sign(
  { userId: user._id }, // 只存储标识符
  secretKey,
  { expiresIn: '1h' },
)

// 需要更多信息时从数据库获取
app.get('/api/profile', authenticateToken, async (req, res) => {
  try {
    const user = await User.findById(req.user.userId).select('-password')
    res.json({ user })
  } catch (err) {
    res.status(500).json({ message: '服务器错误' })
  }
})

注意事项

  1. 不要在 Token 中存储敏感信息
  2. 使用环境变量存储密钥
  3. 设置合理的过期时间
  4. 使用 HTTPS 传输 Token
  5. 定期轮换密钥
  6. 验证所有必要的声明(如 exp, iss 等)
  7. 考虑使用非对称加密算法(RS256)增强安全性
  8. 实现令牌刷新机制,减少长期令牌的风险
  9. 对关键操作使用额外的验证机制

要保持清醒 永远不抱有意外的幻想 凭空的期待最要命