TSQL md5 哈希不同于 C# .NET md5

TSQL md5 hash different to C# .NET md5

我生成了一个 md5 散列如下:

DECLARE @varchar varchar(400) 

SET @varchar = 'è'

SELECT CONVERT(VARCHAR(2000), HASHBYTES( 'MD5', @varchar ), 2)

输出:

785D512BE4316D578E6650613B45E934

然而生成 MD5 散列使用:

System.Text.Encoding.UTF8.GetBytes("è")

生成:

0a35e149dbbb2d10d744bf675c7744b1

C# .NET 方法中的编码设置为 UTF8,我假设 varchar 也是 UTF8,关于我做错了什么有什么想法吗?

SQL 服务器使用 UCS-2 而不是 UTF-8 来编码字符数据。

如果您使用的是 NVarChar 字段,则以下方法可行:

System.Text.Encoding.Unicode.GetBytes("è"); // Updated per @srutzky's comments

有关 SQL 和 C# 散列的更多信息,请参阅

http://weblogs.sqlteam.com/mladenp/archive/2009/04/28/Comparing-SQL-Server-HASHBYTES-function-and-.Net-hashing.aspx

如果您正在处理 NVARCHAR / NCHAR 数据(存储为 UTF-16 Little Endian),那么您将使用 Unicode编码,不是BigEndianUnicode。在 .NET 中,UTF-16 称为 Unicode,而其他 Unicode 编码则使用它们的实际名称:UTF7、UTF8 和 UTF32。因此,Unicode 本身就是 Little Endian 而不是 BigEndianUnicode更新:请参阅末尾有关 UCS-2 和增补字符的部分。

在数据库端:

SELECT HASHBYTES('MD5', N'è') AS [HashBytesNVARCHAR]
-- FAC02CD988801F0495D35611223782CF

.NET 方面:

System.Text.Encoding.ASCII.GetBytes("è")
// D1457B72C3FB323A2671125AEF3EAB5D

System.Text.Encoding.UTF7.GetBytes("è")
// F63A0999FE759C5054613DDE20346193

System.Text.Encoding.UTF8.GetBytes("è")
// 0A35E149DBBB2D10D744BF675C7744B1

System.Text.Encoding.UTF32.GetBytes("è")
// 86D29922AC56CF022B639187828137F8

System.Text.Encoding.BigEndianUnicode.GetBytes("è")
// 407256AC97E4C5AEBCA825DEB3D2E89C

System.Text.Encoding.Unicode.GetBytes("è")  // this one matches HASHBYTES('MD5', N'è')
// FAC02CD988801F0495D35611223782CF

然而,这个问题涉及到 VARCHAR / CHAR 数据,它是 ASCII,所以事情有点复杂。

在数据库端:

SELECT HASHBYTES('MD5', 'è') AS [HashBytesVARCHAR]
-- 785D512BE4316D578E6650613B45E934

我们已经在上面看到了 .NET 方面。从这些散列值应该有两个问题:

  • 为什么 中的任何 都不匹配 HASHBYTES 值?
  • 为什么@Eric J. 的回答中链接的 "sqlteam.com" 文章显示其中三个(ASCIIUTF7UTF8)都匹配HASHBYTES 值?

有一个答案涵盖了这两个问题:代码页。在 "sqlteam" 文章中完成的测试使用了 "safe" ASCII 字符,这些字符在 0 - 127 范围内(根据 int / decimal 值),代码页之间没有变化。但是 128 - 255 范围——我们找到“è”字符的地方——是 Extended 集,它确实因代码页而异(这是有道理的,因为这是有代码的原因页数)。

现在试试:

SELECT HASHBYTES('MD5', 'è' COLLATE SQL_Latin1_General_CP1255_CI_AS) AS [HashBytes]
-- D1457B72C3FB323A2671125AEF3EAB5D

ASCII 散列值相匹配(同样,因为 "sqlteam" 文章/测试使用了 0 - 127 范围内的值,所以在使用 [=39= 时他们没有看到任何变化]).太好了,现在我们终于找到了匹配 VARCHAR / CHAR 数据的方法。还好吗?

嗯,不是真的。让我们来看看我们实际哈希的内容:

SELECT 'è' AS [TheChar],
       ASCII('è') AS [TheASCIIvalue],
       'è' COLLATE SQL_Latin1_General_CP1255_CI_AS AS [CharCP1255],
       ASCII('è' COLLATE SQL_Latin1_General_CP1255_CI_AS) AS [TheASCIIvalueCP1255];

Returns:

TheChar TheASCIIvalue   CharCP1255  TheASCIIvalueCP1255
è       232             ?           63

一个??只是为了验证,运行:

SELECT CHAR(63) AS [WhatIs63?];
-- ?

啊,所以代码页 1255 没有 è 字符,所以它被翻译成大家最喜欢的 ?。但是,为什么在使用 ASCII 编码时会匹配 .NET 中的 MD5 哈希值?难道我们实际上没有匹配 è 的散列值,而是匹配 ?:

的散列值
SELECT HASHBYTES('MD5', '?') AS [HashBytesVARCHAR]
-- 0xD1457B72C3FB323A2671125AEF3EAB5D

