65.9K
CodeProject 正在变化。 阅读更多。
Home

通过C#对象生成MD5哈希

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.56/5 (12投票s)

2007年11月15日

Apache

5分钟阅读

viewsIcon

234525

downloadIcon

4227

本文介绍如何为通用的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),它会为您执行“魔法”。将该类包含到您的项目中,然后像这样使用它:

要使用该类(作为发布者),您需要执行以下操作:

  1. 将对象标记为Serializable()。将所有不应序列化的变量标记为NonSerializable()
  2. 调用static 方法MD5HashGenerator.generateKey(Object sourceObject)。您将获得该对象的MD5哈希,作为String
  3. 序列化对象,发布/存储它以及哈希。

如果您是接收者,那么:

  1. 反序列化收到的对象。
  2. 在反序列化对象上调用static 方法MD5HashGenerator.generateKey(Object sourceObject)
  3. 比较哈希。

示例

我们想要序列化一个包含stringint 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 数组的问题。我们知道我们的对象是可序列化的。因此,我们可以将其序列化到内存中(使用MemoryStreamBinaryFormatter),然后从内存中获取所需的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日 -- 文章初版
© . All rights reserved.