在开发实践中,所有的用户密码都必须加密之后,再存储到数据库中。
用户的原始密码(例如1234
)通常称之为原文或明文,加密后得到的结果(例如lkjfadshfdslafndshdsfaj
)通常称之为密文。
在处理加密时,通常应该选取消息摘要算法对用户的密码进行处理!
注意:不可以使用加密算法对密码进行加密并存储,通常,加密算法是用于保障传输过程的安全的!
消息摘要算法是不可逆的算法,是适合对密码进行加密的!
消息摘要算法的主要特点有:
- 同一种算法,无论消息长度多少,摘要的长度是固定的
- 当消息相同时,摘要必然相同
- 当消息不同时,摘要理论上不会相同(有概率是相同的)
- 消息的长度是无限的,摘要的长度是有限且固定的
需要注意:理论上有n种不同的消息对应同一个摘要,但是,出现这样的现象的概率极低!
典型的消息摘要算法有:
- MD系列(Message Digest):
MD2
/MD4
/MD5
- MD系列的全部是128位算法
- SHA家族(Secure Hash Algorithm):
SHA-1
/SHA-256
/SHA-384
/SHA-512
- SHA-1是160位算法,其它则是与算法名称对应,例如
SHA-256
就是256位算法
- SM3(国家加密算法)
- SM3是256位算法
在Spring Boot中,spring-boot-starter
依赖项就包含DigestUtils
工具类,可以简便的实现MD5算法的处理,例如:
package cn.tedu.csmall.passport;
import org.junit.jupiter.api.Test;
import org.springframework.util.DigestUtils;
public class MessageDigestTests {
@Test
public void testMd5() {
String rawPassword = "123456";
String encodedPassword = DigestUtils.md5DigestAsHex(rawPassword.getBytes());
System.out.println("rawPassword = " + rawPassword);
System.out.println("encodedPassword = " + encodedPassword);
// 123456 >>> e10adc3949ba59abbe56e057f20f883e
}
}
如果想要使用其它消息摘要算法,可以自行在项目中添加
commons-codec
依赖项,此依赖中也有一个名为DigestUtils
的工具类,提供了多种算法的API
由于消息算法的特点包括“消息相同,摘要必然相同”,所以,在互联网上有一些平台记录了消息与摘要的对应关系,记录在数据库,可以根据摘要进行反向查询,从而得知摘要对应的消息!但是,由于这些平台能够记录的对应关系非常有限,可以使用更复杂的消息,大概率是没有被这些平台收录的,则不会被这些平台反向查询出原消息!
换言之,只要原始密码足够复杂,则不会被这些平台“破解”。
但是,某些场景中并不支持使用复杂的消息(密码),也有些用户不愿意使用复杂的原始密码,则很容易被穷举出消息与摘要的对应列表,为解决此问题,应该在加密过程中使用“盐”,盐的本质就是一个字符串,其作用是使得被运算数据变得更加复杂,例如:
@Test
public void testMd5() {
String salt = "kjkhglkjjg";
String rawPassword = "123456";
// 123456kjkhglkjjg
String encodedPassword = DigestUtils.md5DigestAsHex((rawPassword + salt).getBytes());
System.out.println("rawPassword = " + rawPassword);
System.out.println("encodedPassword = " + encodedPassword);
}
而盐值的具体值并没有明确的要求,包括其使用方式也没有明确的要求!
另外,还可以尝试多重加密,即循环调用以上算法。
所以,为了提高密码的安全性:
- 强制要求使用强度更高的密码
- 加盐
- 多重加密
- 使用更安全的算法
- 综合使用以上做法
关于盐的补充:通常,可以使用随机的盐值,则即使完全相同的原始密码,得到的加密结果也完全不同,例如:
@Test
public void testMd5() {
for (int i = 0; i < 5; i++) {
String salt = UUID.randomUUID().toString();
String rawPassword = "123456";
String encodedPassword = DigestUtils.md5DigestAsHex((salt + rawPassword).getBytes());
System.out.println("rawPassword = " + rawPassword);
System.out.println("encodedPassword = " + encodedPassword);
System.out.println();
}
}
以上运行结果例如(每次都不同):
rawPassword = 123456
encodedPassword = 678408c66bef83edf72b11ad5b505161
rawPassword = 123456
encodedPassword = 99c3da1ef1d1e9ea976c91a00af0b4c0
rawPassword = 123456
encodedPassword = 52c809ab1ef18607c0f357d1caa4082f
rawPassword = 123456
encodedPassword = faf506f5d7a8d5109fc24d4c700fb136
rawPassword = 123456
encodedPassword = e89b5401bfbd233e24cb3862425ccdb8
需要注意的是,一旦使用随机的盐值,则必须将此随机的盐值记录下来(可以在添加数据时,在数据表中使用专门的字段进行记录,或者,将盐址和加密结果合并成1个字符串作为记录下来的密码),否则,在后续的验证密码时,将无法运算得到匹配的结果!
使用示例:
rawPassword = 123456
salt = 4da1ba18-e9c5-4adc-bc0e-3768aca841ad
encodedPassword = ef3bcab34967ab87d9a3002366439898
得到最终密码(盐值拼接密文):
4da1ba18-e9c5-4adc-bc0e-3768aca841adef3bcab34967ab87d9a3002366439898
当使用了Spring Security框架后,此框架中还包含了BCryptPasswordEncoder
类,此类可以使用BCrypt算法对密码进行处理,调用此类对象的encode()
方法即可实现加密,调用matches()
方法就可以实现将原文和密文进行对比!(这2个方法都是在PasswordEncoder
接口中定义的)