JWT vs PASETO v2 vs TECTO:2026 年选择合适的令牌协议

发布: (2026年2月22日 GMT+8 12:13)
12 分钟阅读
原文: Dev.to

Source: Dev.to

令牌在现代认证流程中随处可见。但并非所有令牌都一样。
在本文中,我们将并排比较三种方法——经典的 JWT (HS256)、更现代的 PASETO v2,以及全新的 TECTO——从安全性、易用性和实际代码示例三个维度进行对比。

快速比较表

属性JWT (HS256)PASETO v2TECTO
负载可见?✅ 是(base64)✅ 是(已签名,未加密)❌ 完全加密
密码算法无(HMAC)Ed25519(签名)/ XChaCha20(加密)XChaCha20‑Poly1305
Nonce不适用每个令牌24字节每个令牌24字节的CSPRNG
密钥大小可变可变正好256位(强制)
篡改检测HMAC签名Ed25519 / Poly1305标签Poly1305认证标签
错误细节揭示原因揭示原因通用“无效令牌”
算法混淆攻击⚠️ 是(alg: none问题)✅ 否✅ 否
内置密钥轮换❌ 自行实现❌ 自行实现✅ 原生(令牌中包含kid)

1️⃣ JWT – 老牌可靠

jsonwebtoken 是 Node.js 中使用最广泛的 token 库。它经受了大量实战考验,拥有庞大的生态系统,入门极其简单。

npm install jsonwebtoken
import jwt from "jsonwebtoken";

const SECRET = "my-secret-key"; // ← This is the problem

// Sign
const token = jwt.sign(
  { userId: 42, role: "admin" },
  SECRET,
  { expiresIn: "1h", issuer: "my-app" }
);

console.log(token);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjQyLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMCwiaXNzIjoibXktYXBwIn0.SIGNATURE

// Verify
const payload = jwt.verify(token, SECRET) as { userId: number; role: string };
console.log(payload.userId); // 42

Payload 是可读的

// Decode the middle segment of any JWT
const [, payload] = token.split(".");
const decoded = Buffer.from(payload, "base64url").toString("utf-8");
console.log(decoded);
// {"userId":42,"role":"admin","iat":1700000000,"exp":1700003600,"iss":"my-app"}

任何拦截到 token 的人都可以读取其中的 payload——不需要密钥。这本就是设计如此:JWT 是签名的,而不是加密的。许多开发者起初并未意识到这一点。

算法混淆问题

JWT 允许在头部指定使用的算法。这导致了臭名昭著的 alg: none 攻击,攻击者可以通过将算法设为 none 来伪造 token。即使现代库已经阻止了 noneHMAC 与 RSA 混淆攻击在接受来自多个发行者的 token 时仍然是一个真实的风险。

何时使用 JWT 合理

  • 公共的、非敏感的 payload(如用户 ID、角色)
  • 与需要 JWT 的第三方服务集成(OAuth、OIDC)
  • 团队已经拥有 JWT 基础设施

2️⃣ PASETO – 跨平台安全令牌

PASETO 旨在修复 JWT 的致命缺陷。它完全去除了算法灵活性——你只需选择一个版本,就会得到一个固定且精心挑选的算法。没有 alg: none,也没有混淆攻击。

npm install paseto
import { V2 } from "paseto";

const key = await V2.generateKey("local");

// ---- Encrypt (v2.local) ----
const token = await V2.encrypt(
  { userId: 42, role: "admin" },
  key,
  { expiresIn: "1h", issuer: "my-app" }
);
console.log(token);
// v2.local.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

// Decrypt
const payload = await V2.decrypt(token, key);
console.log(payload.userId); // 42

// ---- Sign (v2.public) ----
const { privateKey, publicKey } = await V2.generateKey("public");

// Sign (payload is visible, like JWT)
const signed = await V2.sign({ userId: 42 }, privateKey, { expiresIn: "1h" });

// Verify
const verified = await V2.verify(signed, publicKey);

亮点

  • 无算法混淆 —— 版本号 (v2) 固定了使用的算法。
  • v2.local 加密 负载(XChaCha20‑Poly1305)。
  • 干净、现代的异步 API。
  • 没有原生的密钥轮换机制 —— kid 不是令牌格式的一部分;你必须自行管理密钥版本。
  • 错误信息仍可能泄露失败原因。
  • 对密钥不进行熵校验——即使提供弱密钥,库也会悄然接受。

3️⃣ TECTO – 传输加密紧凑令牌对象

TECTO 采用不同的理念:每个令牌始终都是完整加密的。没有“签名‑但可读”模式。

bun add tecto
import {
  generateSecureKey,
  MemoryKeyStore,
  TectoCoder,
  InvalidSignatureError,
  TokenExpiredError,
  assertEntropy,
} from "tecto";

// 1️⃣ 生成一个密码学安全的 256‑bit 密钥
const key = generateSecureKey();

// 2️⃣ 设置密钥存储
const store = new MemoryKeyStore();
store.addKey("my-key-2026", key);

// 3️⃣ 创建编码器
const coder = new TectoCoder(store);

// 4️⃣ 加密
const token = coder.encrypt(
  { userId: 42, role: "admin" },
  { expiresIn: "1h", issuer: "my-app" }
);
console.log(token);
// tecto.v1.my-key-2026.base64url_nonce.base64url_ciphertext

// 5️⃣ 解密
const payload = coder.decrypt(token);
console.log(payload.userId); // 42

令牌格式

tecto.v1...
  • kid(密钥 ID)嵌入在令牌本身,实现原生密钥轮换——无需额外的元数据或头部。
