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

我一直在寻找一种简单的 Java 算法来生成伪随机的字母数字字符串。在我的情况下,它将用作唯一的会话 / 密钥标识符,在 “超过500K+世代中 “可能” 是唯一的(我的需求实际上不需要任何更复杂的东西)。

理想情况下,我可以根据自己的独特性要求指定长度。例如,生成的长度为 12 的字符串可能看起来像"AEYGF7K0DM1X"

答案

算法

要生成随机字符串,请连接从可接受的符号集中随机抽取的字符,直到字符串达到所需的长度为止。

实作

这是一些相当简单且非常灵活的代码,用于生成随机标识符。 阅读以下信息以获取重要的应用笔记。

public class RandomString {

    /**
     * Generate a random string.
     */
    public String nextString() {
        for (int idx = 0; idx < buf.length; ++idx)
            buf[idx] = symbols[random.nextInt(symbols.length)];
        return new String(buf);
    }

    public static final String upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    public static final String lower = upper.toLowerCase(Locale.ROOT);

    public static final String digits = "0123456789";

    public static final String alphanum = upper + lower + digits;

    private final Random random;

    private final char[] symbols;

    private final char[] buf;

    public RandomString(int length, Random random, String symbols) {
        if (length < 1) throw new IllegalArgumentException();
        if (symbols.length() < 2) throw new IllegalArgumentException();
        this.random = Objects.requireNonNull(random);
        this.symbols = symbols.toCharArray();
        this.buf = new char[length];
    }

    /**
     * Create an alphanumeric string generator.
     */
    public RandomString(int length, Random random) {
        this(length, random, alphanum);
    }

    /**
     * Create an alphanumeric strings from a secure generator.
     */
    public RandomString(int length) {
        this(length, new SecureRandom());
    }

    /**
     * Create session identifiers.
     */
    public RandomString() {
        this(21);
    }

}

用法示例

为 8 个字符的标识符创建不安全的生成器:

RandomString gen = new RandomString(8, ThreadLocalRandom.current());

为会话标识符创建一个安全的生成器:

RandomString session = new RandomString();

创建具有易于阅读的代码的生成器以进行打印。字符串比完整的字母数字字符串长,以补偿使用较少的符号:

String easy = RandomString.digits + "ACEFGHJKLMNPQRUVWXYabcdefhijkprstuvwx";
RandomString tickets = new RandomString(23, new SecureRandom(), easy);

用作会话标识符

生成可能唯一的会话标识符还不够好,或者您可以只使用一个简单的计数器。使用可预测的标识符时,攻击者会劫持会话。

长度和安全性之间存在张力。标识符越短越容易猜测,因为可能性较小。但是更长的标识符会消耗更多的存储空间和带宽。较大的一组符号会有所帮助,但如果标识符包含在 URL 中或手动重新输入,则可能会导致编码问题。

会话标识符的基本随机性或熵源应来自为密码学设计的随机数生成器。但是,初始化这些生成器有时会在计算上昂贵或缓慢,因此应努力在可能的情况下重新使用它们。

用作对象标识符

并非每个应用程序都需要安全性。随机分配可能是多个实体在共享空间中生成标识符而无需任何协调或分区的有效方法。协调可能会很慢,尤其是在群集或分布式环境中,当实体最终共享的份额太小或太大时,划分空间会引起问题。

如果攻击者能够像大多数 Web 应用程序一样查看和操纵它们,则未采取措施使它们无法预测的所产生的标识符应受到其他保护。应该有一个单独的授权系统来保护对象,这些对象的标识符可以在没有访问权限的情况下被攻击者猜中。

考虑到预期的标识符总数,还必须小心使用足够长的标识符,以免发生碰撞。这被称为 “生日悖论”。 发生碰撞的概率 p大约为 n 2 /(2q x ),其中n是实际生成的标识符的数量, q是字母中不同符号的数量,并且x是标识符的长度。这应该是一个很小的数字,例如 2 -50或更少。

得出的结论表明,500k 个 15 个字符的标识符之间发生冲突的机会约为 2 到52 ,这比宇宙射线等未检测到的错误的可能性要小。

与 UUID 的比较

根据其规范, UUID并非不可预测,因此不应用作会话标识符。

标准格式的 UUID 占用大量空间:36 个字符仅代表 122 位熵。 (并非 “随机” UUID 的所有位都是随机选择的。)随机选择的字母数字字符串仅 21 个字符就包含了更多的熵。

