简介
2011年12月21日,有人在网络上公开了一个包含600万个 CSDN 用户资资料的数据库,数据全部为明文储存,包含用户名、密码以及注册邮箱。事件发生后CSDN在微博、官方网站等渠道发出了声明,解释说此数据库系2009年备份所用,因不明原因泄漏 已经向警方报案,后又在官网发出了公开道歉信。在接下来的十多天里,金山、网易 京东、当当、新浪等多家公司被卷入到这次事件中。整个事件中最触目惊心的莫过于 CSDN 把用户密码明文存储,由于很多用户是多个网站共用一个密码,因此一个网站密码泄漏就会造成很大的安全隐患。 由于有了这么多前车之鉴,我们现在做系统时,密码都要加密处理
在前面的案例中,凡是涉及密码的地方,我们都采用明文存储,在实际项目中这肯定是不可取的,因为这会带来极高的安全风险。在企业级应用中,密码不仅需要加密,还需要加盐,最大程度地保证密码安全。
常见方案
最早我们使用类似 SHA-256、SHA-512、MD5 等这样的单向 Hash 算法。用户注册成功后,保存在数据库中不再是用户的明文密码,而是经过 SHA-256 加密计算的一个字行串,当用户进行登录时,用户输入的明文密码用 SHA-256 进行加密,加密完成之后,再和存储在数据库中的密码进行比对,进而确定用户登录信息是否有效。如果系统遭遇攻击,最多也只是存储在数据库中的密文被泄漏
这样就绝对安全了吗?由于彩虹表这种攻击方式的存在以及随着计算机硬件的发展,每秒执行数十亿次 HASH 计算己经变得轻轻松松,这意味着即使给密码加密加盐也不再安全
在Spring Security中,我们现在是用一种自适应单向函数(AdaptiveOne-wayFunctions)来处理密码问题,这种自适应单向函数在进行密码匹配时,会有意占用大量系统资源(例如 CPU、内存等),这样可以增加恶意用户攻击系统的难度,在 Spring Securiy 中,开发者可以通过 bcrypt、PBKDF2、sCrypt 以及 argon2 来体验这种自适应单向函数加密。由于自适应单向函数有意占用大量系统资源,因此每个登录认证请求都会大大降低应用程序的性能,但是 Spring Secuity 不会采取任何措施来提高密码验证速度,因为它正是通过这种方式来增强系统的安全性
密码介绍
参考1:https://byronhe.gitbooks.io/libsodium/content/password_hashing
BCryptPasswordEncoder 使用 bcrypt 算法对密码进行加密,为了提高密码的安全性,bcrypt 算法故意降低运行速度,以增强密码破解的难度。同时 BCryptPasswordEncoder “为自己带盐” 开发者不需要额外维护一个 “盐” 字段,使用 BCryptPasswordEncoder 加密后的字符串就已经 “带盐” 了,即使相同的明文每次生成的加密字符串都不相同
Argon2PasswordEncoder 使用 Argon2 算法对密码进行加密,Argon2 曾在 Password Hashing Competition竞赛中获胜。为了解决在定制硬件上密码容易被破解的问题, Argon2 也是故意降低运算速度,同时需要大量内存,以确保系统的安全性
Pbkdf2PasswordEncoder 使用 PBKDF2 算法对密码进行加密,和前面几种类似,PBKDF2 算法也是一种故意降低运算速度的算法,当需要 FIPS(Federal Information Processing Standard,美国联邦信息处理标准)认证时,PBKDF2算法是一个很好的选择
SCryptPasswordEncoder 使用 scrypt 算法对密码进行加密,和前面的几种类似,scrypt 也是一种故意降低运算速度的算法,而且需要大量内存
PasswrodEncoder 与 DelegatingPasswordEncoder 关系



可以看到这里的调用了this.passwordEncoder.matches(presentedPassword, userDetails.getPassword()),这个this.passwordEncoder 是 PasswordEncoder 接口

接口 PasswordEncoder 有诸多实现类,都是不同类型的密码匹配规则

继续debug,发现这个PasswordEncoder 实现类叫做 DelegatingPasswordEncoded

