在开发实践中,所有的用户密码都必须加密之后,再存储到数据库中。

用户的原始密码(例如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接口中定义的)