Skip to content

JWT令牌验证

JWT令牌验证概述

JWT令牌验证是确保JWT令牌有效性和完整性的过程。当客户端向服务器发送JWT令牌以访问受保护的资源时,服务器必须验证令牌的真实性、完整性和有效性。验证过程通常包括检查签名、验证声明和检查令牌是否过期。

JWT令牌验证原理

JWT令牌验证主要基于以下原理:

  1. 签名验证:验证令牌的签名是否有效,确保令牌在传输过程中没有被篡改
  2. 声明验证:验证令牌中的各种声明,如过期时间、签发人等
  3. 上下文验证:根据应用程序的特定需求进行额外的验证

1. 签名验证

签名验证是JWT验证中最关键的步骤。验证过程如下:

  1. 从JWT令牌中提取编码后的头部和负载
  2. 使用相同的签名算法和密钥重新计算签名
  3. 将重新计算的签名与令牌中的签名进行比较
  4. 如果两个签名相同,则表示令牌没有被篡改

2. 声明验证

声明验证涉及检查JWT令牌中的各种声明是否满足预期条件。常见的声明验证包括:

  • exp(过期时间):验证令牌是否已过期
  • nbf(生效时间):验证令牌是否已生效
  • iat(签发时间):验证令牌的签发时间是否合理
  • iss(发行人):验证令牌的发行人是否可信任
  • aud(受众):验证令牌是否面向当前应用程序
  • sub(主题):验证令牌的主题是否为预期的用户

3. 上下文验证

上下文验证是根据应用程序的特定需求进行的额外验证,例如:

  • 验证用户是否存在于数据库中
  • 验证用户的权限是否与令牌中声明的一致
  • 验证令牌是否已被撤销

使用jsonwebtoken库验证JWT令牌

在Node.js中,我们可以使用jsonwebtoken库来验证JWT令牌,该库提供了简单易用的API。

基本验证

javascript
const jwt = require('jsonwebtoken');

// 要验证的JWT令牌
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';

// 密钥(与生成令牌时使用的密钥相同)
const secretKey = process.env.JWT_SECRET_KEY || 'your-secret-key';

// 验证令牌
try {
  const decoded = jwt.verify(token, secretKey);
  console.log('验证成功,解码后的信息:', decoded);
  // 使用解码后的信息进行后续操作
} catch (error) {
  console.error('验证失败:', error.message);
  // 处理验证失败的情况
}

处理验证错误

jsonwebtoken库在验证失败时会抛出不同类型的错误,我们可以根据错误类型进行相应的处理:

javascript
const jwt = require('jsonwebtoken');

function verifyToken(token) {
  try {
    const decoded = jwt.verify(token, secretKey);
    return { valid: true, decoded };
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return { valid: false, error: '令牌已过期' };
    } else if (error.name === 'JsonWebTokenError') {
      if (error.message === 'invalid signature') {
        return { valid: false, error: '无效的签名' };
      } else if (error.message === 'jwt malformed') {
        return { valid: false, error: '令牌格式不正确' };
      } else if (error.message === 'jwt signature is required') {
        return { valid: false, error: '令牌缺少签名' };
      } else {
        return { valid: false, error: '无效的令牌' };
      }
    } else if (error.name === 'NotBeforeError') {
      return { valid: false, error: '令牌尚未生效' };
    } else {
      return { valid: false, error: '验证失败' };
    }
  }
}

// 使用示例
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const result = verifyToken(token);

if (result.valid) {
  console.log('验证成功,用户信息:', result.decoded);
} else {
  console.error('验证失败:', result.error);
}

自定义验证选项

jsonwebtoken库允许我们提供自定义的验证选项:

javascript
const jwt = require('jsonwebtoken');

// 自定义验证选项
const options = {
  algorithms: ['HS256'], // 只接受HS256算法的令牌
  issuer: 'my-application', // 验证令牌的发行人
  audience: 'my-audience', // 验证令牌的受众
  clockTolerance: 10 // 允许的时钟偏差(秒)
};

