密码存储历史:
最初,密码都以明文的方式进行存储,如用户设置的密码是123,那么落库的密码就是123。然而,恶意用户利用诸如sql注入或其他手段,获取到了数据库中的明文密码,此时用户在当前系统的隐私以及权限遭到泄露,更糟糕的是,如若用户有多系统共用一个密码的习惯,那么危害会更大(例如本人,qq、微信等许多账号都用一个密码
)。
开发人员开始利用单向散列的方式存储密码,也可以叫做消息摘要算法,通过此算法计算后得到的值叫做散列,此算法有很多实现,例如SHA256、MD5、HMAC等。此方式的特点:
- 单向性。原密码通过计算,散列出所谓的“加密“密码,但并不能由加密后的密码反推出原明文密码。
- 无论输入的密码多长,都会散列输出为定长。通常为128~256位之间,越长越安全。
注意,单向散列并不是一种加密算法,真正的加密算法,是可以通过相应算法生成密文,又可以通过相应算法反身生成原文,是双向的,通常被分为两大类:对称式和非对称式。对称式的方式,加密和解密用的是一个密钥。非对称式,加密用的密钥叫公钥,解密用的密钥叫私钥,公钥和私钥都是密钥。
使用这种方式,我们落库的密码为明文密码的散列值,当需要登录认证时,就将用户输入的明文密码用相同的散列算法进行散列,与数据库中所存散列进行比较。
通过这种方式,使用户密码变得安全了,就算数据库中的密码泄露出去,也只是一个个的散列值,恶意用户使用散列值登录系统,并不会登陆成功。
但是没有银弹,看似安全了,依旧有被破解的可能。黑客可以对每一个密码散列进行穷举计算,如果破解大量密码,就会消耗大量时间,所以他们做了一张字典表。把所有简单密码的散列一次性算出存储其中,此方式需要大量的存储空间,但换回了时间。穷举字典之所以只计算存储简单的密码,是因为密码过于复杂的系统,要穷举出所有散列,那么空间的代价是巨大的,甚至可能没有上限,所以他们对穷举字典进行改进,做出一张叫彩虹表的东西。彩虹表不对所有密码进行穷举计算存储,只是存储一些特定的散列值,每个散列都能反推出一条链的明文信息,这样的话,存储空间变少了,虽然在需要破解时还会进行计算,但消耗的时间总是好过最初的暴力散列破解(彩虹表很复杂,可以查阅相关专业资料)。
为了降低彩虹表的有效性,开发人员可以在原密码中加入一串随机字符,称作盐,每次散列,都是散列盐和密码拼接后的字符串,这样,就算是散列值泄露,通过彩虹表找出原值,也只是和盐组合后的原值而已,仍然不会登录成功,当然了,这也只是降低了彩虹表的有效性。
由于当代计算机的运算速度越来越快,对输入的消息进行散列也越来越快,黑客们的计算速度也越来越快,此时传统的单向散列算法可能不够安全了,我们可以使用自适应的单向散列算法,它可以有意的占用大量的CPU、内存,使散列过程更耗时,这样就等于降低了恶意用户计算机的速度。我们可以指定让这个算法的散列过程有多耗时,根据您系统的吞吐量以及访问情况,酌情设置,建议是1秒,这样既可以对恶意用户造成打击,又不会给系统造成压力。
SpringSecurity对密码的处理:
SpringSecurity5.3版本以后,官方推荐使用由自适应单向散列算法实现的编码器对密码进行散列。框架内置的编码器如下:
* bcrypt - {@link BCryptPasswordEncoder} (Also used for encoding)
* argon2 - {@link Argon2PasswordEncoder}
* pbkdf2 - {@link Pbkdf2PasswordEncoder}
* scrypt - {@link SCryptPasswordEncoder}
---------------------------------------------------------------
* ldap - {@link org.springframework.security.crypto.password.LdapShaPasswordEncoder}
* MD4 - {@link org.springframework.security.crypto.password.Md4PasswordEncoder}
* MD5 - {@code new MessageDigestPasswordEncoder("MD5")}
* noop - {@link org.springframework.security.crypto.password.NoOpPasswordEncoder}
* SHA-1 - {@code new MessageDigestPasswordEncoder("SHA-1")}
* SHA-256 - {@code new MessageDigestPasswordEncoder("SHA-256")}
* sha256 - {@link org.springframework.security.crypto.password.StandardPasswordEncoder}
其中BCryptPasswordEncoder
、Argon2PasswordEncoder
、Pbkdf2PasswordEncoder
、SCryptPasswordEncoder
均是由自适应单向散列算法实现的密码编码器,其他几种编码器被认为不在安全,但是考虑到有些老系统的依赖性,所以SpringSecurity并
没有将他们移除,但均被标注为过时状态,不建议使用。
为了增加对密码散列的扩展性以及适应性,SpringSecurity5.3以后还加入了一个名为DelegatingPasswordEncoder
的编码器,它持有所有的密码编码器,扩展性以及适应性极强,可以轻松完成各种密码系统的过渡。所以,就算我们丢弃掉SpringSecurity框架内置的验证过滤器自己实现一个,也推荐采用此编码器进行密码的加密以及校验。
DelegatingPasswordEncoder的工作原理
DelegatingPasswordEncoder
是一个密码编码器,它的功能是当我们保存用户密码的时候,对密码进行散列。当我们登录系统的时候,对密码进行验证。但是,真正做散列和比较的其实是上边我们提到的某个编码器,这更像是一种外观模式。
它默认被框架通过PasswordEncoderFactories
初始化,并以HashMap的方式持有所有类型的编码器,同时指定了一个默认加密的编码器BCryptPasswordEncoder
,当我们调用DelegatingPasswordEncoder
加密的时候,就调用这个默认加密的编码器散列加密。
每一个编码器都有一个存入HashMap的键,也叫id,就像我上边列出的一样,比如BCryptPasswordEncoder
的id就是bcrypt
。
当某个真正干活的编码器对密码进行散列后,它会把干活的编码器id用{}拼接到散列后的密码。例如:DelegatingPasswordEncoder
的默认干活的编码器是BCryptPasswordEncoder,如果我们不做调整,那么密码散列后的值就是$2a$10$ka.goPOwDmQSPQnTx0ROWu./3oebqdPYaTKp7MlI796IBt51PasCO
,最后返回的会是{bcrypt }$2a$10$ka.goPOwDmQSPQnTx0ROWu./3oebqdPYaTKp7MlI796IBt51PasCO
,在登陆系统需要进行比对的时候,需要从散列后的密码中根据{id}分析出用的哪个编码器,然后对用户当前键入的密码用此编码器进行散列,最后比较。
以下是PasswordEncoderFactories
源码,用来创建一个DelegatingPasswordEncoder
,它很简单:
public class PasswordEncoderFactories {
/**
* Creates a {@link DelegatingPasswordEncoder} with default mappings. Additional
* mappings may be added and the encoding will be updated to conform with best
* practices. However, due to the nature of {@link DelegatingPasswordEncoder} the
* updates should not impact users. The mappings current are:
*
* <ul>
* <li>bcrypt - {@link BCryptPasswordEncoder} (Also used for encoding)</li>
* <li>ldap - {@link org.springframework.security.crypto.password.LdapShaPasswordEncoder}</li>
* <li>MD4 - {@link org.springframework.security.crypto.password.Md4PasswordEncoder}</li>
* <li>MD5 - {@code new MessageDigestPasswordEncoder("MD5")}</li>
* <li>noop - {@link org.springframework.security.crypto.password.NoOpPasswordEncoder}</li>
* <li>pbkdf2 - {@link Pbkdf2PasswordEncoder}</li>
* <li>scrypt - {@link SCryptPasswordEncoder}</li>
* <li>SHA-1 - {@code new MessageDigestPasswordEncoder("SHA-1")}</li>
* <li>SHA-256 - {@code new MessageDigestPasswordEncoder("SHA-256")}</li>
* <li>sha256 - {@link org.springframework.security.crypto.password.StandardPasswordEncoder}</li>
* <li>argon2 - {@link Argon2PasswordEncoder}</li>
* </ul>
*
* @return the {@link PasswordEncoder} to use
*/
@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);
}
private PasswordEncoderFactories() {}
}
以下是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<String, PasswordEncoder> idToPasswordEncoder;
private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
/**
* Creates a new instance
* @param idForEncode the id used to lookup which {@link PasswordEncoder} should be
* used for {@link #encode(CharSequence)}
* @param idToPasswordEncoder a Map of id to {@link PasswordEncoder} used to determine
* which {@link PasswordEncoder} should be used for {@link #matches(CharSequence, String)}
*/
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;
this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
}
/**
* Sets the {@link PasswordEncoder} to delegate to for
* {@link #matches(CharSequence, String)} if the id is not mapped to a
* {@link PasswordEncoder}.
*
* <p>
The encodedPassword provided will be the full password
* passed in including the {"id"} portion.* For example, if the password of
* "{notmapped}foobar" was used, the "id" would be "notmapped" and the encodedPassword
* passed into the {@link PasswordEncoder} would be "{notmapped}foobar".
* </p>
* @param defaultPasswordEncoderForMatches the encoder to use. The default is to
* throw an {@link IllegalArgumentException}
*/
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);
}
/**
* 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 + "\"");
}
}
}
感兴趣的可以自己打开源码看一看这两个类,以及上边提到的各种密码编码器,后续SpringSecurity源码,我会深入讲解他们的用处。