【问题标题】:Random String generation - avoiding duplicates随机字符串生成 - 避免重复
【发布时间】:2014-02-05 06:32:33
【问题描述】:

我正在使用下面的代码来生成如下的随机密钥 TX8L1I

public string GetNewId()
        {
            string result = string.Empty;

            Random rnd = new Random();
            short codeLength = 5;
            string chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
            StringBuilder builder = new StringBuilder(codeLength);

            for (int i = 0; i < codeLength; ++i)
                builder.Append(chars[rnd.Next(chars.Length)]);

            result = string.Format("{0}{1}", "T", builder.ToString()); 

            return result;
        }

每次生成一个键时,都会在数据库上创建一条对应记录,使用生成的结果作为主键。当然,这是不安全的,因为可能会发生主键违规。

解决这个问题的正确方法是什么?我是否应该加载所有现有密钥并根据该列表验证该密钥是否已经存在,如果是,则生成另一个?

或者更有效的方法是将这个逻辑移到数据库端?但即使在数据库端,我仍然需要检查生成的随机键是否在当前表中不存在。

有什么想法吗?

谢谢

【问题讨论】:

  • ...uniqueidentifier... 类型的主键呢?
  • 密钥长度为6,以T开头有什么要求吗?
  • 有 60466176 个可能的代码。在数据库中生成它们并标记每个已使用的(按列或删除已使用的行)。然后你只需要确保“选择一个代码,将其标记为已使用,返回值”是序列化的或避免冲突 - 在大多数数据库中很容易做到。
  • 我会通过不使用随机键来完全避免整个问题,但要回答我如何不使用随机键,我需要了解您要解决的问题通过使用它们。
  • @JanneMatikainen 是的,我需要使用我提供的格式生成字符串(例如 TX8L1I)

标签: c# sql random


【解决方案1】:

将决策移至数据库。添加uniqueidentifier 类型的主键。然后它是一个简单的“即发即弃”算法。数据库决定密钥是什么。

【讨论】:

  • 我不能使用你的方法,因为我需要一个小密钥,而不是一个 Guid。
  • 好吧,我想我会使用它。这不是一个小代码,但谁在乎呢。复制粘贴是当今的规则。
【解决方案2】:

我认为你应该在你的方法之外使用Random 实例。

由于Random对象是seeded from the system clock,这意味着如果你非常快速地多次调用你的方法,它每次都会使用相同的种子,这意味着你最终会得到相同的字符串。

