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

1968

我一直在寻找一个简单的Java算法来生成伪随机的字母数字字符串。在我的情况下,它将被用作唯一的会话/键标识符,可能在500K+次生成中是唯一的(我的需求并不需要更复杂的东西)。

理想情况下,我希望能够根据我的唯一性需求指定长度。例如,长度为12的生成字符串可能看起来像"AEYGF7K0DM1X"


165
注意生日悖论。 - pablosaraiva
63
即使考虑到生日悖论,如果您使用12个字母数字字符(共62个字符),仍需要超过340亿个字符串才能达到悖论。而且,生日悖论并不保证一定会发生冲突,它只是表示有超过50%的概率。 - NullUserException
6
@NullUserException:每次尝试的成功率达到50%真是太高了,即使尝试10次,成功率也达到0.999。考虑到你可以在24小时内尝试很多次,因此你不需要340亿个字符串就足以确信至少猜中其中一个。这就是为什么某些会话令牌应该非常非常长的原因。 - Pijusn
20
我认为这3个单行代码非常有用。Long.toHexString(Double.doubleToLongBits(Math.random())); 生成一个16进制的随机字符串。UUID.randomUUID().toString(); 生成一个随机唯一标识符。RandomStringUtils.randomAlphanumeric(12); 生成一个包含12个字符的随机字母数字组合。 - Manindar
25
我知道这已经是老话题了,但是,在生日悖论中,“50%的机率”不是“每一次尝试”,而是“存在至少一对重复的机率为50%,在(这种情况下)340亿个字符串中”。你需要拥有1.6*10^21-1.6e21个条目,才能每次尝试有50%的机会。 - Tin Wizard
显示剩余3条评论
46个回答

1632

算法

生成随机字符串的方法是从可接受符号集中随机抽取字符,将其连接起来直到达到所需长度。

实现

下面是一个相当简单且非常灵活的代码,用于生成随机标识符。请阅读接下来的信息以获取重要的应用程序注释。

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约为n2 /(2qx),其中n是实际生成的标识符数量,q是字母表中不同符号的数量,x是标识符的长度。这应该是一个非常小的数字,例如2 -50或更少。

计算显示,在500k个15个字符的标识符中发生碰撞的机会约为2 -52 ,这可能比来自宇宙射线等的未检测到的错误更不可能发生。

与UUID的比较

根据其规范,UUID不是设计为不可预测的,并且不应用作会话标识符。

以标准格式表示的UUID占用了很多空间:仅122位熵需要36个字符。(“随机”UUID的并非所有位都是随机选择的。)随机选择的字母数字字符串仅需21个字符即可打包更多的熵。

UUID不够灵活;它们具有标准化的结构和布局。这既是它们的主要优点,也是它们的主要弱点。在与外部方合作时,UUID提供的


6
如果你需要在结果中添加空格,你可以在return new BigInteger(130, random).toString(32);这行代码末尾添加.replaceAll("\\d", " ");来进行正则表达式替换。它将所有数字替换为空格。对我来说效果很好:我正在将其用作前端Lorem Ipsum的替代品。 - weisjohn
4
这是一个不错的想法。你可以使用第二种方法类似的方式,通过从“symbols”中删除数字并使用空格代替;你可以通过改变“symbols”中空格的数量来控制平均“单词”长度(更多的空格表示更短的单词)。对于一个真正夸张的伪文本解决方案,你可以使用马尔可夫链! - erickson
4
这些标识符是从特定大小的空间中随机选择的,它们可能只有1个字符长。如果你需要一个固定长度,可以使用第二种解决方案,将 SecureRandom 实例分配给 random 变量。 - erickson
17
因为32等于2的5次方,所以每个字符将代表确切的5位比特,而130位可以均匀地被分成若干个字符。 - erickson
3
@erickson,BigInteger.toString(int) 并不是那样工作的,它实际上调用了 Long.toString(long, String) 以确定字符值(这提供了更好的 JavaDoc 描述它实际上做了什么)。基本上,执行 BigInteger.toString(32) 意味着你只能得到字符 0-9 + a-v,而不是 0-9 + a-z - Vala
显示剩余10条评论

893

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

36
请注意,该解决方案仅生成由十六进制字符组成的随机字符串,这在某些情况下可能是可以接受的。 - Dave
6
UUID类很有用,但是它们不像我的答案生成的标识符那样紧凑。这可能会成为一个问题,例如在URL中。具体情况取决于您的需求。 - erickson
6
目标是“字母数字字符串”,将输出扩展到任意可能的字节如何符合该目标? - erickson
76
根据RFC4122,使用UUID作为令牌是不明智的:不要假设UUID很难猜测;例如,它们不应该用作安全能力(仅凭持有就授予访问权限的标识符)。可预测的随机数源会加剧这种情况。 - Somatik
39
UUID.randomUUID().toString().replaceAll("-", ""); 将字符串变成字母数字混合格式,与所请求的一致。 - Numid
显示剩余7条评论

638
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();
}

