首先,SpringSecurity中的加密算法是哈希算法,这种算法是不可逆的,也就是说,加密完成后无法进行解密(暴力字典法除外)得到原明文内容。
简单总结一下,严格来说SpringSecurity没有解密这一说,在做匹配时,也是将需要匹配的内容采用同样的加密方式,同样的盐进行加密映射,然后再与已存储的内容进行比对。
这里简单介绍一下加密算法,常用的三类算法:
哈希加密:MD5、SHA1、HMAC
对称加密:DES、3DES、AES(加密解密使用同一个密钥)
非对称加密:RSA(私钥加密,公钥解密)
说是哈希算法不可逆,但暴力解密也可以得到原文。
言归正传,搜索了很多SpringSecurity是怎么解密的,都是纯文字,说的有鼻子有眼的,还是直接看源码,比较有安全感。
PasswordEncoder接口
BCryptPasswordEncoder类实现了PasswordEncoder接口,接口里有两个方法,encode用于加密,matches用于匹配验证。
package org.springframework.security.crypto.password;
public interface PasswordEncoder {
String encode(CharSequence var1);
boolean matches(CharSequence var1, String var2);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
BCryptPasswordEncoder类实现
endode方法 实现
首先按照一定规则生成盐,然后执行hashpw(明文,盐)方法,hashpw(明文,盐)方法中,对salt进行二次加工,生成real_salt,这个是最终的盐,之后生成加密字符串。
// 按照一定规则生成盐,然后执行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);
}
// 盐的生成
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");
}
}
// hashpw(明文,盐)方法中,对salt进行二次加工,生成real_salt,这个是最终的盐,之后生成加密字符串。
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");
}
}
}
matches
首先校验密文是否BCrypt加密以及是否为空,之后执行checkpw(明文,密文)方法,进行对比确认是否匹配。
// 参数:前端传入的明文,数据库中的密文。
// 首先校验密文是否BCrypt加密以及是否为空,之后执行checkpw(明文,密文)方法。
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;
}
}
执行equalsNoEarlyReturn(数据库中密文,hashpw(前端明文,数据库中密文))方法。上边已经说过,hashpw()方法是最终获取加密字符串的。
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匹配逻辑,是将前端明文加密(采用同样的算法规则,同样的盐),然后与数据库中密文进行比较验证。hashpw(明文,盐),可以看到第二个参数是盐耶,可是匹配的时候,数据库中的密文是作为第二个参数(盐)传进去的。
所以可以得到我们库中的加密字符串中是存在盐的,这样匹配验证的时候,从加密字符串(salt)中获取到最终盐(real_salt),然后再对前端传入明文加密验证比对。