是的。真正的 ASCII 字符集是 只是 前 128 个字符(值 0 - 127)。正如我们刚刚看到的,è 是 232。因此,在 .NET 中使用 ASCII 编码没有多大帮助。在 T-SQL 端也没有使用 COLLATE

是否有可能在 .NET 端获得更好的编码?是的,通过使用 Encoding.GetEncoding(Int32),它允许指定代码页。可以使用以下查询发现要使用的代码页(使用列而不是文字或变量时使用 sys.columns):

SELECT sd.[collation_name],
       COLLATIONPROPERTY(sd.[collation_name], 'CodePage') AS [CodePage]
FROM   sys.databases sd
WHERE  sd.[name] = DB_NAME(); -- replace function with N'{db_name}' if not running in the DB

上面的查询returns(对我来说):

Latin1_General_100_CI_AS_SC    1252

所以,让我们试试代码页 1252:

System.Text.Encoding.GetEncoding(1252).GetBytes("è") // Matches HASHBYTES('MD5', 'è')
// 785D512BE4316D578E6650613B45E934

呜呜呜!我们有一个 VARCHAR 数据的匹配项,它使用我们默认的 SQL 服务器排序规则 :)。当然,如果数据来自数据库或字段设置为不同的排序规则,那么 GetEncoding(1252) 可能 不起作用,您将不得不使用以下命令找到实际匹配的代码页上面显示的查询(代码页用于许多归类,因此不同的归类 不一定 暗示不同的代码页)。

要查看可能的代码页值是什么,以及它们所属的文化/区域设置,请参阅代码页列表 here(列表位于 "Remarks" 部分)。


NVARCHAR / NCHAR 字段中实际存储内容相关的附加信息:

可以存储任何 UTF-16 字符(2 或 4 个字节),尽管内置函数的默认行为假定所有字符都是 UCS-2(每个 2 个字节),这是一个子集UTF-16 的。从 SQL Server 2012 开始,可以访问一组 Windows 支持称为补充字符的 4 字节字符的排序规则。使用这些以 _SC 结尾的 Windows 排序规则之一,无论是为列指定还是直接在查询中指定,都将允许内置函数正确处理 4 字节字符。

-- The database's collation is set to: SQL_Latin1_General_CP1_CI_AS
SELECT  N'' AS [SupplementaryCharacter],
        LEN(N'') AS [LEN],
        DATALENGTH(N'') AS [DATALENGTH],
        UNICODE(N'') AS [UNICODE],
        LEFT(N'', 1) AS [LEFT],
        HASHBYTES('MD5', N'') AS [HASHBYTES];

SELECT  N'' AS [SupplementaryCharacter],
        LEN(N'' COLLATE Latin1_General_100_CI_AS_SC) AS [LEN],
        DATALENGTH(N'' COLLATE Latin1_General_100_CI_AS_SC) AS [DATALENGTH],
        UNICODE(N'' COLLATE Latin1_General_100_CI_AS_SC) AS [UNICODE],
        LEFT(N'' COLLATE Latin1_General_100_CI_AS_SC, 1) AS [LEFT],
        HASHBYTES('MD5', N'' COLLATE Latin1_General_100_CI_AS_SC) AS [HASHBYTES];

Returns:

SupplementaryChar   LEN   DATALENGTH   UNICODE   LEFT   HASHBYTES
                  2     4             55393    �     0x7A04F43DA81E3150F539C6B99F4B8FA9
                  1     4            165739         0x7A04F43DA81E3150F539C6B99F4B8FA9

如您所见,DATALENGTHHASHBYTES 均未受到影响。有关详细信息,请参阅 Collation and Unicode Support 的 MSDN 页面(特别是 "Supplementary Characters" 部分)。

我遇到了同样的问题,正如@srutzky 评论的那样,可能发生的情况是我没有在查询之前使用大写字母 N,而我得到的是 8 位扩展 ASCII ( VARCHAR /不以大写 N 为前缀的字符串)而不是 16 位 UTF-16 Little Endian(NVARCHAR / 以大写 N 为前缀的字符串)

{Id, UserName, PasswordString, PasswordHashed}

如果你这样做:

SELECT TOP 1 CONVERT(char(32),HashBytes('MD5', 'abc123'),2) FROM [Users]

它将输出: E99A18C428CB38D5F260853678922E03

但是如果你这样做,密码相同 ('abc123'):

SELECT CONVERT(char(32),HashBytes('MD5', [PasswordString]),2) FROM [Users]

它将输出: 6E9B3A7620AAF77F362775150977EEB8

我应该做的是:

SELECT CONVERT(char(32),HashBytes('MD5', N'abc123'),2) FROM [Users]

输出相同的结果:6E9B3A7620AAF77F362775150977EEB8

sql 服务器 hashbytes 总是像 System.Text.Encoding.Unicode 在像阿拉伯波斯语这样的unicode字符上,... 如果你使用 Utf8.Unicode 或 Ascii.Unicode 你会看到差异 如果您使用 Utf8.Unicode,sql 服务器和 c# 的 return 结果将相同