在C#中带条件生成随机字母数字字符串

3
根据以下代码,我需要根据方法输入在不同情况下生成字符串。我的问题是当我想要生成 9A9A (至少1个数字和1个字母)或者 9A9A9A (至少2个数字和2个字母)时。在大多数情况下,这些条件没有被满足。
private AuthMessage GetAuthCode(string CodeType) //(out string Message)
    {
        Guid Guid = Guid.NewGuid();
        Random Random = new Random();
        string AuthCode = string.Empty;
        string RefCode = string.Empty;

        RefCode = Guid.ToString("N");

        switch (CodeType)
        {
            case "0": //9999
                {
                    AuthCode = Random.Next(1000, 9999).ToString();
                    break;
                }
            case "1": //99999
                {
                    AuthCode = Random.Next(10000, 99999).ToString();
                    break;
                }
            case "2": //999999
                {
                    AuthCode = Random.Next(100000, 999999).ToString();
                    break;
                }
            case "3": //999-999
                {
                    AuthCode = Regex.Replace(Random.Next(100000, 999999).ToString(), @"^(.{3})(.{3})$", "$1-$2");
                    break;
                }
            case "4": //9A9A
                {
                    AuthCode = Guid.ToString("N").Substring(14, 4).ToUpper();
                    break;
                }
            case "5": //9A9A9
                {
                    AuthCode = Guid.ToString("N").Substring(15, 5).ToUpper();
                    break;
                }
            case "6": //9A9A9A
                {
                    AuthCode = Guid.ToString("N").Substring(6, 6).ToUpper();
                    break;
                }
            case "7": //9A9-A9A
                {
                    AuthCode = Regex.Replace(Guid.ToString("N").Substring(6, 6), @"(.{3})(.{3})", @"$1-$2").ToUpper();
                    break;
                }
            case "8": //9A9-A9A
                {
                    AuthCode = Regex.Replace(Regex.Replace(Convert.ToBase64String(Guid.ToByteArray()), "[/+=]", "").Substring(0, 6), @"(.{3})(.{3})", @"$1-$2").ToUpper();
                    break;
                }
            default:
                {
                    AuthCode = Random.Next(1000, 9999).ToString();
                    break;
                }
        }

        AuthMessage Response = new AuthMessage();
        Response.AuthCode = AuthCode;
        Response.RefCode = RefCode;

     return Response;
    }

4
这是对 GUID 的可怕误用,你不应这样做。GUID 有一个且仅有一个目的:生成一个全球唯一标识符。GUID 并不保证是随机的,任何部分也不能保证是唯一的。解决你现在的问题,就要通过解决你现在的问题来解决,而不能使用 GUID 生成器作为引擎来解决与唯一标识生成完全无关的问题。 - Eric Lippert
2
特别是,即使对于随机类型4 GUID,您的某些情况包括一些在类型4 GUID中的非随机数据。当然,GUID永远不会包含除A-F之外的字母。 - Eric Lippert
3
此外,你还有一个更大的问题。你正在使用非加密强度的随机数生成方式,这意味着已经攻破了其中一个授权码的攻击者可以轻易地生成所有其余的授权码,或者至少可以很好地猜测其他所有授权码是什么。我的建议是,你应该停止编写这个代码并雇用一个真正懂得如何安全实现授权机制的安全专家。 - Eric Lippert
3
事实上,情况比这还要糟糕,现在我仔细查看了代码。如果你没有遵循管理“Random”实例的最佳实践,攻击者可以仅通过知道生成代码的时间来确定可能的授权代码。 - Eric Lippert
3
@OmegaMan: "想要编写安全代码"和编写安全代码之间存在着巨大的差异。前者指的是业务代码,它不会意外引入漏洞。而后者则包括加密、授权系统等内容,是计算机编程中最难的任务之一,应该留给那些花费数年时间学习它的专家来完成。你不能让业余爱好者随意编写代码,然后让陌生人在互联网上批评它,就能获得正确的安全代码。 - Eric Lippert
显示剩余3条评论
5个回答

