SHA1Managed.ComputeHash 在不同服务器上偶尔会产生不同的结果

10

背景(您可以跳过本节)

我有大量的数据(约3 MB),需要在数百台机器上保持最新状态。一些机器运行C#,一些运行Java。数据随时可能发生变化,并且需要在几分钟内传播到客户端。数据以Json格式从4个负载平衡服务器传递。这4个服务器使用ASP.NET 4.0,Mvc 3和C# 4.0运行。

在4个服务器上运行的代码具有散列算法,该算法对Json响应进行散列,然后将散列转换为字符串。此哈希值提供给客户端。然后,每隔几分钟,客户端会发送带有哈希的请求到服务器,如果哈希已过期,则返回新的Json对象。如果哈希仍然有效,则返回一个带有空正文的304。

偶尔,4个盒子生成的哈希不一致,这意味着客户端不断下载数据(每个请求可能会命中不同的服务器)。

代码片段

以下是用于生成哈希的代码。

internal static HashAlgorithm Hasher { get; set; }
...
Hasher = new SHA1Managed();
...
Convert.ToBase64String(Hasher.ComputeHash(Encoding.ASCII.GetBytes(jsonString)));

为了尝试调试问题,我将其拆分如下:
Prehash = PreHashBuilder.ToString();
ASCIIBytes = Encoding.ASCII.GetBytes(Prehash);
HashedBytes = Hasher.ComputeHash(ASCIIBytes);
Hash = Convert.ToBase64String(HashedBytes);

我随后添加了一个路由,输出了以上数值,并使用Beyond Compare比较了它们之间的差异。

为了在BeyondCompare中使用,字节数组被转换成字符串格式,方法如下:

private static string GetString(byte[] bytes)
{
    StringBuilder sb = new StringBuilder();
    foreach (byte b in bytes)
    {
        sb.Append(b);
    }
    return sb.ToString();
} 

如您所见,字节数组以字节序列的形式直接显示,而不是进行“转换”。
问题:
我发现Prehash和ASCIIBytes值相同,但HashedBytes值不同,这意味着哈希也不同。
我多次重启了4台服务器上的IIS网站,并在它们具有不同哈希值时使用BeyondCompare比较了数值。在每种情况下都有一个不同的"HashedBytes"值(即SHA1Managed.ComputeHash(...)的结果)。
问题是什么?ComputeHash函数的输入是相同的。SHA1Managed是否与机器相关?那不太可能,因为四台机器有一半的时间具有相同的哈希值。
我已经在StackOverFlow和Bing上搜索过,但未能找到其他人有此问题。我能找到的最接近的问题是编码问题,但我认为我已经证明了编码并不是问题。
输出:
由于内容较长,我希望不要全部展示,但是以下是我正在比较的片段:
哈希:o1ZxBaVuU6OhE6De96wJXUvmz3M=
哈希字节:163861135165110831631611916022224717299375230207115
ASCII字节:1151169710310146991111094779114100101114831011141181059910147115101114118105991014611511899591151051031101171129511510111411810599101114101102101114101110991011159598979910710111010011111410010111411510111411810599101951185095117114108611041161161125847471051159897991071011101004610910211598101115116971031014699111109477911410010111483101114118105991014711510111411810599101461151189947118505911510510311011711295115101114118105991011141011021011141011109910111595989799107101110100112971211091011101161151161111141011151011141.... Prehash:...

当我在不同的服务器上比较这两个页面时,ASCII字节相同,但HashedBytes不同。我用于字节转储的方法不进行任何转换,只是连续地将每个字节转储出来。我可以用"."分隔字节。
跟进:
我已对代码进行了更改,将b.ToString(CultureInfo.InvariantCulture)和HashAlgorithm更改为本地变量而不是静态属性。 我正在等待代码部署到服务器。

4
不要简单地倾倒一个字符串并检查它(你的工具可能会忽略某些差异),而是倾倒字节并查看它们的比较情况。 - CrazyCasta
6
如果哈希不一致会导致重大错误,我会信任哈希,因此怀疑源代码..也许是PreHashBuilder?或者它的源代码?是否存在可能导致意外改变的线程/并发问题? - user166390
1
你是说 ASCIIBytes 字节相同,你是怎么确定的? - Magnus
1
@JALLRED 你可以使用 SequenceEqual 来比较数组:var areSame = bytes1.SequenceEqual(bytes2); 不需要转换为字符串。 - Magnus
2
关于您的GetString方法,由于StringBuilder.Append(byte)调用byte.ToString(CultureInfo.CurrentCulture),不同的文化和配置可能会产生不同的值。如果您传递一个使用不变文化转换的字符串而不是传递一个字节值到sb.Append,会发生什么? - hatchet - done with SOverflow
显示剩余10条评论
3个回答

13

我一直在尝试重现这个问题,但一旦将SHA1Managed属性更改为局部变量而不是全局静态变量后,就无法这样做。

问题出在多线程上。我的代码是线程安全的,除了我标记为静态的SHA1Managed类。我以为在内部标记为静态的情况下,SHA1Managed.ComputeHash会在底层是线程安全的,但显然它不是。

重申一遍,如果将其标记为内部静态,则 SHA1Managed.ComputeHash 不是线程安全的。

MSDN说明如下:

Any public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.

我不知道为什么internal static和public static的行为不同。

我会把@pst标记为答案并添加一条评论来澄清问题,但是@pst已经发表了评论,所以我无法将其标记为答案。

感谢您所有的输入。


5
SHA1Managed.ComputeHash是实例方法而不是静态方法,因此您不能安全地在多个线程同时对同一SHA1Managed实例调用该方法。 Hasher是公共的或内部的并不重要。 - Michael Liu
为了解决这个问题,我只需在代码周围添加一个锁(sha1obj) { }。尚未进行测试以查看锁定是否比创建多个实例更慢,但这对我的用例不是问题。 - fabspro

0

你的GetString方法在不同文化环境的机器上可能会产生不同的结果,因为StringBuilder.Append(byte)调用byte.ToString(CultureInfo.CurrentCulture)。请尝试

private static string GetString(byte[] bytes)
{
    StringBuilder sb = new StringBuilder();
    foreach (byte b in bytes)
    {
        sb.Append(b.ToString(CultureInfo.InvariantCulture));
    }
    return sb.ToString();
} 

但是使用不使用字节值的十进制字符串表示的方法会更好。


0
问题在于你的代码可能会影响前导0,使用以下代码将数组转换为字符串进行比较。它将产生可靠的结果,并专门设计用于将字节数组转换为字符串,以便在机器之间传输。
using System.Runtime.Remoting.Metadata.W3cXsd2001;

public byte[] StringToBytes(string value)
{
    SoapHexBinary soapHexBinary = SoapHexBinary.Parse(value);
    return soapHexBinary.Value;
}

public string BytesToString(byte[] value)
{
    SoapHexBinary soapHexBinary = new SoapHexBinary(value);
    return soapHexBinary.ToString();
}

此外,我建议您检查JSON是否存在微妙的差异,因为这将创建完全不同的哈希值。例如,一些文化将数字“一千六百点七”表示为1,600.71 000.7或甚至1 600,7(请参见维基百科页面)。

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