指定长度与格式密码生成

197 阅读6分钟
  • 问题:在开发过程中,需要设置一些初始化账户来供用户操作使用。这时候就需要为账户设置密码,为了让密码尽量随机,可以采用程序随机生成的方式,生成满足一定要求的密码。

常见密码要求

1.  至少包含一个英文字母和一个数字;
2.  至少包含一个大写字母,一个小写字母和一个数字;
3.  至少包含一个字母,一个数字和一个特殊字符;
4.  至少包含一个大写字母,一个小写字母,一个数字和一个特殊字符。

设计思路

  • 明确密码常见格式,以及对应正则表达式;随机生成密码,所以使用Random类,用来生成随机数。
  • 需要生成固定长度、固定格式的密码,这里首先获取密码的长度,之后便可以根据对应格式的格式来生成密码。

至少包含一个英文字母和一个数字

首先创建两个集合,分别包含大小写英文字母(LOWER_AND_UPPER_LETTERS),数字(NUMBERS)。

假设生成密码的长度为8.

数组创建完毕后,首先生成范围为[1,7]的随机数,预留以为英文字母,这里假设生成的随机数为6,那么就创建一个循环6次的for循环,每一次循环,都在数组NUMBERS中随机取一个数值,这样循环完毕后,就有了6个随机数;

之后创建一个循环2次的for循环,每次循环在数组LOWER_AND_UPPER_LETTERS中随机取一个字符,循环结束后,就有了2个随机字符。

将循环后得到的两部分字符数组合并为一个字符数组,并调用Collections.shuffle方法对数组进行打乱,这样就得到了至少包含一个英文字母和一个数字,且长度为8位的密码。

其他格式

对于其他格式的密码生成策略,也是采用以上的思路进行生成。比如至少包含一个一个字母,一个数字和一个特殊字符,这时候密码就有三部分:

假设密码长度为8。

  1. 第一部分,随机数范围为[1,6],假设生成的值为first
  2. 第二部分,随机数范围为[1,8-first-1],假设生成的值为second
  3. 第三部分,长度为8-first-second

四部分的密码思路如下:

  1. 第一部分,随机数范围为[1,5],假设生成的值为first
  2. 第二部分,随机数范围为[1,8-first-2],假设生成的值为second
  3. 第三部分,随机数范围为[1,8-first-second-2],假设生成的值为third
  4. 第四部分,长度为8-first-second-third

之后每一部分都循环对应的次数,每一次循环都随机取给定格式字符数组中的任一个字符,将各个部分合并后,再进行打乱,这样就得到了符合要求的密码。

密码格式

  • 预设密码的最短长度为8个字符
1. 至少包含一个英文字母和一个数字:
    ^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$
2. 至少包含一个大写字母,一个小写字母和一个数字:
    ^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$
3. 至少包含一个字母,一个数字和一个特殊字符:
    ^(?=.*[A-Za-z])(?=.*\\d)(?=.*[\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"])[A-Za-z\\d\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"]{8,}$
4. 至少包含一个大写字母,一个小写字母,一个数字和一个特殊字符:
    ^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"])[a-zA-Z0-9\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"]{8,}$

代码实现

import com.google.common.collect.Lists;  
  
import java.util.ArrayList;  
import java.util.Collections;  
import java.util.List;  
import java.security.SecureRandom; 
import java.util.stream.Collectors;  
import java.util.stream.Stream;  
  
/**  
* @author zmy  
* @date 2023-10-30 15:34:16  
*/  
public class PasswordGenerator {  
  