// 使用自定义选项验证令牌
try {
  const decoded = jwt.verify(token, secretKey, options);
  console.log('验证成功:', decoded);
} catch (error) {
  console.error('验证失败:', error.message);
}

使用公钥验证RSA签名的令牌

如果令牌是使用RSA私钥签名的,则需要使用对应的公钥进行验证:

javascript
const jwt = require('jsonwebtoken');
const fs = require('fs');

// 读取公钥
const publicKey = fs.readFileSync('public.key');

// 验证RSA签名的令牌
try {
  const decoded = jwt.verify(token, publicKey, {
    algorithms: ['RS256']
  });
  console.log('验证成功:', decoded);
} catch (error) {
  console.error('验证失败:', error.message);
}

在Express应用中实现JWT验证中间件

在Express应用中,通常使用中间件来验证JWT令牌。以下是一个完整的JWT验证中间件实现:

javascript
const jwt = require('jsonwebtoken');

/**
 * JWT验证中间件
 * @param {Object} req - Express请求对象
 * @param {Object} res - Express响应对象
 * @param {Function} next - Express下一个中间件函数
 */
function authenticateToken(req, res, next) {
  // 从请求头中提取令牌
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  
  // 检查令牌是否存在
  if (token == null) {
    return res.status(401).json({ message: '未提供令牌' });
  }
  
  // 验证令牌
  jwt.verify(token, process.env.JWT_SECRET_KEY, (err, user) => {
    // 处理验证错误
    if (err) {
      if (err.name === 'TokenExpiredError') {
        return res.status(401).json({ message: '令牌已过期' });
      } else if (err.name === 'JsonWebTokenError') {
        return res.status(401).json({ message: '无效的令牌' });
      } else {
        return res.status(401).json({ message: '验证失败' });
      }
    }
    
    // 将解码后的用户信息添加到请求对象中
    req.user = user;
    next();
  });
}

// 使用示例
app.get('/protected', authenticateToken, (req, res) => {
  res.json({ message: '访问受保护的资源成功', user: req.user });
});

基于角色的访问控制

在验证令牌后,通常还需要根据用户的角色或权限来控制对资源的访问:

javascript
/**
 * 基于角色的访问控制中间件
 * @param {Array} roles - 允许访问的角色列表
 */
function authorizeRoles(...roles) {
  return (req, res, next) => {
    // 检查用户是否有足够的权限
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ message: '权限不足' });
    }
    next();
  };
}

// 使用示例
app.get('/admin', authenticateToken, authorizeRoles('admin'), (req, res) => {
  res.json({ message: '访问管理员资源成功', user: req.user });
});

app.get('/user', authenticateToken, authorizeRoles('user', 'admin'), (req, res) => {
  res.json({ message: '访问用户资源成功', user: req.user });
});

令牌撤销机制

虽然JWT令牌本身是无状态的,一旦颁发就无法直接撤销,但我们可以通过一些额外的机制来实现令牌的撤销功能:

1. 使用令牌黑名单

维护一个令牌黑名单,将已撤销的令牌添加到黑名单中:

javascript
// 使用Redis或其他存储来维护令牌黑名单
const tokenBlacklist = new Set();

/**
 * 将令牌添加到黑名单
 * @param {string} token - 要撤销的令牌
 */
function revokeToken(token) {
  // 获取令牌的过期时间
  try {
    const decoded = jwt.decode(token);
    if (decoded && decoded.exp) {
      const now = Math.floor(Date.now() / 1000);
      const ttl = decoded.exp - now;
      
      if (ttl > 0) {
        // 将令牌添加到黑名单
        tokenBlacklist.add(token);
        
        // 设置令牌在黑名单中的过期时间
        setTimeout(() => {
          tokenBlacklist.delete(token);
        }, ttl * 1000);
      }
    }
  } catch (error) {
    console.error('撤销令牌错误:', error);
  }
}

/**
 * 检查令牌是否在黑名单中
 * @param {string} token - 要检查的令牌
 * @returns {boolean} - 令牌是否在黑名单中
 */
function isTokenRevoked(token) {
  return tokenBlacklist.has(token);
}