UUID 不灵活;它们具有标准化的结构和布局。这是他们的主要美德,也是他们的主要弱点。与外部方合作时,UUID 提供的标准化可能会有所帮助。仅用于内部使用,它们可能效率很低。

Java 提供了一种直接执行此操作的方法。如果您不想使用破折号,则很容易将其删除。只需使用uuid.replace("-", "")

import java.util.UUID;

public class randomStringGenerator {
    public static void main(String[] args) {
        System.out.println(generateString());
    }

    public static String generateString() {
        String uuid = UUID.randomUUID().toString();
        return "uuid = " + uuid;
    }
}

输出:

uuid = 2d7428a6-b58c-4008-8575-f05549f16316
static final String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
static SecureRandom rnd = new SecureRandom();

String randomString( int len ){
   StringBuilder sb = new StringBuilder( len );
   for( int i = 0; i < len; i++ ) 
      sb.append( AB.charAt( rnd.nextInt(AB.length()) ) );
   return sb.toString();
}

如果您愿意使用 Apache 类,则可以使用org.apache.commons.text.RandomStringGenerator (公共文本)。

例:

RandomStringGenerator randomStringGenerator =
        new RandomStringGenerator.Builder()
                .withinRange('0', 'z')
                .filteredBy(CharacterPredicates.LETTERS, CharacterPredicates.DIGITS)
                .build();
randomStringGenerator.generate(12); // toUpperCase() if you want

从 commons-lang 3.6 开始,不推荐使用RandomStringUtils

一行:

Long.toHexString(Double.doubleToLongBits(Math.random()));

http://mynotes.wordpress.com/2009/07/23/java-generating-random-string/

您可以为此使用 Apache 库: RandomStringUtils

RandomStringUtils.randomAlphanumeric(20).toUpperCase();

无需任何外部库即可轻松实现。

1. 加密伪随机数据生成

首先,您需要加密的 PRNG。 Java 为此提供了SecureRandom ,通常使用机器上最好的熵源(例如/dev/random )。 在这里阅读更多。

SecureRandom rnd = new SecureRandom();
byte[] token = new byte[byteLength];
rnd.nextBytes(token);

注意: SecureRandom是 Java 中生成随机字节的最慢但最安全的方法。但是,我建议不要在这里考虑性能,因为它通常不会对您的应用程序产生实际影响,除非您必须每秒生成数百万个令牌。

2. 可能值的要求空间

