← 中文资源

简体中文版本

用于稳定签名的 JSON 规范化器

按键排序、严格校验数字并拒绝循环引用,让不同服务生成完全一致的签名载荷。

阅读英文原文

功能说明

该示例生成确定性的 JSON 文本:对象键按 UTF-16 顺序排序,数组保持原顺序,字符串与数字采用 ECMAScript 的 JSON 表示。相同语义的数据无论属性插入顺序如何,都会得到完全相同的字符串,适合作为 HMAC 签名、幂等指纹、缓存键或内容哈希的输入。

关键代码

序列化函数用栈集合检测循环引用,进入数组或对象时加入、完成时移除。对象只接受普通对象或 null 原型对象,拒绝类实例、Map 等无法用 JSON 表达的值。键排序使用默认的 sort,即按 UTF-16 码元比较,与常见 JCS 实现的排序规则一致。数字通过 JSON.stringify 输出,非有限值直接拒绝,数组元素与对象键值都递归处理,不忽略 undefined。

使用注意

该实现是 RFC 8785 思路的简化版,未处理全部 Unicode 转义与数字格式细节;与第三方 JCS 库互通前应做交叉测试。undefined 在标准 JSON.stringify 中会被省略或变成 null,这里选择直接抛错,避免签名双方对缺失字段理解不一致。循环引用与访问器无法规范化。签名前还应明确字符编码为 UTF-8,并把结果按字节而不是字符串长度处理。

验证方式

对键顺序不同的两个对象断言输出相同;嵌套数组与对象比较输出;测试字符串含引号、换行与 emoji 时的转义;确认 NaN、Infinity、undefined、BigInt 与函数抛 TypeError;构造自引用对象与数组,断言因循环抛错;类实例与 Map 抛错;对同一输入多次调用结果完全一致。

代码示例

export function canonicalizeJson(value: unknown): string {
  return serialize(value, new Set<object>());
}

function serialize(value: unknown, stack: Set<object>): string {
  if (value === null) return "null";
  const type = typeof value;

  if (type === "string") return JSON.stringify(value) as string;
  if (type === "boolean") return value ? "true" : "false";
  if (type === "number") {
    if (!Number.isFinite(value)) throw new TypeError("non-finite numbers are not allowed");
    return JSON.stringify(value) as string;
  }
  if (type === "undefined" || type === "function" || type === "symbol" || type === "bigint") {
    throw new TypeError("value of type " + type + " is not valid JSON");
  }

  if (Array.isArray(value)) {
    if (stack.has(value)) throw new TypeError("cyclic array");
    stack.add(value);
    const items = value.map((item) => serialize(item, stack));
    stack.delete(value);
    return "[" + items.join(",") + "]";
  }

  if (type === "object") {
    const object = value as Record<string, unknown>;
    const prototype = Object.getPrototypeOf(object);
    if (prototype !== Object.prototype && prototype !== null) {
      throw new TypeError("only plain objects can be canonicalized");
    }
    if (stack.has(object)) throw new TypeError("cyclic object");
    stack.add(object);
    const keys = Object.keys(object).sort();
    const parts = keys.map((key) =>
      (JSON.stringify(key) as string) + ":" + serialize(object[key], stack));
    stack.delete(object);
    return "{" + parts.join(",") + "}";
  }

  throw new TypeError("unsupported value");
}