用戶密碼存儲與校驗方案

一、密碼存儲流程

用戶密碼使用 隨機加鹽 方式存儲,存儲流程如下:

  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.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.8</version>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.4.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.4.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>

七、參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章