Canonicalize 算法用于将一个或多个参数 map(如 Query、Body)规范化成排序后的 key=value&... 查询串,确保参数顺序和格式可用于签名或比对。
特性
✅ 类型自适应:支持字符串、数值、布尔值、null、数组、对象等所有 JSON 数据类型。
✅ 结果稳定:通过排序保证跨语言、跨平台一致性。
✅ 多 Map 支持:合并多个 Map 参数
✅ 递归深度限制:防止结构过深导致的不确定性和性能问题。
✅ URL 安全编码:处理空格/Unicode/特殊字符,保证签名安全。
✅ 允许重复键:允许多个 Map 有同名键,最终顺序由排序结果决定。
✅ 不可逆前处理:结果只适合比对/签名,不用于还原数据结构。
算法流程

- 收集键值对:将可变参数中的多个 `map[string]interface{}` 合并为键值对列表。
- 值规范化:对每个值进行字符串化与 URL 编码。
- 组装键值对:将规范化后的键值对转换为 "key=value" 字符串切片。
- 排序:对 "key=value" 键值切片整体按字典序排序,保证稳定一致。
- 拼接:用 `&` 连接为最终的规范化参数串。
值规范化
值规范化包含两步,第一步字符串化(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%87toString(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%3Dxencode("") // ""
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) stringfunction 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 resfunction 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%5Dlogin_dates=%5B2025-01-01+2025-01-02%5D
name=john
nullVal=null
pet=food%3Dcolor%253Dred%2526name%253Dapple%26isBoy%3Dtrue%26name%3Ddog
score=15login_dates=%5B2025-01-01+2025-01-02%5D&name=john&nullVal=null&pet=food%3Dcolor%253Dred%2526name%253Dapple%26isBoy%3Dtrue%26name%3Ddog&score=15JavaScript
Python
PHP
JAVA
演示示例
假定的请求主体
- 收集键值对
- 值规范化
- 组装键值对
- 排序
- 拼接
注意事项
- 字符串需要 trim 前后空白。
- 递归深度仅支持三层,超过三层且还需迭返则回空字符串,防止无限递归。
- 数值超过阈值:|n| > 9,007,199,254,740,991,需要使用字符串,否则有精度丢失问题。
- undefined 不属于 JSON 里的数据类型,需要移除。
- 只编码值,不编码键。
- 数组/对象内部先对子元素值编码,拼接后整体再次编码,表现为“值层级编码 + 汇总再编码”。
- 对空数组、空对象值规范化直接输出空字符串。
相关代码
- Go:canonicalize.go
- JavaScript:canonicalize.js
- Python:canonicalize.py
- PHP:canonicalize.php
- Java:Canonicalize.java
- 测试用例:canonicalize-master.zip