密码生成器功能

JGA*_*JGA 4 sql-server functions password

如果无法在函数中使用非确定性函数,那么在 SQL Server 中创建密码生成器用户定义函数函数的最佳方法是什么?

我想创建一个函数,该函数使用给定字符串中的字符返回随机生成的 varchar(10),如下所示:“1234567890QWERTYUIOPASDFGHJKLZXCVBNM”

我有自己的想法,但不是很自然的解决方案。

该功能背后的想法是,如果有人需要重置密码,我可以执行以下操作:

UPDATE Users SET Password = dbo.GeneratePassword() WHERE UserID = @UserID
Run Code Online (Sandbox Code Playgroud)

一个函数比单独的预分配密码表更容易部署和重用。

Sol*_*zky 8

您有几个选项(TL;DR -> 工作功能接近选项 #2 的末尾,但也请参阅选项 #3 之后的更新部分!):

  1. 如果您只接受十六进制值(0 - 9 和 A - F),您可以调用SQL Server 2008 中引入的CRYPT_GEN_RANDOM

    SELECT CRYPT_GEN_RANDOM(5) AS [Hex],
           CONVERT(VARCHAR(20), CRYPT_GEN_RANDOM(5), 2) AS [HexStringWithout0x],
           CONVERT(VARCHAR(20), CRYPT_GEN_RANDOM(10)) AS [Translated-ASCII],
           CONVERT(NVARCHAR(20), CRYPT_GEN_RANDOM(20)) AS [Translated-UCS2orUTF16]
    
    Run Code Online (Sandbox Code Playgroud)

    返回:

    Hex             HexStringWithout0x    Translated-ASCII    Translated-UCS2orUTF16
    0x4F7D9ABBC4    0ECF378A7A            ¿"bü<ݱØï           ??????????
    
    Run Code Online (Sandbox Code Playgroud)

    如您所见,您需要使用函数2上的格式选项,CONVERT这样它就不会为您提供虽然更加多样化但可能更难输入的字符,并且您不会获得领先的0x. 但是您也可以看到,即使只有一行,多次调用也会得到不同的返回值(就像NEWID())。

    请注意传递给CRYPT_GEN_RANDOM函数的“长度”值。由于一个十六进制字节是 2 个字符,因此您只需要生成一个 5 字节的值。的CONVERT(VARCHAR...,因为每个十六进制字节成为一个字符使用的10“长度”。并且CONVERT(NVARCHAR...使用 20 的“长度”,因为每 2 个字节将成为一个字符(在大多数情况下,在默认排序规则不以结尾的数据库中运行时_SC)。

  2. 您始终可以创建自己的算法并传入CRYPT_GEN_RANDOM随机方面的值。并且,如果您可以将您的算法构建为单一的SELECT,以便可以在内联表值函数/iTVF(通常使用 CTE)中完成,那么您在技术上可以使用非确定性函数(至少有效),因为函数调用将传入表达式而不是该表达式的结果,就像多语句 TVF 和标量 UDF 一样:

    USE [tempdb];
    GO
    
    CREATE FUNCTION dbo.GeneratePassword_InlineTVF(@RandomValue VARBINARY(10))
    RETURNS TABLE
    AS RETURN
      SELECT CONVERT(VARCHAR(20), @RandomValue, 2) +
             '~~' +
             CONVERT(VARCHAR(20), @RandomValue, 2) AS [HowRandomAmI?];
    GO
    
    CREATE FUNCTION dbo.GeneratePassword_MultistatementTVF(@RandomValue VARBINARY(10))
    RETURNS @Result TABLE ([HowRandomAmI?] VARCHAR(100))
    AS
    BEGIN
      INSERT INTO @Result ([HowRandomAmI?])
             VALUES (CONVERT(VARCHAR(20), @RandomValue, 2) +
                     '~~' +
                     CONVERT(VARCHAR(20), @RandomValue, 2));
      RETURN;
    END;
    GO
    
    CREATE FUNCTION dbo.GeneratePassword_ScalarUDF(@RandomValue VARBINARY(10))
    RETURNS VARCHAR(100)
    AS
    BEGIN
      RETURN CONVERT(VARCHAR(20), @RandomValue, 2) +
             '~~' +
             CONVERT(VARCHAR(20), @RandomValue, 2);
    END;
    GO
    
    Run Code Online (Sandbox Code Playgroud)

    然后运行测试:

    SELECT * FROM tempdb.dbo.GeneratePassword_InlineTVF(CRYPT_GEN_RANDOM(10));
    -- 27169EEC2B9CF7CC2731~~E9A95F49060BD41C0FF6
    
    SELECT * FROM tempdb.dbo.GeneratePassword_MultistatementTVF(CRYPT_GEN_RANDOM(10));
    -- 68EBC132814EA78602DE~~68EBC132814EA78602DE
    
    SELECT tempdb.dbo.GeneratePassword_ScalarUDF(CRYPT_GEN_RANDOM(10)) AS [HowRandomAmI?];
    -- 702DAF1C441C42FFBF5F~~702DAF1C441C42FFBF5F
    
    Run Code Online (Sandbox Code Playgroud)

    只是为了确定 iTVF 中的多行:

    SELECT TOP 5 rnd.[HowRandomAmI?]
    FROM tempdb.dbo.GeneratePassword_InlineTVF(CRYPT_GEN_RANDOM(10)) rnd
    CROSS JOIN [master].[sys].[objects];
    
    Run Code Online (Sandbox Code Playgroud)

    返回:

    HowRandomAmI?
    ---------------
    1D57F0ABFDE44BCAED00~~AD57E9E2FF01768BB86F
    264674BE1C9ABBC1572E~~6C75CD4D472935FDFA40
    CB54CC6BB5F31F42FDEA~~B3EC7061027FCD36C9AC
    44525355FC15655C1F4D~~4DDF874CD06BC4F2D7A4
    0E51B6364F193F588C93~~08E2ED40ED9752267EF7
    
    Run Code Online (Sandbox Code Playgroud)

    将所有这些信息付诸实践,我们可以对原始请求执行以下操作:

    CREATE FUNCTION dbo.GeneratePassword_Real(@RandomValue VARBINARY(30))
    RETURNS TABLE
    WITH SCHEMABINDING
    AS RETURN
      WITH base(item) AS
      (
        SELECT NULL UNION ALL SELECT NULL UNION ALL SELECT NULL UNION ALL
        SELECT NULL UNION ALL SELECT NULL UNION ALL SELECT NULL
      ), items(item) AS
      (
        SELECT NULL
        FROM   base b1
        CROSS JOIN base b2
      )
      SELECT (
         SELECT TOP (LEN(@RandomValue))
                SUBSTRING('1234567890QWERTYUIOPASDFGHJKLZXCVBNM',
                          (CONVERT(TINYINT, SUBSTRING(@RandomValue, 1, 1)) % 36) + 1,
                          1) AS [text()]
         FROM   items
         FOR XML PATH('')
      ) AS [RandomPassword];
    
    Run Code Online (Sandbox Code Playgroud)

    然后执行如下:

    DECLARE @PasswordLength INT = 10;
    
    SELECT * FROM tempdb.dbo.GeneratePassword_Real(CRYPT_GEN_RANDOM(@PasswordLength));
    
    Run Code Online (Sandbox Code Playgroud)

    注意:请参阅下面与为多行运行此相关的第 2 项!

  3. 您可以创建一个可以访问随机化功能的 SQLCLR 函数。这里的主要缺点是标量函数比 iTVF 更有可能缓存其返回值。


更新

  1. 鉴于问题更新中新提供的使用此函数的上下文:

    该功能背后的想法是,如果有人需要重置密码,我可以执行以下操作:

    UPDATE Users SET Password = dbo.GeneratePassword() WHERE UserID = @UserID
    
    Run Code Online (Sandbox Code Playgroud)

    我们可以解决对此答案的评论中表达的以下问题:

    • 需要将参数传递给具有随机值的函数。这强制要求知道一种创建随机字符串的方法才能使用该函数......

      真的。诚然,这有点不方便。但是,这并不否定这种方法解决手头问题的能力。它只需要一些关于如何正确使用此功能的评论/文档。

    • ...该函数无法在另一个函数内执行,因为不允许您使用非确定性函数在容器函数内生成随机值。

      真的。但是没有必要在另一个函数中调用这个函数。这不需要是标量函数即可用于UPDATE

      USE [tempdb];
      CREATE TABLE #Users (UserID INT NOT NULL PRIMARY KEY, [Password] VARCHAR(20));
      INSERT INTO #Users (UserID, [Password]) VALUES (1, 'a'), (2, 'b');
      
      DECLARE @PasswordLength INT = 10;
      
      UPDATE tmp
      SET    tmp.[Password] = pass.RandomPassword
      FROM   #Users tmp
      CROSS APPLY dbo.GeneratePassword_Real(CRYPT_GEN_RANDOM(@PasswordLength)) pass
      WHERE  tmp.UserID = 1;
      
      SELECT * FROM #Users;
      
      Run Code Online (Sandbox Code Playgroud)
  2. 关于处理多行(请参阅上面选项 2 中的多行测试),任何类型的函数(至少是用户创建的)——标量 UDF、多语句 TVF、iTVF、SQLCLR UDF 或 SQLCLR TVF——都不是保证每行运行!您可能需要在如何说服查询优化器缓存结果方面发挥创意,但可能会出现没有任何效果的情况。以下示例显示两次调用相同的 iTVF。传入只会@PasswordLength导致输出值被缓存。但是,当您传入一个包含当前行中的字段的公式时,它的行为可能与您期望的一样:

    DECLARE @PasswordLength INT = 10;
    SELECT pass.RandomPassword, pass2.RandomPassword
    FROM [master].[sys].[objects] sac
    CROSS APPLY tempdb.dbo.GeneratePassword_Real(CRYPT_GEN_RANDOM(@PasswordLength)) pass
    CROSS APPLY tempdb.dbo.GeneratePassword_Real(CRYPT_GEN_RANDOM((sac.[object_id] % 1)
                + @PasswordLength)) pass2
    
    Run Code Online (Sandbox Code Playgroud)

    返回:

    RandomPassword    RandomPassword
    --------------  --------------
    KX1M1NSKE7        8I1OK6TJI3
    KX1M1NSKE7        S0RA6AWKAJ
    KX1M1NSKE7        MKXV9UAB0Y
    KX1M1NSKE7        4RQ2K6O1RP
    KX1M1NSKE7        8J8PKJP7K2
    
    Run Code Online (Sandbox Code Playgroud)
  3. @BaconBits 在对该问题的评论中提出的关于最好不要存储可读密码的观点是一个有效且很好的观点。但是,情况可能会阻止该选项成为可能,或者至少在此时是可能的。