Wefund Canonicalize 算法

更新时间:2026-06-06 12:00:00开放接口

Canonicalize 算法用于将一个或多个参数 map(如 Query、Body)规范化成排序后的 key=value&... 查询串,确保参数顺序和格式可用于签名或比对。

特性

✅ 类型自适应:支持字符串、数值、布尔值、null、数组、对象等所有 JSON 数据类型。

✅ 结果稳定:通过排序保证跨语言、跨平台一致性。

✅ 多 Map 支持:合并多个 Map 参数

✅ 递归深度限制:防止结构过深导致的不确定性和性能问题。

✅ URL 安全编码:处理空格/Unicode/特殊字符,保证签名安全。

✅ 允许重复键:允许多个 Map 有同名键,最终顺序由排序结果决定。

✅ 不可逆前处理:结果只适合比对/签名,不用于还原数据结构。

算法流程

图标  描述已自动生成

  1. 收集键值对:将可变参数中的多个 `map[string]interface{}` 合并为键值对列表。
  2. 值规范化:对每个值进行字符串化与 URL 编码。
  3. 组装键值对:将规范化后的键值对转换为 "key=value" 字符串切片。
  4. 排序:对 "key=value" 键值切片整体按字典序排序,保证稳定一致。
  5. 拼接:用 `&` 连接为最终的规范化参数串。

值规范化

值规范化包含两步,第一步字符串化(toString),第二步转义(encode)

toString(1) // "1"
toString(1.2) // "1.2"
toString(100.0) // "100"
encode("1+2=3") // "1%2B2%3D3"
encode("中文") // %E4%B8%AD%E6%96%87
toString(true) // "true"
toString(false) // "false"
toString(null) // "null"
const val = [1, "a", true, null]
const strItems = map(val, item => toString(item))
const strVal = join(strItems, ",") // "1,a,true,null"
encode(strVal) // "1%2Ca%2Ctrue%2Cnull"
const val = { name: "x", age: 1, isBoy: true }
const strKVs = map(value, (k, v) => {
  return k + "=" + encode(toString(v))
}) // ["name=ming","age=1","isBoy=true"]
const sortKVs = sort(strKVs)
const strVal = join(sortKVs, "&") // age=1&isBoy=true&name=x
encode(toString(strVal)) // age%3D1%26isBoy%3Dtrue%26name%3Dx
encode("") // ""
encode([]) // ""
encode({}) // ""
  • 数值(Number):转换为字符串。数值超过阈值:|n| > 9,007,199,254,740,991,需要使用字符串,否则有精度丢失问题。浮点类型整数,需要转换为整数。
  • 字符串(String):去除前后空白后转义。
  • 布尔(Boolean):转换为字符串 "true" 和 "false"。
  • null:线转换为字符串 "null",再转义。
  • 数组(Array):遍历元素并将元素转换为字符串,接着使用逗号(,)将字符串化的元素拼接成长字符串,最后转义。
  • 对象(Object):遍历对象对值按不同类型分别处理,并使用等号(=)拼接键和处理后的值,对拼接后的键值对正序排序后使用 & 再次拼接成最终字符串,最后转义。
  • undefined:undefined 不属于 JSON 里的数据类型,需要移除。
  • 空字符串、空数组、空对象:直接输出空字符串。

深度限制

限制递归深度防止无限递归。

深度定义

在嵌套结构(对象和数组)中,深度是以对象/数组为单元计算,最外层对象为第一层,依次递归嵌套自增,以下面 JSON 数据为例,共有五层。

图标  描述已自动生成

超出限制处理方式

数据类型

深度限制

行为

字符串

无限制

始终正常处理

数值

无限制

始终正常处理

布尔值

无限制

始终正常处理

null

无限制

始终正常处理

数组

depth >= 3

超过限制返回空字符串

对象

depth >= 3

超过限制返回空字符串

转义函数

func QueryEscape(s string) string
function encodeURIString(str: string | number): string {
  let result = encodeURIComponent(str);
  // 几个特殊字符需要单独编码;!'()*
  result = result.replace(/!/g, '%21');
  result = result.replace(/'/g, '%27');
  result = result.replace(/\(/g, '%28');
  result = result.replace(/\)/g, '%29');
  result = result.replace(/\*/g, '%2A');
  // 空格前端会被编码为%20,后端会把空格编码为+号,以后端为准
  result = result.replace(/%20/g, '+');
  return result;
}
def encode_uri_string(s: str) -> str:
  s = str(s)
  # 移除 quote 默认 safe="/"
  res = quote(s, safe="")
  # 空格 %20 -> +
  res = res.replace("%20", "+")
  return res
function encodeURIString(string $str): string
{
    // 使用 rawurlencode 进行编码(类似 encodeURIComponent)
    $result = rawurlencode($str);
    // 空格会被编码为%20,Go 会把空格编码为+号,以 Go 为准
    $result = str_replace('%20', '+', $result);
    return $result;
}
private static String encodeURIString(String str) {
  String enc = URLEncoder.encode(str, StandardCharsets.UTF_8);
  
  // "*" 保留不转义(但 Go 要转义为 "%2A")
  enc = enc.replace("*", "%2A");
  // "~" -> "%7E"(但 Go 保留 "~")
  enc = enc.replaceAll("(?i)%7e", "~");
  return enc;
}

转义函数以 Go 的 QueryEscape 为参考函数

{
  "score": 15,
  "name": "john",
  "pet": {
    "name": "dog",
    "isBoy": true,
    "food": {
      "name": "apple",
      "color": "red"
    }
  },
  "nullVal": null,
  "login_dates": ["2025-01-01", "2025-01-02"]
}

Go

{score 15}
{name john}
{pet map[food:map[color:red name:apple] isBoy:true name:dog]}
{nullVal <nil>}
{login_dates [2025-01-01 2025-01-02]}
{score 15}
{name john}
{pet food%3Dcolor%253Dred%2526name%253Dapple%26isBoy%3Dtrue%26name%3Ddog}
{nullVal null}
{login_dates %5B2025-01-01+2025-01-02%5D}
score=15
name=john
pet=food%3Dcolor%253Dred%2526name%253Dapple%26isBoy%3Dtrue%26name%3Ddog
nullVal=null
login_dates=%5B2025-01-01+2025-01-02%5D
login_dates=%5B2025-01-01+2025-01-02%5D
name=john
nullVal=null
pet=food%3Dcolor%253Dred%2526name%253Dapple%26isBoy%3Dtrue%26name%3Ddog
score=15
login_dates=%5B2025-01-01+2025-01-02%5D&name=john&nullVal=null&pet=food%3Dcolor%253Dred%2526name%253Dapple%26isBoy%3Dtrue%26name%3Ddog&score=15

JavaScript

Python

PHP

JAVA

演示示例

假定的请求主体

  1. 收集键值对
  2. 值规范化
  3. 组装键值对
  4. 排序
  5. 拼接

注意事项

  1. 字符串需要 trim 前后空白。
  2. 递归深度仅支持三层,超过三层且还需迭返则回空字符串,防止无限递归。
  3. 数值超过阈值:|n| > 9,007,199,254,740,991,需要使用字符串,否则有精度丢失问题。
  4. undefined 不属于 JSON 里的数据类型,需要移除。
  5. 只编码值,不编码键。
  6. 数组/对象内部先对子元素值编码,拼接后整体再次编码,表现为“值层级编码 + 汇总再编码”。
  7. 对空数组、空对象值规范化直接输出空字符串。

相关代码

相关文档