72
除了使用Commons Lang中的RandomStringUtils之外,生成指定长度的随机字符串的最简单解决方案是+1。 - Jonik
15
考虑使用SecureRandom类代替Random类。如果密码是在服务器上生成的,则可能容易受到时间攻击。 - foens
10
我会加入小写字母: AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 还有其他一些允许的字符。 - ACV
1
为什么不将static Random rnd = new Random();放在方法内部? - Micro
6
每次方法调用都创建Random对象是否有充分的理由?我认为没有。 - cassiomolin
显示剩余2条评论

504
如果您乐意使用Apache类,您可以使用org.apache.commons.text.RandomStringGeneratorApache Commons Text)。
示例:
RandomStringGenerator randomStringGenerator =
        new RandomStringGenerator.Builder()
                .withinRange('0', 'z')
                .filteredBy(CharacterPredicates.LETTERS, CharacterPredicates.DIGITS)
                .build();
randomStringGenerator.generate(12); // toUpperCase() if you want

自从 Apache Commons Lang 3.6 版本起,RandomStringUtils 已被弃用。


24
刚刚查看了Apache Commons Lang 3.3.1库中提到的那个类,发现它仅使用java.util.Random来生成随机序列,因此会产生不安全的序列。 - Yuriy Nakonechnyy
16
在使用RandomStringUtils时,请确保使用SecureRandom: public static java.lang.String random(int count, int start, int end, boolean letters, boolean numbers, @Nullable char[] chars, java.util.Random random) - Ruslans Uralovs
这会创建不安全的序列! - Patrick
2
使用以下代码构建您的RandomStringGenerator,以便序列是安全的:new RandomStringGenerator.Builder().usingRandom(RANDOM::nextInt).build(); - Rohan
1
@YuriyNakonechnyy 返回RandomStringUtils.random(12, 0, length, true, true, characterSetArray, new SecureRandom()); 这里的characterSetArray是您想要的字符集。例如(假设所有数字和所有小写字母)将是“0123456789abcdefghijklmnopqrstuvwxyz”.toCharArray()。而length是characterArray的长度。 - anuj pradhan

145

你可以使用Apache Commons库来实现这个功能,RandomStringUtils是其中一个可选的工具:

RandomStringUtils.randomAlphanumeric(20).toUpperCase();

20
@kamil,我查看了RandomStringUtils的源代码,它使用一个没有参数实例化的java.util.Random实例。java.util.Random的文档说明如果没有提供种子,则使用当前系统时间。这意味着它不能用于会话标识符/密钥,因为攻击者可以轻松地预测在任何给定时间生成的会话标识符是什么。 - Inshallah
44
@Inshallah: 你过度设计了这个系统。虽然我同意它使用时间作为种子,但攻击者必须获得以下数据才能真正得到他想要的:
  1. 代码种子时的毫秒级精确时间
  2. 到目前为止发生的调用次数
  3. 自己调用的原子性(以使调用次数保持不变)
如果攻击者拥有这三个条件,那么你手头的问题就更大了...
- Ajeet Ganga
4
Gradle 依赖项:compile 'commons-lang:commons-lang:2.6'这行代码表示使用 Gradle 构建工具的项目将会引用 Commons Lang 库的版本 2.6,用于提供一些常用的、易于操作字符串、数组等功能的代码库。 - younes0
5
@Ajeet,这不是真的。您可以从随机数生成器的输出中推导出其状态。如果攻击者可以生成几千个调用以生成随机API令牌,则攻击者将能够预测所有未来的API令牌。 - Thomas Grainger
5
与过度工程无关。如果您想创建会话ID,您需要一个加密的伪随机生成器。每个使用时间作为种子的PRNG都是可预测的,并且用于应该是不可预测的数据非常不安全。只需使用“SecureRandom”,就可以了解决这个问题。 - Patrick
显示剩余4条评论

115

2
它对我也有帮助,但只能使用十六进制数字 :( - noquery
@Zippoxer,你可以多次连接它 =) - daniel.bavrin
7
楼主的例子展示了一个字符串 AEYGF7K0DM1X,这不是十六进制。让我感到担忧的是人们经常将字母数字与十六进制混淆,它们并不是同一件事情。 - hfontanez
@daniel.bavrin,Zippoxer的意思是十六进制字符串只有6个字母(ABCDEF)。他并不是在谈论长度,无论你连接多少次都没有关系。 - jcesarmobile
9
鉴于字符串长度,这不太符合随机性应有的水平。因为Math.random()生成0到1之间的double类型数字,指数部分很少被使用。建议使用random.nextLong生成随机的long数字,而不是采用这种丑陋的方法。 - maaartinus

91

这可以轻松实现,无需任何外部库。

1. 密码学伪随机数据生成(PRNG)

