一、密码加密
2011年12月21日,有人在网络上公开了一个包含600万个CSDN 用户资料的数据库,数据全部为明文储存,包含用户名、密码以及注册邮箱。事件发生后CSDN 在微博、官方网站等渠道发出了声明、解释说此数据库系2009 年备份所用,因不明原因泄漏,已经向警方报案,后又在官网发出了公开道歉信。在接下来的十多天里,金山,网易,京东、当当、新等多家公司被卷入到这次事件中。整个事件中最触目惊心的莫过于CSDN把用户密码明文存储,由于很多用户是多个网站共用一个密码,因此一个网站密码泄漏就会造成很大的安全患。由于有了这么多前车之鉴、我们现在做系统时,密码都要加密处理。
在前面的案例中,凡是涉及密码的地方,我们都采用明文存储,在实际项目中这肯定是不可取的,因为这会带来极高的安全风险。在企业级应用中,密码不仅需要加密,还需要加大程度地保证密码安全,防止用户信息泄露;
二、密码加密的常见方案
2.1.1 Hash 算法
最早我们使用类似 SHA-256、SHA-512、MD5等这样的单向 Hash 算法。用户注册成功后,保存在数据库中不再是用户的明文密码,而是经过 SHA-256 加密计算的一个字行串当用户进行登录时,用户输入的明文密码用SHA-256 进行加密,加密完成之后,再和存储在数据库中的密码进行比对,进而确定用户登录信息是否有效。如果系统遭遇攻击,最多也只是存储在数据库中的密文被泄漏。
这样就绝对安全了吗?由于彩表这种攻击方式的存在以及随着计算机硬件的发展,每利执行数十亿次 HASH计算已经变得轻轻松松,这意味着即使给密码加密加盐也不再安全;
2.1.2 单向自适应函数
在Spring Security 中,我们现在是用一种自适应单向函数(Adaptive One-way Functions)来处理密码问题,这种自适应单向函数在进行密码匹配时,会有意占用大量系统资源(例如CPU、内存等),这样可以增加恶意用户攻击系统的难度。在Spring Securiv 中,开发者可以通过bcrypt、PBKDF2、sCrvpt以及argon2 来体验这种自适应单向函数加密。由于自适应单向函数有意占用大量系统资源,因此每个登录认证请求都会大大降低应用程序的性能,作是SpringSecuity不会采取任何措施来提高密码验证速度,因为它正是通过这种方式来增强系统的安全性。
三、PasswordEncoder 密码加密器
Spring Security的PasswordEncoder接口用于执行密码的单向转换,以允许安全地存储密码。鉴于PasswordEncoder是一种单向转换,当密码转换需要双向转换时(即存储用于向数据库进行身份验证的凭据),就不打算这样做。通常,PasswordEncoder用于存储在身份验证时需要与用户提供的密码进行比较的密码。
3.1 BCryptPasswordEncoder
BCryptPasswordEncoder使用 bcrypt 算法对密码进行加密,为了提高密码的安全性,bcrypt算法故意降低运行速度,以增强密码破解的难度。同时 BCryptP asswordEncode“为自己带盐”开发者不需要额外维护一个“盐”字段,使用BCryptPasswordEncoder 加密后的字符串就已经“带盐”了,即使相同的明文每次生成的加密字符串都不相同。
3.2 Argon2PasswordEncoder
Argon2PasswordEncoder 使用Argon2 算法对密码进行加密,Argon2 曾在 PassworHashing Competition 竞赛中获胜。为了解决在定制硬件上密码容易被破解的问题Argon2也是故意降低运算速度,同时需要大量内存,以确保系统的安全性。
3.3 Pbkdf2PasswordEncoder
Pbkdf2PasswordEncoder使用PBKDF2算法对密码进行加密和前面几种类似,PBKDF2算法也是一种故意降低运算速度的算法,当需要FIPS(Federal Information ProcessingStandard美国联邦信息处理标准)认证时,PBKDF2 算法是一个很好的选择。
3.4 SCryptPasswordEncoder
SCryptPasswordEncoder 使用scrypt 算法对密码进行加密,和前面的几种类似,serypt也是一种故意降低运算速度的算法,而且需要大量内存。
3.5 默认的几种加密方式
@Test
public void test() f
//1 .BCryptPasswordEncoder
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder()
System.out.println(bCryptPasswordEncoder.encode( "123"));
//2.Pbkdf2PasswordEncode
Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder()
System.out.println(pbkdf2PasswordEncoder.encode("123"));
//3.SCryptPasswordEncoder //需要额外引入依赖
SCryptPasswordEncoder sCryptPasswordEncoder = new SCryptPasswordEncoder()
System.out.println(sCryptPasswordEncoder.encode("123")):
1/4.Argon2PasswordEncoder //需要额外入依邮
Argon2PasswordEncoder argon2PasswordErcoder = new Argon2PasswordEncoder():
System.out.println(argon2PasswordEncoder.encode("123"));
}
四、DelegatingPasswordEncoder 密码加密器代理
在Spring Security 5.0之前,默认的PasswordEncoder是NoOpPasswordEncoder,它需要纯文本密码。根据密码历史部分,您可能会认为默认的PasswordEncoder现在类似于BCryptPasswordEncoder。然而,这忽略了三个现实世界的问题:
1、有许多使用旧密码编码的应用程序无法轻松迁移
2、密码存储的最佳做法将再次更改
3、作为一个框架,Spring Security不能频繁进行破坏性更改
相反,Spring Security引入了DelegatingPasswordEncoder,通过以下方式解决了所有问题:
1、确保使用当前的密码存储建议对密码进行编码
2、允许验证现代和传统格式的密码
3、允许将来升级编码
4.1 创建一个DelegatingPasswordEncoder
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());
PasswordEncoder passwordEncoder =
new DelegatingPasswordEncoder(idForEncode, encoders);
4.2 为什么在密码前面加上{noop}其他前缀就可以匹配
4.2.1 源码部分:WebSecurityConfigurerAdapter
总结:通过源码我们看到当我们加上了noop 最后会调用NoOpPasswordEncoder 实现,直接通过equals 判断,直接字符串比较得到是否相等
4.3.2 密码在何处使用
总结: 通过源码分析得知如果在工厂中指定了PasswordEncoder,就会使用指定PasswordEncoder. 否则就会使用默认DelegatingPasswordEncoder。
五、如何使用 PasswordEncoder
我们可以使用动态配置和固定配置两种模式,默认就是动态配置模式,什么都不写
5.1 使用动态密码配置
实际上就写自己的加密方式,不要手动创建 PasswordEncoder 的bean对象就可以了
@SpringBootTest
public class SecurityConfigurationTest {
@Test
public void userDetailsService() {
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String encode = encoder.encode("123456");
System.out.println(encode);
}
}
得到密码:b694b853c2a55717df36e90fd46dae9bbaad46461f9c8bcc3564ef06908e77400f35f3184c11150d
/**
* 自定义数据源
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(User.withUsername("test").password("{pbkdf2}b694b853c2a55717df36e90fd46dae9bbaad46461f9c8bcc3564ef06908e77400f35f3184c11150d").authorities("admin").build());
return userDetailsManager;
}
5.1.1 测试登录是否能匹配
5.2 使用固定密码加密方案
系统使用指定的某一种加密方案,不基于动态配置
5.2.1 注入加密编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new Pbkdf2PasswordEncoder();
}
5.2.2 去掉密码的前缀 {pbkdf2}
/**
* 自定义数据源
* @return {pbkdf2}
*/
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(User.withUsername("test").password("b694b853c2a55717df36e90fd46dae9bbaad46461f9c8bcc3564ef06908e77400f35f3184c11150d").authorities("admin").build());
return userDetailsManager;
}
5.2.3 源码实现
六、密码自动升级
spring security 基于安全考虑,设计了一套能自动进行升级的配置,只需要实现UserDetailsPasswordService 就会更新
通过源码分析我们得知,需要实现UserDetailsPasswordService 方法,而且密码必须是{noop}12345 这种模式
6.1 代码配置
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(User.withUsername("test").password("{noop}12345").authorities("admin").build());
return userDetailsManager;
}
6.2 源码
更新后,将user密码重新赋值