在 Java 中生成具有最低特殊字符要求的安全随机密码

Generate a Secure Random Password in Java with Minimum Special Character Requirements

如何在Java中创建一个满足系统长度和字符集要求的随机密码?

我必须创建一个长度为 10-14 个字符且至少包含一个大写字母、一个小写字母和一个特殊字符的随机密码。不幸的是,一些特殊字符 特殊而不能使用,所以我不能只使用打印的 ASCII。

本网站上的许多示例生成的随机密码或会话密钥在字符中没有足够的熵,或者在商业环境中没有像上面给出的那样的现实要求,所以我问更尖锐的问题以获得更好的答案。

我的字符集,标准美式键盘上的每个特殊字符,除了 space:

A-Z
a-z
0-9
~`!@#$%^&*()-_=+[{]}\|;:'",<.>/?

我最近了解到 Passay. It provides the required functionality needed in its PasswordGenerator class。它随机生成满足要求的密码,类似于下面使用 CharacterRules 而不是 PasswordCharacterSets 所写的要求,就像我在下面所做的那样。它不是为随机字符插入保存未使用的索引列表,而是在插入符合要求的字符后简单地洗牌字符缓冲区。

下面是之前遗留下来的,如果您的许可允许,我建议使用 Passay,否则这段代码应该可以工作,并提供了为什么生成的密码在密码学上很强大的详细信息

这段代码我写了两次。曾经得到一个随机字符结果,但事实证明字符的分布取决于字符集的大小(糟糕!)。我重写了它,现在您只需 copy/paste 代码并将 Main.java 更改为您想要的字符集。尽管可以采用不同的方式,但我认为这是获得正确结果的相对直接的方法,我鼓励重复使用、评论、批评和深思熟虑的编辑。

PasswordGenerator代码控件如下:

  • Min/Max长度:使用随机数设置
  • PasswordCharacterSet: 假设所有传递给 PasswordGenerator 的 PasswordCharacterSets 都由唯一的字符集组成,否则,随机字符将偏向重复项。
  • PasswordCharacterSet Min Characters:用于此字符集的最少字符数。

实际密码生成的主要部分:

  • Random 的随机性:我们使用的是 SecureRandom,它由强大的加密 PRNG 支持,而不是 Random class,后者不是。
  • 密码随机字符顺序: pw char数组的所有索引都添加到remainingIndexes数组中。当我们调用 addRandomCharacters 时,它会随机删除一个索引,我们使用删除的索引来填充数组。
  • 随机字符:在 addRandomCharacters 中,从我们使用的字符索引中选择一个随机索引并将其添加到 pw 数组。
  • 设置了每种类型的保证最小字符数:我们简单地先划出最小字符数。我们从每个字符集中选择最小数量的随机值,然后继续。
  • 其余字符随机分布:设置最小值后,我们希望其余字符在所有字符集中随机分布。所有字符都添加到一个数组中。剩余的槽位使用之前字符集相同的策略填充。

密码复杂度说明:密码​​复杂度通常用位熵来表示。以下是您的键空间的可能性数量:

至少有一个大写字母字符(共 26 个)、一个小写字母字符(共 26 个)、一位数字(共 10 个)和一个特殊字符(共 32 个),您的方式计算可能性的数量是每个字符的可能性数乘以字符数,因为它们是随机放置在字符串中的。所以我们知道四个字符的可能性是:

Required Characters = 26*26*10*32=216,320

所有剩余的字符每个都有94(26+26+10+32)种可能性

我们的计算是:

Characters  Possibilities                                       Bits of Entropy
10 chars    216,320*94^6  = 149,232,631,038,033,920             ~2^57
11 chars    216,320*94^7  = 14,027,867,317,575,188,480          ~2^63
12 chars    216,320*94^8  = 1,318,619,527,852,067,717,120       ~2^70
13 chars    216,320*94^9  = 123,950,235,618,094,365,409,280     ~2^76
14 chars    216,320*94^10 = 11,651,322,148,100,870,348,472,320  ~2^83

考虑到这一点,如果您想要最安全的密码,您应该始终选择尽可能多的字符,在本例中为 14。

Main.java

package org.redtown.pw;

import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;

import org.redtown.pw.PasswordGenerator.PasswordCharacterSet;

public class Main {

    public static void main(String[] args) {
        Set<PasswordCharacterSet> values = new HashSet<PasswordCharacterSet>(EnumSet.allOf(SummerCharacterSets.class));
        PasswordGenerator pwGenerator = new PasswordGenerator(values, 10, 14);
        for(int i=0; i < 10; ++i) {
            System.out.println(pwGenerator.generatePassword());
        }
    }

    private static final char[] ALPHA_UPPER_CHARACTERS = { 'A', 'B', 'C', 'D',
            'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q',
            'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };
    private static final char[] ALPHA_LOWER_CHARACTERS = { 'a', 'b', 'c', 'd',
            'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
            'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };
    private static final char[] NUMERIC_CHARACTERS = { '0', '1', '2', '3', '4',
            '5', '6', '7', '8', '9' };
    private static final char[] SPECIAL_CHARACTERS = { '~', '`', '!', '@', '#',
            '$', '%', '^', '&', '*', '(', ')', '-', '_', '=', '+', '[', '{',
            ']', '}', '\', '|', ';', ':', '\'', '"', ',', '<', '.', '>', '/',
            '?' };

    private enum SummerCharacterSets implements PasswordCharacterSet {
        ALPHA_UPPER(ALPHA_UPPER_CHARACTERS, 1),
        ALPHA_LOWER(ALPHA_LOWER_CHARACTERS, 1),
        NUMERIC(NUMERIC_CHARACTERS, 1),
        SPECIAL(SPECIAL_CHARACTERS, 1);

        private final char[] chars;
        private final int minUsage;

        private SummerCharacterSets(char[] chars, int minUsage) {
            this.chars = chars;
            this.minUsage = minUsage;
        }

        @Override
        public char[] getCharacters() {
            return chars;
        }

        @Override
        public int getMinCharacters() {
            return minUsage;
        }
    }
}