首先你需要一个密码学的伪随机数生成器(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的UUID2”。我们将选择128位,因为它恰好适合16字节,并且被认为对于基本上每个使用案例都是非常充足的唯一性,而且您不必考虑重复。这是一个包括简单分析的熵比较表生日问题

Comparison of token sizes

对于简单的要求,8或12字节的长度可能足够,但使用16字节更加“安全”。
基本上就是这样。最后一件事是考虑编码,以便可以表示为可打印的文本(即String)。

3. 二进制到文本编码

典型的编码包括:
  • Base64每个字符编码6位,创建了33%的额外开销。幸运的是,在Java 8+Android中有标准实现。对于旧版本的Java,您可以使用任何众多的第三方库。如果您希望您的令牌在URL中安全使用,请使用RFC4648的URL安全版本(大多数实现通常都支持)。例如,使用填充编码16字节:XfJhfv3C0P6ag7y9VQxSbw==

  • Base32每个字符编码5位,创建了40%的额外开销。它将使用A-Z2-7,使其在占用空间方面相当高效,同时又不区分大小写的字母数字。在JDK中没有任何标准实现。例如,不使用填充编码16字节:WUPIL5DQTZGMF4D3NX5L7LNFOY

  • Base16(十六进制)每个字符编码四位,每个字节需要两个字符(即,16字节创建一个长度为32的字符串)。因此,十六进制比Base32占用更多的空间,但在大多数情况下(URL)使用是安全的,因为它只使用0-9AF。例如,编码16字节:4fa3dd0f57cb3bf331441ed285b27735在这里查看有关转换为十六进制的Stack Overflow讨论

附加的编码方式,如Base85和奇特的Base122,具有更好/更差的空间效率。您可以创建自己的编码(基本上这个帖子中的大多数答案都是如此),但我建议您不要这样做,除非您有非常具体的要求。在维基百科文章中查看更多编码方案
4. 总结和示例
- 使用SecureRandom - 使用至少16字节(2^128)的可能值 - 根据您的要求进行编码(通常使用hexbase32,如果您需要它是字母数字) 不要
  • ... 使用自定义编码:如果其他人能看到你使用的标准编码而不是奇怪的循环逐个创建字符,那么代码更易于维护和阅读。
  • ... 使用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); // Hexadecimal encoding (omits leading zeros)
}

//generateRandomHexToken(16) -> 2189df7475e96aa3982dbeab266497cd

示例:Base64令牌生成器(URL安全)
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命令行工具

如果您想要一个现成的命令行工具,您可以使用dice

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

如果您已经拥有一个ID(例如,在实体中使用了一个合成的long),但是不想公开内部值, 您可以使用这个库对其进行加密和混淆:https://github.com/patrickfav/id-mask

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

3
这个答案完整且没有添加任何依赖项即可运行。如果您想避免输出中可能出现的负号,可以使用构造函数参数来避免负的 BigInteger:使用 BigInteger(1, token) 替代 BigInteger(token) - francoisr
感谢@francoisr的提示,我编辑了代码示例。 - Patrick
需要导入import java.security.SecureRandom;import java.math.BigInteger;才能使示例正常工作,但它运行得非常好! - anothermh
好的回答,但/dev/random是一种阻塞方法,这就是为什么如果熵太低,它会变得非常缓慢而被阻塞的原因。更好和非阻塞的方法是/dev/urandom。可以通过<jre>/lib/security/java.security进行配置,并设置securerandom.source=file:/dev/./urandom。 - Muzammil
@Muzammil 请查看https://tersesystems.com/blog/2015/12/17/the-right-way-to-use-securerandom/(也在答案中链接) - new SecureRandom()使用/dev/urandom - Patrick
显示剩余2条评论

42

使用 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

可以在shuffle中使用SecureRandom吗? - iwein

36

这是 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

4
这将产生不安全的序列,即容易被猜测的序列。 - Yuriy Nakonechnyy
9
这种双层随机整数生成的方式在设计上就是有问题的,而且速度慢,难以阅读。可以使用 Random#nextInt 或者 nextLong 代替。如果需要更高的安全性可以考虑切换到 SecureRandom - maaartinus

35

一种简短而易于实现的解决方案,但它只使用小写字母和数字:

Random r = new java.util.Random ();
String s = Long.toString (r.nextLong () & Long.MAX_VALUE, 36);

这个大小是基于36进制的12位数字,无法再进一步提高。当然你可以添加多个实例。


11
请记住,结果前面有50%的可能出现负号!如果你不想要负号,可以将r.nextLong()用Math.abs()包装起来:Long.toString(Math.abs(r.nextLong()), 36); - Ray Hulha
5
如果你不想要负号,那么应该割掉它,因为令人惊讶的是,Math.abs对于Long.MIN_VALUE返回一个负数。 - user unknown
有趣的是Math.abs返回负数。更多信息请参见:http://bmaurer.blogspot.co.nz/2006/10/mathabs-returns-negative-number.html - Phil
1
使用位运算符清除最高有效位可以解决abs的问题。这对所有值都适用。 - Radiodef
1
@Radiodef 这本质上就是@userunkown所说的。我想你也可以使用<< 1 >>> 1 - shmosel

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