Spring Security中DelegatingPasswordEncoder的由來


一、密碼存儲歷史

多年來,用於存儲密碼的標准機制已經發展。最初,密碼以純文本格式存儲。假定密碼是安全的,因為數據存儲密碼已保存在訪問它所需的憑據中。但是,惡意用戶能夠使用SQL Injection這樣的攻擊找到方法來獲取用戶名和密碼的大型“數據轉儲”。隨着越來越多的用戶憑證成為公共安全專家,我們意識到我們需要做更多的事情來保護用戶的密碼。

然后鼓勵開發人員在通過諸如SHA-256之類的單向哈希運行密碼后存儲密碼。當用戶嘗試進行身份驗證時,會將哈希密碼與他們鍵入的密碼哈希進行比較。這意味着系統僅需要存儲密碼的單向哈希。如果發生違規,則僅暴露密碼的一種哈希方式。由於散列是一種方式,並且計算給出的哈希密碼很難計算,因此,找出系統中的每個密碼都不值得。為了擊敗這個新系統,惡意用戶決定創建稱為彩虹表。他們不必每次都猜測每個密碼,而是只計算一次密碼並將其存儲在查找表中。

為了減輕彩虹表的有效性,鼓勵開發人員使用加鹽的密碼。不僅將密碼用作哈希函數的輸入,還將為每個用戶的密碼生成隨機字節(稱為salt)。鹽和用戶的密碼將通過散列函數運行,從而產生唯一的散列。鹽將以明文形式與用戶密碼一起存儲。然后,當用戶嘗試進行身份驗證時,會將哈希密碼與存儲的鹽的哈希值和他們鍵入的密碼進行比較。唯一的鹽意味着彩虹表不再有效,因為每種鹽和密碼組合的哈希值都不同。

在現代,我們意識到密碼哈希(例如SHA-256)不再安全。原因是使用現代硬件,我們可以每秒執行數十億次哈希計算。這意味着我們可以輕松地分別破解每個密碼。

現在鼓勵開發人員利用自適應單向功能來存儲密碼。帶有自適應單向功能的密碼驗證有意占用大量資源(即CPU,內存等)。自適應單向功能允許配置“工作因數”,該因數會隨着硬件的改進而增加。建議將“工作因數”調整為大約1秒鍾,以驗證系統上的密碼。這種權衡使攻擊者難以破解密碼,但代價卻不高,它給您自己的系統增加了負擔。Spring Security試圖為“工作因素”提供一個良好的起點,但是鼓勵用戶為自己的系統自定義“工作因素”,因為不同系統之間的性能會有很大差異。bcrypt,PBKDF2,scrypt和argon2。

由於自適應單向功能有意占用大量資源,因此為每個請求驗證用戶名和密碼都會大大降低應用程序的性能。Spring Security(或任何其他庫)無法執行任何操作來加快密碼的驗證速度,因為通過增加驗證資源的強度來獲得安全性。鼓勵用戶將長期憑證(即用戶名和密碼)交換為短期憑證(即會話,OAuth令牌等)。可以快速驗證短期憑證,而不會損失任何安全性。

二、PasswordEncoder的使用

PasswordEncoder的實現類有

直接使用PasswordEncoder的encode方法就可以實現對字符串加密了。

PasswordEncoder passwordEncoder=new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("123456");
System.out.println(encode);

結果輸出

$2a$10$6B4sR7rg9WlxOxfojodBFeUINh8UuDUt5pRkHtX3cLPEzW2AJ5dbC

如果需要改變加密算法,只需要修改實現類就OK

三、密碼編碼升級帶來的問題

隨機技術的發展,密碼存儲的最佳做法也將再次發生更改。那么隨之帶來的問題將如何解決?

  • 許多應用程序使用舊的密碼編碼無法輕松遷移

  • 密碼存儲的最佳做法將再次更改。

  • 作為一個框架,Spring Security不能經常進行重大更改

三、DelegatingPasswordEncoder的使用

通過DelegatingPasswordEncoder我們可以解決上訴的問題

例如之前我們加密的方式

String password="123456";
PasswordEncoder passwordEncoder=new MessageDigestPasswordEncoder("MD5");
String encodePassword = passwordEncoder.encode(password);
System.out.println(encodePassword);

加密后的密碼為

