如何生成随机的字母数字字符串?

1303

我该如何在C#中生成一个包含8个随机字母和数字的字符串?


2
你对字符集有什么限制吗?只能使用英文字符和0-9数字吗?可以混合大小写吗? - Eric J.
8
也许是前往https://dev59.com/MnRB5IYBdhLWcg3wET1J或https://dev59.com/C3NA5IYBdhLWcg3wBpBm。 - Jonas Elfström
9
请注意,生成密码时不应使用基于“Random”类的任何方法。因为“Random”的种子熵非常低,所以它并不真正安全。对于密码,应该使用加密伪随机数生成器(PRNG)。 - CodesInChaos
2
最好在这个问题中包含语言本地化。特别是如果你的GUI需要为中文或保加利亚语提供支持! - Peter Jamsmenson
22
这么多赞和高质量回答的内容不应该被标记为关闭。我投票支持重新开放它。 - John Coleman
显示剩余4条评论
38个回答

14
埃里克·J写的代码相当随意(很明显是6年前写的...他今天可能不会写那样的代码),甚至还存在一些问题。
与提出的某些选择不同,这个选择在密码学上是可靠的。
不真实... 密码中存在偏差(如评论中所述),bcdefgh比其他字符更可能出现(a不是因为在GetNonZeroBytes中它不会生成值为零的字节,因此a的偏差被平衡了),因此它并不真正具有密码学安全性。
这应该可以解决所有问题。
public static string GetUniqueKey(int size = 6, string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
{
    using (var crypto = new RNGCryptoServiceProvider())
    {
        var data = new byte[size];

        // If chars.Length isn't a power of 2 then there is a bias if
        // we simply use the modulus operator. The first characters of
        // chars will be more probable than the last ones.

        // buffer used if we encounter an unusable random byte. We will
        // regenerate it in this buffer
        byte[] smallBuffer = null;

        // Maximum random number that can be used without introducing a
        // bias
        int maxRandom = byte.MaxValue - ((byte.MaxValue + 1) % chars.Length);

        crypto.GetBytes(data);

        var result = new char[size];

        for (int i = 0; i < size; i++)
        {
            byte v = data[i];

            while (v > maxRandom)
            {
                if (smallBuffer == null)
                {
                    smallBuffer = new byte[1];
                }

                crypto.GetBytes(smallBuffer);
                v = smallBuffer[0];
            }

            result[i] = chars[v % chars.Length];
        }

        return new string(result);
    }
}

8
DTB的解决方案的稍微简洁一点的版本。
    var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    var random = new Random();
    var list = Enumerable.Repeat(0, 8).Select(x=>chars[random.Next(chars.Length)]);
    return string.Join("", list);

您的样式偏好可能会有所不同。


这比被接受的答案要好得多,而且更有效率。 - Wedge

8
 public static string RandomString(int length)
    {
        const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        var random = new Random();
        return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray());
    }

8

.NET Core 3.0+提供最简单和最灵活的加密安全解决方案:

如果您使用的是.NET Core 3.0或更高版本,您可以使用RandomNumberGenerator类上的新静态方法GetInt32(具有加密安全性),为给定字符集生成随机索引并轻松填充结果。

此回答提出的方法相比,这种方法要简单得多。而且它也提供了完全的灵活性,因为您可以传入任何想要的字符集。

