详细文档参考官方说明https://docs.spring.io/spring-security/site/docs/5.0.9.RELEASE/reference/htmlsingle/#core-services-password-encoding
PasswordEncoder是spring-security的一个接口,主要用于对密码进行编码或加密,它有什么用途呢?说白了就是俩字“安全”,它的作用就是为了让密码不要以明文形式保存在数据库或其他文件中,我们先看一下这个接口的源码,如下:
public interface PasswordEncoder {
/**
* Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
* greater hash combined with an 8-byte or greater randomly generated salt.
*/
String encode(CharSequence rawPassword);
/**
* Verify the encoded password obtained from storage matches the submitted raw
* password after it too is encoded. Returns true if the passwords match, false if
* they do not. The stored password itself is never decoded.
*
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* @return true if the raw password, after encoding, matches the encoded password from
* storage
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
}
很简单,只有两个方法:encode和matches,顾名思义,encode是对明文密码进行编码,生成一段不明所以的字符串,encode方法主要用于保存密码至数据库或其他文件前将密码进行编码。matches方法用于将原始密码与编码过的密码进行匹配,matches方法主要用于验证密码的正确性。
默认情况下,spring-security自带了常用的PasswordEncoder实现,比如默认的BCryptPasswordEncoder, Pbkdf2PasswordEncoder,SCryptPasswordEncoder,NoOpPasswordEncoder等。
那么,我们在spring-security中是如何使用PasswordEncoder的呢?
// 实例化一个PasswordEncoder的bean
@Bean
public PasswordEncoder passwordEncoder() {
DelegatingPasswordEncoder passwordEncoder = (DelegatingPasswordEncoder) PasswordEncoderFactories
.createDelegatingPasswordEncoder();
passwordEncoder.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder());
return passwordEncoder;
//还有一种更简单的方法
// return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider daoAuthenticationProvider(UserDetailsService userDetailsService) {
DaoAuthenticationProvider dap = new DaoAuthenticationProvider();
// 将实例化的passwordEncoder应用在AuthenticationProvider
dap.setPasswordEncoder(passwordEncoder());
dap.setHideUserNotFoundExceptions(false);
dap.setUserDetailsService(userDetailsService);
return dap;
}
我们先看一下PasswordEncoder的实例化,spring-security默认情况下使用的是BCryptPasswordEncoder,我们可以直接new一个BCryptPasswordEncoder即可,但是为什么又用DelegatingPasswordEncoder呢?这是因为在spring-security 5.0之前默认使用的是NoOpPasswordEncoder,有些公司在之前的老项目中可能直接使用了明文密码保存在数据库,如果直接升级到5.0可能会导致密码验证失败,因此官方很体贴的推出了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 Map idToPasswordEncoder;
private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
public DelegatingPasswordEncoder(String idForEncode,
Map 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;
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) {
//只是在普通的encode方法的结果前面加了{id}前缀而已
return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
if(rawPassword == null && prefixEncodedPassword == null) {
return true;
}
// 在密文 {id}encodedPassword 中找到id
String id = extractId(prefixEncodedPassword);
PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
// 若id在idToPasswordEncoder中不存在,则使用默认的passwordEncoder,代码中默认使用的是
//UnmappedIdPasswordEncoder,看它的源码我们可以知道,这个默认实现除了弹出异常没什么卵用,
//所以我们最好还是使用setDefaultPasswordEncoderForMatches方法设置一个可用的passwordEncoder
if(delegate == null) {
return this.defaultPasswordEncoderForMatches
.matches(rawPassword, prefixEncodedPassword);
}
// 在比较之前,将前缀 {id}去掉
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);
}
private String extractEncodedPassword(String prefixEncodedPassword) {
int start = prefixEncodedPassword.indexOf(SUFFIX);
return prefixEncodedPassword.substring(start + 1);
}
/**
* Default {@link PasswordEncoder} that throws an exception that a id could
*/
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 + "\"");
}
}
}
通过源码分析我们发现,如果实例化PasswordEncoder时只是new一个BCryptPasswordEncoder,那么编码后的密码前面是没有{id}前缀的,同时它也不具备兼容性,然而我们如果使用了DelegatingPasswordEncoder,那感觉就不一样了哦,我们编码后的密码格式是这样的 {id}encodedPassword
举个栗子:
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
如果我们同时也使用了DelegatingPasswordEncoder的setDefaultPasswordEncoderForMatches方法设置了默认的编码器,比如BCryptPasswordEncoder,那么我最终存储的密码格式也可以不带{id}前缀,此时spring-security会默认使用{bcrypt}作为前缀,因此最终的密码格式可以是这样的
$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
细心的同学可能会发现,我们没有直接new一个DelegatingPasswordEncoder,而是使用了PasswordEncoderFactories 的createDelegatingPasswordEncoder方法创建了一个PasswordEncoder,话不多说,上源码你懂的
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
是不是很贴心,spring-security默认已经将我们可能需要用到的算法都放进去了,我们直接使用就可以了。
刚才我们主要说了PasswordEncoder的实例化,最后我们说一说,我们是如何使用PasswordEncoder的。回顾一下之前的源码,有这样的一段
@Bean
public AuthenticationProvider daoAuthenticationProvider(UserDetailsService userDetailsService) {
DaoAuthenticationProvider dap = new DaoAuthenticationProvider();
// 将实例化的passwordEncoder应用在AuthenticationProvider
dap.setPasswordEncoder(passwordEncoder());
dap.setHideUserNotFoundExceptions(false);
dap.setUserDetailsService(userDetailsService);
return dap;
}
我们看到,实例化的passwordEncoder被用到了DaoAuthenticationProvider 的属性中,然后我们看一下DaoAuthenticationProvider 的additionalAuthenticationChecks方法,里面使用passwordEncoder的matches方法验证了用户密码的正确性。
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还有一个encode方法用在哪里呢?前面我也提到过,这个主要用于保存密码的时候,在我们的service中可以注入已经实例化的PasswordEncoder对用户传过来的明文密码进行encode,然后再保存至数据库。