我一直在寻找一种简单的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);

其他回答

使用美元应简单如下:

// "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

最佳随机字符串生成器方法

public class RandomStringGenerator{

    private static int randomStringLength = 25 ;
    private static boolean allowSpecialCharacters = true ;
    private static String specialCharacters = "!@$%*-_+:";
    private static boolean allowDuplicates = false ;

    private static boolean isAlphanum = false;
    private static boolean isNumeric = false;
    private static boolean isAlpha = false;
    private static final String alphabet = "abcdefghijklmnopqrstuvwxyz";
    private static boolean mixCase = false;
    private static final String capAlpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final String num = "0123456789";

    public static String getRandomString() {
        String returnVal = "";
        int specialCharactersCount = 0;
        int maxspecialCharacters = randomStringLength/4;

        try {
            StringBuffer values = buildList();
            for (int inx = 0; inx < randomStringLength; inx++) {
                int selChar = (int) (Math.random() * (values.length() - 1));
                if (allowSpecialCharacters)
                {
                    if (specialCharacters.indexOf("" + values.charAt(selChar)) > -1)
                    {
                        specialCharactersCount ++;
                        if (specialCharactersCount > maxspecialCharacters)
                        {
                            while (specialCharacters.indexOf("" + values.charAt(selChar)) != -1)
                            {
                                selChar = (int) (Math.random() * (values.length() - 1));
                            }
                        }
                    }
                }
                returnVal += values.charAt(selChar);
                if (!allowDuplicates) {
                    values.deleteCharAt(selChar);
                }
            }
        } catch (Exception e) {
            returnVal = "Error While Processing Values";
        }
        return returnVal;
    }

    private static StringBuffer buildList() {
        StringBuffer list = new StringBuffer(0);
        if (isNumeric || isAlphanum) {
            list.append(num);
        }
        if (isAlpha || isAlphanum) {
            list.append(alphabet);
            if (mixCase) {
                list.append(capAlpha);
            }
        }
        if (allowSpecialCharacters)
        {
            list.append(specialCharacters);
        }
        int currLen = list.length();
        String returnVal = "";
        for (int inx = 0; inx < currLen; inx++) {
            int selChar = (int) (Math.random() * (list.length() - 1));
            returnVal += list.charAt(selChar);
            list.deleteCharAt(selChar);
        }
        list = new StringBuffer(returnVal);
        return list;
    }   

}

使用UUID是不安全的,因为UUID的一部分根本不是随机的。erickson的过程非常简单,但它不会创建相同长度的字符串。以下代码片段应该足够了:

/*
 * The random generator used by this class to create random keys.
 * In a holder class to defer initialization until needed.
 */
private static class RandomHolder {
    static final Random random = new SecureRandom();
    public static String randomKey(int length) {
        return String.format("%"+length+"s", new BigInteger(length*5/*base 32,2^5*/, random)
            .toString(32)).replace('\u0020', '0');
    }
}

为什么选择长度*5?让我们假设一个长度为1的随机字符串的简单情况,即一个随机字符。要获得包含所有数字0-9和字符a-z的随机字符,我们需要一个介于0和35之间的随机数来获得每个字符中的一个。

BigInteger提供了一个构造函数来生成一个随机数,均匀分布在0到(2^numBits-1)的范围内。不幸的是,35不是一个可以由2^numBits-1接收的数字。

所以我们有两个选择:要么选择2^5-1=31,要么选择2^ 6-1=63。如果我们选择2^6,我们会得到很多“不必要”/“更长”的数字。因此,2^5是更好的选择,即使我们丢失了四个字符(w-z)。现在要生成特定长度的字符串,我们可以简单地使用2^(length*numBits)-1数字。最后一个问题是,如果我们想要一个具有一定长度的字符串,随机可能会生成一个小数字,因此长度不符合要求,因此我们必须将字符串填充到所需的长度,并加上前缀零。

我找到了生成随机十六进制编码字符串的解决方案。所提供的单元测试似乎符合我的主要用例。虽然,它比提供的一些其他答案稍微复杂一些。

/**
 * Generate a random hex encoded string token of the specified length
 *  
 * @param length
 * @return random hex string
 */
public static synchronized String generateUniqueToken(Integer length){ 
    byte random[] = new byte[length];
    Random randomGenerator = new Random();
    StringBuffer buffer = new StringBuffer();

    randomGenerator.nextBytes(random);

    for (int j = 0; j < random.length; j++) {
        byte b1 = (byte) ((random[j] & 0xf0) >> 4);
        byte b2 = (byte) (random[j] & 0x0f);
        if (b1 < 10)
            buffer.append((char) ('0' + b1));
        else
            buffer.append((char) ('A' + (b1 - 10)));
        if (b2 < 10)
            buffer.append((char) ('0' + b2));
        else
            buffer.append((char) ('A' + (b2 - 10)));
    }
    return (buffer.toString());
}

@Test
public void testGenerateUniqueToken(){
    Set set = new HashSet();
    String token = null;
    int size = 16;

    /* Seems like we should be able to generate 500K tokens 
     * without a duplicate 
     */
    for (int i=0; i<500000; i++){
        token = Utility.generateUniqueToken(size);

        if (token.length() != size * 2){
            fail("Incorrect length");
        } else if (set.contains(token)) {
            fail("Duplicate token generated");
        } else{
            set.add(token);
        }
    }
}
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();
}