// 修改验证中间件以检查黑名单
function authenticateTokenWithBlacklist(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  
  if (token == null) {
    return res.status(401).json({ message: '未提供令牌' });
  }
  
  // 检查令牌是否在黑名单中
  if (isTokenRevoked(token)) {
    return res.status(401).json({ message: '令牌已被撤销' });
  }
  
  jwt.verify(token, process.env.JWT_SECRET_KEY, (err, user) => {
    if (err) {
      return res.status(401).json({ message: '无效的令牌' });
    }
    
    req.user = user;
    next();
  });
}

2. 使用令牌版本控制

为每个用户维护一个令牌版本号,当需要撤销令牌时,增加用户的令牌版本号:

javascript
// 模拟用户数据库,包含令牌版本号
const users = [
  { id: 1, username: 'admin', tokenVersion: 1 },
  { id: 2, username: 'user', tokenVersion: 1 }
];

// 修改令牌生成逻辑,包含令牌版本号
function generateToken(user) {
  return jwt.sign(
    {
      id: user.id,
      username: user.username,
      tokenVersion: user.tokenVersion
    },
    process.env.JWT_SECRET_KEY,
    {
      expiresIn: '1h'
    }
  );
}

// 修改验证逻辑,检查令牌版本号
function authenticateTokenWithVersioning(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  
  if (token == null) {
    return res.status(401).json({ message: '未提供令牌' });
  }
  
  jwt.verify(token, process.env.JWT_SECRET_KEY, (err, decoded) => {
    if (err) {
      return res.status(401).json({ message: '无效的令牌' });
    }
    
    // 检查令牌版本号
    const user = users.find(u => u.id === decoded.id);
    if (!user || user.tokenVersion !== decoded.tokenVersion) {
      return res.status(401).json({ message: '令牌已被撤销' });
    }
    
    req.user = decoded;
    next();
  });
}

// 撤销用户的所有令牌
function revokeAllTokens(userId) {
  const user = users.find(u => u.id === userId);
  if (user) {
    user.tokenVersion += 1;
  }
}

JWT令牌验证的最佳实践

  1. 始终验证签名:确保JWT令牌的签名有效,防止令牌被篡改
  2. 验证所有必要的声明:特别是exp、iss、aud等重要声明
  3. 使用安全的密钥管理
    • 密钥长度至少为256位
    • 将密钥存储在安全的地方(如环境变量、密钥管理服务)
    • 定期轮换密钥
  4. 设置合理的过期时间:令牌的过期时间应该合理,不要过长
  5. 实现令牌撤销机制
    • 使用令牌黑名单
    • 实现令牌版本控制
    • 考虑使用引用令牌而不是自包含令牌
  6. 处理验证错误:提供清晰的错误消息,帮助客户端理解验证失败的原因
  7. 使用HTTPS:始终通过HTTPS传输JWT令牌,防止中间人攻击
  8. 验证令牌来源:确保令牌来自可信任的来源,例如验证iss声明
  9. 限制令牌的使用范围:通过aud声明限制令牌的使用范围
  10. 实现基于角色的访问控制:在验证令牌后,根据用户的角色或权限控制对资源的访问

实践项目

创建一个完整的JWT令牌验证系统:

  1. 实现JWT验证中间件

    • 验证令牌的签名和声明
    • 处理各种验证错误
    • 支持不同的签名算法
  2. 实现令牌撤销功能

    • 使用Redis实现令牌黑名单
    • 实现令牌版本控制
    • 提供撤销特定令牌和所有令牌的API
  3. 实现基于角色的访问控制

    • 根据用户的角色控制对资源的访问
    • 支持复杂的权限系统
  4. 实现令牌刷新机制

    • 验证刷新令牌的有效性
    • 生成新的访问令牌
    • 可选地生成新的刷新令牌
  5. 创建监控和日志系统

    • 记录验证请求和错误
    • 监控令牌使用情况
    • 检测异常访问模式

通过这个项目,您将掌握JWT令牌验证的原理和实践,为构建安全、可靠的认证系统提供有力保障。