票据生成机制:服务端生成策略、JWT标准详解、签名算法对比与选择

聊到会话票据,很多人第一反应就是 JWT。没错,JWT 确实是目前最主流的票据格式。但我要说一句:会用 JWT 和用好 JWT,中间差着好几个坑。这一章,我就把票据生成的底层逻辑掰开揉碎,从服务端策略到签名算法选择,一次性讲透。

服务端生成策略:不是所有票据都该一样

票据怎么生成?很多人直接一把梭,所有用户都用同一套逻辑。我建议你换个思路——按场景分策略。

核心原则:票据的生成策略,取决于它的生命周期和敏感度。

我个人习惯把票据分成三类:

  • 短期访问票据(Access Token):15分钟到2小时过期。生成时只放必要信息,比如用户ID、角色、时间戳。别塞一堆业务数据进去。
  • 长期刷新票据(Refresh Token):7天到30天。这东西要单独存数据库,生成时加一个随机串,保证不可预测。
  • 一次性票据(One-Time Token):比如密码重置链接里的token。用完即废,服务端必须记录已使用状态。

我在项目中遇到过一个问题:有人把用户的所有权限列表都塞进 Access Token 里。结果 token 体积暴涨,每次请求都多传好几 KB。你想想看,一个系统几万个用户,网关带宽就这么被浪费了。后来我改成只存角色 ID,权限实时查缓存,问题就解决了。

我的习惯:服务端生成票据时,永远先问自己三个问题——这个票据谁用?用多久?丢了后果多严重?答案不同,策略就不同。

JWT 标准详解:结构拆开看

JWT 说白了就是一个三段式字符串,用点号分隔。我刚开始学的时候觉得这东西很神秘,拆开一看,其实就三部分。

// 一个典型的 JWT 长这样
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

三段分别是:

  • Header(头部):声明类型和签名算法。比如 {"alg":"HS256","typ":"JWT"}。Base64URL 编码后就是第一段。
  • Payload(载荷):放实际数据的地方。注意,这里只是 Base64 编码,不是加密!我见过有人把密码放进去,以为别人看不到——这是大忌。
  • Signature(签名):用 Header 里声明的算法,对前两段加一个密钥算出来的。这是防篡改的关键。

我曾经踩过的坑:有人把 JWT 的 Payload 当加密用,放了敏感信息。记住,Payload 是明文,谁拿到都能解码。真要保密,请用 JWE(加密的 JWT),或者干脆别放敏感数据。

JWT 标准里定义了一些注册声明(Registered Claims),我建议你尽量用标准字段:

字段 含义 我是否必用
iss 签发者 必用。多服务间做来源校验
sub 主题(通常是用户ID) 必用。标识谁在用这个票据
exp 过期时间 必用。服务端必须校验
iat 签发时间 建议用。方便做签发时间校验
jti JWT唯一ID 建议用。用于防重放攻击

一个小技巧:我习惯在 Payload 里加一个 scope 字段,用来做细粒度权限控制。比如 "scope":"read:orders write:profile"。这样网关层就能直接做权限校验,不用每次都查数据库。

签名算法对比:HS256 vs RS256

这是很多人纠结的地方。选 HS256 还是 RS256?我直接说结论:看你的架构场景。

先看一张对比图,把核心差异说清楚:

HS256 vs RS256 核心差异对比 HS256(对称) • 同一个密钥签名和验证 • 速度快,计算开销小 • 密钥分发是最大风险 • 适合单体或内部服务 RS256(非对称) • 私钥签名,公钥验证 • 速度稍慢,但更安全 • 公钥可公开,私钥不外传 • 适合微服务、第三方集成 选择建议 单体应用、内部服务 → HS256(简单高效) 微服务、开放API、第三方集成 → RS256(安全可控) 关键区别一句话 HS256:谁有密钥谁就能签,也就能验 RS256:私钥只有签发者有,谁都能验

HS256:简单但别大意

HS256 用的是 HMAC-SHA256,同一个密钥既签名又验证。好处是快,坏处是——密钥一旦泄露,整个系统就完了。

