spring security的BCryptPasswordEncoder加密和对密码验证的原理分析
作者:农码天下
一、加密算法和hash算法
很多项目中有些机密的信息需要进行加密来保护用户或者公司的信息安全,这时这些信息会采用加密以密文的形式暴露在外面。
加密算法是一种可逆的算法,是通过一定的规则对明文进行各种计算的到的密文从而实现加密的效果。
- hash算法是不可逆的,常见的MD5加密采用的就是hash的算法进行加密。加密算法是可逆的,所以很多情况下加密规则是很重要的,一旦暴露就可以根据规则进行逆推得到明文,所以加密算法通常会加盐,对称加密和非对称加密就是加盐的一种。
- hash算法虽然不可逆,但通过大数据进行匹配很多数据可以被找到,所以hash算法也是需要加盐来保证一定的机密性。
二、BCryptPasswordEncoder 加密和解密的原理
BCryptPasswordEncoder对同样的数据比如“11111”进行加密,每次加密的结果是不相同的,此时就要思考一个问题,同样的数据每次加密不同,那么它是如何进行解密的呢?
下面来分析一下此方式的加密源码,参考了网上的一些资料:
三、源码解析
BCryptPasswordEncoder类实现了PasswordEncoder接口,这个接口中定义了两个方法
public interface PasswordEncoder { String encode(CharSequence rawPassword); boolean matches(CharSequence rawPassword, String encodedPassword); }
其中encode(...)是对字符串进行加密的方法,matches使用来校验传入的明文密码rawPassword是否和加密密码encodedPassword相匹配的方法。
即对密码进行加密时调用encode,登录认证时调用matches
下面我们来看下BCryptPasswordEncoder类中这两个方法的具体实现
1. encode方法
public String encode(CharSequence rawPassword) { String salt; if (strength > 0) { if (random != null) { salt = BCrypt.gensalt(strength, random); } else { salt = BCrypt.gensalt(strength); } } else { salt = BCrypt.gensalt(); } return BCrypt.hashpw(rawPassword.toString(), salt); }
可以看到,这个方法中先基于某种规则得到了一个盐值,然后在调用BCrypt.hashpw方法,传入明文密码和盐值salt。所以我们再看下BCrypt.hashpw方法中做了什么
2. BCrypt.hashpw方法
public static String hashpw(String password, String salt) throws IllegalArgumentException { BCrypt B; String real_salt; byte passwordb[], saltb[], hashed[]; char minor = (char) 0; int rounds, off = 0; StringBuilder rs = new StringBuilder(); if (salt == null) { throw new IllegalArgumentException("salt cannot be null"); } int saltLength = salt.length(); if (saltLength < 28) { throw new IllegalArgumentException("Invalid salt"); } if (salt.charAt(0) != '$' || salt.charAt(1) != '2') { throw new IllegalArgumentException("Invalid salt version"); } 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"); } // Extract number of rounds if (salt.charAt(off + 2) > '$') { throw new IllegalArgumentException("Missing salt rounds"); } rounds = Integer.parseInt(salt.substring(off, off + 2)); real_salt = salt.substring(off + 3, off + 25); try { passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8"); } catch (UnsupportedEncodingException uee) { throw new AssertionError("UTF-8 is not supported"); } saltb = decode_base64(real_salt, BCRYPT_SALT_LEN); B = new BCrypt(); 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(); }
可以看到,这个方法中先根据传入的盐值salt
,然后基于某种规则从salt得到real_salt
,后续的操作都是用这个real_salt来进行,最终得到加密字符串。
所以这里有一个重点:传入的盐值salt
并不是最终用来加密的盐,方法中通过salt得到了real_salt
,记住这一点,因为后边的匹配方法matches中要用到这一点。
3. matches方法
matches方法用来判断一个明文是否和一个加密字符串对应。
public boolean matches(CharSequence rawPassword, String encodedPassword) { if (encodedPassword == null || encodedPassword.length() == 0) { logger.warn("Empty encoded password"); return false; } if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) { logger.warn("Encoded password does not look like BCrypt"); return false; } return BCrypt.checkpw(rawPassword.toString(), encodedPassword); }
这个方法中先对密文字符串进行了一些校验,如果不符合规则直接返回不匹配,然后调用校验方法BCrypt.checkpw,第一个参数是明文,第二个参数是加密后的字符串。
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; } byte ret = 0; for (int i = 0; i < caa.length; i++) { ret |= caa[i] ^ cab[i]; } return ret == 0; }
注意:
equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed))
这里,第一个参数是加密后的字符串,而第二个参数是用刚才提过的hashpw方法对明文字符串进行加密。
hashpw(plaintext, hashed)
第一个参数是明文,第二个参数是加密字符串,但是在这里是作为盐值salt传入的,所以就用到了刚才说的 hashpw 内部通过传入的salt得到real_salt
,这样就保证了对现在要校验的明文的加密和得到已有密文的加密用的是同样的加密策略,算法和盐值都相同,这样如果新产生的密文和原来的密文相同,则这两个密文对应的明文字符串就是相等的。
这也说明了加密时使用的盐值被写在了最终生成的加密字符串中。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。