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(""))
。我们可以简单测试一下。
按照 散列两次相当于 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 算法可看做一个「函数」,任意的二进制串都可以作为变量输入到此「函数」中,经过散列运算返回固定 128 位的二进制串(大整数),此大整数再经过 16 进制转换,最终得到 32 个字符的 MD5 值。
散列运算的大致过程如下:
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 位字符进行二次散列。