如何可靠地对JavaScript对象进行哈希?

65

有没有一种可靠的方法 JSON.stringify 一个JavaScript对象,以确保在所有浏览器,Node.js等中创建的JSON字符串相同,假设JavaScript对象相同?

我想像散列JavaScript对象一样

{
  signed_data: object_to_sign,
  signature:   md5(JSON.stringify(object_to_sign) + secret_code)
}

并在不同的Web应用程序(例如Python和Node.js)以及用户之间传递它们,以便用户可以对一个服务进行身份验证,并向下一个服务显示“已签名数据”,以便该服务检查数据是否真实。

然而,我遇到了这个问题:JSON.stringify 在不同实现下返回的 JSON 字符串并不是完全一致的:

  • 在 Node.js / V8 中,JSON.stringify 返回一个没有不必要空格的 JSON 字符串,例如 '{"user_id":3}'。
  • Python 的 simplejson.dumps 会保留一些空格,例如 '{"user_id": 3}'
  • 可能其他 stringify 实现也有不同的处理方式,例如空格、属性的顺序等等。

是否有可靠的跨平台 stringify 方法?是否有“标准化 JSON”?

您是否推荐其他方式来哈希对象?

更新:

这是我使用的解决方法:

normalised_json_data = JSON.stringify(object_to_sign)
{
  signed_data: normalised_json_data,
  signature:   md5(normalised_json_data + secret_code)
}
所以在这种方法中,签名的不是对象本身,而是其JSON表示形式(特定于签名平台)。这种方法很有效,因为我现在签名的是一个明确的字符串,我可以在检查签名哈希后轻松地使用 JSON.parse 解析数据。
这里的缺点是,如果我也将整个{signed_data, signature} 对象作为 JSON 一起传输,我就必须调用两次 JSON.parse,而且看起来并不那么好,因为内部的引号被转义了。
{"signature": "1c3763890298f5711c8b2ea4eb4c8833", "signed_data": "{\"user_id\":5}"}

2
你正在使用JSON.stringify作为序列化机制来进行哈希操作。我不确定这是否是一个好主意,正如你所遇到的问题一样。无论如何,JSON.stringify非常有限,我不会信任它来哈希或序列化我的信息。例如,尝试一下JSON.stringify(new Error('not going to work'))。 - nathan g
我还想自评一下,MD5并不是在这里使用的最佳哈希函数。 - nh2
3
JSON.stringify不是准备哈希的好方法,例如JSON.stringify({b:2,a:1}) => '{"b":2,"a":1}',而JSON.stringify({a:1,b:2}) => '{"a":1,"b":2}'。 - WoLfPwNeR
6个回答

82

您可能会对npm软件包object-hash感兴趣,它似乎具有相当不错的活跃度和可靠性水平。

var hash = require('object-hash');

var testobj1 = {a: 1, b: 2};
var testobj2 = {b: 2, a: 1};
var testobj3 = {b: 2, a: "1"};

console.log(hash(testobj1)); // 214e9967a58b9eb94f4348d001233ab1b8b67a17
console.log(hash(testobj2)); // 214e9967a58b9eb94f4348d001233ab1b8b67a17
console.log(hash(testobj3)); // 4a575d3a96675c37ddcebabd8a1fea40bc19e862

1
或者在浏览器中使用,console.log(objectHash(testobj1)) - phyatt
3
尽管如此,这与OP所要求的跨平台性格格不入。还有基于JSON的确定性版本包装器,比object-hash运行得更快。请参阅我制作的这个基准测试:https://jsperf.com/stringify-vs-object-hash - oligofren
@oligofren https://www.npmjs.com/package/json-stringify-deterministic 看起来很有趣。我建议您将评论发布为另一个答案。 关于跨平台支持,我同意Mark Kahn的答案... +也许将JS哈希函数作为Web服务,然后从不同的平台调用它,可以在不太多的开发成本的情况下解决问题。 - Maxime Pacary
我测试了json-stringify-deterministic,但发现它非常慢。我自己写的一个简单函数将哈希的总时间从4毫秒减少到2毫秒:`function ObjectToUniqueStringNoWhiteSpace(obj: any){let SortedObject: any = sortObjectKeys(obj); let jsonstring = JSON.stringify(SortedObject, function(k, v) { return v === undefined ? "undef" : v; }); // Remove all whitespace let jsonstringNoWhitespace: string = jsonstring.replace(/\s+/g, ''); return jsonstringNoWhitespace;}` - isgoed
请注意,此库使用的SHA1和MD5不具有加密安全性。因此,请小心使用,不要用于加密目的。 - zarnoevic

10

这是一个老问题,但我想为任何谷歌裁判添加一个当前解决方案。

现在签名和哈希JSON对象的最佳方法是使用JSON Web Tokens。这允许对象进行签名、哈希,然后基于签名由其他人验证。它适用于许多不同的技术,并且有一个活跃的开发团队。