    protected static final List<Character> NUMBERS = Lists.newArrayList('0', '1', '2', '3', '4', '5', '6', '7', '8', '9');  
    protected static final List<Character> LOWER_LETTERS = Lists.newArrayList('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');  
    protected static final List<Character> UPPER_LETTERS = Lists.newArrayList('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');  
    protected static final List<Character> LOWER_AND_UPPER_LETTERS = mergeLists(LOWER_LETTERS, UPPER_LETTERS);  
    protected static final List<Character> SPECIAL_CHARACTERS = Lists.newArrayList('!', '"', '#', '$', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~');  
  
    /**  
    * 生成给定长度的随机密码,至少一个数字,至少一个小写字母,至少一个大写字母,至少一个字符  
    * Format: ^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[\\!@#$%^&*()_+-=\[\]{};':|,.<>/?~`"])[a-zA-Z0-9\\!@#$%^&*()_+-=\[\]{};':|,.<>/?~`"]{4,}$  
    * <p>  
    * 例如: VTQ0[vP'u|3<  
    *  
    * @param length 密码长度  
    * @return 随机密码  
    */  
    public static String generatePassword(int length) {  
        return generatePassword(length, true, true);  
    }  
  
    /**  
    * 生成给定长度的随机密码  
    *  
    * @param length 密码长度  
    * @param useSpecialCharacters 是否使用特殊字符  
    * @return 随机面  
    */  
    public static String generatePassword(int length, boolean useSpecialCharacters) {  
        // 随机生成数字列表(预留3位,大小写字母和特殊字符长度)  
        List<Character> numbers = getSpecifiedTypeLetters(NUMBERS, getRandomLength(length - 3));  
        // 随机生成小写字母列表(预留2位,大写字母和特殊字符长度)  
        List<Character> lowerLetters = getSpecifiedTypeLetters(LOWER_LETTERS, getRandomLength(length - numbers.size() - 2));  
        // 随机生成大写字母和特殊字符列表  
        final int lettersSize = numbers.size() + lowerLetters.size();  
        final List<Character> upperAndSpecial = hasSpecialLetters(useSpecialCharacters, length, lettersSize, UPPER_LETTERS);  
        final List<Character> passwordList = mergeLists(numbers, lowerLetters, upperAndSpecial);  

        return getRandomPassword(passwordList);  
    }  
  
    /**  
    * 生成给定长度的随机密码  
    *  
    * @param length 密码长度  
    * @param useSpecialCharacters 是否使用特殊字符  
    * @param isCaseSensitive 是否区分大小写(包含字母,包含大写或小写,或都包含)  
    * @return 随机密码  
    */  
    public static String generatePassword(int length, boolean useSpecialCharacters, boolean isCaseSensitive) {  
        if (isCaseSensitive) {  
        return generatePassword(length, useSpecialCharacters);  
        }  
        // 不区分大小写,生成随机数字列表,预留2位(字母,特殊字符)  
        List<Character> numbers = getSpecifiedTypeLetters(NUMBERS, getRandomLength(length - 2));  
        final List<Character> letterAndSpecial = hasSpecialLetters(useSpecialCharacters, length, numbers.size(), LOWER_AND_UPPER_LETTERS);  
        final List<Character> passwordList = mergeLists(numbers, letterAndSpecial);  

        return getRandomPassword(passwordList);  
    }  
  
    /**  
    * 用给定列表生成随机面  
    *  
    * @param password 密码字符列表  
    * @return the random password  
    */  
    private static String getRandomPassword(List<Character> password) {  
        Collections.shuffle(password);  
        return password.stream().map(Object::toString).collect(Collectors.joining());  
    }  

    /**  
    * 生成包含或者不包含特殊字符的列表  
    *  
    * @param useSpecialCharacters 是否使用特殊字符  
    * @param length 密码长度  
    * @param lettersSize 其他字符长度  
    * @param englishLetters 英文字母列表(小写或大写或小写和大写)  
    * @return 生成包含或者不包含特殊字符的列表  
    */  
    private static List<Character> hasSpecialLetters(boolean useSpecialCharacters, int length, int lettersSize, List<Character> englishLetters) {  
        List<Character> specialLetters;  
        List<Character> upperLetters;  
        if (useSpecialCharacters) {  
        // 随机生成大写字母列表(预留1位,特殊字符长度)  
        upperLetters = getSpecifiedTypeLetters(englishLetters, getRandomLength(length - lettersSize - 1));  
        // 随机生成特殊字符列表  
        specialLetters = getSpecifiedTypeLetters(SPECIAL_CHARACTERS, length - lettersSize - upperLetters.size());  
        return mergeLists(upperLetters, specialLetters);  
        }  
        return getSpecifiedTypeLetters(englishLetters, length - lettersSize);  
    }  

    /**  
    * 合并列表  
    *  
    * @param lists 要合并的俩表  
    * @return 合并列表  
    */  
    @SafeVarargs  
    private static List<Character> mergeLists(List<Character>... lists) {  
        return Stream.of(lists).flatMap(List::stream).collect(Collectors.toList());  
    }  

    /**  
    * 获取随机长度  
    *  
    * @param max 最大长度  
    * @return 生成[1,max]随机数  
    */  
    public static int getRandomLength(int max) {  
        return new SecureRandom().nextInt(max) + 1;  
    }  

    /**  
    * 从给定列表中获取指定长度的随机列表。  
    *  
    * @param characterList 字符列表  
    * @param size 列表大小  
    * @return 随机列表  
    */  
    private static List<Character> getSpecifiedTypeLetters(List<Character> characterList, int size) {  
        List<Character> tmpList = new ArrayList<>();  
        for (int i = 0; i < size; i++) {  
        tmpList.add(characterList.get(new SecureRandom().nextInt(characterList.size())));  
        }  
        return tmpList;  
    }  
  
}

测试类

import org.junit.Test;  
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;  
  
/**  
* @author zmy  
* @date 2023-10-31 10:59:56  
*/  
public class PasswordGeneratorTest {  
  
