springSecurity Bcrypt hash password

SpringSecurity之加密篇—BCryptpassword类

  1. 要点概括
    BCryptpassword类是SpringSecurity的加密工具,封装了对密码混淆加密的方法,主要是采用盐(salt)对原始密码进行混淆。
    本篇介绍的是利用BCryptpassword随机生成盐(salt),使用该盐值对原始密码进行混淆加密。
    这种加密方式有两个特点:(1)将随机生成的盐(salt)存到混淆后的代码中;(2)对于相同的明文每一次加密,加密之后的密文都是不一样的;因为盐值(salt)不同。这样的好处就是更增加了密码的安全性。

SpringSecurity中的BCryptPassword采用Hash处理,其过程是不可逆的。

BCryptPassword的加密过程:
(1)加密(encode):用户注册时,使用SHA256+盐(salt)把用户输入的密码进行hash混淆处理,得到密码的hash值,然后将其存入数据库中。
(2)密码校验(matches):用户登录时,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是使用相同的算法结合盐值salt把用户输入的密码进行hash处理,得到密码的hash值,然后将其与从数据库中查询到的密码hash值进行比较。如果两者相同,说明用户输入的密码正确,这里的盐值(salt)是从数据库中查询到的密码hash值中解析出来的。

  1. 核心代码分析
    //1. BCryptPasswordEncoder类,生成盐salt并混淆rawpassword的代码;
    //涉及到两个问题,生成salt盐gensalt(),混淆密码hashpw()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public String encode(CharSequence rawPassword) {
    String salt;
    if (this.strength > 0) {
    if (this.random != null) {
    salt = BCrypt.gensalt(this.strength, this.random);
    } else {
    salt = BCrypt.gensalt(this.strength);
    }
    } else {
    salt = BCrypt.gensalt();
    }

    return BCrypt.hashpw(rawPassword.toString(), salt);
    }

2.1 生成盐(salt):
:包名:org.springframework.security.crypto.bcrypt;类名:BCrypt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//生成salt盐gensalt() 
public static String gensalt(int log_rounds, SecureRandom random) {
if (log_rounds >= 4 && log_rounds <= 31) {
StringBuilder rs = new StringBuilder();
byte[] rnd = new byte[16];
random.nextBytes(rnd);
rs.append("$2a$");
if (log_rounds < 10) {
rs.append("0");
}

rs.append(log_rounds);
rs.append("$");
encode_base64(rnd, rnd.length, rs);
return rs.toString();
} else {
throw new IllegalArgumentException("Bad number of rounds");
}
}

2.2 混淆方法:
包名:org.springframework.security.crypto.bcrypt;类名:BCrypt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//混淆密码hashpw()
public static String hashpw(String password, String salt) throws IllegalArgumentException {
char minor = 0;
int off = false;
StringBuilder rs = new StringBuilder();
if (salt == null) {
throw new IllegalArgumentException("salt cannot be null");
} else {
int saltLength = salt.length();
if (saltLength < 28) {
throw new IllegalArgumentException("Invalid salt");
} else if (salt.charAt(0) == '$' && salt.charAt(1) == '2') {
byte off;
if (salt.charAt(2) == '$') {
off = 3;
} else {
minor = salt.charAt(2);
if (minor != 'a' || salt.charAt(3) != '$') {
throw new IllegalArgumentException("Invalid salt revision");
}

off = 4;
}

if (saltLength - off < 25) {
throw new IllegalArgumentException("Invalid salt");
} else if (salt.charAt(off + 2) > '$') {
throw new IllegalArgumentException("Missing salt rounds");
} else {
int rounds = Integer.parseInt(salt.substring(off, off + 2));
String real_salt = salt.substring(off + 3, off + 25);

byte[] passwordb;
try {
passwordb = (password + (minor >= 'a' ? "\u0000" : "")).getBytes("UTF-8");
} catch (UnsupportedEncodingException var13) {
throw new AssertionError("UTF-8 is not supported");
}

byte[] saltb = decode_base64(real_salt, 16);
BCrypt B = new BCrypt();
byte[] hashed = B.crypt_raw(passwordb, saltb, rounds);
rs.append("$2");
if (minor >= 'a') {
rs.append(minor);
}

rs.append("$");
if (rounds < 10) {
rs.append("0");
}

rs.append(rounds);
rs.append("$");
encode_base64(saltb, saltb.length, rs);
encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1, rs);
return rs.toString();
}
} else {
throw new IllegalArgumentException("Invalid salt version");
}
}
}