3
尝试生成JWT签名,但似乎它并没有忽略负载JSON中的字段顺序。因此,我不理解它如何有所帮助,因为OP想在哈希计算过程中忽略字段顺序。请问您能解释一下您的想法吗? - Kirill
@derp Javascript从来没有保证对象键的顺序 - 这不是在使用对象时应考虑的事情。我认为OP并没有特别说明他想忽略字段顺序,只是认为不同的实现会以不同的方式处理它们。他的解决方法有效地处理了这个问题,因为他签署的是最终结果,而不是另一个stringify调用。 - Matt Forster
3
他在“stringify”函数的第三项缺点中提到,生成的字符串可能会有不同的字段顺序。我仍然不明白JWT如何为{"foo":"bar", "bar":"foo"}{"bar":"foo","foo":"bar"}这两个基本相同但字段顺序不同的对象生成相同的哈希/签名。如果JWT没有这种功能,你的回答就不是OP(和我)正在寻找的确切内容。 - Kirill
7
不要混淆签名和哈希,它们解决不同的问题。签名就像强化版的哈希,重点在于安全性。哈希与安全无关。JWT可能是OP提问的最佳解决方案,因为它自动包含安全哈希、验证、过期和信任。每次对相同对象进行哈希处理时,由于时间戳会自动包含在哈希数据中,您将获得不同的签名。听起来@Derp只是想在逻辑上相同的对象表示之间获得相同的哈希值,在这种情况下请参见object-hash - Phil
@MattForster 实际上,根据ECMAScript规范,从ES6开始,对象键的顺序是有保证的。 - David Pfeffer

9

您希望在多种语言中实现某种东西的一致性,这几乎是不可能的。您有两个选择:

  • 查看www.json.org的实现,看看它们是否更加标准化
  • 在每种语言中自己编写(使用json.org的实现作为基础,应该只需要非常少的工作)

1
好的,对于像签名认证这样的关键事项,看起来我确实无法使用这种方法。我更新了我的帖子,展示了如何签署不是对象,而是哈希创建平台的特定JSON字符串。 - nh2

8

在尝试了一些哈希算法和JSON转字符串方法后,我发现这个方法效果最好(抱歉,它是TypeScript,当然可以重写成JavaScript):

// From: https://dev59.com/4m035IYBdhLWcg3wW-v0
function sortObjectKeys(obj){
    if(obj == null || obj == undefined){
        return obj;
    }
    if(typeof obj != 'object'){ // it is a primitive: number/string (in an array)
        return obj;
    }
    return Object.keys(obj).sort().reduce((acc,key)=>{
        if (Array.isArray(obj[key])){
            acc[key]=obj[key].map(sortObjectKeys);
        }
        else if (typeof obj[key] === 'object'){
            acc[key]=sortObjectKeys(obj[key]);
        }
        else{
            acc[key]=obj[key];
        }
        return acc;
    },{});
}
let xxhash64_ObjectToUniqueStringNoWhiteSpace = function(Obj : any)
{
    let SortedObject : any = sortObjectKeys(Obj);
    let jsonstring = JSON.stringify(SortedObject, function(k, v) { return v === undefined ? "undef" : v; });

    // Remove all whitespace
    let jsonstringNoWhitespace :string = jsonstring.replace(/\s+/g, '');

    let JSONBuffer: Buffer = Buffer.from(jsonstringNoWhitespace,'binary');   // encoding: encoding to use, optional.  Default is 'utf8'
    return xxhash.hash64(JSONBuffer, 0xCAFEBABE, "hex");
}

使用的npm模块:https://cyan4973.github.io/xxHash/https://www.npmjs.com/package/xxhash

它的好处:

  • 结果是确定性的
  • 忽略键的顺序(保持数组顺序)
  • 跨平台(如果您可以找到JSON-stringify的等效项) JSON-stringify将希望不会有不同的实现,并且空格删除将希望使其与JSON格式无关。
  • 64位
  • 十六进制字符串为结果
  • 最快的(2177B JSON,0.021毫秒,150KB JSON,2.64毫秒)

7
您可以通过应用以下规则来归一化stringify()的结果:
  • 删除不必要的空格
  • 对哈希中的属性名称进行排序
  • 定义明确、一致的引号风格
  • 归一化字符串内容(使"\u0041"和"A"成为相同的内容)
这将使您得到一个对象的规范JSON表示,您可以可靠地进行哈希。

1
引用样式已规范化(它是JSON规范的一部分),但空格和属性顺序不是。 - user578895
1
也许吧,但是那些已经失效了,而且我真的怀疑你会在任何一种本地语言的JSON实现中看到它们。 - user578895
@oligofren,那其实是个修辞问题!感谢你的建议,不过从那时起我有了自己的解决方案(jsum),它在其他解决方案中拥有最快的性能。 - Yan Foto
@YanFoto 很有趣,这基本上是 object-hash 库的更快版本。我需要将其添加到我的基准测试中 :-) 不确定由于 Node 的 crypto 依赖项是否可以导出到浏览器,但测试仅针对 stringify 函数与其他函数进行比较将会很有趣。 - oligofren
@oligofren 是和不是的。首先,它从未被设计用于浏览器使用(因此对原始问题无关),其次它仅用于 JSON 对象而非 JS 对象,即它只忽略函数和其他非 JSON 成员。 - Yan Foto
显示剩余3条评论

1
你可能会发现bencode符合你的需求。它是跨平台的,并且从每个实现中保证编码相同。
缺点是它不支持null或booleans。但如果你在编码之前进行像将bools转换为0 | 1和nulls转换为"null"这样的转换,那么这可能对你来说是可以接受的。

同时,不支持浮点数。 - Shakil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接