Microsoft SQL Server 2000 的健壮 MD5 摘要扩展存储过程






3.25/5 (4投票s)
带错误处理和完整 Unicode 支持的 MD5 摘要 XSP,功能全面。
引言
在开发一个小型应用程序的过程中,我预计在未来几年内会对其进行扩展,我需要计算并存储一些 MD5 摘要字符串到 SQL Server 2000 数据库中。当我寻找可用的代码时,以免自己重复发明我认为肯定存在的轮子,我发现了一个关于 MS SQL Server v7.0 扩展存储过程的教程,它几乎满足了我的需求。我已经努力将代码升级到支持 SQL Server 2000,并支持输入输出的 Unicode 文本,我决定应该通过在此处发布更新来回馈社区。
背景
虽然您可以将 MD5 摘要代码放在较高层,或者甚至(难以置信!)将算法实现为一个常规的用户定义函数,但一个真正健壮的实现,能够高效地处理长字符串、二进制对象(如图像)甚至整个文件,对 CPU 的要求非常高。这类应用程序需要一个编写良好的 C 程序的速度。由于我已有一个稳定、经过验证的纯 ANSI C 实现,我认为将其移植到 T-SQL 将是浪费。即使可以做到,结果也会缓慢且丑陋,并严重限制任何使用它的应用程序的可伸缩性。
扩展存储过程旨在解决这类问题,因为它们允许您通过将 CPU 密集型算法集成到 SQL Server 本身来在数据库的最低架构层(表和视图)上运行它们。
扩展存储过程通过 Open Data Services API 与 SQL Server 通信,该 API 使用一系列回调到运行中的 SQL Server 服务,以识别和获取函数所需的输入参数,返回结果,并报告错误,如缺少或无效的参数。
ODS 接口非常灵活。您可以向函数传递几乎任何内容,也可以从中获取几乎任何内容,包括整个表。
使用代码
扩展存储过程实现为标准的 Windows DLL,SQL Server 会根据需要通过 LoadLibrary
加载它们。该 DLL 导出两个函数:xp_md5
和 xp_md5W
。建议包含另一个函数 __GetXpVersion
,以便 SQL Server 服务使用它来检索库版本,并包含在错误消息中。
xp_md5
接受文本或二进制变量、列值作为输入,并返回其 MD5 摘要,为一个 32 个字符的十六进制字符串。此函数将文本视为 ANSI,并以 ANSI 字符字符串的形式返回摘要。xp_md5W
接受文本或二进制变量、列值作为输入,并返回其 MD5 摘要,为一个 32 个字符的十六进制字符串。此函数将文本视为 Unicode,并以 Unicode 字符字符串的形式返回摘要。
除了原生的 Unicode 支持(与 ANSI 相反)之外,这两个函数是相同的。它们都接受一个必需参数和两个可选参数,如下表所示。
名称 | 类型 | 输入或输出 | 必需 | 注释 |
明文 | CHAR 、VARCHAR 、TEXT 或 BINARY |
In | 是 | 1 |
明文长度 | 长整数 | In | 否 | 2 |
摘要 | CHAR (32) 或 NCHAR (32) |
输出 | 是 | 3 |
注释
- 实际上,任何文本,包括定长和变长字符字符串、图像和二进制格式都是允许的。只要能放入 SQL Server 变量中,您就可以对其进行摘要。明文是传递给任何加密算法的文本的技术术语。
- 无论您将什么类型的数据传递给函数,ODS 库都会以字节为单位报告长度。这使得处理二进制对象和 Unicode 文本变得异常简单,因为长度可以传递给核心 MD5 算法。由于 MD5 摘要算法处理的是字节,因此它不受二进制数据中嵌入的空字符或完全由 ASCII 字符组成的 Unicode 字符串的影响。这也意味着您可以传递长度 -1,这会告诉扩展存储过程使用库报告的第一个参数的长度。
- MD5 摘要的长度始终是 16 字节,它们始终转换为 32 个字符的十六进制字符串。它们可以存储在
CHAR (32)
列(用于 ANSI)或NCHAR (32)
列(用于 Unicode)中。如果您省略第三个参数,函数将返回一个包含 MD5 摘要的单行单列表。
在使用扩展存储过程之前,必须将其安装到 SQL Server 中,并在Master数据库中注册。
- 将库xp_md5.dll复制到您的 Microsoft SQL Serverbinaries目录。对于 Microsoft SQL Server 的默认安装,这通常是C:\Program Files\Microsoft SQL Server\MSSQL\Binn\。
- 修改 T-SQL 脚本xm_md5.sql,该脚本包含在下载的\Reference目录中,在 SQL Server Query Analyzer 中打开它,确保当前数据库是Master,然后执行它。
- 修改 T-SQL 脚本ww_md5.sql和ww_md5W.sql,它们也位于\Reference目录中,并将它们安装到Master数据库或需要 MD5 摘要函数的各个数据库中。
- 像使用任何内置函数或常规用户定义函数一样使用函数
[dbo].[ww_md5]
和dbo].[ww_md5W]
。您可以在计算列、视图、查询和其他 T-SQL 脚本中使用它们。
以下简单的 T-SQL 脚本计算 Unicode 字符串 "Hello, World!" 的 MD5 摘要,并将结果作为单单元格表返回。
declare @data nchar (32)
declare @text nvarchar (255)
SELECT [MyPlaceMassage].[dbo].[ww_md5W]( 'Hello, world!' )
上述示例在Unicode_Hello.sql中,该文件包含在\Reference目录中。在 Query Analyzer 中连接到您的数据库,打开文件,然后按 F5。
一个配套脚本ANSI_Hello.sql计算相同字符串(以 ANSI 文本呈现)的 MD5 摘要。如果您同时运行这两个脚本,您会发现它们产生不同的字符串。
ANSI 字符串 'Hello, world!' |
6cd3556deb0da54bca060b4c39479839
|
Unicode 字符串 'Hello, world!' |
d227f77fc6c5c3969a0af57ce1789144
|
关注点
与大多数 C 函数不同,扩展存储过程的原型(如 C 头文件中可能出现的那样)对普通程序员来说并没有多大用处。这意味着您必须找到其他方法来记录参数。我选择导出用于安装辅助用户定义函数的 T-SQL 脚本,这些函数使这两个扩展 SP 可供数据库使用。对于更复杂的存储过程,这些 UDF 将是存放文档的理想位置,因为它们可以从 Query Analyzer 和 Enterprise Manager 中轻松访问。
我完全省略了头文件,并且打算省略.LIB文件,因为您永远不会从常规 C 或 C++ 程序链接到此 DLL。如果此库有一个,它将列出每个导出函数的相同签名,因为这些是由 ODS API 决定的。这是因为 ODS 库opends60.dll代表用户代码调用所有扩展存储过程。
您的函数通过类似于 COM 和其他支持后期绑定的接口使用的方法回调到opends60.dll。与这类接口通常的情况一样,您必须将接口提供的数据复制到自己的工作存储区,然后才能进行传递。因此,扩展存储过程的最外层,也是它与普通 C 代码不同的唯一方面,是获取参数并将数据返回给接口的代码。
仅仅因为调用者应该给我们至少一个,最多三个参数,并不意味着我们可以假设他这样做了。因此,第一项任务是从接口获取参数计数并进行测试。
int nArgs = srv_rpcparams ( pSrvProc ) ;
if ( nArgs > 0 )
由于所有数据绑定都是后期的,并且两个过程都接受任意大的数据块,因此下一项任务是使用空指针调用库,以获取要处理的数据的大小和类型。
srv_paraminfo (
pSrvProc ,
PARAM_ORDINAL_1 ,
&cType ,
&uMaxLen ,
&uLen ,
NULL ,
&fNull ) ;
// Get info about param 1 (type & size), but not data.
if ( IsValidPlaintextType ( cType ) == FALSE )
{
srv_sendmsg (
pSrvProc ,
SRV_MSG_ERROR ,
XP_MD5_ARG1_INV_TYPE ,
SRV_INFO ,
XP_STATE_1 ,
NULL ,
0 ,
( DBUSMALLINT ) __LINE__ ,
"Extended Stored Procedure xp_md5: Parameter 1 "
"must be a Character, Unicode Character, or binary type." ,
SRV_NULLTERM ) ;
srv_senddone (
pSrvProc ,
SRV_DONE_ERROR ,
0 ,
0 ) ;
return FAIL ;
} // End of if ( IsValidPlaintextType ( cType ) == FALSE )
函数 IsValidPlaintextType
是一个常规 C 函数,它评估接口提供的数据的类型。
/*******************************************************************************
Name: IsValidPlaintextType
Synopsis: This function is used internally to evaluate the type of the
paramter that is being offered as plaintext.
Arguments: BYTE pbytType = Parameter type returned by Extended Stored
Procedure API function srv_paraminfo.
Revision History
Date Version Author Synopsis
---------- ---------- ------ -----------------------------------------------
2008/10/26 1, 0, 0, 7 DAG/WW This is the initial version of this function.
*******************************************************************************/
BOOL _cdecl IsValidPlaintextType ( BYTE pbytType )
{
if ( pbytType == SRVVARCHAR )
return TRUE ;
if ( pbytType == SRVCHAR )
return TRUE ;
if ( pbytType == SRVTEXT )
return TRUE ;
if ( pbytType == SRVNTEXT )
return TRUE ;
if ( pbytType == SRVBIGCHAR )
return TRUE ;
if ( pbytType == SRVNVARCHAR )
return TRUE ;
if ( pbytType == SRVNCHAR )
return TRUE ;
if ( pbytType == SRVBINARY )
return TRUE ;
if ( pbytType == SRVIMAGE )
return TRUE ;
if ( pbytType == SRVBIGVARBINARY )
return TRUE ;
if ( pbytType == SRVBIGVARCHAR )
return TRUE ;
if ( pbytType == SRVBIGBINARY )
return TRUE ;
return FALSE ;
} // End of function IsValidPlaintextType
此函数使用的符号常量定义在srv.h中。如果您从头开始构建此项目,请确保将C:\Program Files\Microsoft SQL Server\80\Tools\DevTools\Include移到您的包含路径列表的顶部附近。否则,您可能会获得 Visual Studio 6 附带的旧版srv.h,该版本适用于 SQL Server 7.0,但缺少此过程中使用的许多较新接口和常量,而此过程旨在与 SQL Server 2000 一起使用。
我将常量复制到一个 Excel 工作簿srv_constants.xls中,该工作簿包含在\Reference目录中,因为它以十六进制(来自头文件)、十进制甚至二进制格式表示常量值。二进制版本的目的是识别我可能用来简化 IsValidPlaintextType
并使其稍快一些的位模式。唉,我没有发现任何。
在确定了输入数据的类型并确认其可接受后,下一步是为数据分配缓冲区。这是通过调用 malloc
,然后调用 SecureZeroMemory
来完成的,如下所示。
psize = ( uLen + TRAILING_NULL_ALLOWANCE ) ; // Compute requird size.
pData = ( BYTE* ) malloc ( psize ) ; // Allocate buffer from heap.
SecureZeroMemory ( pData , psize ) ; // Zero the input buffer before use.
或者,您可以调用 HeapAlloc
并为其 dwFlags
参数指定 HEAP_ZERO_MEMORY
。但是,由于它源自的原始版本以及我所有的其他 MD5 包装函数都分两步执行分配,如这里所示,我选择与它们保持一致。请注意使用了 SecureZeroMemory
,而不是 ZeroMemory
或 memset
,因为与后两者不同,SecureZeroMemory
的编写方式可以防止优化器将其优化掉。这对于安全而言很重要,因为加密函数使用的所有内存都必须在使用前和使用后进行清除,以防止意外的信息泄露。
现在我们有了一个足够大的缓冲区来存储数据,我们再次调用 srv_paraminfo
,传递一个指向我们整洁的空缓冲区的指针及其大小(以字节为单位)。
srv_paraminfo (
pSrvProc ,
PARAM_ORDINAL_1 ,
&cType ,
&uMaxLen ,
&uLen ,
pData ,
&fNull ) ;
接下来,我们再次调用,期望一个整数,除非它大于零,否则我们忽略它。
if ( nArgs > 1 )
{
LONG nInputLen ;
srv_paraminfo (
pSrvProc ,
PARAM_ORDINAL_2 ,
&cType ,
&uMaxLen ,
&uLen ,
( BYTE* ) &nInputLen ,
&fNull ) ;
if ( IsValidPlaintextLen ( cType ) )
{
if ( nInputLen >= 0 )
{
uDataLen = ( ULONG ) nInputLen ;
}
}
else
IsValidPlaintextLen
类似于 IsValidPlaintextType
,但它查找的是整数。
终于到了对文本进行摘要的时候了。
MD5String ( pData , uDataLen , szHash ) ;
szHash
是一个 33 个字符的数组(32 个用于摘要,加上强制性的终止空字符,使其成为有效的 C 字符串)。它会被返回给调用者。
由于我们已完成对 pData
的使用,并且其内容是敏感的,因此我们调用 SecureZeroMemory
来清除它,然后调用 free
来释放它。
SecureZeroMemory ( pData , psize ) ;
free ( pData ) ;
与获取 pData
相反,清除和释放它没有捷径,因为 HeapFree
没有选项告诉 Windows 在释放内存之前先清除它。
鉴于此,而且我们是从 malloc
获取的,我们最好调用 free
。
无论如何,我们将使用 HeapFree
,直接或间接,因为 free
在后台就是这样做的。
如果参数计数为 3,用 FILL_CALLERS_BUFFER
表示,我们调用 srv_paramsetoutput
返回指向哈希的指针。
if ( nArgs > FILL_CALLERS_BUFFER )
{
// Caller provided us with a buffer.
if ( srv_paramsetoutput (
pSrvProc ,
PARAM_ORDINAL_3 ,
( BYTE* ) szHash ,
HEX_CHAR_HASH_SIZE ,
FALSE ) == FAIL )
请注意,szHash
被转换为指向 BYTE
的指针,尽管它实际上是一个 unsigned char
类型的数组。无论您返回什么类型的数据,指针始终是指向字节的指针,长度始终以字节为单位指定。
如果调用者未传递 out
参数的指针,我们必须调用 srv_describe
一次,然后调用 srv_sendrow
,将我们的摘要作为单行单列表返回。
{ // Caller expects a row.
// Send the first, and only, column.
if ( srv_describe (
pSrvProc ,
1 ,
"MD5" ,
SRV_NULLTERM ,
SRVNCHAR ,
HEX_CHAR_HASH_SIZE ,
SRVNCHAR ,
HEX_CHAR_HASH_SIZE ,
( void* ) szHash ) == SRV_DESCRIBE_ERROR )
{
srv_sendmsg (
pSrvProc ,
SRV_MSG_ERROR ,
XP_MD5_SENDCOL_ERROR ,
SRV_INFO ,
XP_STATE_1 ,
NULL ,
0 ,
( DBUSMALLINT ) __LINE__ ,
"Extended Stored Procedure xp_md5W: An error was encountered "
"while returning the message digest as column 1 of row 1." ,
SRV_NULLTERM ) ;
srv_senddone (
pSrvProc ,
SRV_DONE_ERROR ,
0 ,
0 ) ;
return FAIL ;
}
// Send the first, and only, row.
if ( srv_sendrow ( pSrvProc ) == FAIL )
值得注意的是,srv_paramsetoutput
和 srv_describe
都期望值为 32(以 HEX_CHAR_HASH_SIZE
表示的摘要中的字符数),作为数据块的大小。因此,似乎有可能,尽管不明智,只为消息摘要分配 32 个字符的空间,而不使用终止空字符。
Unicode
ANSI 和 Unicode 版本存储过程之间几乎没有区别。但是,有两个区别,而且两者都很关键。
- 摘要数组
szHash
的类型为wchar_t
。 - 数据长度(必须计算两次)为
HEX_CHAR_HASH_SIZE * sizeof ( wchar_t )
。
数据长度问题困扰了我。第一次运行 Unicode 函数时,我只收到了摘要的一半。糟糕!
大多数时候,Visual C++ 编译器都能做得非常好,但在它失败时,结果几乎肯定是缓冲区溢出。我不仅在堆上留下了我的一半摘要,还把堆搞得一团糟。幸运的是,调试器非常清晰地向我指出了这一点。我很幸运;有很多情况下,即使在调试模式下,我也从未收到过这样的警告。
历史
- 2008年10月27日星期一 是在 The Code Project 的首次发布。