接下来,您必须确定令牌需要 “多么独特”。考虑熵的全部也是唯一的要点是确保系统可以抵抗暴力攻击:可能值的空间必须足够大,以至于任何攻击者在非荒谬的时间内1只能尝试忽略不计的值。唯一标识符(例如随机UUID具有 122 位的熵(即 2 ^ 122 = 5.3x10 ^ 36)- 碰撞的机率是 “ *(...),因为十亿分之一的重复机率是 103 万亿版本 4 UUID 必须生成2 “。 我们将选择 128 位,因为它恰好适合 16 个字节,并且被视为足以在几乎所有情况下都具有唯一性,但是在最极端的用例中,您不必考虑重复项。这是一个简单的熵比较表,其中包括对生日问题的简单分析。

代币大小的比较

对于简单的要求,8 或 12 个字节的长度就足够了,但是对于 16 个字节,您就处于 “安全方面”。

基本上就是这样。最后一件事是考虑编码,以便可以将其表示为可打印的文本(读取为String )。

3. 二进制到文本编码

典型的编码包括:

  • Base64每个字符编码为 6 位,从而产生 33%的开销。幸运的是,在Java 8+Android 中有标准实现。使用较旧的 Java,您可以使用众多第三方库中的任何一个。如果您希望令牌是网址安全的,请使用 RFC4648 的网址安全版本(大多数实现通常都支持该版本)。使用填充编码 16 个字节的示例: XfJhfv3C0P6ag7y9VQxSbw==

  • Base32每个字符都编码 5 位,从而产生 40%的开销。这将使用AZ2-7从而使其在空间上有效,同时不区分大小写字母数字。 JDK 中没有标准实现 。编码不带填充的 16 个字节的示例: WUPIL5DQTZGMF4D3NX5L7LNFOY

  • Base16 (十六进制)每个字符编码 4 位,每个字节需要 2 个字符(即 16 个字节创建一个长度为 32 的字符串)。因此,十六进制的空间效率不及Base32但在大多数情况下(url)都可以安全使用,因为它仅使用0-9AF编码 16 个字节的示例: 4fa3dd0f57cb3bf331441ed285b27735在此处查看有关转换为十六进制的 SO 讨论。

存在诸如Base85和奇异的Base122 之类的其他编码,其空间效率更高 / 更差。您可以创建自己的编码(基本上该线程中的大多数答案都可以做到),但是如果您没有非常具体的要求,我建议您不要这样做。请参阅Wikipedia 文章中的更多编码方案。

4. 总结和例子

  • 使用SecureRandom
  • 使用至少 16 个字节(2 ^ 128)的可能值
  • 根据您的要求进行编码(如果需要为字母数字,通常为hexbase32

  • ... 使用您的家用 Brew 编码: 如果其他人看到您使用哪种标准编码,而不是一次一次创建字符的怪异循环,则可以更好地保持其可读性。
  • ... 使用 UUID: 它不能保证随机性;您浪费了 6 位的熵并具有冗长的字符串表示形式

示例:十六进制令牌生成器

public static String generateRandomHexToken(int byteLength) {
    SecureRandom secureRandom = new SecureRandom();
    byte[] token = new byte[byteLength];
    secureRandom.nextBytes(token);
    return new BigInteger(1, token).toString(16); //hex encoding
}

//generateRandomHexToken(16) -> 2189df7475e96aa3982dbeab266497cd

示例:Base64 令牌生成器(网址安全)

public static String generateRandomBase64Token(int byteLength) {
    SecureRandom secureRandom = new SecureRandom();
    byte[] token = new byte[byteLength];
    secureRandom.nextBytes(token);
    return Base64.getUrlEncoder().withoutPadding().encodeToString(token); //base64 encoding
}

//generateRandomBase64Token(16) -> EEcCCAYuUcQk7IuzdaPzrg

示例:Java CLI 工具

如果您想使用现成的 cli 工具,可以使用骰子: https : //github.com/patrickfav/dice

示例:相关问题 - 保护您当前的 ID

如果您已经有一个 ID 可以使用(例如,实体中的合成long ),但是不想发布内部值 ,则可以使用此库对其进行加密和混淆: https : //github.com / 帕特里克 · 法夫

IdMask<Long> idMask = IdMasks.forLongIds(Config.builder(key).build());
String maskedId = idMask.mask(id);
//example: NPSBolhMyabUBdTyanrbqT8
long originalId = idMask.unmask(maskedId);

使用Dollar应该很简单,因为:

// "0123456789" + "ABCDE...Z"
String validCharacters = $('0', '9').join() + $('A', 'Z').join();

String randomString(int length) {
    return $(validCharacters).shuffle().slice(length).toString();
}

@Test
public void buildFiveRandomStrings() {
    for (int i : $(5)) {
        System.out.println(randomString(12));
    }
}

它输出如下内容:

DKL1SBH9UJWC
JH7P0IT21EA5
5DTI72EO6SFU
HQUMJTEBNF7Y
1HCR6SKYWGT7

在 Java 中:

import static java.lang.Math.round;
import static java.lang.Math.random;
import static java.lang.Math.pow;
import static java.lang.Math.abs;
import static java.lang.Math.min;
import static org.apache.commons.lang.StringUtils.leftPad

public class RandomAlphaNum {
  public static String gen(int length) {
    StringBuffer sb = new StringBuffer();
    for (int i = length; i > 0; i -= 12) {
      int n = min(12, abs(i));
      sb.append(leftPad(Long.toString(round(random() * pow(36, n)), 36), n, '0'));
    }
    return sb.toString();
  }
}

这是一个示例运行:

scala> RandomAlphaNum.gen(42)
res3: java.lang.String = uja6snx21bswf9t89s00bxssu8g6qlu16ffzqaxxoy

令人惊讶的是,这里没有人建议这样做,但是:

import java.util.UUID

UUID.randomUUID().toString();

简单。

这样做的好处是,UUID 很好且很长,并且保证几乎不可能发生碰撞。

维基百科对此有很好的解释:

“... 仅在接下来的 100 年中每秒生成 10 亿个 UUID 之后,仅创建一个副本的可能性就约为 50%。”

http://zh.wikipedia.org/wiki/Universally_unique_identifier#Random_UUID_probability_of_duplicates

前 4 位是版本类型,而第 2 位是版本,因此您可以获取 122 位随机数。因此,如果您愿意 ,可以从末尾截断以减小 UUID 的大小。不建议这样做,但是您仍然有很多随机性,足以轻松完成 500k 条记录。