暮光博客

小心你讨厌的东西,因为你很可能被它塑造成某种形状

Shiro 中 SimpleHash MD5 的多次散列

技术 11 条评论

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("")),我随手找了几篇秉此观点的文章:

其实这些教程都在误人子弟,散列两次并不等于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 值。

散列运算的大致过程如下:

  1. 二进制位补充
  2. 将位平均分成 16 组,每组 32 个位,初始化四个常数(默认值为标准幻数)进行四轮循环运算,每轮将运算结果依次更新到常数中
  3. 全部运算完毕后,将四个常数按照低内存到高内存排列,共 128 位
  4. 将结果进行 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 位字符进行二次散列。

[C#] 英雄时刻下载器v3.0
评论区 / 取消回复
选择表情选择表情
  1. 一名智障

    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.

    回复
    1. @一名智障

      没有问题需要被解决,就是指出了部分教程里的错误。这些错误教程可能会误导开发者,导致使用 shiro 时不能正确地进行注册与登录认证。正确的用法是:如果 shiro 登录认证时使用了 SimpleHash 进行 md5 多次散列,那么在用户注册时,也需要使用 SimpleHash 对密码进行 md5 多次散列(而不是嵌套md5()的方式)并持久化到数据库中以保证散列结果的一致。

      回复
      1. 一名智障
        @大袋鼠

        icon_sad.gif i.e 就是说,我重写了shiro默认的matcher,将二次迭代时的输入参数转换成一个hexstring就行了

        回复
      2. 一名智障
        @大袋鼠

        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.

        回复
  2. 加油! icon_mrgreen.gif

    回复
  3. 膜拜大佬了

    回复
  4. 冯毅

    兄弟怎么联系你

    回复
    1. @冯毅

      QQ:9**736

      回复
  5. 学习了
    感谢分享

    回复
  6. icon_smile.gif 舔舔

    回复
  7. Lucida

    icon_cool.gif

    回复