    @Test  
    public void should_generate_password_with_num_lower_upper_special() {  
        for (int i = 0; i < 1000; i++) {  
            String password = PasswordGenerator.generatePassword(8);  
            boolean matches = password.matches("^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"])[a-zA-Z0-9\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"]{8,}$");  
            if (!matches){  
                return;  
            }  
            System.out.println(i + " password = " + password + "------" + matches);  
       }  
    }  

    /**  
    * 数字和大小写字母  
    */  
    @Test  
    public void should_generate_password_with_num_lower_upper() {  
        for (int i = 0; i < 1000; i++) {  
            String password = PasswordGenerator.generatePassword(8, false);  
            boolean matches = password.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$");  
            if (!matches){  
                return;  
            }  
            System.out.println(i + " password = " + password + "------" + matches);  
        }  
    }  

    @Test  
    public void should_generate_password_with_num_lower_or_upper_special() {  
        for (int i = 0; i < 1000; i++) {  
            String password = PasswordGenerator.generatePassword(8, true, false);  
            boolean matches = password.matches("^(?=.*[A-Za-z])(?=.*\\d)(?=.*[\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"])[A-Za-z\\d\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"]{8,}$");  
            if (!matches){  
                return;  
            }  
            System.out.println(i + " password = " + password + "------" + matches);  
        }  
    }  

    @Test  
    public void should_generate_password_with_num_lower_or_upper() {  
        for (int i = 0; i < 1000; i++) {  
            String password = PasswordGenerator.generatePassword(8, false, false);  
            boolean matches = password.matches("^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$");  
            if (!matches){  
                return;  
            }  
            System.out.println(i + " password = " + password + "------" + matches);  
        }  
    }  

    @Test  
    public void should_generate_password_with_num_lower_or_upper_16() {  
        for (int i = 0; i < 1000; i++) {  
            String password = PasswordGenerator.generatePassword(16, false, false);  
            boolean matches = password.matches("^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{16}$");  
            if (!matches){  
                return;  
            }  
            System.out.println(i + " password = " + password + "------" + matches);  
        }  
    }  

    @Test  
    public void should_generate_password_with_num_lower_upper_special_12_and_encode() {  
        for (int i = 0; i < 1000; i++) {  
            String password = PasswordGenerator.generatePassword(12);  
            final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();  
            boolean matches = password.matches("^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"])[a-zA-Z0-9\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"]{12,}$");  
            if (!matches){  
                return;  
            }  
            System.out.println(i + " password = " + password + "------" + matches);  
            final String encode = bCryptPasswordEncoder.encode(password);  
            System.out.println("encode: " + encode);  
        }  
    }  
  
}