我记得有一次帮一个团队做代码审查,发现他们把 HS256 的密钥硬编码在配置文件里,还提交到了 Git 仓库。嗯,这相当于把家门钥匙贴在门上。后来我建议他们用密钥管理服务(KMS)来托管密钥,运行时动态获取。

避坑指南:用 HS256 时,密钥长度至少 256 位(32字节)。别用短密钥,也别用常见字符串。我曾经见过有人用 "secret123" 当密钥——这种密钥,暴力破解用不了几分钟。

RS256:微服务架构的首选

RS256 用 RSA 非对称算法,私钥签名,公钥验证。为什么我推荐微服务用这个?

  • 公钥可以公开:各个微服务只需要拿到公钥就能验证 JWT,不需要知道私钥。
  • 私钥集中管理:只有认证服务持有私钥,泄露面大大缩小。
  • 支持密钥轮换:公钥可以发布到 /.well-known/jwks.json,客户端自动拉取。

你想想看,一个微服务架构里可能有十几个服务。如果用 HS256,每个服务都得配置同一个密钥,密钥轮换时所有服务都得更新——这运维成本太高了。RS256 就简单得多,认证服务换私钥,公钥自动同步,其他服务无感。

我的实践经验:在微服务架构中,我习惯用 RS256 + JWKS(JSON Web Key Set)。认证服务把公钥列表发布到一个标准端点,其他服务定时拉取。这样密钥轮换时,只需要更新认证服务的私钥,其他服务自动适配。

签名算法选择决策表

为了方便你快速决策,我整理了一个对比表:

维度 HS256 RS256
算法类型 对称(HMAC-SHA256) 非对称(RSA-SHA256)
签名速度 快(约 0.01ms) 慢(约 0.5ms)
验证速度 快(约 0.01ms) 较慢(约 0.3ms)
密钥管理 单一密钥,分发困难 私钥保密,公钥公开
密钥轮换 所有服务同步更新 仅更新私钥,公钥自动同步
适用场景 单体应用、内部服务 微服务、开放API、第三方
安全风险 密钥泄露即全盘崩溃 私钥泄露风险低

代码示例:两种签名算法的实现

光说不练假把式。我写两个简单的示例,用 Node.js 的 jsonwebtoken 库演示:

// HS256 签名与验证
const jwt = require('jsonwebtoken');

// 签名
const hs256Token = jwt.sign(
  { userId: 123, role: 'admin' },
  'your-256-bit-secret-key-here-must-be-long-enough',
  { algorithm: 'HS256', expiresIn: '1h' }
);

// 验证
try {
  const decoded = jwt.verify(hs256Token, 'your-256-bit-secret-key-here-must-be-long-enough');
  console.log('HS256 验证通过:', decoded);
} catch (err) {
  console.error('HS256 验证失败:', err.message);
}
// RS256 签名与验证
const jwt = require('jsonwebtoken');
const fs = require('fs');

// 读取密钥文件(生产环境请用密钥管理服务)
const privateKey = fs.readFileSync('private.pem', 'utf8');
const publicKey = fs.readFileSync('public.pem', 'utf8');

// 签名(用私钥)
const rs256Token = jwt.sign(
  { userId: 456, role: 'user' },
  privateKey,
  { algorithm: 'RS256', expiresIn: '2h' }
);

// 验证(用公钥)
try {
  const decoded = jwt.verify(rs256Token, publicKey);
  console.log('RS256 验证通过:', decoded);
} catch (err) {
  console.error('RS256 验证失败:', err.message);
}

一个小建议:生产环境别把密钥文件放在代码目录里。用环境变量或者专门的密钥管理服务。我习惯用 HashiCorp Vault 或者 AWS KMS 来管理私钥,运行时通过 API 获取。

总结一下核心要点

票据生成这件事,说复杂也复杂,说简单也简单。关键在于想清楚你的场景:

  • 服务端生成策略:按票据类型区分策略,别一刀切。
  • JWT 结构:Header、Payload、Signature 三段,Payload 不加密,别放敏感信息。
  • 签名算法选择:单体用 HS256,微服务用 RS256。别为了省事选错。