在实际开发中, 我们往往需要对密码进行加密存储。
在Spring Security中是通过一种自适应单向函数来处理密码问题,这种自适应单向函数的方式在进行密码匹配时会有意占用大量系统资源(CPU,内存等),这样就可以增加恶意用户攻击系统的难度。当然开发者也可以将用户名/密码的方式换成会话,OAuth2令牌等方式既可以快速验证用户凭证信息,也不损失系统安全性

PasswordEncoder

在SpringSecurity中通过PasswordEncoder接口定义了密码加密和对比的操作,我们看下源码

public interface PasswordEncoder {
	String encode(CharSequence rawPassword);
	boolean matches(CharSequence rawPassword, String encodedPassword);
	default boolean upgradeEncoding(String encodedPassword) {
		return false;
	}
}

一共有3个方法,encode方法对明文进行加密,matches方法进行密码比对,upgraderEncoding方法判断当前密码是否需要再次加密默认为false

PasswordEncoder常见实现类

Spring Security提供了一些PasswordEncoder的实现类,比如BCryptPasswordEncoder,Argon2PasswordEncoder,Pbkdf2PasswordEncoder,SCryptPasswordEncoder

下面着重介绍一下BCryptPasswordEncoder

BCryptPasswordEncoder

BCryptPasswordEncoder使用bcrypt算法对密码进行加密,同时会为密码加上“盐”,开发者不需要自己加“盐”,即使相同的明文字段生成的加密字符串也不同。匹配时,从密文中取出“盐”,用该盐值加密明文和最终密文作对比。

BCryptPasswordEncoder的默认强度为10,开发者可以根据自己服务器的速度进行调整,以确保密码验证的时间约为1秒(官方建议)

PasswordEncoder 中的 encode 方法,是我们在用户注册的时候手动调用。

matches 方法,则是由系统调用,默认是在 DaoAuthenticationProvider#additionalAuthenticationChecks 方法中调用的。

protected void additionalAuthenticationChecks(UserDetails userDetails,
		UsernamePasswordAuthenticationToken authentication)
		throws AuthenticationException {
	if (authentication.getCredentials() == null) {
		logger.debug("Authentication failed: no credentials provided");
		throw new BadCredentialsException(messages.getMessage(
				"AbstractUserDetailsAuthenticationProvider.badCredentials",
				"Bad credentials"));
	}
	String presentedPassword = authentication.getCredentials().toString();
	if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
		logger.debug("Authentication failed: password does not match stored value");
		throw new BadCredentialsException(messages.getMessage(
				"AbstractUserDetailsAuthenticationProvider.badCredentials",
				"Bad credentials"));
	}
}

可以看到,密码比对就是通过 passwordEncoder.matches 方法来进行的

那么 DaoAuthenticationProvider 中的 passwordEncoder 从何而来呢?是不是就是我们自己在SecurityConfig 中配置的那个 Bean 呢?

我们看下 DaoAuthenticationProvider 中关于 passwordEncoder 的定义

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
	private PasswordEncoder passwordEncoder;
	public DaoAuthenticationProvider() {
		setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
	}
	public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
		this.passwordEncoder = passwordEncoder;
		this.userNotFoundEncodedPassword = null;
	}

	protected PasswordEncoder getPasswordEncoder() {
		return passwordEncoder;
	}
}

//我们再来看看 DaoAuthenticationProvider 是怎么初始化的。
//DaoAuthenticationProvider 的初始化是在 InitializeUserDetailsManagerConfigurer#configure 方法中完成的,我们一起来看下该方法的定义:
public void configure(AuthenticationManagerBuilder auth) throws Exception {
	if (auth.isConfigured()) {
		return;
	}
	UserDetailsService userDetailsService = getBeanOrNull(
			UserDetailsService.class);
	if (userDetailsService == null) {
		return;
	}
	PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
	UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
	DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
	provider.setUserDetailsService(userDetailsService);
	if (passwordEncoder != null) {
		provider.setPasswordEncoder(passwordEncoder);
	}
	if (passwordManager != null) {
		provider.setUserDetailsPasswordService(passwordManager);
	}
	provider.afterPropertiesSet();
	auth.authenticationProvider(provider);
}

