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