持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第16天,点击查看活动详情
SpringSecurity密码加密
在之前写的认证流程,在里面构建用户的时候的凡是涉及密码的地方,我们都是采用{noop}明文存储,在实际的项目中这肯定是不可能的,因为这样会给系统带来极高的安全隐患。因为用户在使用的时候,可能存在一个密码在多个系统里使用的情况,比如微信和qq的密码是一样的,这样的话密码一旦泄露,用户可能在多个系统上的数据就存在很大和风险,在企业级应用中,密码不仅需要加密,还需要加“盐”。“盐值”可以理解为一一个随机数(即盐),在数据库里和密码明文混合在一起进行加密,这样即使密码明文相同,生成的加密字符串也是不同的。当然,这个随机数也需要以明文形式和密码一起存储在数据库中。当用户需要登录时,拿到用户输入的明文密码和存储在数据库中的盐--起进行Hash运算,再将运算结果和存储在数据库中的密文进行比
较,进而确定用户的登录信息是否有效。
在Spring Security中,一种自适应单向函数(Adaptive One way Functions )来处理密码问题,这种自适应单向函数在进行密码匹配时,会有意占用大量系统资源(例如CPU、内存等),这样可以增加恶意用户攻击系统的难度。开发者也可以将用户名/密码这种长期凭证兑换为短期凭证,如会话、OAuth2令牌等,这样既可以快速验证用户凭证信息,又不会损失系统的安全性。
PasswordEncoder
PasswordEncoder接口
Spring Security中通过PasswordEncoder接口定义了密码加密以及比对的相关方法:
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
PasswordEncoder的源码比较简单,在接口中一共有三个方法:
- encode:该方法就是用来对原始密码进行加密。
- matches:该方法就是验证从存储中获得的编码密码与提交的原始密码是否匹配
- upgradeEncoding:如果编码后的密码应该重新编码以提高安全性,则返回true,否则返回false。默认实现总是返回false
常见实现类
在PasswordEncoder 中,针对密码的所有操作都定义好了,不同的实现类将采用不同的密码加密方案对密码进行处理。
- BCryptPasswordEncoder
BCryptPasswordEncoder使用bcrypt算法对密码进行加密,为了提高密码的安全性, bcrypt算法故意降低运行速度,以增强密码破解的难度。同时BCryptPasswordEncoder自己带有盐值,开发者不需要额外维护一个“盐”字段,使用BCryptPasswordEncoder加密后的字符串就已经“带盐”了,即使相同的明文每次生成的加密字符串都不相同。BCryptPasswordEncoder的默认强度为10,开发者可以根据自己的服务器性能进行调整,以确保密码验证时间约为1秒钟(官方建议密码验证时间为1秒钟,这样既可以提高系统安全性,又不会过多影响系统运行性能)。- Argon2PasswordEncoder
Argon2PasswordEncoder使用Argon2算法对密码进行加密, Argon2曾在Password Hashing Competition竞赛中获胜。为了解决在定制硬件上密码容易被破解的问题,Argon2也是故意降低运算速度,同时需要大量内存,以确保系统的安全性。- Pbkdf2PasswordEncoder
Pbkdf2PasswordEncoder使用PBKDF2算法对密码进行加密,和前面几种类似,PBKDF2算法也是一种故意降低运算速度的算法,当需要FIPS (全称Federal Information Processing Standard,美国联邦信息处理标准)认证时,PBKDF2 算法是一个很好的选择。- SCryptPasswordEncoder
SCryptPasswordEncoder使用scrypt算法对密码进行加密,和前面的几种类似,scrypt也是种故意降低运算速度的算法,而且需要大量内存。
这四种就是我们前面所说的自适应单向函数加密。除了这几种,还有一些基于消息摘要算法的加密方案,这些方案都已经不再安全,但是出于兼容性考虑,Spring Security并未移除。
DelegatingPasswordEncoder
在Spring Security 5.0之后,默认的密码加密方案其实是DelegatingPasswordEncoder,从名字上来看,DelegatingPasswordEncoder 是一个代理类,而不是向上面四种加密方案一样,它是全新的密码加密方案。DelegatingPasswordEncoder 主要用来代理上面介绍的不同的密码加密方案。
使用代理主要考虑了如下三方面的因素:
(1)兼容性:它可以在一个系统中同时支持多种不同的密码加密方案。
(2)便捷性:密码存储的最佳方案是一直变化, 使用DelegatingPasswordEncoder作为默认的密码加密方案,只需要修改小部分代码就可以实现加密方案的更换。
(3)稳定性:作为一个框架,Spring Security不能经常进行重大变化,而使用DelegatingPasswordEncoder可以方便地对密码进行升级(自动从一个加密方案升级到另外一个加密方案)。\
DelegatingPasswordEncoder对加密方法的实现
@Override
public String encode(CharSequence rawPassword) {
return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
}
encode方法的实现逻辑一目了然,具体的加密流程交给各个加密类来完成,只不过在密码加密完成后,给密文加上一个前缀,用来标记所采用的具体加密方案。
@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);
}
在matches方法中,首先调用extractld方法从加密字符串中提取出具体的加密方案id,也就是读取前缀,具体的提取方式就是字符串截取,然后获取到对应的加密方案。
其次进行判断,如果获取到的为null,说明不存在对应的加密实例,那么就会采用默认的密码匹配器defaultPasswordEncoderForMatches; 如果根据id获取到了对应的加密实例,则调用其matches方法完成密码校验。这就是matches方法的灵活之处,可以根据加密字符串的前缀,去匹配加密方案,进而完成密码校验。同一个系统中,加密字符串是使用不同的前缀,进而互不影响。
@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);
}
}
upgradeEncoding方法的逻辑是:如果当前加密字符串所采用的加密方案不是默认的加密方案,就会自动进行密码升级,否则就调用默认加密方案的upgradeEncoding方法判断密码是否需要升级。