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. BCryptPasswordEncoder类,生成盐salt并混淆rawpassword的代码;
//涉及到两个问题,生成salt盐gensalt(),混淆密码hashpw()
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);
}
:包名:org.springframework.security.crypto.bcrypt;类名:BCrypt
//生成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");
}
}
包名:org.springframework.security.crypto.bcrypt;类名:BCrypt
//混淆密码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");
}
}
}
提供的密码与加密之后的密码的校验,使用是BCryptpassword的matches()接口;
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值,用此值混淆明文,与密文做比较。 */
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;
}
}
以上是SpringSecurity的BCryptPassword加密方式,上面是介绍其SHA256+随机salt生成密文的基本点。应该还有其他的一些用法,容当后研究。
Springboot框架整合SpringSecurity组件,使用需要使用该加密方式,有一点需要注意,就是在springSecurity的配置文件中注入PasswordEncode的bean
//SpringSecurityConfigutarion配置类中加入
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
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.