public static string GenerateRandomString(int length, IEnumerable<char> charSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
{
    var charArray = charSet.Distinct().ToArray();
    char[] result = new char[length];
    for (int i = 0; i < length; i++)
        result[i] = charArray[RandomNumberGenerator.GetInt32(charArray.Length)];
    return new string(result);
}

用法:

string randomAlphanumericString = GenerateRandomString(length: 10);

8
我们还使用自定义字符串随机数,但我们将其实现为字符串的帮助程序,因此它提供了一些灵活性...
public static string Random(this string chars, int length = 8)
{
    var randomString = new StringBuilder();
    var random = new Random();

    for (int i = 0; i < length; i++)
        randomString.Append(chars[random.Next(chars.Length)]);

    return randomString.ToString();
}

使用方法

var random = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".Random();

或者

var random = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".Random(16);

6
另一个选项是使用Linq将随机字符聚合到StringBuilder中。
var chars = "abcdefghijklmnopqrstuvwxyz123456789".ToArray();
string pw = Enumerable.Range(0, passwordLength)
                      .Aggregate(
                          new StringBuilder(),
                          (sb, n) => sb.Append((chars[random.Next(chars.Length)])),
                          sb => sb.ToString());

6
在查看其他答案、考虑CodeInChaos的评论以及CodeInChaos仍然有偏见(尽管较少)的答案后,我认为需要一个最终的剪切和粘贴解决方案。因此,在更新我的答案时,我决定全力以赴。
请访问Bitbucket上的新Hg存储库以获取最新版本的代码:https://bitbucket.org/merarischroeder/secureswiftrandom。我建议您从https://bitbucket.org/merarischroeder/secureswiftrandom/src/6c14b874f34a3f6576b0213379ecdf0ffc7496ea/Code/Alivate.SolidSwiftRandom/SolidSwiftRandom.cs?at=default&fileviewer=file-view-default复制和粘贴代码(确保单击“原始”按钮以使其更容易复制,并确保您拥有最新版本,我认为此链接指向代码的特定版本,而不是最新版本)。
更新说明:
1.与其他答案相关-如果您知道输出的长度,则不需要StringBuilder,而使用ToCharArray会创建并填充数组(您不需要先创建空数组) 2.与其他答案相关-出于性能考虑,应该使用NextBytes,而不是一次获取一个 3.从技术上讲,您可以固定字节数组以获得更快的访问速度。当您对字节数组进行6-8次以上的迭代时,通常值得这样做。(此处未完成) 4.使用RNGCryptoServiceProvider获得最佳随机性 5.缓存1MB随机数据缓冲区的使用-基准测试显示缓存的单个字节访问速度约为未缓存的1000倍-在1MB上花费9ms,而未缓存的则需要989ms。 6.在我的新类中优化了偏差区的拒绝。
问题的最终解决方案:
static char[] charSet =  "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".ToCharArray();
static int byteSize = 256; //Labelling convenience
static int biasZone = byteSize - (byteSize % charSet.Length);
public string GenerateRandomString(int Length) //Configurable output string length
{
    byte[] rBytes = new byte[Length]; //Do as much before and after lock as possible
    char[] rName = new char[Length];
    SecureFastRandom.GetNextBytesMax(rBytes, biasZone);
    for (var i = 0; i < Length; i++)
    {
        rName[i] = charSet[rBytes[i] % charSet.Length];
    }
    return new string(rName);
}

但是你需要我的新(未经测试的)类:

/// <summary>
/// My benchmarking showed that for RNGCryptoServiceProvider:
/// 1. There is negligable benefit of sharing RNGCryptoServiceProvider object reference 
/// 2. Initial GetBytes takes 2ms, and an initial read of 1MB takes 3ms (starting to rise, but still negligable)
/// 2. Cached is ~1000x faster for single byte at a time - taking 9ms over 1MB vs 989ms for uncached
/// </summary>
class SecureFastRandom
{
    static byte[] byteCache = new byte[1000000]; //My benchmark showed that an initial read takes 2ms, and an initial read of this size takes 3ms (starting to raise)
    static int lastPosition = 0;
    static int remaining = 0;

    /// <summary>
    /// Static direct uncached access to the RNGCryptoServiceProvider GetBytes function
    /// </summary>
    /// <param name="buffer"></param>
    public static void DirectGetBytes(byte[] buffer)
    {
        using (var r = new RNGCryptoServiceProvider())
        {
            r.GetBytes(buffer);
        }
    }

    /// <summary>
    /// Main expected method to be called by user. Underlying random data is cached from RNGCryptoServiceProvider for best performance
    /// </summary>
    /// <param name="buffer"></param>
    public static void GetBytes(byte[] buffer)
    {
        if (buffer.Length > byteCache.Length)
        {
            DirectGetBytes(buffer);
            return;
        }

        lock (byteCache)
        {
            if (buffer.Length > remaining)
            {
                DirectGetBytes(byteCache);
                lastPosition = 0;
                remaining = byteCache.Length;
            }

            Buffer.BlockCopy(byteCache, lastPosition, buffer, 0, buffer.Length);
            lastPosition += buffer.Length;
            remaining -= buffer.Length;
        }
    }

    /// <summary>
    /// Return a single byte from the cache of random data.
    /// </summary>
    /// <returns></returns>
    public static byte GetByte()
    {
        lock (byteCache)
        {
            return UnsafeGetByte();
        }
    }

    /// <summary>
    /// Shared with public GetByte and GetBytesWithMax, and not locked to reduce lock/unlocking in loops. Must be called within lock of byteCache.
    /// </summary>
    /// <returns></returns>
    static byte UnsafeGetByte()
    {
        if (1 > remaining)
        {
            DirectGetBytes(byteCache);
            lastPosition = 0;
            remaining = byteCache.Length;
        }

        lastPosition++;
        remaining--;
        return byteCache[lastPosition - 1];
    }

    /// <summary>
    /// Rejects bytes which are equal to or greater than max. This is useful for ensuring there is no bias when you are modulating with a non power of 2 number.
    /// </summary>
    /// <param name="buffer"></param>
    /// <param name="max"></param>
    public static void GetBytesWithMax(byte[] buffer, byte max)
    {
        if (buffer.Length > byteCache.Length / 2) //No point caching for larger sizes
        {
            DirectGetBytes(buffer);

            lock (byteCache)
            {
                UnsafeCheckBytesMax(buffer, max);
            }
        }
        else
        {
            lock (byteCache)
            {
                if (buffer.Length > remaining) //Recache if not enough remaining, discarding remaining - too much work to join two blocks
                    DirectGetBytes(byteCache);

                Buffer.BlockCopy(byteCache, lastPosition, buffer, 0, buffer.Length);
                lastPosition += buffer.Length;
                remaining -= buffer.Length;

                UnsafeCheckBytesMax(buffer, max);
            }
        }
    }

    /// <summary>
    /// Checks buffer for bytes equal and above max. Must be called within lock of byteCache.
    /// </summary>
    /// <param name="buffer"></param>
    /// <param name="max"></param>
    static void UnsafeCheckBytesMax(byte[] buffer, byte max)
    {
        for (int i = 0; i < buffer.Length; i++)
        {
            while (buffer[i] >= max)
                buffer[i] = UnsafeGetByte(); //Replace all bytes which are equal or above max
        }
    }
}

关于历史 - 我以前的解决方案是使用随机对象:

    private static char[] charSet =
      "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".ToCharArray();

    static rGen = new Random(); //Must share, because the clock seed only has Ticks (~10ms) resolution, yet lock has only 20-50ns delay.
    static int byteSize = 256; //Labelling convenience
    static int biasZone = byteSize - (byteSize % charSet.Length);
    static bool SlightlyMoreSecurityNeeded = true; //Configuration - needs to be true, if more security is desired and if charSet.Length is not divisible by 2^X.
    public string GenerateRandomString(int Length) //Configurable output string length
    {
      byte[] rBytes = new byte[Length]; //Do as much before and after lock as possible
      char[] rName = new char[Length];
      lock (rGen) //~20-50ns
      {
          rGen.NextBytes(rBytes);

          for (int i = 0; i < Length; i++)
          {
              while (SlightlyMoreSecurityNeeded && rBytes[i] >= biasZone) //Secure against 1/5 increased bias of index[0-7] values against others. Note: Must exclude where it == biasZone (that is >=), otherwise there's still a bias on index 0.
                  rBytes[i] = rGen.NextByte();
              rName[i] = charSet[rBytes[i] % charSet.Length];
          }
      }
      return new string(rName);
    }

性能:

  1. SecureFastRandom - 第一次运行 = ~9-33毫秒。几乎不可察觉。 持续进行:10,000次迭代中的5毫秒(有时会增加到13毫秒)。单次平均迭代= 1.5微秒。注意:通常需要2个,但偶尔需要8个缓存刷新-这取决于超出偏差区域的单个字节数量
  2. 随机数 - 第一次运行 = ~0-1毫秒。几乎不可察觉。 持续进行:10,000次迭代中的5毫秒。单次平均迭代= 0.5微秒。速度大致相同。

还可以查看:

这些链接是另一种方法。可以向这个新代码库添加缓冲,但最重要的是探索不同的消除偏差的方法,并对速度和优缺点进行基准测试。


我发现了一些对你的方法进行微小性能优化的方法,这似乎是最快的方法 - https://dev59.com/qHM_5IYBdhLWcg3whzjx#17092645 - drzaus
5
  1. 为什么使用这些神奇的常量?你指定了输出长度 三次。只需将其定义为常量或参数即可。您可以使用 charSet.Length 代替 62
  2. 没有锁定的静态 Random 意味着此代码不是线程安全的。
  3. 减少 0-255 mod 62 会引入可检测的偏差。
  4. 你不能在字符数组上使用 ToString,它总是返回 "System.Char[]"。你需要使用 new String(rName)
- CodesInChaos
谢谢@CodesInChaos,我以前从未考虑过这些事情。仍然只使用Random类,但这应该更好。我想不出任何更好的方法来检测和纠正偏差输入。 - Kind Contributor
从一个弱的随机数生成器(System.Random)开始,然后小心地避免你自己代码中的任何偏见,这有点傻。这让人想起了“抛砖引玉”的表达方式。 - CodesInChaos
@CodesInChaos 现在徒弟已经超越了他的师傅。 - Kind Contributor
显示剩余5条评论

6

问题:为什么我要浪费时间使用Enumerable.Range而不是输入"ABCDEFGHJKLMNOPQRSTUVWXYZ0123456789"

using System;
using System.Collections.Generic;
using System.Linq;

public class Test
{
    public static void Main()
    {
        var randomCharacters = GetRandomCharacters(8, true);
        Console.WriteLine(new string(randomCharacters.ToArray()));
    }

    private static List<char> getAvailableRandomCharacters(bool includeLowerCase)
    {
        var integers = Enumerable.Empty<int>();
        integers = integers.Concat(Enumerable.Range('A', 26));
        integers = integers.Concat(Enumerable.Range('0', 10));

        if ( includeLowerCase )
            integers = integers.Concat(Enumerable.Range('a', 26));

        return integers.Select(i => (char)i).ToList();
    }

    public static IEnumerable<char> GetRandomCharacters(int count, bool includeLowerCase)
    {
        var characters = getAvailableRandomCharacters(includeLowerCase);
        var random = new Random();
        var result = Enumerable.Range(0, count)
            .Select(_ => characters[random.Next(characters.Count)]);

        return result;
    }
}

回答:魔法字符串是不好的。有没有人注意到我上面的字符串中没有 "I"?我的母亲教导我不要使用魔法字符串,正是因为这个原因...

n.b. 1: 正如其他人 @dtb 所说,如果需要加密安全,请不要使用 System.Random...

n.b. 2: 这个答案可能不是最有效或最短的,但我想留出空间将答案与问题分开。我的答案的目的更多是警告不要使用魔法字符串,而不是提供一个花哨的创新答案。


为什么我关心没有"I"? - Christine
1
字母数字(不区分大小写)为 [A-Z0-9]。如果您的随机字符串只包含 [A-HJ-Z0-9],则结果未涵盖全部允许范围,这可能会引起问题。 - Wai Ha Lee
那会有什么问题吗?所以它不包含“I”。是因为少了一个字符,这样更容易破解吗?在包含35个字符的可破解密码和36个字符范围内的可破解密码方面的统计数据是什么?我想我宁愿冒险……或者只是校对一下字符范围……而不是在我的代码中包含所有那些额外的垃圾。但是,那是我。我的意思不是要成为一个讨厌鬼,我只是说。有时候我认为程序员有倾向于走额外复杂的路线,只是为了变得更加复杂。 - Christine
1
这取决于使用情况。在这些随机字符串中排除字符(如IO)以避免人们将它们与10混淆是非常常见的。如果您不关心是否有可读性,那么可以忽略这些字符,但如果有人需要输入它们,那么删除这些字符实际上是明智的选择。 - Chris Pratt

5

尝试将两个部分结合起来:唯一标识(序列号、计数器或日期)和随机数。

public class RandomStringGenerator
{
    public static string Gen()
    {
        return ConvertToBase(DateTime.UtcNow.ToFileTimeUtc()) + GenRandomStrings(5); //keep length fixed at least of one part
    }

    private static string GenRandomStrings(int strLen)
    {
        var result = string.Empty;

        using (var gen = new RNGCryptoServiceProvider())
        {
            var data = new byte[1];

            while (result.Length < strLen)
            {
                gen.GetNonZeroBytes(data);
                int code = data[0];
                if (code > 48 && code < 57 || // 0-9
                    code > 65 && code < 90 || // A-Z
                    code > 97 && code < 122   // a-z
                )
                {
                    result += Convert.ToChar(code);
                }
            }

            return result;
        }
    }

    private static string ConvertToBase(long num, int nbase = 36)
    {
        const string chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; //if you wish to make the algorithm more secure - change order of letter here

        // check if we can convert to another base
        if (nbase < 2 || nbase > chars.Length)
            return null;

        int r;
        var newNumber = string.Empty;

        // in r we have the offset of the char that was converted to the new base
        while (num >= nbase)
        {
            r = (int)(num % nbase);
            newNumber = chars[r] + newNumber;
            num = num / nbase;
        }
        // the last number to convert
        newNumber = chars[(int)num] + newNumber;

        return newNumber;
    }
}

测试:

    [Test]
    public void Generator_Should_BeUnigue1()
    {
        //Given
        var loop = Enumerable.Range(0, 1000);
        //When
        var str = loop.Select(x=> RandomStringGenerator.Gen());
        //Then
        var distinct = str.Distinct();
        Assert.AreEqual(loop.Count(),distinct.Count()); // Or Assert.IsTrue(distinct.Count() < 0.95 * loop.Count())
    }

  1. 您可以使用字符字面值而不是与这些字符相关联的ASCII值。
  2. 在您的区间匹配代码中存在一个偏移一位的错误。您需要使用<=>=而不是<>
  3. 我会在&&表达式周围添加不必要的括号,以明确它们的优先级,但当然这只是一种风格选择。
- CodesInChaos
+1 有助于消除偏见和添加测试。但我不确定为什么您要在随机字符串前加上一个基于时间戳的字符串? 此外,您仍需要处理您的RNGCryptoServiceProvider。 - monty

5
生成加密算法Aes密钥可能是一种简单且高度安全的方法。
public static string GenerateRandomString()
{
    using Aes crypto = Aes.Create();
    crypto.GenerateKey();
    return Convert.ToBase64String(crypto.Key);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接