用户密码存储与校验方案

一、密码存储流程

用户密码使用 随机加盐 方式存储,存储流程如下:

  1. 生成随机盐s
  2. 随机盐s与密码明文p拼接得到待哈希串m
  3. hash(m)得到密文e
  4. 哈希函数代号c+特定分隔符+随机盐s+特定分隔符a+密文e=最终入库的字符串i

二、校验密码流程

  1. 网站使用HTTPS,前端将用户在界面输入的用户名user和密码明文pwd传输到后台;
  2. 登录接口接收到登录请求后,根据用户名user提取上述存储流程中的密码字符串i
  3. 后台对字符串i使用分隔符a切割得到哈希函数版本c、随机盐s和密码密文e
  4. 后台使用随机盐s和传入的密码明文pwd拼接,根据哈希函数版本c使用对应的算法计算e'
  5. 判断ee'是否相等,如果相等则登录校验成功,发放登录token,否则返回登录失败提示;

三、细节补充说明

  • 随机盐使用JDK自带的SecureRandom生成;
  • 哈希算法使用JDK自带的PBKDF2;
  • 分隔符为双冒号::
  • 哈希函数代号为0

四、密码工具类代码

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.BinaryOperator;

/**
 * 密码工具类
 **/
public class PasswordUtils {
	private static final Logger log = LoggerFactory.getLogger(PasswordUtils.class);
	private static final String SEPARATOR = "::";
	private static final char[] hexArray = "0123456789ABCDEF".toCharArray();
	private static final Map<String, BinaryOperator<String>> supportedHashFunctions = new ConcurrentHashMap<>();

	private static final String PBKDF2 = "0";
	private static final BinaryOperator<String> pbkdf2Function = (password, salt) -> {
		int iteration = 65536;
		int strength = 128;
		String algorithm = "PBKDF2WithHmacSHA1";
		KeySpec spec = new PBEKeySpec(password.toCharArray(), hexStringToBytes(salt), iteration, strength);
		try {
			SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm);
			return bytesToHexString(factory.generateSecret(spec).getEncoded());
		} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
			log.error(e.getMessage(), e);
			throw new IllegalStateException(e);
		}
	};

	static {
		// add more hash functions if necessary
		supportedHashFunctions.put(PBKDF2, pbkdf2Function);
	}

	private PasswordUtils() {
	}

	/**
	 * 根据密码明文使用随机加盐生成密码密文
	 *
	 * @param plainPassword 用户密码明文
	 * @param hashOption 哈希函数选项,可不填
	 * @return 随机加盐后的密码密文
	 */
	public static String hashPassword(String plainPassword, String... hashOption) {
		SecureRandom random = new SecureRandom();
		byte[] salt = new byte[16];
		random.nextBytes(salt);
		String option;
		if (hashOption != null && hashOption.length > 0 && StringUtils.isNotBlank(hashOption[0])) {
			// user specified hash function
			option = hashOption[0];
		} else {
			// pick a random hash function
			option = String.valueOf(ThreadLocalRandom.current().nextInt(supportedHashFunctions.size()));
		}
		if (!supportedHashFunctions.containsKey(option)) {
			throw new IllegalArgumentException("there is no such hash option: " + option);
		}
		return option + SEPARATOR + bytesToHexString(salt) + SEPARATOR + supportedHashFunctions.get(option)
				.apply(plainPassword, bytesToHexString(salt));
	}

	/**
	 * 校验用户输入的明文密码是否正确
	 *
	 * @param plainPassword 用户输入的明文密码
	 * @param dbPassword 来自数据库的密码密文
	 * @return 密码校验是否通过
	 */
	public static boolean validatePassword(String plainPassword, String dbPassword) {
		String[] optionSaltAndPass = StringUtils.split(dbPassword, SEPARATOR);
		if (optionSaltAndPass == null || optionSaltAndPass.length != 3) {
			throw new IllegalStateException("split db password array should be of length 3");
		}
		String option = optionSaltAndPass[0];
		if (!supportedHashFunctions.containsKey(option)) {
			throw new IllegalStateException("hash function not found by option: " + option);
		}
		String salt = optionSaltAndPass[1];
		String encryptedPassword = optionSaltAndPass[2];
		return StringUtils.equals(supportedHashFunctions.get(option).apply(plainPassword, salt), encryptedPassword);

	}

	private static String bytesToHexString(byte[] bytes) {
		char[] hexChars = new char[bytes.length * 2];
		for (int j = 0; j < bytes.length; j++) {
			int v = bytes[j] & 0xFF;
			hexChars[j * 2] = hexArray[v >>> 4];
			hexChars[j * 2 + 1] = hexArray[v & 0x0F];
		}
		return new String(hexChars);
	}

	private static byte[] hexStringToBytes(String s) {
		int len = s.length();
		byte[] data = new byte[len / 2];
		for (int i = 0; i < len; i += 2) {
			data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
		}
		return data;
	}
}

五、测试代码

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.hamcrest.core.IsEqual;
import org.hamcrest.core.IsNull;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.*;

public class PasswordUtilsTest {
	@Test
	public void testHashAndValidatePassword() {
		String password = "this_1$_seCret";
		System.out.println("plain: " + password); // NOSONAR
		String encrypted = PasswordUtils.hashPassword(password);
		System.out.println("encrypted: " + encrypted); // NOSONAR
		assertThat(encrypted, IsNull.notNullValue());
		assertThat(ArrayUtils.getLength(StringUtils.split(encrypted, "::")), IsEqual.equalTo(3));
		assertTrue(PasswordUtils.validatePassword(password, encrypted));
		password = "fake_password";
		assertFalse(PasswordUtils.validatePassword(password, encrypted));
		encrypted = PasswordUtils.hashPassword(password, "0");
		assertTrue(PasswordUtils.validatePassword(password, encrypted));
		assertThrows(IllegalArgumentException.class, () -> PasswordUtils.hashPassword("shouldThrow", "999"));
		assertThrows(IllegalStateException.class, () -> PasswordUtils.validatePassword("shouldThrow", "0::asdfasdf"));
		assertThrows(IllegalStateException.class,
				() -> PasswordUtils.validatePassword("shouldThrow", "999::asdfasdf::sdfasdfasdf"));
	}
}

六、Maven依赖

<dependency>
    <groupId>org.apache.commonsgroupId>
    <artifactId>commons-lang3artifactId>
    <version>3.8version>
dependency>
<dependency>
    <groupId>org.junit.jupitergroupId>
    <artifactId>junit-jupiter-apiartifactId>
    <version>5.4.2version>
    <scope>testscope>
dependency>
<dependency>
    <groupId>org.junit.jupitergroupId>
    <artifactId>junit-jupiter-engineartifactId>
    <version>5.4.2version>
    <scope>testscope>
dependency>
<dependency>
    <groupId>org.hamcrestgroupId>
    <artifactId>hamcrest-allartifactId>
    <version>1.3version>
    <scope>testscope>
dependency>

七、参考资料

  • 用户密码加密存储十问十答,一文说透密码安全存储
  • Hashing a Password in Java

你可能感兴趣的:(Java)