时间:2025-08-05 11:04
人气:
作者:admin
如何保证使用 node 和 go 传递 json 数据给后台后, node 和 go 后台接收到的 json 格式和用户端传递的是一致的, 经过 HMAC 计算后得到的结果也是一致的
注: 这篇文章只讨论签名和验签问题, 对密钥管理,重放攻击等其他安全问题暂时不做讨论
JSON 标准并未规定对象中“键” (key) 的顺序。因此,一个像 { "b": 2, "a": 1 } 的对象,在 Node.js 中可能被序列化为 "{"b":2,"a":1}" ,而在 Go 中则可能因为 map 的特性被序列化为 "{"a":1,"b":2}" 。由于 HMAC 是对原始字节流进行计算的,这两个不同的字符串自然会产生两个完全不同的签名,导致校验失败。
为了确保在使用 Go 和 Node.js 进行 HMAC 校验时, 两端对 JSON 数据生成的签名总是一致的, 核心在于解决 JSON 规范化(Canonicalization)的问题, 这能保证同一个数据对象无论在何种环境下, 都能被序列化成完全相同的字符串
要解决这个问题, 必须遵循以下两个原则:
这是一个非常关键的问题, 以下详细解释了这个流程:
经过排序的对象序列化成一个 JSON 字符串。这个字符串就是 "{"action":"update","target":"system","value":42}" 。原始请求体去构建它自己的 stringToSign ,并计算服务端的 HMAC 签名。客户端用于生成签名的那个经过排序的 JSON 字符串,与服务端接收到的原始请求体, 是同一个东西 。客户端把它放进请求体里,服务端再把它从请求体里读出来。
正是因为它们是完全相同的字节序列,所以当使用相同的密钥(secret)、相同的算法(HMAC-SHA256)和相同的其他参数(方法、路径、时间戳)时,两端计算出的签名才会完全一致,从而使验证得以成功。
关键的错误做法(需要避免的) 是在服务端接收到请求后,先将请求体解析成一个 JSON 对象,然后再重新序列化它来计算签名。这个过程会破坏原始的、经过客户端精心排序的字节序列,导致签名验证失败。
参考示例1
// 规范化 JSON, 确保 key 的顺序一致
// 注意: 该函数不处理特殊对象类型(如 Date/Map/Set 等)
const canonicalizeJSON = (obj: any): any => {
// 处理数组
if (Array.isArray(obj)) {
return obj.map(canonicalizeJSON);
}
// 处理非空对象(排除数组、null、基础类型)
if (typeof obj === "object" && obj !== null) {
const sorted: { [key: string]: any } = {};
// sort 会按键名字母顺序排序
Object.keys(obj).sort().forEach(key => {
sorted[key] = canonicalizeJSON(obj[key]);
});
return sorted;
}
return obj;
};
参考示例2
// 准备请求体
payload := map[string]interface{}{
"action": "update",
"value": 42,
"target": "system",
}
body, _ := json.Marshal(payload)
// 准备时间戳
timestamp := fmt.Sprintf("%d", time.Now().Unix())
// 创建签名字符串
path := "/api/data"
method := "POST"
stringToSign := method + "\n" + path + "\n" + timestamp + "\n" + string(body)
// 生成签名
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(stringToSign))
signature := hex.EncodeToString(h.Sum(nil))
参考示例3
// 获取原始body的中间件
const rawBodyParser = bodyParser.json({
verify: (req, res, buf) => {
(req as any).rawBody = buf;
}
});
// some code ...
// 获取原始请求体
const rawBody = (req as any).rawBody || Buffer.from('');
// 构建签名字符串
const path = req.path;
const method = req.method.toUpperCase();
const stringToSign = `${method}\n${path}\n${timestamp}\n${rawBody.toString('utf8')}`;
参考示例4
// 获取原始请求体
rawBody, err := io.ReadAll(c.Request.Body)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"})
return
}
// 恢复Body供后续使用
c.Request.Body = io.NopCloser(bytes.NewBuffer(rawBody))
// 构建签名字符串
path := c.Request.URL.Path
method := c.Request.Method
stringToSign := method + "\n" + path + "\n" + timestamp + "\n" + string(rawBody)
上一篇:信息收集
下一篇:第六章 流量特征分析-蚂蚁爱上树