在 DaoAuthenticationProvider 创建之时,会制定一个默认的 PasswordEncoder,如果我们没有配置任何 PasswordEncoder,将使用这个默认的 PasswordEncoder,如果我们自定义了 PasswordEncoder 实例,那么会使用我们自定义的 PasswordEncoder 实例!

DelegatingPasswordEncoder

实际上在Spring Security中默认的加密方式是DelegatingPasswordEncoder,

看下源码:

public DaoAuthenticationProvider() {
	setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}

DelegatingPasswordEncoder是一个代理类,主要用来代理上面介绍的不同加密方式,它允许系统中存在不同的加密方案,很方便的完成对加密方案的升级

我们可以看下PasswordEncoderFactories.createDelegatingPasswordEncoder(),默认就是通过它创建了DelegatingPasswordEncoder实例

public class PasswordEncoderFactories {
	public static PasswordEncoder createDelegatingPasswordEncoder() {
		String encodingId = "bcrypt";
		Mapencoders = 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);
	}
	private PasswordEncoderFactories() {}
}

可以看到:

(1)在 PasswordEncoderFactories 中,首先构建了一个 encoders,然后给所有的加密方式都取了一个名字,再把名字做 key,加密类做 value,统统存入 encoders 中。

(2)最后返回了一个 DelegatingPasswordEncoder 实例,同时传入默认的 encodingId 就是 bcrypt,以及 encoders 实例,相当于在DelegatingPasswordEncoder默认使用的是BCryptPasswordEncoder加密

我们看下DelegatingPasswordEncoder源码

public class DelegatingPasswordEncoder implements PasswordEncoder {
	private static final String PREFIX = "{";
	private static final String SUFFIX = "}";
	private final String idForEncode;
	private final PasswordEncoder passwordEncoderForEncode;
	private final MapidToPasswordEncoder;
	private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
	public DelegatingPasswordEncoder(String idForEncode,
		MapidToPasswordEncoder) {
		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;
		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;
	}

	@Override
	public String encode(CharSequence rawPassword) {
		return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
	}

	@Override
	public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
		if (rawPassword == null && prefixEncodedPassword == null) {
			return true;
		}
		String id = extractId(prefixEncodedPassword);
		PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
		if (delegate == null) {
			return this.defaultPasswordEncoderForMatches
				.matches(rawPassword, prefixEncodedPassword);
		}
		String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
		return delegate.matches(rawPassword, encodedPassword);
	}

	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);
		}
	}

	private String extractEncodedPassword(String prefixEncodedPassword) {
		int start = prefixEncodedPassword.indexOf(SUFFIX);
		return prefixEncodedPassword.substring(start + 1);
	}
	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 + "\"");
		}
	}
}

(1)首先定义了前缀{ 和后缀 }来包裹加密方案的id 比如{MD5}

(2)继承了PasswordEncoder 接口,所以它里边的核心方法也是两个:encode 方法用来对密码进行编码,matches 方法用来校验密码。

(3)encode方法很简单,就是使用idForEncode(默认加密方案)加密类加密,加上{加密方法} 前缀

(4)matches方法根据前缀找到对应的加密方案,由对应的加密类匹配,如果没有找到加密方案,则由defaultPasswordEncoderForMatches(默认是UnmappedIdPasswordEncoder)进行匹配,而这个UnmappedIdPasswordEncoder匹配方法返回一个IllegalArgumentException异常

总之如果我们想同时使用多个密码加密方案,使用 DelegatingPasswordEncoder 就可以了,而 DelegatingPasswordEncoder 默认还不用配置。

就比如说 如果我们想要更改系统的加密方法,我们不可能让之前的老用户重新注册一次,我们只需要使用 DelegatingPasswordEncoder,同时在我们的数据库中为密码加上 之前加密方案的 前缀,就可以使系统完成对之前加密方案的支持,调用DelegatingPasswordEncoder的setDefaultPasswordEncoderForMatches方法就可以重新设置系统的加密方案。

实例

由于Spring Security默认使用DelegatingPasswordEncoder,DelegatingPasswordEncoder默认使用BCryptPasswordEncoder加密。 如果我们需要其他的密码加密方案,只需要向Spring容器中注入一个PasswordEncoder实例,代代替DelegatingPasswordEncoder即可

比如:

@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

用户注册时调用PasswordEncoder 的 encode 方法!