PasswordGenerator.java

package org.redtown.pw;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Random;

public class PasswordGenerator {

    private final List<PasswordCharacterSet> pwSets;
    private final char[] allCharacters;
    private final int minLength;
    private final int maxLength;
    private final int presetCharacterCount;

    public PasswordGenerator(Collection<PasswordCharacterSet> origPwSets, int minLength, int maxLength) {
        this.minLength = minLength;
        this.maxLength = maxLength;

        // Make a copy of the character arrays and min-values so they cannot be changed after initialization
        int pwCharacters = 0;
        int preallocatedCharacters = 0;
        List<PasswordCharacterSet> pwSets = new ArrayList<PasswordCharacterSet>(origPwSets.size());
        for(PasswordCharacterSet origpwSet : origPwSets) {
            PasswordCharacterSet newPwSet = new PwSet(origpwSet);
            pwSets.add(newPwSet);
            pwCharacters += newPwSet.getCharacters().length;
            preallocatedCharacters += newPwSet.getMinCharacters();
        }
        this.presetCharacterCount = preallocatedCharacters;
        this.pwSets = Collections.unmodifiableList(pwSets);

        if (minLength < presetCharacterCount) {
            throw new IllegalArgumentException("Combined minimum lengths "
                    + presetCharacterCount
                    + " are greater than the minLength of " + minLength);
        }

        // Copy all characters into single array so we can evenly access all members when accessing this array
        char[] allChars = new char[pwCharacters];
        int currentIndex = 0;
        for(PasswordCharacterSet pwSet : pwSets) {
            char[] chars = pwSet.getCharacters();
            System.arraycopy(chars, 0, allChars, currentIndex, chars.length);
            currentIndex += chars.length;
        }
        this.allCharacters = allChars;
    }