进入这个 DelegatingPasswordEncoded 代理类实现的matches()方法,发现这个代理 PasswordEncoder 根据密文的前缀{noop}来获取真正用来加密的实现类(典型的策略设计模式)




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

bcrypt 方式加密
@Test
void contextLoads() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
// 参数是用户输入的明文
String encode = bCryptPasswordEncoder.encode("123");
System.out.println(encode);
// 这里的"10"代表散列次数,在创建 BCryptPasswordEncoder 时,可以设置参数
// 第一次加密: $2a$10$oCDDewSNLloZlagnFW0SYexBBBl6QnXwspj2..WZGNwO7tv7AbTPy
// 第二次加密: $2a$10$EeuYmlPFpuxJHDTe.jXRzuUrDRu0YrChz4NqUVu5n6BqTZgtc/aoS
// 第三次加密: $2a$10$AiUtLkjsyQ7m3oHVu6hZs.10/lf4ibUDs8ScmSC.XXRNNywsv8CjS
}

WebSecurityConfigurerAdapter -> AuthenticationManager -> PasswordEncoder -> DelegatingPasswordEncoder -> PasswordEncoderFactories


DelegatingPasswordEncoder
根据上面 PasswordEncoder 的介绍,可能会以为 SpringSecurity 中默认的密码加密方案应该是四种自适应单向加密函数中的一种,其实不然,在 SpringSecurity 5.0 之后,默认的密码加密方案其实是 DelegatingPasswordEncoder。从名字上来看, DelegatingPaswordEncoder 是一个代理类,而并非一种全新的密码加密方案,DelegatingPaswordEncoder 主要用来代理上面介绍的不同类型的密码加密方案。为什么采用 DelegatingPasswordEncoder 而不是某一个具体加密方式作为默认的密码加密方案呢?主要考虑了如下两方面的因素:
通过源码分析得知,如果再工厂中指定了 PasswordEncoder,就会使用指定 PasswordEncoder,否则就会使用默认 DelegatingPasswordEncoder,在 WebSecurityConfigurerAdapter 配置类中注入指定加密方式即可!
// 指定单一的加密方式
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 注意密码不同区别
@Bean
public UserDetailsService userDetailsService() {
// 定义内存用户信息管理者对象,将用户信息存储在内存当中
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
// 创建用户对象
UserDetails userDetails = User.withUsername("aaa").password("$2a$10$AiUtLkjsyQ7m3oHVu6hZs.10/lf4ibUDs8ScmSC.XXRNNywsv8CjS").roles("admin").build();
// 将用户对象交由用户管理者去管理
userDetailsService.createUser(userDetails);
return userDetailsService;
}
推荐使用 DelegatingPasswordEncoder 的另外一个好处就是自动进行密码加密方案的升级,这个功能在整合一些老的系统时非常有用


并 PasswordEncoder 对象是否需要进行 upgradeEncoding() 操作,去实现密码的更新

总结
对原有的密码进行升级(认证之后会随着框架的版本升级进行默认算法的更新),我们只需要同时对 UserDetailsService 和 UserDetailsPasswordService 进行实现即可,这需要我们手动去配置才会生效,如果不配置不会生效
package com.vinjcent.config.security;
import com.vinjcent.pojo.Role;
import com.vinjcent.pojo.User;
import com.vinjcent.service.RoleService;
import com.vinjcent.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.util.List;
@Component
public class DivUserDetailsService implements UserDetailsService, UserDetailsPasswordService {
// dao ===> springboot + mybatis
private final UserService userService;
private final RoleService roleService;
@Autowired
public DivUserDetailsService(UserService userService, RoleService roleService) {
this.userService = userService;
this.roleService = roleService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.查询用户
User user = userService.queryUserByUsername(username);
if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确!");
// 2.查询权限信息
List<Role> roles = roleService.queryRolesByUid(user.getId());
user.setRoles(roles);
return user;
}
// 默认使用 DelegatingPasswordEncoder,对密码进行更新时,使用的时 Bcrypt 加密
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
Integer result = userService.updatePassword(user.getUsername(), newPassword);
if (result > 0) {
((User) user).setPassword(newPassword);
}
return user;
}
}
开启了自动更新密码的功能之后,认证会将原有的密码进行升级更新(原本是"{noop}123")