4

Guid表示由十六进制数字组成,即字符0-9a-f。依靠它来获取字母和数字的混合存在问题,因为在任何给定位置上,字符可以是字母或十进制数字,概率大约倾向于十进制数字。

如果您想生成特定的数字和字母混合,请逐个字符生成字符串,不要依赖于Guid表示。


正则表达式方法怎么样,可以使用吗? - Amir Hussein Khaniki
1
@AmirHusseinKhaniki 正则表达式方法用于搜索字符串,而不是生成它们。您可以生成随机字符串,然后使用正则表达式进行检查,但如果您一次性生成所需内容,而不是反复试错,您的代码将更加清晰易于维护。 - Sergey Kalinichenko
我打算使用正则表达式方法根据条件过滤生成的GUID字符串。 - Amir Hussein Khaniki
1
@AmirHusseinKhaniki 你可以进行过滤,但是你会一遍又一遍地尝试生成字符串直到找到匹配吗?如果在合理的时间内没有找到匹配怎么办?毕竟,这是一个随机字符串,可能需要一段时间才能找到匹配。为什么不自己生成字符串呢? - Sergey Kalinichenko

2

我想尝试一下 - 这给了我一个被嘲笑的好机会。这不是最有效的生成代码的方法,但应该相当随机。

private string GetAuthCode(string CodeType)
{
    var patterns = new Dictionary<char, Func<Char>>()
    {
        { '9', () => RandomBytes().Where(x => x >= '0' && x <= '9').First() },
        { 'A', () => RandomBytes().Where(x => x >= 'A' && x <= 'Z').First() },
        { '-', () => '-' },
    };

    return
        String.IsNullOrEmpty(CodeType)
            ? ""
            : patterns[CodeType[0]]().ToString() + GetAuthCode(CodeType.Substring(1));
}

private IEnumerable<char> RandomBytes()
{
    using (var rng = System.Security.Cryptography.RNGCryptoServiceProvider.Create())
    {
        var bytes = new byte[256];
        while (true)
        {
            rng.GetBytes(bytes);
            foreach (var @byte in bytes)
            {
                yield return (char)@byte;
            }
        }
    }
}

现在,由于实现迭代器方法的时髦猴状态机,尽管有 while (true),但这段代码确实处理了 RNG 的释放。

我稍微简化了 GetAuthCode 方法,但我认为这展示了一种适合生成代码的方法。


这是超级低效的:愚蠢地使用递归,生成你没有使用的 RNG,重复地重新生成服务提供者(这是安全的,不像 Random)。另外,CodeType 影响性能很丑陋,尽管 CodeType 不是秘密,所以这不是一场灾难。话虽如此,这是我找不到安全漏洞的第一个基于代码的答案。当然,我只是个业余爱好者。Schneier's Law 适用。 - Brian
@Brian - 是的,我同意这种方法不够高效,但我猜它不会在紧密循环中使用。我对它感到相当满意。 - Enigmativity

-2
public class TokenCreator
{
 private Random random = new Random();
 private const string[] chars= "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
 public static string CreateToken(int length)
 {
    return new string(Enumerable.Repeat(chars, length)
      .Select(s => s[random.Next(s.Length)]).ToArray());
 }
}

AuthCode = TokenCreator.CreateToken(5);

这个实现可能会对你有所帮助,但它并不能保证你得到字母数字组合。有时你可能只会得到字母,有时你可能只会得到数字。尽管机会很小。


不要使用“Random”生成身份验证代码。 - Brian

-2

概述 以下代码使用Regex.ReplaceIsMatch,允许创建任意长度的模式,例如XXXXXXXX-XX,它将用随机字符或字母替换任何X

