通过C#对象生成MD5哈希






4.56/5 (12投票s)
本文介绍如何为通用的C#对象生成MD5哈希字符串。
引言
有时我们需要序列化对象,例如为了在网络上传输它们,或者在本地存储和恢复它们,或者出于任何其他原因。现在,在反序列化过程之后,如果我们能知道对象是否已正确恢复,那将非常有用。尤其是在您拥有具有内部状态的对象,或者必须管理类的多个实例的情况下。一种可能的解决方案是使用System.Guid
结构来标识对象。但通过这种方式,您不能确定内部状态等已正确反序列化(有关解释请参见背景)。
互联网上常用的技术是提供MD5哈希字符串,以便接收方可以比较文件是否在传输过程中没有任何修改。
背景
.NET Framework为我们提供了一个结构来唯一标识我们的对象,即mscorlib.dll中的System.Guid
结构。此结构可用于为每个类分配自己的标识符。而这就是问题的关键。我们需要的是每个类的实例的标识符,而不是类的标识符。此标识符必须隐含地代表某些内部值(例如状态)。否则,我们的对象接收者将无法确定他是否收到了/反序列化了同一个对象。此外,我们的接收者无法自行“创建”GUID。一旦发送者创建了它,就无法重现。
我们还必须提供一个功能,该功能可以由发送者和接收者双方执行,以标识一个对象。此标识符还必须隐含地考虑对该对象至关重要的字段。而且这些重要的字段对于每个类都可能不同!
我的想法是为此使用MD5哈希。每个对象都有一个内置函数,名为.GetHashCode()
。此方法返回一个Integer
,尽管根据方法名称,您可能会期望一个string
。这是因为这些HashValues
旨在用作例如HashTable
中的键。
但幸运的是,在System.Security.Cryptography
命名空间中有一个名为MD5CryptoServiceProvider
的类。不幸的是,这个类并不容易使用。对于大多数程序员来说,主要问题可能是该类仅接受字节数组作为输入,而不接受对象的引用。因此,我决定将所有所需功能封装到一个生成器类中。然后,该类就可以为我生成哈希,而我只需要写一行代码。
Using the Code
上面的代码文件包含一个名为MD5HashGenerator
的类。该类有一个static
方法.generateKey(Object sourceObject)
,它会为您执行“魔法”。将该类包含到您的项目中,然后像这样使用它:
要使用该类(作为发布者),您需要执行以下操作:
- 将对象标记为
Serializable()
。将所有不应序列化的变量标记为NonSerializable()
。 - 调用
static
方法MD5HashGenerator.generateKey(Object sourceObject)
。您将获得该对象的MD5哈希,作为String
。 - 序列化对象,发布/存储它以及哈希。
如果您是接收者,那么:
- 反序列化收到的对象。
- 在反序列化对象上调用
static
方法MD5HashGenerator.generateKey(Object sourceObject)
。 - 比较哈希。
示例
我们想要序列化一个包含string
、int
和DateTime
的类。dateTime
成员在创建时设置,因此它对于类的每个实例都不同。如上所述,类必须被标记为可序列化。它(可以)看起来像这样:
using System;
using System.Runtime.Serialization;
[Serializable]
public class SimpleClass
{
private string justAString;
private int justAnInt;
private DateTime justATime;
/// <summary>
/// Default Constructor. The fields are filled with some standard values.
/// </summary>
public SimpleClass()
{
justAString = "Some useless text";
justAnInt = 345678912;
justATime = DateTime.Now;
}
}
由于我们使用系统方法DateTime.Now
来初始化字段justATime
,所以类的每个实例都应该不同。重要的是要将类“标记”为Serializable
,因为这是MD5HashGenerator
类所要求的。
生成器类使用BinaryFormatter
进行序列化,因此所有字段(无论它们是private
还是非私有)都会自动包含在序列化过程中。但是,如果您使用的是句柄和指针,请排除它们。有关详细信息,请参阅[1]。
“发布”对象的类然后必须执行以下操作:
...
SimpleObject simpleObject = new SimpleObject();
string simpleObjectHash = MD5HashGenerator.generateKey(simpleObject);
//Now serialize the simpleObject e.g. with a XmlSerializer and
//store the hash somewhere
...
现在,“消费者”可以反序列化SimpleObject
,并在反序列化的对象上调用MD5HashGenerator.generateKey(simpleObject)
。然后,他可以比较哈希字符串并决定它是否是同一个对象。
工作原理
MD5HashGenerator.generateKey(Object SourceObject)
方法的代码如下:
public static String GenerateKey(Object sourceObject)
{
String hashString;
//Catch unuseful parameter values
if (sourceObject == null)
{
throw new ArgumentNullException("Null as parameter is not allowed");
}
else
{
//We determine if the passed object is really serializable.
try
{
//Now we begin to do the real work.
hashString = ComputeHash(ObjectToByteArray(sourceObject));
return hashString;
}
catch (AmbiguousMatchException ame)
{
throw new ApplicationException("Could not definitely decide
if object is serializable. Message:"+ame.Message);
}
}
}
让我们更深入地研究以下代码行:
hashString = ComputeHash(ObjectToByteArray(sourceObject));
如上所述,我使用了MD5CryptoServiceProvider
类来生成Hashstring
。我将该方法的使用封装在ComputeHash(byte[] objectAsBytes)
方法中。这是实现:
private static string ComputeHash(byte[] objectAsBytes)
{
MD5 md5 = new MD5CryptoServiceProvider();
try
{
byte[] result = md5.ComputeHash(objectAsBytes);
// Build the final string by converting each byte
// into hex and appending it to a StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < result.Length; i++)
{
sb.Append(result[i].ToString("X2"));
}
// And return it
return sb.ToString();
}
catch (ArgumentNullException ane)
{
//If something occurred during serialization,
//this method is called with a null argument.
Console.WriteLine("Hash has not been generated.");
return null;
}
如您所见,MD5CryptoServiceProvider
类需要一个byte
数组作为输入。它不直接接受对象。您从中获得的结果不是我们想要的string
,而是一个byte
数组。因此,我添加了从byte
数组到十六进制的转换。转换是通过使用Byte.ToString()
方法完成的。该方法接受一个格式字符串作为输入。这里的“X2
”意味着每个字节都被转换为一个两个字符的字符串序列(例如,01011100 => 5C 或 00000111 => 07)。
现在仍然存在如何将对象转换为byte
数组的问题。我们知道我们的对象是可序列化的。因此,我们可以将其序列化到内存中(使用MemoryStream
和BinaryFormatter
),然后从内存中获取所需的byte
数组。由于整个过程应该是线程安全的,因此我们锁定对象的序列化。
private static readonly Object locker = new Object();
private static byte[] ObjectToByteArray(Object objectToSerialize)
{
MemoryStream fs = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
try
{
//Here's the core functionality! One Line!
//To be thread-safe we lock the object
lock (locker)
{
formatter.Serialize(fs, objectToSerialize);
}
return fs.ToArray();
}
catch (SerializationException se)
{
Console.WriteLine("Error occurred during serialization. Message: " +
se.Message);
return null;
}
finally
{
fs.Close();
}
}
结论
生成MD5哈希可能很有用,如果您需要一个双方都可以执行的过程来确保对象序列化/反序列化的唯一性和不变性。对我来说,最困难的部分是如何将对象转换为byte
数组以及如何将byte
数组转换为十六进制String
。使用GUID也是一种选择。但是GUID是在对象初始化时创建的,并且消费者无法“重新创建”GUID来确保对象没有发生任何更改。他只知道他收到了与生产者创建的对象相同的对象。
我没有做的是所有安全问题。仅使用MD5哈希不足以保证可靠性。如果需要强大的安全性,请提供RSA加密通道或其他加密方法。
参考文献
历史
- V1.2 -- 2008年7月28日 -- 经过一些讨论后重构了文章
- V1.1 -- 2008年7月25日 -- 根据Adam Tibi的帖子添加了一些修改
- V1.0 -- 2007年11月15日 -- 文章初版