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

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

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.25/5 (4投票s)

2008年10月27日

CPOL

11分钟阅读

viewsIcon

29432

downloadIcon

416

带错误处理和完整 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_md5xp_md5W。建议包含另一个函数 __GetXpVersion,以便 SQL Server 服务使用它来检索库版本,并包含在错误消息中。

  • xp_md5 接受文本或二进制变量、列值作为输入,并返回其 MD5 摘要,为一个 32 个字符的十六进制字符串。此函数将文本视为 ANSI,并以 ANSI 字符字符串的形式返回摘要。
  • xp_md5W 接受文本或二进制变量、列值作为输入,并返回其 MD5 摘要,为一个 32 个字符的十六进制字符串。此函数将文本视为 Unicode,并以 Unicode 字符字符串的形式返回摘要。

除了原生的 Unicode 支持(与 ANSI 相反)之外,这两个函数是相同的。它们都接受一个必需参数和两个可选参数,如下表所示。

名称 类型 输入或输出 必需 注释
明文 CHARVARCHARTEXTBINARY In 1
明文长度 长整数 In 2
摘要 CHAR (32)NCHAR (32) 输出 3

注释

  1. 实际上,任何文本,包括定长和变长字符字符串、图像和二进制格式都是允许的。只要能放入 SQL Server 变量中,您就可以对其进行摘要。明文是传递给任何加密算法的文本的技术术语。
  2. 无论您将什么类型的数据传递给函数,ODS 库都会以字节为单位报告长度。这使得处理二进制对象和 Unicode 文本变得异常简单,因为长度可以传递给核心 MD5 算法。由于 MD5 摘要算法处理的是字节,因此它不受二进制数据中嵌入的空字符或完全由 ASCII 字符组成的 Unicode 字符串的影响。这也意味着您可以传递长度 -1,这会告诉扩展存储过程使用库报告的第一个参数的长度。
  3. MD5 摘要的长度始终是 16 字节,它们始终转换为 32 个字符的十六进制字符串。它们可以存储在 CHAR (32) 列(用于 ANSI)或 NCHAR (32) 列(用于 Unicode)中。如果您省略第三个参数,函数将返回一个包含 MD5 摘要的单行单列表。

在使用扩展存储过程之前,必须将其安装到 SQL Server 中,并在Master数据库中注册。

  1. 将库xp_md5.dll复制到您的 Microsoft SQL Serverbinaries目录。对于 Microsoft SQL Server 的默认安装,这通常是C:\Program Files\Microsoft SQL Server\MSSQL\Binn\
  2. 修改 T-SQL 脚本xm_md5.sql,该脚本包含在下载的\Reference目录中,在 SQL Server Query Analyzer 中打开它,确保当前数据库是Master,然后执行它。
  3. 修改 T-SQL 脚本ww_md5.sqlww_md5W.sql,它们也位于\Reference目录中,并将它们安装到Master数据库或需要 MD5 摘要函数的各个数据库中。
  4. 像使用任何内置函数或常规用户定义函数一样使用函数[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,而不是 ZeroMemorymemset,因为与后两者不同,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_paramsetoutputsrv_describe 都期望值为 32(以 HEX_CHAR_HASH_SIZE 表示的摘要中的字符数),作为数据块的大小。因此,似乎有可能,尽管不明智,只为消息摘要分配 32 个字符的空间,而不使用终止空字符。

Unicode

ANSI 和 Unicode 版本存储过程之间几乎没有区别。但是,有两个区别,而且两者都很关键。

  1. 摘要数组 szHash 的类型为 wchar_t
  2. 数据长度(必须计算两次)为 HEX_CHAR_HASH_SIZE * sizeof ( wchar_t )

数据长度问题困扰了我。第一次运行 Unicode 函数时,我只收到了摘要的一半。糟糕!

大多数时候,Visual C++ 编译器都能做得非常好,但在它失败时,结果几乎肯定是缓冲区溢出。我不仅在堆上留下了我的一半摘要,还把堆搞得一团糟。幸运的是,调试器非常清晰地向我指出了这一点。我很幸运;有很多情况下,即使在调试模式下,我也从未收到过这样的警告。

历史

  • 2008年10月27日星期一 是在 The Code Project 的首次发布。
© . All rights reserved.