在开发过程中,我们常常需要设置一些初始化账户来供用户操作使用。这时候就需要为账户设置密码,为了让密码尽量随机,可以采用程序随机生成的方式,生成满足一定要求的密码。
1. 常见密码格式
密码要求
在一些网站的注册中,注册账户时,密码通常具有以下要求:
- 至少包含一个英文字母和一个数字;
- 至少包含一个大写字母,一个小写字母和一个数字;
- 至少包含一个一个字母,一个数字和一个特殊字符;
- 至少包含一个大写字母,一个小写字母,一个数字和一个特殊字符。
密码格式
以上四种密码在java中,可以用正则表达式进行验证,这里假设密码的最短长度为8个字符,那么对应的正则表达式如下所示:
"^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$"
"^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$"
"^(?=.*[A-Za-z])(?=.*\\d)(?=.*[\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"])[A-Za-z\\d\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"]{8,}$"
"^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"])[a-zA-Z0-9\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"]{8,}$"
特殊字符
一共有33个特殊字符(可以用键盘输入的),其中就包括空格。如果考虑空格,那么密码在校验过程中就会比较繁琐,因此使用剩下的32个特殊字符作为种子,用来随机生成密码。
在Java中,一些字符的匹配需要转义后才能进行正确的匹配,如:
\\\\
匹配\
\\[
匹配[
- …
2.设计思路
概述
明确了密码的常见格式,以及对应的正则表达式后,就可以进行代码的编写。
由于需要随机生成密码,所以这里用到了Java内置的SecureRandom
类,用它来生成随机数。
由于我们需要生成固定长度、固定格式的密码,这里首先获取密码的长度,之后便可以根据对应格式的格式来生成密码。
这里举例进行说明。
至少包含一个英文字母和一个数字
首先创建两个数组,分别包含大小写英文字母(LOWER_AND_UPPER_LETTERS),数字(NUMBERS)。
假设生成密码的长度为8.
数组创建完毕后,首先生成范围为[1,8)
的随机数,这里假设生成的随机数为6,那么就创建一个循环6次的for
循环,每一次循环,都在数组NUMBERS中随机取一个数值,这样循环完毕后,就有了6个随机数;
之后创建一个循环2次的for
循环,每次循环在数组LOWER_AND_UPPER_LETTERS中随机取一个字符,循环结束后,就有了2个随机字符。
将循环后得到的两部分字符数组合并为一个字符数组,并调用Collections.shuffle
方法对数组进行打乱,这样就得到了至少包含一个英文字母和一个数字,且长度为8位的密码。
其他格式
对于其他格式的密码生成策略,也是采用以上的思路进行生成。比如至少包含一个一个字母,一个数字和一个特殊字符,这时候密码就有三部分:
假设密码长度为8。
- 第一部分,随机数范围为
[1,7)
,假设生成的值为first - 第二部分,随机数范围为
[1,8-first-1)
,假设生成的值为second - 第三部分,长度为
8-first-second
四部分的密码思路如下:
- 第一部分,随机数范围为
[1,6)
,假设生成的值为first - 第二部分,随机数范围为
[1,8-first-2)
,假设生成的值为second - 第三部分,随机数范围为
[1,8-first-second-1)
,假设生成的值为third - 第四部分,长度为
8-first-second-third
之后每一部分都循环对应的次数,每一次循环都随机取给定格式字符数组中的任一个字符,将各个部分合并后,再进行打乱,这样就得到了符合要求的密码。
3.实现代码
package com.flywinter.maplebillbackend.utils;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Created by IntelliJ IDEA
* User:Zhang Xingkun
* Date:2022/6/23 16:50
* Description:
*/
public class PasswordGenerator {
public static final SecureRandom RANDOM = new SecureRandom();
protected static final List<Character> NUMBERS = List.of('0', '1', '2', '3', '4', '5', '6', '7', '8', '9');
protected static final List<Character> LOWER_LETTERS = List.of('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 = List.of('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 = List.of('!', '"', '#', '$', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~');
private PasswordGenerator() {
}
/**
* Generate a random password with the given length.
* Format: ^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[\\!@#$%^&*()_+-=\[\]{};':|,.<>/?~`"])[a-zA-Z0-9\\!@#$%^&*()_+-=\[\]{};':|,.<>/?~`"]{4,}$
* at least 4 characters, at least one number, at least one lowercase letter, at least one uppercase letter, at least one special character.
* such as 2K,l @2pL
* @param length the length of the password
* @return the generated password
*/
public static String generatePassword(int length) {
return generatePassword(length, true,true);
}
/**
* Generate a random password with the given length.
* Format:
* useSpecial is true ^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[\\!@#$%^&*()_+-=\[\]{};':|,.<>/?~`"])[a-zA-Z0-9\\!@#$%^&*()_+-=\[\]{};':|,.<>/?~`"]{4,}$
* such as 1^Lm
* useSpecial is false ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{4,}$
* such as 1kMn
* @param length the length of the password
* @param useSpecialCharacters whether to use special characters
* @return the generated password
*/
public static String generatePassword(int length, boolean useSpecialCharacters) {
List<Character> numbers = getSpecifiedTypeLetters(NUMBERS, getRandomLength(length - 3));
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 var passwordList = mergeLists(numbers, lowerLetters, upperAndSpecial);
return getRandomPassword(passwordList);
}
/**
* Generate a random password with the given length.
* Format:
* useSpecial and isCaseSensitive are true that is generatePassword(int length)
* usCaseSensitive is true that is generatePassword(int length, boolean useSpecialCharacters)
* useCaseSensitive and useSpecialCharacters are false.
* It will be at least 3 characters, at least one number, at least one letter(lowercase letter or uppercase letter).
* ^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{3,}$
* such as 1Km K2p KH2
* useCaseSensitive is false and useSpecialCharacters are true.
* It will be at least 3 characters, at least one number, at least one letter(lowercase letter or uppercase letter), at least one special character.
* ^(?=.*[A-Za-z])(?=.*\d)(?=.*[\\!@#$%^&*()_+-=\[\]{};':|,.<>/?~`"])[A-Za-z\d\\!@#$%^&*()_+-=\[\]{};':|,.<>/?~`"]{8,}$
* such as 1L, 3o@
* @param length the length of the password
* @param useSpecialCharacters whether to use special characters
* @param isCaseSensitive whether is case sensitive
* @return the generated password
*/
public static String generatePassword(int length, boolean useSpecialCharacters, boolean isCaseSensitive) {
if (isCaseSensitive) {
return generatePassword(length, useSpecialCharacters);
}
List<Character> numbers = getSpecifiedTypeLetters(NUMBERS, getRandomLength(length - 2));
final List<Character> letterAndSpecial = hasSpecialLetters(useSpecialCharacters, length, numbers.size(), LOWER_AND_UPPER_LETTERS);
final var passwordList = mergeLists(numbers, letterAndSpecial);
return getRandomPassword(passwordList);
}
/**
* Generate a random password with the given list.
* @param numbers the list of origin password characters
* @return the random password
*/
private static String getRandomPassword(List<Character> numbers) {
Collections.shuffle(numbers);
return numbers.stream().map(Object::toString).collect(Collectors.joining());
}
/**
* Generate a character list with the special characters or not.
* @param useSpecialCharacters whether to use special characters
* @param length the length of the password
* @param lettersSize the size of the other letters(numbers or English letters)
* @param englishLetters the list of English letters(lowercase or uppercase or lowercase and uppercase)
* @return password list with special characters or not
*/
private static List<Character> hasSpecialLetters(boolean useSpecialCharacters, int length, int lettersSize, List<Character> englishLetters) {
List<Character> specialLetters;
List<Character> upperLetters;
if (useSpecialCharacters) {
final int upperLetterLength = getRandomLength(length - lettersSize - 1);
upperLetters = getSpecifiedTypeLetters(englishLetters, upperLetterLength);
final int specialLetterLength = length - lettersSize - upperLetters.size();
specialLetters = getSpecifiedTypeLetters(SPECIAL_CHARACTERS, specialLetterLength);
return mergeLists(upperLetters, specialLetters);
}
return getSpecifiedTypeLetters(englishLetters, length - lettersSize);
}
/**
* Merge lists.
* @param lists the lists to merge
* @return the merged list
*/
@SafeVarargs
private static List<Character> mergeLists(List<Character>... lists) {
return Stream.of(lists)
.flatMap(List::stream)
.collect(Collectors.toList());
}
/**
* Get a random length.
* @param max the max length
* @return the random length
*/
private static int getRandomLength(int max) {
return RANDOM.nextInt(1, max);
}
/**
* Get specified length random list from the given list.
* @param characterList the source list
* @param size the size of the random list
* @return the random list
*/
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(RANDOM.nextInt(characterList.size())));
}
return tmpList;
}
}
4.测试类
package com.flywinter.maplebillbackend.utils;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import static org.junit.jupiter.api.Assertions.*;
/**
* Created by IntelliJ IDEA
* User:Zhang Xingkun
* Date:2022/6/23 16:51
* Description:
*/
class PasswordGeneratorTest {
@Test
void should_generate_password_with_num_lower_upper_special() {
String password = PasswordGenerator.generatePassword(8);
assertTrue(password.matches("^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"])[a-zA-Z0-9\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"]{8,}$"));
}
@Test
void should_throw_error_when_password_length_smaller_than_3() {
assertThrows(IllegalArgumentException.class, () -> PasswordGenerator.generatePassword(3));
}
@Test
void should_generate_password_with_num_lower_upper() {
String password = PasswordGenerator.generatePassword(8, false);
assertTrue(password.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$"));
}
@Test
void should_generate_password_with_num_lower_or_upper_special() {
String password = PasswordGenerator.generatePassword(8, true, false);
assertTrue(password.matches("^(?=.*[A-Za-z])(?=.*\\d)(?=.*[\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"])[A-Za-z\\d\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"]{8,}$"));
}
@Test
void should_generate_password_with_num_lower_or_upper() {
String password = PasswordGenerator.generatePassword(8, false, false);
assertTrue(password.matches("^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$"));
}
@Test
void should_generate_password_with_num_lower_or_upper_16() {
String password = PasswordGenerator.generatePassword(16, false, false);
assertTrue(password.matches("^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{16}$"));
}
@Test
void should_generate_password_with_num_lower_upper_special_12_and_encode() {
String password = PasswordGenerator.generatePassword(12);
assertTrue(password.matches("^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"])[a-zA-Z0-9\\\\!@#$%^&*()_+-=\\[\\]{};':|,.<>/?~`\"]{12,}$"));
final var bCryptPasswordEncoder = new BCryptPasswordEncoder();
final var encode = bCryptPasswordEncoder.encode(password);
System.out.println("password: " + password);
System.out.println("encode: " + encode);
// password: P42F1_6r$2$711
// encode: $2a$10$we1KwoVzwkchAMfvRJ2NdurNk3.KzcnDcrEfrD17uT3itfnEaNVdG
}
}
5.备注
随机数
关于随机数,实际上Java内部实现的随机数大部分是伪随机数,即可以被预测,如果随机数的种子相同,生成的随机数就是一样的。
对于Random
类,默认使用时间作为种子,所以比较容易被预测。
而SecureRandom
类无法指定种子,而是使用RNG算法来实现的,在底层实现中,有的底层实现使用的是真正的随机数(即根据电脑CPU本身的噪音等生成,基本可以认为是真随机数),所以不容易甚至无法预测。
其实这里使用Random
类也是可以的,因为默认生成的密码只要长度够长,又使用时间作为种子,基本上也是无法预测的。