    public char[] generatePassword() {
        SecureRandom rand = new SecureRandom();

        // Set pw length to minLength <= pwLength <= maxLength
        int pwLength = minLength + rand.nextInt(maxLength - minLength + 1);
        int randomCharacterCount = pwLength - presetCharacterCount;


        // Place each index in an array then remove them randomly to assign positions in the pw array
        List<Integer> remainingIndexes = new ArrayList<Integer>(pwLength);
        for(int i=0; i < pwLength; ++i) {
            remainingIndexes.add(i);
        }

        // Fill pw array
        char[] pw = new char[pwLength];
        for(PasswordCharacterSet pwSet : pwSets) {
            addRandomCharacters(pw, pwSet.getCharacters(), pwSet.getMinCharacters(), remainingIndexes, rand);
        }
        addRandomCharacters(pw, allCharacters, randomCharacterCount, remainingIndexes, rand);
        return pw;
    }

    private static void addRandomCharacters(char[] pw, char[] characterSet,
            int numCharacters, List<Integer> remainingIndexes, Random rand) {
        for(int i=0; i < numCharacters; ++i) {
            // Get and remove random index from the remaining indexes
            int pwIndex = remainingIndexes.remove(rand.nextInt(remainingIndexes.size()));

            // Set random character from character index to pwIndex
            int randCharIndex = rand.nextInt(characterSet.length);
            pw[pwIndex] = characterSet[randCharIndex];
        }
    }

    public static interface PasswordCharacterSet {
        char[] getCharacters();
        int getMinCharacters();
    }

    /**
     * Defensive copy of a passed-in PasswordCharacterSet
     */
    private static final class PwSet implements PasswordCharacterSet {
        private final char[] chars;
        private final int minChars;

        public PwSet(PasswordCharacterSet pwSet) {
            this.minChars = pwSet.getMinCharacters();
            char[] pwSetChars = pwSet.getCharacters();
            // Defensive copy
            this.chars = Arrays.copyOf(pwSetChars, pwSetChars.length);
        }

        @Override
        public char[] getCharacters() {
            return chars;
        }

        @Override
        public int getMinCharacters() {
            return minChars;
        }
    }
}

我建议使用 apache commons RandomStringUtils。使用已经完成的东西。

String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^&*()-_=+[{]}\|;:\'\",<.>/?";
String pwd = RandomStringUtils.random( 15, characters );
System.out.println( pwd );

如果你使用的是 maven

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
</dependency>

否则下载 jar

更新 具有安全随机性的版本。所以剩下的所需字符的问题可以像评论中那样解决,分别生成所需的部分和正常的部分。然后随机加入他们。

char[] possibleCharacters = (new String("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^&*()-_=+[{]}\|;:\'\",<.>/?")).toCharArray();
String randomStr = RandomStringUtils.random( randomStrLength, 0, possibleCharacters.length-1, false, false, possibleCharacters, new SecureRandom() );
System.out.println( randomStr );

这是一个仅使用 vanilla Java 并实现要求的实用程序。它基本上获得了每个必需的字符集之一。然后用整个集合中的随机字符填充其余部分。然后全部洗牌。

public class PasswordUtils {

    static char[] SYMBOLS = "^$*.[]{}()?-\"!@#%&/\,><':;|_~`".toCharArray();
    static char[] LOWERCASE = "abcdefghijklmnopqrstuvwxyz".toCharArray();
    static char[] UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
    static char[] NUMBERS = "0123456789".toCharArray();
    static char[] ALL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789^$*.[]{}()?-\"!@#%&/\,><':;|_~`".toCharArray();
    static Random rand = new SecureRandom();