【讨论】:

    【解决方案3】:

    我会将逻辑移到数据库中。即使数据库仍然需要检查密钥是否存在,但此时它已经在数据库操作中,因此不会发生来回。

    另外,如果这个随机生成的密钥上有一个索引,一个简单的 if exists 将足以快速确定该密钥之前是否被使用过。

    【讨论】:

      【解决方案4】:

      您可以执行以下操作:

      生成您的字符串并将以下内容连接到它上面

      string newUniqueString = string.Format("{0}{1}", result, DateTime.Now.ToString("yyyyMMddHHmmssfffffff"));
      

      这样,您再也不会拥有相同的密钥了!

      或者使用

      var StringGuid = Guid.NewGuid();
      

      【讨论】:

      • 感谢 Pierre,但我需要使用我提供的格式生成字符串 (例如 TX8L1I)
      • 玩转new Guid(),你也可以添加参数来格式化它。
      【解决方案5】:

      你犯了Random典型罪:不应该创建Random 实例 每次他们想要生成随机值时(否则会得到 严重偏斜 分布与 许多重复)。把Random放到表单函数里:

      // Let Generator be thread safe
      private static ThreadLocal<Random> s_Generator = new ThreadLocal<Random>(
       () => new Random());
      
      public static Random Generator {
        get {
          return s_Generator.Value;
        }
      }
      
      // For repetition test. One can remove repetion test if 
      // number of generated ids << 15000
      private HashSet<String> m_UsedIds = new HashSet<String>();
      
      public string GetNewId() {
        int codeLength = 5;
        string chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
      
        while (true) {
          StringBuilder builder = new StringBuilder(codeLength);
      
          for (int i = 0; i < codeLength; ++i)
            builder.Append(chars[Generator.Next(chars.Length)]); // <- same generator for all calls
      
          result = string.Format("{0}{1}", "T", builder.ToString()); 
      
          // Test if random string in fact a repetition
          if (m_UsedIds.Contains(result))
            continue;
      
          m_UsedIds.Add(result);
      
          return result;
        }
      }
      

      对于codeLength = 5,有Math.Pow(chars.Length, codeLength) 可能 字符串 (Math.Pow(36, 5) == 60466176 = 6e7)。根据生日悖论 您可以在 2 * sqrt(possible strings) == 2 * sqrt(6e7) == 15000 附近进行第一次重复。如果没问题可以跳过重复测试,否则HashSet&lt;String&gt;可以 成为一个解决方案。

      【讨论】:

        【解决方案6】:

        我决定使用随机数是因为我不希望用户知道密钥是如何生成的(格式),因为网站是公开的,我不希望用户尝试访问键。

        您已将两个问题合并为一个真正独立的问题:

        1. 识别数据库中的一行。
        2. 具有与客户端传递和从客户端传递的标识符,该标识符与此匹配,但不可预测。

        这是一组更一般的两个问题的具体案例:

        1. 识别数据库中的一行。
        2. 有一个与客户端相匹配的标识符。

        在这种情况下,当我们不关心猜测时,最简单的处理方法就是使用相同的标识符。例如。对于由整数42 标识的数据库行,我们使用字符串"42",并且映射在一个方向上是平凡的int.Parse()int.TryParse(),在另一个方向上是平凡的.ToString() 或隐式.ToString()

        因此,这就是我们使用的模式,甚至没有考虑它;公共 ID 和数据库密钥是相同的(可能需要进行一些转换)。

        但是对于您想要防止猜测的特定情况的最佳解决方案是不是更改密钥,而是更改密钥和公共标识符之间的映射。

        首先,使用自增整数(SQL Server 中的“IDENTITY”,以及其他数据库中的各种类似概念)。

        然后,当您将密钥发送给客户端时(即在表单值中使用它或将其附加到 URI)然后将其映射为:

        private const string Seed = "this is my secret seed ¾Ÿˇʣכ ↼⊜┲◗ blah balh";
        private static string GetProtectedID(int id)
        {
          using(var sha = System.Security.Cryptography.SHA1.Create())
          {
            return string.Join("", sha.ComputeHash(Encoding.UTF8.GetBytes(id.ToString() + Seed)).Select(b => b.ToString("X2"))) + id.ToString();
          }
        }
        

        例如,如果 ID 为 123,则生成 "989178D90470D8777F77C972AF46C4DED41EF0D9123"

        现在映射回一个键:

        private static bool GetIDFromProtectedID(string str, out int id)
        {
          int chkID;
          if(int.TryParse(str.Substring(40), out chkID))
          {
            using(var sha = System.Security.Cryptography.SHA1.Create())
            {
              if(string.Join("", sha.ComputeHash(Encoding.UTF8.GetBytes(chkID.ToString() + Seed)).Select(b => b.ToString("X2"))) == str.Substring(0, 40))
              {
                id = chkID;
                return true;
              }
            }
          }
          id = 0;
          return false;
        }
        

        对于"989178D90470D8777F77C972AF46C4DED41EF0D9123",这将返回true,并将id 参数设置为123。对于"989178D90470D8777F77C972AF46C4DED41EF0D9122"(因为我试图猜测ID 来攻击你的站点)它返回false 并将id 设置为0。 (122 键的正确 ID 是 "F8AD0F55CA1B9426D18F684C4857E0C4D43984BA122",从 123 中看到这一点并不容易猜到。

        如果需要,您可以删除输出的 40 个字符中的一些以生成更小的 ID。这降低了它的安全性(攻击者可以暴力破解的 ID 更少),但对于许多用途来说仍然是合理的。

        显然,您应该为 Seed 使用与此处不同的值,以便阅读此答案的人无法使用它来预测您的 ID;换种子,换身份证。 Seed一旦设置就无法更改,除非更改系统中的每个 ID。有时这是一件好事(如果标识符永远不会具有长期价值的话),但通常情况下它是坏事(您在使用它的网站的每个页面上都只有 404d)。

        【讨论】:

        • 感谢您的回答。我真的很困惑。我了解您使用内部程序来匹配公共 ID 与数据库 ID 的方法,但是一般公共 ID 必须很短(例如 6 个字符),并且每个人都会或多或少地知道这些 ID 的外观(例如 T45RED),所以即使使用这种逻辑我只能有一个 T45RED,因此您建议的长数据库 ID 将仅用于此代码。因此用户可能会尝试猜测不同的代码(例如 T45DDX)。我不太了解您建议我的好处,抱歉,也许我遗漏了一些东西。
        • 啊。我没有看到对短公共标识符的要求。他们是要手动输入还是什么?我的答案的优点是 DB 代码很简单(它只使用 1、2、3、4...),但 ID 真的很难猜。根据您的要求,我可能会预先计算一组说 10,000 个代码并依次使用它们,直到我降至 2,000 个左右。
        • @SonerGönül 不过,它确实未能达到他们对短 ID 大小的要求。
        猜你喜欢
        • 2017-12-26
        • 2010-12-07
        • 1970-01-01
        • 2015-08-16
        • 1970-01-01
        • 2017-01-06
        • 2017-04-05
        • 1970-01-01
        相关资源
        最近更新 更多