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

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


当前回答

这在没有任何外部库的情况下很容易实现。

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 UUID 2”。我们将选择128位,因为它正好适合16个字节,而且对于基本上每一个但最极端的用例来说,它都是唯一的,而且您不必考虑重复。这是一个简单的熵比较表,包括生日问题的简单分析。

对于简单的需求,8或12字节的长度可能就足够了,但对于16字节,您处于“安全侧”。

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

3.二进制到文本编码

典型编码包括:

Base64每个字符编码6位,产生33%的开销。幸运的是,Java 8+和Android中有标准实现。对于较旧的Java,您可以使用众多第三方库中的任何一个。如果您希望令牌是URL安全的,请使用RFC4648的URL安全版本(大多数实现通常支持该版本)。使用填充编码16个字节的示例:XfJhfv3C0P6ag7y9VQxSbw==Base32每个字符编码5位,产生40%的开销。这将使用A-Z和2-7,使其具有合理的空间效率,同时不区分大小写。JDK中没有任何标准实现。无填充编码16字节的示例:WUPIL5DQZGMF4D3NX5L7LNFOYBase16(十六进制)每个字符编码四位,每个字节需要两个字符(即,16字节创建长度为32的字符串)。因此,十六进制的空间效率低于Base32,但在大多数情况下(URL)使用它是安全的,因为它只使用0-9和A到F。示例编码16个字节:4fadd0f57cb3bf331441ed285b27735。请参阅此处有关转换为十六进制的堆栈溢出讨论。

诸如Base85和奇异Base122之类的附加编码具有更好/更差的空间效率。您可以创建自己的编码(本主题中的大多数答案都是这样做的),但如果您没有非常具体的要求,我建议您不要这样做。请参阅维基百科文章中的更多编码方案。

4.总结和示例

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

不要

……使用您自己编写的编码:如果其他人看到您使用的标准编码,而不是每次创建字符时使用的奇怪循环,则可以更好地维护和阅读。…使用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
}

//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 CLI工具

如果您想要现成的CLI工具,可以使用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);

其他回答

您提到“简单”,但如果其他人正在寻找符合更严格安全要求的产品,您可能需要看看jpwgen。jpwgen在Unix中以pwgen为模型,非常可配置。

我正在使用Apache Commons的库生成一个字母数字字符串:

import org.apache.commons.lang3.RandomStringUtils;

String keyLength = 20;
RandomStringUtils.randomAlphanumeric(keylength);

它又快又简单!

import java.util.Random;

public class passGen{
    // Version 1.0
    private static final String dCase = "abcdefghijklmnopqrstuvwxyz";
    private static final String uCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final String sChar = "!@#$%^&*";
    private static final String intChar = "0123456789";
    private static Random r = new Random();
    private static StringBuilder pass = new StringBuilder();

    public static void main (String[] args) {
        System.out.println ("Generating pass...");
        while (pass.length () != 16){
            int rPick = r.nextInt(4);
            if (rPick == 0){
                int spot = r.nextInt(26);
                pass.append(dCase.charAt(spot));
            } else if (rPick == 1) {
                int spot = r.nextInt(26);
                pass.append(uCase.charAt(spot));
            } else if (rPick == 2) {
                int spot = r.nextInt(8);
                pass.append(sChar.charAt(spot));
            } else {
                int spot = r.nextInt(10);
                pass.append(intChar.charAt(spot));
            }
        }
        System.out.println ("Generated Pass: " + pass.toString());
    }
}

这只是将密码添加到字符串中。。。是的,效果很好。过来看。。。这很简单;我写的。

在一行中:

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

来源:Java-生成随机字符串

您可以在没有外部库的情况下在一行中完成此操作。

int length = 12;
String randomString = new Random().ints(48, 122).filter(i -> (i < 58 || i > 64) && (i < 91 || i > 96)).limit(length).collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
System.out.print(randomString);

我将长度分隔成一个参数,并添加了一行以打印结果。

此代码创建一个以字母数字ascii范围为界的随机整数流。然后它过滤掉一些符号,因为字母数字范围不是连续的。然后它限制长度并将结果收集到字符串中。

因为这种方法丢弃了它生成的大约20%的数字/字符(因为它们是符号),所以对性能的影响很小。

我不觉得它特别可读,但我认为没有人在一行中提出过本机Java解决方案。