// 轮换密钥
store.addKey("key-2026-01", oldKey);          // 旧密钥仍可解密旧令牌
store.rotate("key-2026-06", newKey);          // 新令牌使用新密钥
store.removeKey("key-2026-01");              // 当不再需要时删除旧密钥

强密钥熵检查

// 这些都会抛出 KeyError
assertEntropy(new Uint8Array(32));                 // 全部为零
assertEntropy(new Uint8Array(32).fill(0xaa));      // 重复字节
assertEntropy(new Uint8Array(16));                 // 长度错误

// 这是安全的
const goodKey = generateSecureKey(); // 始终高熵,256‑bit
assertEntropy(goodKey); // ✅ 通过

统一错误处理

try {
  coder.decrypt(tamperedToken);
} catch (err) {
  if (err instanceof InvalidSignatureError) {
    // err.message === "Invalid token"
    // 你不知道 *为什么* 失败——这正是设计初衷。
    // 攻击者无法通过观察错误信息来探测系统。
  }
  if (err instanceof TokenExpiredError) {
    // 令牌已过期——可以安全地告知客户端。
  }
}

TECTO 的优势场景

  • 需要对每个声明完全保密
  • 原生密钥轮换,无需额外的管道。
  • 统一、非信息化的错误消息,以阻止探测攻击。

TL;DR

FeatureJWT (HS256)PASETO v2TECTO
负载可读吗?✅ (base64)✅ (已签名)❌ (已加密)
算法固定吗?❌ (alg 头部)✅ (版本固定)✅ (内置)
原生密钥轮换?✅ (令牌中包含 kid)
强密钥长度强制?✅ (256 位)
统一错误信息?
最适合…公开的、非敏感数据;已有的 JWT 生态系统需要签名令牌且可选加密的现代应用任何必须保密且受益于内置轮换的场景

选择符合您安全和运营需求的令牌格式——并且 不要以为“已签名=安全”。加密同样重要。

令牌格式比较

以下是 JWTPASETO v2TECTO 的快速对比。
三者都可以携带相同的负载,例如 { userId: 42, role: "admin" },但它们在数据保护方式上差异巨大。

具体示例

// 我们想要传输的负载
{ userId: 42, role: "admin" }

JWT(仅签名)

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJ1c2VySWQiOjQyLCJyb2xlIjoiYWRtaW4ifQ   ← { userId: 42, role: "admin" } 的 base64
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

任何人都可以对中间段使用 atob() 并读取负载——不需要密钥。

PASETO v2.local(加密)

v2.local.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxFULL_CIPHERTEXT

使用 XChaCha20‑Poly1305 加密。没有密钥时,密文是不可读的。

TECTO(加密 + 内置密钥标识)

tecto.v1.my-key-2026.NONCE_BASE64URL.CIPHERTEXT_BASE64URL

使用 XChaCha20‑Poly1305 加密
kidmy‑key‑2026)仅作为标签可见;没有密钥时负载仍不可读取。

功能矩阵

功能JWTPASETO v2TECTO
令牌中的密钥 ID❌ Not standard❌ Not standard✅ Built‑in kid
密钥轮换后旧令牌可解密DIYDIYstore.rotate() handles it
撤销旧密钥DIYDIYstore.removeKey() zeroes memory
熵验证❌ No❌ NoassertEntropy() enforced
防篡改✅ HMAC❌ Payload readable by anyone✅ Authenticated encryption (XChaCha20‑Poly1305)
算法灵活性❌ Algorithm confusion attack surface✅ Version pins algorithm✅ Version pins algorithm
原生密钥轮换❌ No❌ No✅ With kid
通用错误(无 oracle 攻击)
时序安全比较
密钥移除时内存清零
负载大小限制(防止 DoS)
类型检查的已注册声明

何时使用哪种令牌

  • 使用 JWT 如果您需要:

    • 与 OAuth / OIDC / 现有基础设施兼容
    • 负载中不包含敏感数据(仅 ID、角色等)
    • 与仅支持 JWT 的第三方服务集成
  • 使用 PASETO v2.local 如果您想要:

    • 一个经过充分审计、标准化的 加密 令牌
    • 跨多语言/平台的互操作性
    • 没有内置的本地密钥轮换(您需要自行处理轮换)
  • 使用 TECTO 如果您想要:

    • 默认加密,且配置错误的可能性
    • 无需额外基础设施的本地密钥轮换
    • 一个全新的 TypeScript/Bun 项目
    • 深度防御:熵验证、通用错误、时序安全、内存清零、负载大小限制等

TECTO 快速入门

bun add tecto
import { generateSecureKey, MemoryKeyStore, TectoCoder } from "tecto";

// 1️⃣ Create a key store and add a key
const store = new MemoryKeyStore();
store.addKey("v1", generateSecureKey());

// 2️⃣ Build a coder that knows about the store
const coder = new TectoCoder(store);

// 🔐 Encrypt a payload (expires in 1 hour)
const token = coder.encrypt({ userId: 42 }, { expiresIn: "1h" });

// 🔓 Decrypt it back (type‑checked)
const { userId } = coder.decrypt(token);

持久化密钥存储

TECTO 附带了 SQLitePostgreSQLMariaDB 的适配器——它们都实现了相同的 KeyStoreAdapter 接口,因此切换后端非常简单。


结论

  • JWT 将继续成为联合身份验证和 OAuth 流程的事实标准——这对于公开的、非敏感的声明来说是可以接受的。
  • PASETO v2.local 为您提供可靠、标准化的加密令牌,但您必须自行管理密钥轮换和熵。
  • TECTO 更进一步:内置密钥轮换、强制熵检查、通用错误处理、时序安全比较以及自动内存清零。

最佳的令牌协议是那种你无法错误配置的。
TECTO 为此提供了有力的论据。

0 浏览
Back to Blog

相关文章

阅读更多 »