Apache Shiro 是一个强大易用的 Java 安全框架,用以执行身份验证、授权、密码和会话管理,而且可以方便地被 Spring Boot 所集成。
大部分 Web 应用的用户密码一般通过散列算法 + 盐的形式持久化在数据库中。在使用 Shiro 进行身份验证时,可以在 Shiro 配置类中配置密码散列匹配器,来对数据库中保存的密码进行验证。下面是在 Shiro 中配置密码散列匹配器的例子:
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5"); // 设置 MD5 散列算法
hashedCredentialsMatcher.setHashIterations(2); // 散列迭代次数
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
我前两天在学习 Shiro 的时候,看到有相当一部分的中文教程,都认为散列两次相当于md5(md5(""))
,我随手找了几篇秉此观点的文章:
- (二) shiro集成 --《springboot与shiro整合》
- springboot+shiro实现登录系数限定,thymeleaf中使用shiro标签
- Spring Boot系列(十五) 安全框架Apache Shiro(一)基本功能
其实这些教程都在误人子弟,散列两次并不等于md5(md5(""))
。我们可以简单测试一下。
以「hello」为例,测试 MD5 两次散列结果
按照 散列两次相当于md5(md5(""))
的说法,对「hello」字符串进行 MD5 两次散列,程序运算结果如下(伪代码):
String hash = "hello";
Integer iteration = 2;
for (int i = 0; i < iteration; i++) {
hash = md5(hash)
}
// 第一次 MD5 结果:5d41402abc4b2a76b9719d911017c592
// 第二次 MD5 结果:69a329523ce1ec88bf63061863d9cb14
在 Shiro 内置封装的 SimpleHash 中执行两次 MD5 散列,结果却是不同的:
String hash = "hello";
String salt = null;
Integer iteration = 2;
SimpleHash simpleHash = new SimpleHash("MD5", hash, salt, iteration);
simpleHash.getHash();
// 迭代两次的 MD5 散列结果:62109206880d38a4010a98e11243924a
可见 Shiro 中 SimpleHash 的多次 MD5 散列并不等于一层套一层的md5()
。
这里需要简单提一下 MD5 的散列过程。
MD5 散列算法原理
MD5 算法可看做一个「函数」,任意的二进制串都可以作为变量输入到此「函数」中,经过散列运算返回固定 128 位的二进制串(大整数),此大整数再经过 16 进制转换,最终得到 32 个字符的 MD5 值。
散列运算的大致过程如下:
- 二进制位补充
- 将位平均分成 16 组,每组 32 个位,初始化四个常数(默认值为标准幻数)进行四轮循环运算,每轮将运算结果依次更新到常数中
- 全部运算完毕后,将四个常数按照低内存到高内存排列,共 128 位
- 将结果进行 16 进制转换,得到最终 MD5 值
通过 SimpleHash 源码窥探多次散列过程
private void hash(ByteSource source, ByteSource salt, int hashIterations) throws CodecException, UnknownAlgorithmException {
byte[] saltBytes = salt != null ? salt.getBytes() : null;
byte[] hashedBytes = this.hash(source.getBytes(),
saltBytes, hashIterations); // 进行散列
this.setBytes(hashedBytes);
}
protected byte[] hash(byte[] bytes, byte[] salt, int hashIterations) throws UnknownAlgorithmException {
MessageDigest digest = this.getDigest(this.getAlgorithmName());
if (salt != null) {
digest.reset();
digest.update(salt);
}
byte[] hashed = digest.digest(bytes);
int iterations = hashIterations - 1;
for(int i = 0; i < iterations; ++i) { // 根据迭代次数进行多次散列
digest.reset();
hashed = digest.digest(hashed);
}
return hashed;
}
看了源码一切就很明了了,SimpleHash 每次散列会得到 128 位的二进制串,多次散列会将得到的二进制串作为输入进行重新散列,而非对 16 进制转换后的 MD5 值进行重新散列。
结论
所以,Shiro SimpleHash 中的两次 MD5 散列,并不等于md5(md5(""))
。他们的区别在于,前者会将得到的二进制串进行二次散列,后者将 MD5 运算(散列并将散列结果进行 16 进制转换)后得到的 32 位字符进行二次散列。
Well, as you pointed out, the Double-Md5 in shiro will not be fully identical to md5(md5(" ")), whereas you haven't still tell us how this problem can be resolved.
没有问题需要被解决,就是指出了部分教程里的错误。这些错误教程可能会误导开发者,导致使用 shiro 时不能正确地进行注册与登录认证。正确的用法是:如果 shiro 登录认证时使用了 SimpleHash 进行 md5 多次散列,那么在用户注册时,也需要使用 SimpleHash 对密码进行 md5 多次散列(而不是嵌套md5()的方式)并持久化到数据库中以保证散列结果的一致。
i.e 就是说,我重写了shiro默认的matcher,将二次迭代时的输入参数转换成一个hexstring就行了
Thanks for replying, I've resolved this problem from which double-md5 was incorrectly resulting, the only thing we need to do is just rewriting the MD5CredentialsMather and be sure it is fitting to what we desired.
加油!
膜拜大佬了
兄弟怎么联系你
QQ:9**736
学习了
感谢分享
舔舔