2.3 密码的校验
提供的密码与加密之后的密码的校验,使用是BCryptpassword的matches()接口;

1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword != null && encodedPassword.length() != 0) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
} else {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
} else {
this.logger.warn("Empty encoded password");
return false;
}
}

/**
 * 密码比较入口,同样是调用了hashpw()方法: 使用的是hashpw()方法,有两个参数:原始明文与数据库中获取的密文。
 * hashpw()方法是从密文中解析出藏在其中的盐salt值,用此值混淆明文,与密文做比较。
 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   public static boolean checkpw(String plaintext, String hashed) {
return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));
}
//这个方法就是比较是否相同而已,无他。
static boolean equalsNoEarlyReturn(String a, String b) {
char[] caa = a.toCharArray();
char[] cab = b.toCharArray();
if (caa.length != cab.length) {
return false;
} else {
byte ret = 0;

for(int i = 0; i < caa.length; ++i) {
ret = (byte)(ret | caa[i] ^ cab[i]);
}

return ret == 0;
}
}
  1. 总结
    以上是SpringSecurity的BCryptPassword加密方式,上面是介绍其SHA256+随机salt生成密文的基本点。应该还有其他的一些用法,容当后研究。

  2. 其它
    Springboot框架整合SpringSecurity组件,使用需要使用该加密方式,有一点需要注意,就是在springSecurity的配置文件中注入PasswordEncode的bean

    //SpringSecurityConfigutarion配置类中加入
    @Bean
    public PasswordEncoder passwordEncoder() {

    return new BCryptPasswordEncoder();
    

    }
    代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public static void main(String[] args) {
    String password = "leo_epam";
    System.out.println(password + ": encrypt");
    int i = 0;
    while(i < 10){
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    String hashedPassword = passwordEncoder.encode(password);
    System.out.println("encryptPassword:" + hashedPassword);
    System.out.println("match result:" + passwordEncoder.matches(password,hashedPassword));
    i ++;
    }
    }

代码输出:

leo_epam: encrypt
encryptPassword:$2a$10$/WIa4YbBMGQX7dBbYwRKx.AmQ3sJ8Ta5wYHh8a7jPlYDbNgMSD/hC
match result:true
encryptPassword:$2a$10$VryRC1lFaTEqUbOUyvu18ulSSGH5hK7JPzAG17ehtkL8aV6fot1ru
match result:true
encryptPassword:$2a$10$0SVRXbfgsqMvpmNDeeqNl.eFPvQ0ojdazk5.x8YDZxIOOP1D4xXge
match result:true
encryptPassword:$2a$10$yWdI4q6DXDZhknTxiT/9ROdhcAARAEY5q4L2YI5uQ3C52Q7Lt/IAa
match result:true
encryptPassword:$2a$10$8yy0.NdQSD2Cig5yIri2eu1XmDRpZSPjsmxDyRg9CZ5afwp/36H2S
match result:true
encryptPassword:$2a$10$QTJpm3jYUCjAoHPVI/uon.FDusm.9tPSc.mk6m.l/kx8aKbIzov3i
match result:true
encryptPassword:$2a$10$CkoGrZqDE86LM1yiX89cpuNDAMdDqemEKMSS3/jquFsxocizgBbX2
match result:true
encryptPassword:$2a$10$6M0N6cQp6kKeXgRl8ftqyOlikAV9YwfMS93xlqTXbd/tmDjFDv3iG
match result:true
encryptPassword:$2a$10$LboMUNAF7vOmucxJI3G/w.cliDRxH1exOmsfy2IlPCUpZU8N7XoO.
match result:true
encryptPassword:$2a$10$ezB0okY.JHv1gt4Y3chiu.5e5R9.vXqd7kNaYH14Vigm9wZ02D8Pe
match result:true
以上代码就是BCryptPassword的代码简单效果示例,相同的明文,每次生成的密文都是不同的,但是和明文做密码校验都是通过的。
还有一个就是密码字段的长度,如果打算采用bcrypt加密存储,字段长度不得低于60.