- 问题:在开发过程中,需要设置一些初始化账户来供用户操作使用。这时候就需要为账户设置密码,为了让密码尽量随机,可以采用程序随机生成的方式,生成满足一定要求的密码。
常见密码要求
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,6],假设生成的值为first - 第二部分,随机数范围为
[1,8-first-1],假设生成的值为second - 第三部分,长度为
8-first-second
四部分的密码思路如下:
- 第一部分,随机数范围为
[1,5],假设生成的值为first - 第二部分,随机数范围为
[1,8-first-2],假设生成的值为second - 第三部分,随机数范围为
[1,8-first-second-2],假设生成的值为third - 第四部分,长度为
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);
}
}
}