例如,XX-XX可能返回A(-1d,或者只有破折号的XXXX变为A(1d

安全性 它使用Web.Security.Membership.GeneratePassword生成一个包含48个随机字符、数字等的字符串,然后根据所需的模式从这些随机字符中提取出一个明文密码。

(至少1个数字和1个字母)

这是在验证正则表达式中完成的,确保至少有一个字母和一个数字。直到验证报告符合您提到的规则的有效匹配为止,才会调用生成方法。

安全传输 最后设置一个安全字符串以返回。

这个方法是通过获取一个

// This pattern enforces our rule that a pwd must have one letter and one digit.
string pattern = @" # This regex pattern enforces the rules before returning matching
(?=.*[a - zA - Z])  # Somewhere there is a an alphabectic character
(?=.*\d)            # Somewhere there is a number; if no number found return no match.
(.+)                # Successful match, rules are satisfied. Return match";

Random rn = new Random(); // Used to cherry pick from chars to use.

// Creates 48 alpha and non alpha (at least 10 non digit alphas) random characters.
string charsToUse = System.Web.Security.Membership.GeneratePassword(48, 5);

// When replacement is done, replace an `X` matched with a random char. 
MatchEvaluator RandomChar = delegate (Match m)
{
    return charsToUse[rn.Next(charsToUse.Length)].ToString();
};

Func<string, string> Validate = 
      (string str) => Regex.IsMatch(str, pattern, RegexOptions.IgnorePatternWhitespace) 
                      ? str : string.Empty; // return empty on failure.

string pwdClear = string.Empty;

// Generate valid pwd based on rules. Loop until rules are met.
while (string.IsNullOrEmpty(pwdClear))
    pwdClear = Validate(Regex.Replace("XXXX-XXXX-XXXX-XXXX-XXXX", "X", RandomChar));

// Create a secure string for the password for transportation.
SecureString ss = new SecureString();

pwdClear.ToList()
        .ForEach(chr => ss.AppendChar(chr));

这个答案基于我博客上的一个非安全实现,请参见C#: 从用户定义的模式和字符生成随机数字和字母序列


不要使用“Random”生成身份验证代码。 - Brian
1
@Brian 好的,但是应该使用什么?你对这个问题的回答在哪里? - ΩmegaMan
我会使用System.Security.Cryptography.RNGCryptoServiceProvider来生成密码。话虽如此,我更喜欢使用System.Web.Security.Membership.GeneratePassword。如果你懒得自己写,也可以滥用Path.GetRandomFileName。这可能有些不合适或不符合规范,但是它确实在文档中被描述为返回"一个具有加密强度的随机字符串"。 - Brian
至于为什么我不提供答案:我不认为自己是安全专家,因此我认为我提供的任何答案都是错误的。虽然我相信我的答案会更好,但它仍然有很高的错误风险,尽管比使用“Random”更微妙地错误。我的一般建议是对于任何安全问题,“使用由专家构建的一个”,但当OP要求特殊情况代码时,这并不是很有帮助。 - Brian
@Brian 使用了你提到的 GeneratePassword 生成了Uber密码,然后从中挑选了一部分。谢谢。 - ΩmegaMan
是的,但你仍然在使用“Random”从“charsToUse”中选择字符。尽管“charsToUse”是随机的,但这仍然是不安全的,因为这使得给定密码字符与另一个密码字符匹配的几率略微不同于真正的随机。 - Brian

-3

为了测试目的,当我需要生成具有特定属性的随机字符串时,我使用类似于这样的东西。您可能需要根据自己的需求进行调整。

public sealed class StringGenerator
{
  private static readonly char[] NumericChars = "0123456789".ToCharArray();
  private static readonly char[] LowerAlphaChars = "abcdefghijklmnopqrstuvwxyz".ToCharArray();
  private static readonly char[] UpperAlphaChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray();

  public StringGenerator(IRandom rnd)
  {
    Rnd = rnd ?? new SecureRandom();
  }

  private IRandom Rnd
  {
    get;
    set;
  }

  public string Generate(int length, int minNumeric = 0, int minAlpha = 0, AlphaCase alphaCase = AlphaCase.Both)
  {
    if (length < 0)
    {
      throw new ArgumentOutOfRangeException("length");
    }
    if (minNumeric < 0)
    {
      throw new ArgumentOutOfRangeException("minNumeric");
    }
    if (minAlpha < 0)
    {
      throw new ArgumentOutOfRangeException("minAlpha");
    }
    if (length < minNumeric + minAlpha)
    {
      throw new ArgumentException();
    }

    if (length == 0)
    {
      return string.Empty;
    }

    var result = new char[length];

    var index = 0;

    foreach(var numeric in GenerateNumeric().Take(minNumeric))
    {
      result[index++] = numeric;
    }

    var alphaCharacters = GetAlphaCharacters(alphaCase);
    foreach (var alpha in Generate(alphaCharacters).Take(minAlpha))
    {
      result[index++] = alpha;
    }


    var restLength = length - index;

    if (restLength > 0)
    {
      var restCharacters = new List<char>(NumericChars.Concat(alphaCharacters));

      foreach (var rest in Generate(restCharacters).Take(restLength))
      {
        result[index++] = rest;
      }
    }

    // shuffle result
    return new string(result.OrderBy(x => Rnd.Next()).ToArray());
  }

  private IList<char> GetAlphaCharacters(AlphaCase alphaCase)
  {
    switch (alphaCase)
    {
      case AlphaCase.Lower:
        return LowerAlphaChars;

      case AlphaCase.Upper:
        return UpperAlphaChars;

      case AlphaCase.Both:
      default:
        return new List<char>(LowerAlphaChars.Concat(UpperAlphaChars));
    }
  }

  public IEnumerable<char> GenerateNumeric()
  {
    return Generate(NumericChars);
  }
  public IEnumerable<char> GenerateLowerAlpha()
  {
    return Generate(LowerAlphaChars);
  }
  public IEnumerable<char> GenerateUpperAlpha()
  {
    return Generate(UpperAlphaChars);
  }

  public IEnumerable<char> Generate(IList<char> characters)
  {
    if (characters == null)
    {
      throw new ArgumentNullException();
    }
    if (!characters.Any())
    {
      yield break;
    }

    while (true)
    {
      yield return characters[Rnd.Next(characters.Count)];
    }
  }
}

public enum AlphaCase
{
  Lower,
  Upper,
  Both
}

public interface IRandom
{
    int Next();
    int Next(int maxValue);
}

public sealed class SecureRandom : IRandom
{
    private readonly RandomNumberGenerator Rng = new RNGCryptoServiceProvider();

    public int Next()
    {
        var data = new byte[sizeof(int)];
        Rng.GetBytes(data);
        return BitConverter.ToInt32(data, 0) & (int.MaxValue - 1);
    }

    public int Next(int maxValue)
    {
        return Next(0, maxValue);
    }

    public int Next(int minValue, int maxValue)
    {
        if (minValue > maxValue)
        {
            throw new ArgumentOutOfRangeException();
        }
        return (int)Math.Floor(minValue + ((double)maxValue - minValue) * NextDouble());
    }

    public double NextDouble()
    {
        var data = new byte[sizeof(uint)];
        Rng.GetBytes(data);
        var randomUint = BitConverter.ToUInt32(data, 0);
        return randomUint / (uint.MaxValue + 1d);
    }
}

编辑:这是回答如何生成带有条件的随机字母数字字符串的问题。仅用于测试目的,不能在安全相关的环境中直接使用。


编辑2:毫不掩饰地借鉴了这个答案,现在的解决方案使用了RNGCryptoServiceProvider的包装器。仍然不确定是否应该在安全相关的情况下使用它,但至少现在比简单地使用Random要“更好”。


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