C#: 生成哈希字符串






4.75/5 (4投票s)
创建哈希并比较
引言
哈希是将值转换为通常更短的固定长度键/值的转换过程,该键/值表示原始值。几天前,我们需要使用哈希比较来通过 API 在两个系统之间同步数据(显然,使用 API 进行数据同步并不是最有效的方法,但是我们无法在源端添加任何更改)。
背景
我们当时在做什么
- 在对象 JSON 反序列化后,在我们这一端创建一个哈希字符串
- 通过唯一标识符(主键)将该哈希字符串与现有数据库行进行比较
- 如果找不到具有唯一标识符(主键)的行,则向数据库添加新行
- 如果哈希字符串不相同,则使用新值更新现有行
- 以及一些其他的同步日志过程
一切都按预期工作,直到我们重构了现有代码(更改了一些模型和属性的名称)。哈希字符串是从整个对象(包括所有值)生成的,而不是考虑特定属性。我们创建哈希字符串的方式实际上是错误的。让我们检查一些哈希字符串的示例。
哈希帮助类
这是管理哈希相关操作的实用程序类。
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)
对象到哈希字符串的过程
- 创建该对象的字节
Byte(object value)
- 从对象字节创建哈希字节
Hash(byte[] value)
- 从哈希字节创建字符串
String(byte[] hash)
多个对象的组合哈希
- 创建每个对象的字节
Byte(object value)
- 组合或求和字节
Combine(params byte[][] values)
- 从组合或求和的字节创建哈希字节
Hash(byte[] value)
- 从哈希字节创建字符串
String(byte[] hash)
或者
- 创建组合的哈希字节
Hash(params object[] values)
- 从哈希字节创建字符串
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;
}
}
这样,模型结构不参与哈希生成过程,仅考虑特定的属性值(Name
,IsActive
,CreatedDateTime
)。
在没有将新值设置为这些属性之前,哈希将保持不变。对模型的任何结构更改(名称更改,属性添加/删除等)都不会影响哈希字符串。
哈希结果
/*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);
结论
- 如果必须考虑值或仅考虑特定值进行比较,则使用数据值的哈希是最佳选择。
- 但是,如果我们需要同时比较对象结构和值,请选择整个对象的哈希。
参考文献
我多年前的第一次阅读
字节
- https://stackoverflow.com/questions/1446547/how-to-convert-an-object-to-a-byte-array-in-c-sharp
- https://stackoverflow.com/questions/33489930/ignore-non-serialized-property-in-binaryformatter-serialization
哈希字节
- 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/
组合字节
字节到字符串
- https://stackoverflow.com/questions/1300890/md5-hash-with-salt-for-keeping-password-in-db-in-c-sharp
限制
我没有考虑所有可能的糟糕情况,或者代码可能会因未经测试的输入而引发意外错误。如果有,请告诉我。
查找 Visual Studio 2017 控制台应用程序示例代码作为附件。
历史
- 2019 年 6 月 26 日:初始版本