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

C#: 生成哈希字符串

2019年6月26日

CPOL

3分钟阅读

viewsIcon

23784

downloadIcon

243

创建哈希并比较

引言

哈希是将值转换为通常更短的固定长度键/值的转换过程,该键/值表示原始值。几天前,我们需要使用哈希比较来通过 API 在两个系统之间同步数据(显然,使用 API 进行数据同步并不是最有效的方法,但是我们无法在源端添加任何更改)。

背景

我们当时在做什么

  1. 在对象 JSON 反序列化后,在我们这一端创建一个哈希字符串
  2. 通过唯一标识符(主键)将该哈希字符串与现有数据库行进行比较
    1. 如果找不到具有唯一标识符(主键)的行,则向数据库添加新行
    2. 如果哈希字符串不相同,则使用新值更新现有行
  3. 以及一些其他的同步日志过程

一切都按预期工作,直到我们重构了现有代码(更改了一些模型和属性的名称)。哈希字符串是从整个对象(包括所有值)生成的,而不是考虑特定属性。我们创建哈希字符串的方式实际上是错误的。让我们检查一些哈希字符串的示例。

哈希帮助类

这是管理哈希相关操作的实用程序类。

using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Cryptography;
using System.Text;

public class HashHelper
{
    /// <summary>
    /// for custom class need [Serializable]
    /// to ignore https://stackoverflow.com/questions/33489930/
    /// ignore-non-serialized-property-in-binaryformatter-serialization
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    public byte[] Byte(object value)
    {
        /*https://stackoverflow.com/questions/1446547/
          how-to-convert-an-object-to-a-byte-array-in-c-sharp*/
        using (var ms = new MemoryStream())
        {
            BinaryFormatter bf = new BinaryFormatter();
            bf.Serialize(ms, value == null ? "null" : value);
            return ms.ToArray();
        }
    }
    
    public byte[] Hash(byte[] value)
    {
        /*https://support.microsoft.com/en-za/help/307020/
          how-to-compute-and-compare-hash-values-by-using-visual-cs*/
        /*https://andrewlock.net/why-is-string-gethashcode-
          different-each-time-i-run-my-program-in-net-core*/
        byte[] result = MD5.Create().ComputeHash(value);
        return result;
    }

    public byte[] Combine(params byte[][] values)
    {
        /*https://stackoverflow.com/questions/415291/
          best-way-to-combine-two-or-more-byte-arrays-in-c-sharp*/
        byte[] rv = new byte[values.Sum(a => a.Length)];
        int offset = 0;
        foreach (byte[] array in values)
        {
            System.Buffer.BlockCopy(array, 0, rv, offset, array.Length);
            offset += array.Length;
        }
        return rv;
    }

    public string String(byte[] hash)
    {
        /*https://stackoverflow.com/questions/1300890/
          md5-hash-with-salt-for-keeping-password-in-db-in-c-sharp*/
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < hash.Length; i++)
        {
            sb.Append(hash[i].ToString("x2"));     /*do not make it X2*/
        }
        var result = sb.ToString();
        return result;
    }

    public byte[] Hash(params object[] values)
    {
        byte[][] bytes = new byte[values.Length][];
        for(int i=0; i < values.Length; i++)
        {
            bytes[i] = Byte(values[i]);
        }
        byte[] combined = Combine(bytes);
        byte[] combinedHash = Hash(combined);
        return combinedHash;
    }

    /*https://stackoverflow.com/questions/5868438/c-sharp-generate-a-random-md5-hash*/
    public string HashString(string value, Encoding encoding = null)
    {
        if (encoding == null)
        {
            encoding = Encoding.ASCII;
        }
        byte[] bytes = encoding.GetBytes(value);
        byte[] hash = Hash(bytes);
        string result = String(hash);
        return result;
    }

    public string HashString(params object[] values)
    {
        var hash = Hash(values);    /*Add more not constant properties as needed*/
        var value = String(hash);
        return value;
    }
}

注意事项

  • 使用 MD5 哈希 Hash(byte[] value)
  • 任何 null 值都被视为 'null' 字符串 Byte(object value)

对象到哈希字符串的过程

  1. 创建该对象的字节 Byte(object value)
  2. 从对象字节创建哈希字节 Hash(byte[] value)
  3. 从哈希字节创建字符串 String(byte[] hash)