{nt8XCm32M9dzeaetdtFACO0jiUMhqeayK7I4NJ+Exmw=}9469cad74c2e2cf18700b3d751d314c8

某一天為了系統安全考慮,我們決定采用bcrypt方式來升級加密算法。那么在如何做到既兼容老的md5密碼可以正確的匹配到,同時新的密碼采用bcrypt來加密呢?

DelegatingPasswordEncoder的密碼格式

{id}encodedPassword

升級代碼實現

String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
// DelegatingPasswordEncoder的第一個參數表示新的加密方式,第二個參數表示支持兼容的加密方式
PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(encodingId, encoders);

String rawPassword = "123456";
// 因為之前沒有使用DelegatingPasswordEncoder,所以原md5加密沒有前綴表示,所以需要手動加上,如果之前是用的DelegatingPasswordEncoder,會自動帶上
String md5EncodedPassword = "{MD5}{nt8XCm32M9dzeaetdtFACO0jiUMhqeayK7I4NJ+Exmw=}9469cad74c2e2cf18700b3d751d314c8";

// 驗證系統中原MD5加密方式的是否兼容
boolean matched = passwordEncoder.matches(rawPassword, md5EncodedPassword);
System.out.println("是否兼容原MD5加密方式:" + matched);

String bcryptEncodedPassword = passwordEncoder.encode(rawPassword);
System.out.println("采用bcrypt的加密密碼:" + bcryptEncodedPassword);

輸出結果

是否兼容原MD5加密方式:true
采用bcrypt的加密密碼:{bcrypt}$2a$10$SZ/PXl5SGotzctKFlCJgDuyG0xD9M8aDEpHPOpRNYPvCnEVcBLAIS

這樣我們就通過DelegatingPasswordEncoder來實現了密碼升級問題。

當然Spring Security不需要我們寫如此多的代碼

PasswordEncoderFactories

PasswordEncoderFactories為了讓我們不需要關心加密算法,而不用未每次升級而煩惱

直接使用PasswordEncoderFactories.createDelegatingPasswordEncoder()就可以創建一個PasswordEncoder

PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

String rawPassword = "123456";
String md5EncodedPassword = "{MD5}{nt8XCm32M9dzeaetdtFACO0jiUMhqeayK7I4NJ+Exmw=}9469cad74c2e2cf18700b3d751d314c8";

// 驗證系統中原MD5加密方式的是否兼容
boolean matched = passwordEncoder.matches(rawPassword, md5EncodedPassword);
System.out.println("是否兼容原MD5加密方式:" + matched);

String bcryptEncodedPassword = passwordEncoder.encode(rawPassword);
System.out.println("采用bcrypt的加密密碼:" + bcryptEncodedPassword);

輸出結果

是否兼容原MD5加密方式:true
采用bcrypt的加密密碼:{bcrypt}$2a$10$RGdxpFjq2D8wkFZW9DzQvOcCkfN2VG53izd4KZPmUow7RBPgslC1y

下面是PasswordEncoderFactories的源碼

public final class PasswordEncoderFactories {

	private PasswordEncoderFactories() {
	}

	@SuppressWarnings("deprecation")
	public static PasswordEncoder createDelegatingPasswordEncoder() {
		String encodingId = "bcrypt";
		Map<String, PasswordEncoder> encoders = new HashMap<>();
		encoders.put(encodingId, new BCryptPasswordEncoder());
		encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
		encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
		encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
		encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
		encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
		encoders.put("scrypt", new SCryptPasswordEncoder());
		encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
		encoders.put("SHA-256",
				new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
		encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
		encoders.put("argon2", new Argon2PasswordEncoder());
		return new DelegatingPasswordEncoder(encodingId, encoders);
	}

}

PasswordEncoderFactories和我們上面開始寫的方式是一致的,只不過維護了更多的加密算法,到Spring Security升級后,PasswordEncoderFactories會自動升級,我們業務代碼就沒必要在改了。

DelegatingPasswordEncoder源碼分析

public class DelegatingPasswordEncoder implements PasswordEncoder {

	/**
	 * PREFIX和SUFFIX用於提取加密方式中{id}encodedPassword的id
 	 */
	private static final String PREFIX = "{";

	private static final String SUFFIX = "}";

	/**
	 * 當前新加密采用的加密算法
	 */
	private final String idForEncode;

	/**
	 * 采用新加密算法的實現類
	 */
	private final PasswordEncoder passwordEncoderForEncode;

