2022年了,密码该如何保存都不会?!
大家好呀,我是小黑。
我们在开发应用时,只要涉及到用户,登录注册功能则是必不可少的。但是,并不是所有人都能做好登录注册功能。比如最基本的密码应该如何保存?应该用哪种加密方式对密码进行加密都不是很清楚。
一旦出现数据库泄漏,密码外泄等问题,会对用户造成极大的损失。
密码该如何保存?
如果我们要在服务器中对用户进行身份验证,我们需要完成以下的步骤:
- 获取到要登录用户的用户名和密码;
- 根据用户名在数据库中查找到用户;
- 比较用户提供的密码和数据库中的密码是否一致。
那我们应该如何存储用户的密码呢?我们来看看都有哪些方式,以及存在的问题。
明文保存
将用户的密码以明文方式保存。
很显然,有点常识的人都应该知道,密码不能用明文保存的。但是话又说回来,系统都是由人开发的,开发系统的人可能并不专业。比如之前某个大型中文开发者社区,因为数据库泄露,导致大批用户的密码泄漏,而他们的密码就是明文保存的。
HASH保存
使用Hash函数计算出密码的hash值保存,可以解决密码直接暴露的问题。
Hash函数是一个单向函数,不能通过结果值反向得出原始值,Hash函数可以将一串密码转换成一个固定长度的字符串。
- 在用户注册时,将用户的密码使用Hash函数计算出Hash值后保存到数据库;
- 当用户登录时,对用户提交的密码使用相同的Hash函数计算出Hash值,和数据库中的Hash值进行比较。
这样可以避免让攻击者直接获取到用户的密码明文,攻击者想通过暴力攻击将字符串计算出hash值则需要花费巨大的精力,并且Hash值越长破解难度越大。
但是通过彩虹表攻击,攻击者仍然可以成功破解。 彩虹表是一个包含许多提前计算出Hash值的表,其中包含数百万个密码对应的hash值,对于一些简单密码可以非常快的破解。
所以,如果你不确定你注册的服务是采用哪种方式保存的密码,尽量将密码复杂度设置高一些。
加盐Hash
为了防止彩虹表攻击,可以使用Hash算法加盐处理。
盐是在进行Hash计算时,和原始密码拼接在一起进行计算的一个随机序列。
- 用户注册时,将密码和盐值组合后进行Hash计算,得到密码结果保存在数据库中;
- 当用户在登录验证时,将原始密码加盐后进行Hash计算,得到结果值和数据库中的密码进行比较。
因为彩虹表中的密码和加盐后的密码不一样,可以防止彩虹表攻击。如果盐值足够长并且随机,那么就可以保证在彩虹表中不能找到和密码相同的hash值。
但是,由于攻击者是有可能获取到盐值的,攻击者可以调整彩虹表生成的算法,用获取到的盐值计算出新的彩虹表,同样可以获取到密码。虽然计算一个新的彩虹表花费的时间巨大,但是随着硬件条件越来越好,要计算出一张彩虹表会变得越来越容易。
所以,使用Hash算法加盐处理,可以保证密码不被快速破解,但是还不够安全。
密码加密函数
Hash函数设计的初衷并不仅仅是对密码进行Hash计算,所以Hash函数的运算速度非常快,但是这样一来,攻击者也能快速计算hash值,进行暴力破解。
为了解决这个问题,我们可以让Hash加密函数变慢。
我们只要让密码加密的时间在用户能接受的时间内,尽量的慢,这样攻击者蛮力破解将会花费无限的时间。
有以下一些专门用来加密密码的算法:
- bcrypt
- scrypt
- PBKDF2
- argon2
这些算法使用一些复杂的加密算法,并会故意让计算变慢。
工作因子
可以通过在算法中配置工作因子,来调整加密函数计算时间的缓慢程度。
每个密码加密算法都有自己的工作因子。工作因子影响密码编码的速度。例如,bcrypt
有参数strength
,该算法将使2的strength
次方来计算哈希值。数字越大,编码越慢。
使用Spring Security加密密码
现在让我们看看 Spring Security 如何支持这些算法,以及我们如何使用它们加密密码。
PasswordEncoder
在Spring Security 中有一个PasswordEncoder
接口。所有密码编码器都实现了该接口。
publicinterface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword){
returnfalse;
}
}
该接口中有两个方法:
encode()
方法用户将明文密码转换为密文形式;
matches()
方法用户将明文密码与密文密码进行比较。
BCryptPasswordEncoder
String plainPassword = "123456";
// 工作因子
int strength = 10;
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(strength, new SecureRandom());
String encodedPassword = bCryptPasswordEncoder.encode(plainPassword);
System.out.println(encodedPassword);
BCryptPasswordEncoder
中的参数strength
是密码加密算法的工作因子,Spring Security中的默认值为10
。
在创建时指定SecureRandom
作为随机加盐生成器。
$2a$10$pYxXvggEgN7znYKofHIr/uRTw.dsYeW9mbxzNMSNOoGIYZU8twXNG
Pbkdf2PasswordEncoder
PBKDF2 算法不是为密码编码专门设计的,而是为了从密码中派生出密钥而设计的。当我们想用密码对某些数据进行加密时,通常需要密钥,但密码的强度不足以用作加密密钥。
String plainPassword = "123456";
//加密秘钥
String pepper = "小黑说JAVA";
// 哈希次数
int iterations = 200000;
// 哈希长度
int hashWidth = 256;
Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder(pepper, iterations, hashWidth);
pbkdf2PasswordEncoder.setEncodeHashAsBase64(true);
String encodedPassword = pbkdf2PasswordEncoder.encode(plainPassword);
System.out.println(encodedPassword);
Pbkdf2PasswordEncoder
会多次在普通密码上运行哈希算法。我们可以定义输出的hash长度,并额外使用pepper
让密码编码更安全。
WnCG4wMZFHPAD9DGg+SChNceQqbeAZRQyf2OHCK5WKdYBRzbeAGsQg==
Pbkdf2PasswordEncoder默认会执行185000哈希计算,默认的哈希长度为256。
SCryptPasswordEncoder
SCryptPasswordEncoder算法可以配置CPU和内存成本,通过这两项配置可以让攻击者破解密码的难度更大。
String plainPassword = "123456";
// cpu消耗
int cpuCost = (int) Math.pow(2, 14);
// 内存消耗
int memoryCost = 8;
// currently not supported by Spring Security
int parallelization = 1;
// 秘钥长度
int keyLength = 32;
// 盐值长度
int saltLength = 64;
SCryptPasswordEncoder sCryptPasswordEncoder = new SCryptPasswordEncoder(
cpuCost,
memoryCost,
parallelization,
keyLength,
saltLength);
String encodedPassword = sCryptPasswordEncoder.encode(plainPassword);
System.out.println(encodedPassword);
输出结果如下:
$e0801$PgZZvXdDjbxMZJi4eidFCHblUdvwOT/n0FZFyCWIHloqL6Wkbk7bAJ2nwVIWsW9PJTodncEtok1qcaWR+u+pZg==$lcqK7ACDTv8gG3ZwGoz0X7rn4EnZvnEcZ7rS0Qq31Ng=
Argon2PasswordEncoder
Argon2算法是2015 年密码哈希竞赛的获胜者。该算法也允许我们调整 CPU 和内存成本。该算法将所有参数保存在结果字符串中。
int saltLength = 16;
int hashLength = 32;
int parallelism = 1;
int memory = 4096;
int iterations = 3;
Argon2PasswordEncoder argon2PasswordEncoder = new Argon2PasswordEncoder(
saltLength,
hashLength,
parallelism,
memory,
iterations);
String encodePassword = argon2PasswordEncoder.encode(plainPassword);
输出结果如下:
$argon2id$v=19$m=4096,t=3,p=1$uft4b+crs6tiwOhDnuFsIg$d/GXjYZnEw+/ubVnPqNeQDFX32GRYe+yTwuwydXLjos
在Spring Boot中设置PasswordEncoder
接下来,为了能更好的了解PasswordEncoder在Spring Boot中如何应用,我们先来开发一个Rest Api接口,并且配置Spring Security支持基于密码验证。
配置PasswordEncoder
首先,我们创建一个需要Spring Security保护的Rest API:
@RestController
publicclass BlogRest {
@GetMapping(path = "/blogs")
public List<Blog> blogs(){
return Lists.newArrayList(new Blog("hello world", "小黑说Java"));
}
}
我们需要/blogs
接口的访问需要经过用户身份的验证。因此,我们使用 Spring Security 配置:
/**
* @author 小黑说Java
* @ClassName SecurityConfiguration
* @Description
* @date 2022/2/3
**/
@Configuration
@EnableWebSecurity
publicclass SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/registration")
.permitAll()
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
// other codes.
}
- 该配置表示除了
/registration
外,其他的请求路径都需要进行身份验证; - 每当向应用程序发送 HTTP 请求时,Spring Security 都会检查
Header
是否包含Authorization: Basic <credentials>
. - 如果未设置Header,则服务器会返回
401
; - 如果 Spring Security 找到对应Header,它将进行身份验证。
Spring Security 在进行身份验证时,需要从数据库中查询用户名、密码信息,需要提供一个UserDetailsService
接口的实现类,实现该接口中的loadUserByUsername
方法。所以我们定义如下接口DatabaseUserDetailsService
:
/**
* @author 小黑说Java
* @ClassName DataBaseUserDetailService
* @Description
* @date 2022/2/3
**/
@Service
@Transactional
publicclass DataBaseUserDetailService implements UserDetailsService {
privatefinal UserDAO userDAO;
privatefinal UserMapper userMapper;
public DataBaseUserDetailService(UserDAO userDAO, UserMapper userMapper){
this.userDAO = userDAO;
this.userMapper = userMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDAO.selectByUserName(username);
return userMapper.toUserDetails(user);
}
}
Spring Security中的AuthenticationProvider
接口的实现在身份验证时将使用UserDetailsService
来执行身份验证逻辑。
AuthenticationProvider
接口的实现有很多,因为我们的用户信息存在数据库中,所以我们使用DaoAuthenticationProvider
:
@Configuration
@EnableWebSecurity
class SecurityConfiguration extends WebSecurityConfigurerAdapter {
privatefinal DatabaseUserDetailsService databaseUserDetailsService;
// constructor ...
@Bean
public AuthenticationProvider daoAuthenticationProvider(){
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
// 设置密码加密器
provider.setPasswordEncoder(passwordEncoder2());
// 设置用户信息查询服务
provider.setUserDetailsService(this.databaseUserDetailsService);
return provider;
}
@Bean
public PasswordEncoder passwordEncoder(){
returnnew BCryptPasswordEncoder();
}
// ...
}
到这里,我们已经完成了Spring Security的配置,如果客户端发送带有基本身份验证的Header的HTTP请求,Spring Security将读取该Header信息,根据username获取数据库中的用户信息,并使用BCryptPasswordEncoder
进行密码验证,如果一致则验证通过。如果不一致,服务器将响应 401。
用户注册服务
在验证用户身份之前,我们需要先在数据库中保存用户,也就是用户需要先注册账号。那么我们来实现一个用户注册的接口:
@RestController
publicclass UserRest {
privatefinal UserRegistrationService userRegistrationService;
public UserRest(UserRegistrationService userRegistrationService){
this.userRegistrationService = userRegistrationService;
}
@PostMapping("/registration")
@ResponseStatus(code = HttpStatus.CREATED)
public void register(@RequestBody UserDTO user){
// 注册用户
userRegistrationService.register(user);
}
}
按照我们对Spring Security规则的定义,/registration
路径的访问不需要进行身份验证。
我们在register
方法中调用userRegistrationService.register(user)
进行用户注册。
@Service
@Transactional
publicclass UserRegistrationService {
privatefinal UserDAO userDAO;
privatefinal PasswordEncoder passwordEncoder;
public UserRegistrationService(UserDAO userDAO, PasswordEncoder passwordEncoder){
this.userDAO = userDAO;
this.passwordEncoder = passwordEncoder;
}
public void register(UserDTO userDTO){
User user = new User();
user.setUserStatus(UserStatusEnum.INFORCE.getStatus());
user.setUsername(userDTO.getUsername());
user.setPassword(passwordEncoder.encode(userDTO.getPassword()));
userDAO.insert(user);
}
}
在进行用户注册时,我们通过PasswordEncoder
将用户提供的明文密码进行加密后,保存到数据库中。
小结
以上就是本期的主要内容,我们讲了密码应该如何在系统中保存,最没有常识和安全意识的就是明文保存;使用Hash算法保存会被彩虹表攻击,同样也不可取;而使用加盐Hash加密虽然能一定程度地降低彩虹表攻击的可能性,但是随着硬件性能的发展,同样可能被彩虹表攻击,所以我们应该选择一些特定的密码加密算法。比如Bcrypt
,Pbkdf2
,Scrypt
,Argon2
等。
最后我们通过SpringBoot+Spring Security完成了一个用户登录和注册的功能。
希望本文能对你有所帮助,写文不易,需要一点正反馈,点赞在看转发三连一下!!!
我是小黑,一名在互联网“苟且”的程序员
流水不争先,贵在滔滔不绝