多个对象的组合哈希

  1. 创建每个对象的字节 Byte(object value)
  2. 组合或求和字节 Combine(params byte[][] values)
  3. 从组合或求和的字节创建哈希字节 Hash(byte[] value)
  4. 从哈希字节创建字符串 String(byte[] hash)

或者

  1. 创建组合的哈希字节 Hash(params object[] values)
  2. 从哈希字节创建字符串 String(byte[] hash)

我们将更频繁地使用的方法

  • 创建任何字符串的哈希字符串 HashString(string value, Encoding encoding = null)
  • 创建任何对象/对象组的哈希/组合哈希字符串 HashString(params object[] values)

整个对象的哈希

数据类或模型

[Serializable]
class PeopleModel
{
    public long? Id { get; set; }
    public string Name { get; set; }
    public bool? IsActive { get; set; }
    public DateTime? CreatedDateTime { get; set; }
}

创建模型的哈希

/*9105d073ad276d742c56a049abd4ddef
 * will change if we change 
 *      1. class name
 *      2. property name
 *      3. property data type
 *      4. add/remove new property etc
 */
var peopleModelHashString = hashHelper.HashString(new PeopleModel()
{
    Id = 1,
    Name = "Anders Hejlsberg",
    IsActive = true,
    CreatedDateTime = new DateTime(1960, 12, 2)
});

重要提示

此哈希取决于对象结构和分配的值。即使我们将相同的值分配给属性,生成的哈希也不会相同,但添加了一些更改,例如

  • 类/模型名称更改
  • 属性名称更改
  • 命名空间名称更改
  • 属性数量更改(添加或删除任何属性)

到模型。并且在开发环境中,随时可能进行重构。

数据值的哈希

让我们仅使用值来创建哈希。创建一个接口 IHash

public interface IHash
{
    string HashString();
}

IHash 应用于模型,并在方法 HashString() 中使用哈希帮助器。

class People : IHash
{
    public long? Id { get; set; }         /*unique identifier, avoid it to use 
                                            in hash calculation*/
    public string Name { get; set; }
    public bool? IsActive { get; set; }
    public DateTime? CreatedDateTime { get; set; }

    public string HashString()
    {
        var value = new HashHelper().HashString
        (Name, IsActive, CreatedDateTime);    /*Add more not constant properties as needed*/
        return value;
    }
}

这样,模型结构不参与哈希生成过程,仅考虑特定的属性值(NameIsActiveCreatedDateTime)。

在没有将新值设置为这些属性之前,哈希将保持不变。对模型的任何结构更改(名称更改,属性添加/删除等)都不会影响哈希字符串。

哈希结果

/*constant: 3953fbec5b81ccca72c98655c0c4b069*/
people = new People()
{
    Id = 1,
    Name = "Dennis Ritchie",
    IsActive = false,
    CreatedDateTime = new DateTime(1941, 9, 9)
};
hashString = people.HashString();

其他测试

使用 null 对象值工作正常

string hashString;
/*constant: 47ccecfc14f9ed9eff5de591b8614077*/
var people = new People();
hashString = people.HashString();

我们将无法创建整个 People 类,因为它未使用 [Serializable]

var hashHelper = new HashHelper(); 
/*throws error as [Serializable] not been used*/ 
//var peopleHashString = hashHelper.HashString(people);

奖励:字符串哈希

创建密码/字符串哈希是很常见的。所以我们在这里有它。

/*constant: e6fb7af54c39f39507c28a86ad98a1fd*/
string name = "Dipon Roy";
string value = new HashHelper().HashString(name);

结论

  • 如果必须考虑值或仅考虑特定值进行比较,则使用数据值的哈希是最佳选择。
  • 但是,如果我们需要同时比较对象结构和值,请选择整个对象的哈希

参考文献

我多年前的第一次阅读

字节

哈希字节

组合字节

字节到字符串

限制

我没有考虑所有可能的糟糕情况,或者代码可能会因未经测试的输入而引发意外错误。如果有,请告诉我。

查找 Visual Studio 2017 控制台应用程序示例代码作为附件。

历史

  • 2019 年 6 月 26 日:初始版本
© . All rights reserved.