	/**
	 * 所有需要用到的加密算法
	 */
	private final Map<String, PasswordEncoder> idToPasswordEncoder;

	private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();

	/**
	 * @param idForEncode 新的加密算法
	 * @param idToPasswordEncoder 所有需要用到的加密算法
	 */
	public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
		if (idForEncode == null) {
			throw new IllegalArgumentException("idForEncode cannot be null");
		}
		if (!idToPasswordEncoder.containsKey(idForEncode)) {
			throw new IllegalArgumentException(
					"idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
		}
		for (String id : idToPasswordEncoder.keySet()) {
			if (id == null) {
				continue;
			}
			if (id.contains(PREFIX)) {
				throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX);
			}
			if (id.contains(SUFFIX)) {
				throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX);
			}
		}
		this.idForEncode = idForEncode;
		// 根據加密算法的key,獲取具體的PasswordEncoder實現類
		this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
		this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
	}


	public void setDefaultPasswordEncoderForMatches(PasswordEncoder defaultPasswordEncoderForMatches) {
		if (defaultPasswordEncoderForMatches == null) {
			throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null");
		}
		this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches;
	}

	/**
	 * 使用新加密算法加密密碼
	 * @param rawPassword 原始密碼
	 * @return 加密過后的密碼,  格式為:{id}encodedPassword
	 */
	@Override
	public String encode(CharSequence rawPassword) {
		return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
	}

	/**
	 * 匹配密碼是否正確
	 * @param rawPassword 原始密碼
	 * @param prefixEncodedPassword 存儲庫中存儲的密碼
	 * @return true:匹配成功,false:匹配失敗
	 */
	@Override
	public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
		if (rawPassword == null && prefixEncodedPassword == null) {
			return true;
		}
		// 根據存儲庫中的密碼,提取id,判斷采用的那種方式進行加密的
		String id = extractId(prefixEncodedPassword);
		// 根據id從Map<String, PasswordEncoder>中獲取PasswordEncoder
		PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
		if (delegate == null) {
			return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
		}
		// 出去掉存儲庫中密碼前面的:{id}
		String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
		// 比較密碼是否匹配
		return delegate.matches(rawPassword, encodedPassword);
	}

	/**
	 * 根據存儲庫中的密碼,提取id,判斷采用的那種方式進行加密的,例如:
	 * {bcrypt}$2a$10$RGdxpFjq2D8wkFZW9DzQvOcCkfN2VG53izd4KZPmUow7RBPgslC1y 將返回bcrypt
	 */
	private String extractId(String prefixEncodedPassword) {
		if (prefixEncodedPassword == null) {
			return null;
		}
		int start = prefixEncodedPassword.indexOf(PREFIX);
		if (start != 0) {
			return null;
		}
		int end = prefixEncodedPassword.indexOf(SUFFIX, start);
		if (end < 0) {
			return null;
		}
		return prefixEncodedPassword.substring(start + 1, end);
	}

	@Override
	public boolean upgradeEncoding(String prefixEncodedPassword) {
		String id = extractId(prefixEncodedPassword);
		if (!this.idForEncode.equalsIgnoreCase(id)) {
			return true;
		}
		else {
			String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
			return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);
		}
	}

	/**
	 * 將存儲庫中的密碼移除掉前面的{id}部分,例如:
	 * {bcrypt}$2a$10$RGdxpFjq2D8wkFZW9DzQvOcCkfN2VG53izd4KZPmUow7RBPgslC1y 將返回$2a$10$RGdxpFjq2D8wkFZW9DzQvOcCkfN2VG53izd4KZPmUow7RBPgslC1y
	 */
	private String extractEncodedPassword(String prefixEncodedPassword) {
		int start = prefixEncodedPassword.indexOf(SUFFIX);
		return prefixEncodedPassword.substring(start + 1);
	}

	/**
	 * Default {@link PasswordEncoder} that throws an exception telling that a suitable
	 * {@link PasswordEncoder} for the id could not be found.
	 */
	private class UnmappedIdPasswordEncoder implements PasswordEncoder {

		@Override
		public String encode(CharSequence rawPassword) {
			throw new UnsupportedOperationException("encode is not supported");
		}

		@Override
		public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
			String id = extractId(prefixEncodedPassword);
			throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
		}

	}

}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM