Skip to content

JWT令牌生成

JWT令牌生成概述

JWT令牌生成是实现基于JWT的认证系统的核心步骤。一个有效的JWT令牌必须包含头部、负载和签名三个部分,并且这三个部分必须使用Base64Url编码并以点(.)分隔。在Node.js中,我们通常使用jsonwebtoken库来简化JWT令牌的生成过程。

JWT令牌生成流程

JWT令牌生成的完整流程如下:

  1. 准备头部(Header):确定令牌类型和签名算法
  2. 准备负载(Payload):添加声明(claims)信息
  3. 生成签名(Signature):使用密钥和选定的算法对头部和负载进行签名
  4. 组合令牌:将编码后的头部、负载和签名组合成完整的JWT令牌

1. 准备头部

头部通常包含令牌类型(typ)和使用的签名算法(alg)。常见的签名算法包括:

  • HS256:HMAC-SHA256,使用同一个密钥进行签名和验证
  • HS384:HMAC-SHA384
  • HS512:HMAC-SHA512
  • RS256:RSA-SHA256,使用私钥签名,公钥验证
  • RS384:RSA-SHA384
  • RS512:RSA-SHA512
  • ES256:ECDSA-SHA256
  • ES384:ECDSA-SHA384
  • ES512:ECDSA-SHA512

2. 准备负载

负载包含各种声明(claims),这些声明是关于实体(通常是用户)和其他数据的声明。在准备负载时,应考虑以下几点:

  • 注册声明:使用标准的注册声明,如sub(主题)、iss(发行人)、exp(过期时间)、iat(签发时间)等
  • 公共声明:避免使用可能引起冲突的公共声明
  • 私有声明:添加应用程序特定的自定义声明
  • 敏感信息:不要在负载中存储敏感信息

3. 生成签名

签名是JWT安全性的关键部分。签名的生成过程如下:

  1. 将头部和负载分别进行Base64Url编码
  2. 使用点(.)连接编码后的头部和负载,形成字符串
  3. 使用选定的签名算法和密钥对这个字符串进行签名

4. 组合令牌

将编码后的头部、编码后的负载和签名使用点(.)连接起来,形成完整的JWT令牌。

使用jsonwebtoken库生成JWT令牌

jsonwebtoken是Node.js中最流行的JWT库,它提供了简单易用的API来生成和验证JWT令牌。

安装依赖

bash
npm install jsonwebtoken

基本令牌生成

javascript
const jwt = require('jsonwebtoken');

// 要包含在令牌中的数据
const payload = {
  id: '1234567890',
  username: 'johndoe',
  role: 'user'
};

// 密钥(在生产环境中应存储在环境变量中)
const secretKey = 'your-secret-key';

// 生成令牌
const token = jwt.sign(payload, secretKey, {
  expiresIn: '1h' // 令牌过期时间
});

console.log('生成的JWT令牌:', token);

设置过期时间

JWT令牌应该有一个合理的过期时间,以减少令牌泄露后的风险。jsonwebtoken库提供了多种设置过期时间的方式:

javascript
// 设置过期时间为1小时
const token1 = jwt.sign(payload, secretKey, {
  expiresIn: '1h'
});

// 设置过期时间为30分钟
const token2 = jwt.sign(payload, secretKey, {
  expiresIn: '30m'
});

// 设置过期时间为2天
const token3 = jwt.sign(payload, secretKey, {
  expiresIn: '2d'
});

// 设置过期时间为100秒
const token4 = jwt.sign(payload, secretKey, {
  expiresIn: 100
});

添加注册声明

可以手动添加注册声明,或者让jsonwebtoken库自动生成一些声明:

javascript
const token = jwt.sign({
  sub: '1234567890', // 主题
  iss: 'my-application', // 发行人
  aud: 'my-audience', // 受众
  username: 'johndoe'
}, secretKey, {
  expiresIn: '1h',
  notBefore: '10s', // 令牌在10秒后才有效
  algorithm: 'HS256' // 签名算法
});

使用不同的签名算法

jsonwebtoken库支持多种签名算法,可以根据需要选择合适的算法:

javascript
// 使用HS512算法
const token1 = jwt.sign(payload, secretKey, {
  algorithm: 'HS512',
  expiresIn: '1h'
});

// 使用RS256算法(需要私钥)
const fs = require('fs');
const privateKey = fs.readFileSync('private.key');
const token2 = jwt.sign(payload, privateKey, {
  algorithm: 'RS256',
  expiresIn: '1h'
});

自定义令牌生成函数

为了代码复用和维护方便,可以创建一个自定义的令牌生成函数:

javascript
const jwt = require('jsonwebtoken');

/**
 * 生成JWT令牌
 * @param {Object} user - 用户信息
 * @param {string} expiresIn - 过期时间
 * @returns {string} 生成的JWT令牌
 */
function generateToken(user, expiresIn = '1h') {
  const payload = {
    id: user.id,
    username: user.username,
    role: user.role,
    email: user.email
  };
  
  const secretKey = process.env.JWT_SECRET_KEY || 'default-secret-key';
  
  return jwt.sign(payload, secretKey, {
    expiresIn,
    issuer: 'my-application',
    subject: user.id.toString()
  });
}

// 使用示例
const user = {
  id: 1,
  username: 'johndoe',
  role: 'user',
  email: 'john@example.com'
};

const token = generateToken(user, '24h');
console.log('生成的令牌:', token);

使用RSA非对称加密生成JWT令牌

对于更高级的安全需求,可以使用RSA非对称加密算法生成JWT令牌。使用RSA算法时,使用私钥生成令牌,使用公钥验证令牌,这样可以更好地保护密钥的安全。

生成RSA密钥对

首先,需要生成RSA密钥对:

bash
# 生成私钥
openssl genrsa -out private.key 2048

# 从私钥生成公钥
openssl rsa -in private.key -pubout -out public.key

使用RSA密钥对生成令牌

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

// 读取私钥
const privateKey = fs.readFileSync('private.key');

// 要包含在令牌中的数据
const payload = {
  id: '1234567890',
  username: 'johndoe',
  role: 'user'
};

// 使用RSA-SHA256算法生成令牌
const token = jwt.sign(payload, privateKey, {
  algorithm: 'RS256',
  expiresIn: '1h'
});

console.log('使用RSA生成的令牌:', token);

在Express应用中实现令牌生成

在实际的Express应用中,通常会在用户登录成功后生成JWT令牌:

javascript
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const router = express.Router();

// 模拟数据库
const users = [
  {
    id: 1,
    username: 'admin',
    password: '$2b$10$Q9u8e8e8e8e8e8e8e8e8e8', // 哈希后的密码
    role: 'admin'
  },
  {
    id: 2,
    username: 'user',
    password: '$2b$10$R8u8e8e8e8e8e8e8e8e8e8', // 哈希后的密码
    role: 'user'
  }
];

// 登录路由
router.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  try {
    // 查找用户
    const user = users.find(u => u.username === username);
    
    if (!user) {
      return res.status(401).json({ message: '用户名或密码错误' });
    }
    
    // 验证密码
    const passwordMatch = await bcrypt.compare(password, user.password);
    
    if (!passwordMatch) {
      return res.status(401).json({ message: '用户名或密码错误' });
    }
    
    // 生成访问令牌
    const accessToken = jwt.sign(
      {
        id: user.id,
        username: user.username,
        role: user.role
      },
      process.env.JWT_ACCESS_SECRET,
      {
        expiresIn: '15m'
      }
    );
    
    // 生成刷新令牌
    const refreshToken = jwt.sign(
      {
        id: user.id,
        username: user.username
      },
      process.env.JWT_REFRESH_SECRET,
      {
        expiresIn: '7d'
      }
    );
    
    // 存储刷新令牌(在实际应用中应存储在数据库中)
    user.refreshToken = refreshToken;
    
    res.json({
      accessToken,
      refreshToken,
      user: {
        id: user.id,
        username: user.username,
        role: user.role
      }
    });
  } catch (error) {
    console.error('登录错误:', error);
    res.status(500).json({ message: '服务器错误' });
  }
});

module.exports = router;

JWT令牌生成的最佳实践

  1. 使用安全的密钥

    • 密钥长度至少为256位
    • 使用随机生成的密钥
    • 将密钥存储在环境变量中,不要硬编码在代码中
  2. 选择合适的签名算法

    • 对于大多数应用,HS256已经足够安全
    • 对于更高的安全需求,考虑使用RS256等非对称加密算法
    • 避免使用不安全的算法,如none(不签名)
  3. 设置合理的过期时间

    • 访问令牌的过期时间应该较短(如15分钟到1小时)
    • 刷新令牌的过期时间可以较长(如7天到30天)
    • 考虑用户的使用场景来决定合适的过期时间
  4. 包含必要的声明

    • 始终包含sub(主题)、iss(发行人)和exp(过期时间)等基本声明
    • 只包含必要的信息,避免令牌过大
    • 不要在令牌中存储敏感信息
  5. 实现令牌版本控制

    • 添加版本号声明,以便在需要时可以强制用户重新登录
    • 考虑使用jti(JWT ID)声明来唯一标识每个令牌
  6. 使用标准化的错误处理

    • 为令牌生成过程中可能出现的错误提供明确的错误消息
    • 记录详细的日志以便于调试
  7. 考虑令牌大小

    • 尽量减少令牌的大小,避免HTTP请求头过大
    • 对于大型应用,考虑使用引用令牌而不是自包含令牌
  8. 实施密钥轮换策略

    • 定期更换签名密钥
    • 实现平滑的密钥过渡机制,避免所有用户同时被登出

实践项目

创建一个完整的JWT令牌生成系统:

  1. 实现用户认证系统

    • 用户注册、登录功能
    • 密码哈希和验证
  2. 实现JWT令牌生成模块

    • 生成访问令牌和刷新令牌
    • 使用不同的密钥和过期时间
    • 支持多种签名算法
  3. 实现令牌管理功能

    • 令牌存储和检索
    • 令牌撤销机制
    • 令牌黑名单功能
  4. 实现令牌版本控制

    • 添加版本号声明
    • 实现平滑的密钥过渡
  5. 创建API文档

    • 详细说明令牌生成的API接口
    • 提供使用示例和最佳实践

通过这个项目,您将深入理解JWT令牌生成的原理和实践,为构建安全、可靠的认证系统打下坚实的基础。