随机字符串生成 - 避免重复

时间:2014-01-16 11:18:42

标签: c# sql random

我使用以下代码生成随机密钥,如下所示 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;
        }

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

对此有什么正确的解决方法?我是否应该加载所有现有密钥并验证密钥是否已存在,如果是,则生成另一个密钥?

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

有什么想法吗?

由于

6 个答案:

答案 0 :(得分:3)

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

答案 1 :(得分:2)

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

由于Random对象是seeded from the system clock,这意味着如果你很快地多次调用你的方法,它每次都会使用相同的种子,这意味着你将获得相同的字符串结束。

答案 2 :(得分:2)

  

我决定使用随机的是我不希望用户能够知道密钥的生成方式(格式),因为网站是公开的,我不希望用户尝试访问那把钥匙。

您已将两个问题合并为一个非常独立的问题:

  1. 识别数据库中的一行。
  2. 拥有与客户端传递的标识符,该标识符与此匹配,但不可预测。
  3. 这是一个更为一般的两个问题的具体案例:

    1. 识别数据库中的一行。
    2. 拥有一个与客户端传递的标识符,与之匹配。
    3. 在这种情况下,当我们不关心猜测时,处理它的最简单方法是使用相同的标识符。例如。对于由整数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。一旦设置而不更改系统中的每个ID,就无法更改Seed。有时候这是一件好事(如果标识符从来没有意味着具有长期价值),但通常它很糟糕(你只是在网站中使用它的每一页都是404d。)

答案 3 :(得分:1)

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

此外,如果这个随机生成的密钥有一个索引,一个简单的if存在将足够快,以确定密钥是否已被使用过。

答案 4 :(得分:1)

您可以执行以下操作:

生成您的字符串以及将以下内容连接到其上

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

这样,你再也不会拥有同样的钥匙了!

或使用

var StringGuid = Guid.NewGuid();

答案 5 :(得分:1)

您已使用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<String>可以 是一个解决方案。