    public static String getPassword(int length) {
        assert length >= 4;
        char[] password = new char[length];

        //get the requirements out of the way
        password[0] = LOWERCASE[rand.nextInt(LOWERCASE.length)];
        password[1] = UPPERCASE[rand.nextInt(UPPERCASE.length)];
        password[2] = NUMBERS[rand.nextInt(NUMBERS.length)];
        password[3] = SYMBOLS[rand.nextInt(SYMBOLS.length)];

        //populate rest of the password with random chars
        for (int i = 4; i < length; i++) {
            password[i] = ALL_CHARS[rand.nextInt(ALL_CHARS.length)];
        }

        //shuffle it up
        for (int i = 0; i < password.length; i++) {
            int randomPosition = rand.nextInt(password.length);
            char temp = password[i];
            password[i] = password[randomPosition];
            password[randomPosition] = temp;
        }

        return new String(password);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(getPassword(8));
        }
    }
}

使用 rt.jar 的 java.util 包的随机功能,我们可以创建任意长度的随机密码。下面是相同的代码段。

public class GeneratePassword {

public static void main(String[] args)
{
        int length = 10;
        String symbol = "-/.^&*_!@%=+>)"; 
        String cap_letter = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 
        String small_letter = "abcdefghijklmnopqrstuvwxyz"; 
        String numbers = "0123456789"; 


        String finalString = cap_letter + small_letter + 
                numbers + symbol; 

        Random random = new Random(); 

        char[] password = new char[length]; 

        for (int i = 0; i < length; i++) 
        { 
            password[i] = 
                    finalString.charAt(random.nextInt(finalString.length())); 

        } 
        System.out.println(password);
}

}

我知道这是一个老问题,但也许我的解决方案可以帮助遇到同样问题的人。 我正在使用 RandomStringUtils 作为密码生成器,然后我使用正则表达式检查密码是否满足条件(至少一个符号、一个大写字母、一个小写字母和一个数字以及 8 个字符长),如果没有,我再次调用密码递归生成器,直到条件不满足。我可以说该方法肯定不会被调用超过 3 次!

public String generatePassword() {
    String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@!#$%&";
    String password = RandomStringUtils.random( 8, characters );

    String regex = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@!#$%&])(?=\S+$).{8,}$";
    Pattern pattern = Pattern.compile( regex );
    Matcher matcher = pattern.matcher( password );

    if (matcher.matches()) {
        return password;
    } else {
        return generatePassword(); // recursion
    }
}

Maven 依赖项:

<dependency>
   <groupId>org.apache.commons</groupId>
   <artifactId>commons-lang3</artifactId>
   <version>3.11</version>
</dependency>
    public static final Character[] ALPHA_UPPER_CHARACTERS = {'A', 'B', 'C', 'D',
                'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q',
                'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
        public static final Character[] ALPHA_LOWER_CHARACTERS = {'a', 'b', 'c', 'd',
                'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
                'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
        public static final Character[] NUMERIC_CHARACTERS = {'0', '1', '2', '3', '4',
                '5', '6', '7', '8', '9'};
        public static final Character[] SPECIAL_CHARACTERS = {'@', '#',
                '$', '%', '^', '&', '*', '|', ';', ':', '?'};  
**Note: I copied char set from @summer** 
    
    
    final List<Character[]> charSets = new ArrayList<>();
        charSets.add(Constant.ALPHA_UPPER_CHARACTERS);
                charSets.add(Constant.ALPHA_LOWER_CHARACTERS);
                charSets.add(Constant.NUMERIC_CHARACTERS);
                charSets.add(Constant.SPECIAL_CHARACTERS);
        
        
        
        
         public String getFilterPassword() {
                    StringBuilder passBuilder = new StringBuilder();
                    final int charSetLen = charSets.size();
                    for (int i = 0; i < 10; i++) {
                        int randomLength = new Random().nextInt(charSetLen - 1);
                        Character[] newAlpha = charSets.get(randomLength);
                        int randomSetLen = newAlpha.length;
            
                        int randomAlphaLen = new Random().nextInt(randomSetLen - 1);
            
                        passBuilder.append(newAlpha[randomAlphaLen]);
                    }
                    return passBuilder.toString();
                }