xiaobaoqiu Blog

Think More, Code Less

Java中MessageDigest计算MD5

最近项目的图片处理的系统出问题比较多,典型的一个问题就是因为计算md5出现的问题。

1.问题

问题的现象很简单,使用这套代码批量切图,会出现一张灵异图片,灵异之处在于,很多人批量切图都会出现这张图片,且这张图片不在他们的原始图片中。

2.原因

最后分析的原因是计算图片md5的代码,使用的是Java自带的MessageDigest,大致使用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
//声明
private static MessageDigest md = null;

//初始化
static {
    md = MessageDigest.getInstance("MD5");
}

//使用
public static byte[] getMD5(byte[] bytes) {
    md.update(bytes);   //-----------------------A
    return md.digest(); //-----------------------B
}

问题的原因就是多线程计算md5的时候getMD5这个函数有问题。

2.1 问题md5

最后定位发现重复出现的md5是

d41d8cd98f00b204e9800998ecf8427e

这是一个特殊的md5,即其是空字符串的md5:

1
MD5("") = d41d8cd98f00b204e9800998ecf8427e

参考: http://zh.wikipedia.org/wiki/MD5

2.1 代码问题

在MessageDigest类的源代码注释有这样一段话:

1
2
3
4
5
6
7
8
9
10
 * <p>A MessageDigest object starts out initialized. The data is
 * processed through it using the {@link #update(byte) update}
 * methods. At any point {@link #reset() reset} can be called
 * to reset the digest. Once all the data to be updated has been
 * updated, one of the {@link #digest() digest} methods should
 * be called to complete the hash computation.
 *
 * <p>The <code>digest</code> method can be called once for a given number
 * of updates. After <code>digest</code> has been called, the MessageDigest
 * object is reset to its initialized state.

上面这段话第二部分的意思是说,digest()这个方法只能被调用一次,一旦调用MessageDigest对象会被重置到初始状态。

根据这个逻辑,getMD5这个函数的代码,多个线程进入这段代码的时候(这里假设两个线程),线程A执行完步骤1,在执行步骤2之前,线程B也执行完步骤1,之后线程B调用了步骤2得到正确的md5,之后线程A再调用步骤2的时候得到的md5就是d41d8cd98f00b204e9800998ecf8427e。

本地验证:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
    String src = "HelloWorld";

    messageDigest.update(src.getBytes());

    String md5 = bytesToHex(messageDigest.digest());
    System.out.println("md5 = " + md5.toLowerCase());

    String md5Again = bytesToHex(messageDigest.digest());
    System.out.println("md5Again = " + md5Again.toLowerCase());
}

得到的结果:

1
2
md5 = 68e109f0f40ca72a15e05cc22786f8e6
md5Again = d41d8cd98f00b204e9800998ecf8427e

3.解决

3.1 MessageDigest正确用法

MessageDigest类的注释里面其实给出了正确的用法,注意其md.clone()的调用:

1
2
3
4
5
6
7
8
9
10
11
 MessageDigest md = MessageDigest.getInstance("MD5");

 try {
     md.update(toChapter1);
     MessageDigest tc1 = md.clone();
     byte[] toChapter1Digest = tc1.digest();
     md.update(toChapter2);
     ...etc.
 } catch (CloneNotSupportedException cnse) {
     throw new DigestException("couldn't make digest of partial content");
}

3.1 Apache的DigestUtils

Apache的DigestUtils是一个线程安全的类,是对MessageDigest的封装,使用很简单,首先加入依赖:

1
2
3
4
5
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.9</version>
</dependency>

然后就可以使用DigestUtils计算md5:

1
DigestUtils.md5(bytes)