Преамбула
Решила как-то моя фирма интегрировать форум, написанный на PHP с нашей системой управления сотрудниками, написанной на Java. Интегрировать в данном случае — это обновлять аккаунт сотрудника на форуме в случае изменения его данных в нашей системе. И поручили это дело мне (PHP часть) и моем коллеге Ивану (Java часть). Я создаю небольшое веб API, он пишет функцию, которая в случае изменений данных сотрудника в нашей системе обращается к API и обновляет аккаунт сотрудника на форуме. Задание небольшое, дня на 3 от силы чтобы все написать и отладить. Возиться с кодированием нам естественно не хотелось — ничего секретного в фамилии, должности, рабочем телефоне и прочих данных сотрудника нет. Но как-то защититься от того, что кто-то чужой мог обратиться к API и изменить данные сотрудника было необходимо. Решено было подписывать сообщение с помощью magic phrase. В качестве magic phrase решено было взять MD5(логин + должность + соль), где соль (salt) — некая константная строка. Реализовали мы все это, начали тестировать — и оказалось, что MD5, вычисленный для некоторого сотрудника в PHP и не совпадает с тем, который был вычислен для того же сотрудника в нашей системе, написанной на Java. Данные у нас на обеих стронах были в UTF8. И я решил разобраться, в чем же дело.
Постановка задачи
Дано: строка в кодировке UTF8, от которой необходимо получить хэш MD5.
Необходимо: определить, почему хэши MD5, вычисленные с помощью Java и PHP различаются.
Процесс поиска решения
Возьмем классическую строку из мануала — "Привет, мир!" в кодировке UTF8 и будем сравнивать ее хэши в PHP и Java.
Пишем PHP скрипт
Ну тут все просто. Создаем файл в кодировке UTF8 (я для этого воспользовался Notepad++) и пишем в него следующий код:
- <?php
- header("Content-Type: text/html; charset=UTF-8");
-
- $utf8string = "Привет, мир!";
- echo '<pre>'.$utf8string.'</pre>';
- echo '<pre>'.md5($utf8string).'</pre>';
- ?>
* This source code was highlighted with Source Code Highlighter.
Строку header("Content-Type: text/html; charset=UTF-8"); я добавил для того, чтобы не переключать кодировку в браузере (у моего апача из состава денвера кодировка по умолчанию оказалась естественно win1251).
Смотрим в браузере что у нас вышло:
Привет, мир!
c446a2994f35689482651b7c7ba8b56c
Пишем консольную программу на Java
Аналогично создаем файл в кодировке UTF8 и пишем следующий код:
- public class Md5Tester {
-
- public static void main (String[] args) throws java.io.UnsupportedEncodingException, java.security.NoSuchAlgorithmException {
- java.io.PrintStream sysout = new java.io.PrintStream(System.out, true, "UTF-8");
-
- String utf8_string = "Привет, мир!";
- sysout.println(utf8_string);
-
- java.security.MessageDigest md5 = java.security.MessageDigest.getInstance("MD5");
- byte[] md5_byte_array = md5.digest(utf8_string.getBytes());
-
- String md5_string = new String(md5_byte_array);
- sysout.println(md5_string);
- }
- }
* This source code was highlighted with Source Code Highlighter.
Запускаем (я делал это в IntelliJ IDEA):
C:\Sun\SDK\jdk\bin\java -Didea.launcher.port=7552 "-Didea.launcher.bin.path=C:\Program Files (x86)\JetBrains\IntelliJ IDEA 8.1.3\bin" -Dfile.encoding=UTF-8 ...
Привет, мир!
�F��O5h��e|{��l
(все, что шло после -Dfile.encoding=UTF-8 в строке запуска я отбросил чтобы не засорять пример).
Как мы видим, у нас в консоли вывелся md5 хэш но не шестнадцатеричном виде. Первая идея — использовать BigInteger для получения строки в шестнадцатеричном виде.
- ...
- java.math.BigInteger md5_biginteger = new java.math.BigInteger(1, md5_byte_array);
- sysout.println(md5_biginteger.toString(16));
* This source code was highlighted with Source Code Highlighter.
Результат:
Привет, мир!
c446a2994f35689482651b7c7ba8b56c
Кажется, мы получили что хотели. Однако не будем спешить и сравним хэши какой-нибудь другой строки. Возьмем строку, хэш которой содержит ведущий 0: "rbablord5". Проверяем:
rbablord5
9736a8436e10bf1991927f2ffc76c12
В то время как как правильный хэш: 09736a8436e10bf1991927f2ffc76c12. Что примечательно, такая ошибка довольно частая, в свое время даже была в MySQL (я нашел багрепорт в их трэкере bugs.mysql.com/bug.php?id=27623). Тут я понял, что явно изобретаю велосипед, и погулглив немного, нашел библиотеку commons.apache.org/codec. Подключив ее, можно просто написать:
- String md5_string = DigestUtils.md5Hex(utf8_string);
* This source code was highlighted with Source Code Highlighter.
И получить желаемый результат. Тем же, кто не хочет ради одной функции md5 подключать к проекту дополнительную библиотеку (там в библиотеке еще много полезного, см. commons.apache.org/codec/api-release/index.html) можно подсмотреть функцию encodeHex:
- private static final char[] DIGITS_LOWER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
-
- private static final char[] DIGITS_UPPER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
-
- protected static String encodeHex(byte[] data, char[] toDigits) {
- int l = data.length;
- char[] out = new char[l << 1];
- // two characters form the hex value.
- for (int i = 0, j = 0; i < l; i++) {
- out[j++] = toDigits[(0xF0 & data[i]) >>> 4];
- out[j++] = toDigits[0x0F & data[i]];
- }
- return new String(out);
- }
* This source code was highlighted with Source Code Highlighter.
Вывод
При организации обмена данными между двумя системами, использующих различные технологии/языки программирования будьте бдительными, не полагайтесь на то, что у функций, реализующих одни и те же алгоритмы полностью совпадают форматы входных и выходных данных. Чаще всего придется приложить усилия для того, чтобы состыковать форматы. Но при этом не нужно пытаться изобрести велосипед (как я), если обе системы достаточно распространенные, то эта задача уже решалась кем-то до Вас.
P.S. Я, к сожалению, потерял кусок кода, который раньше использовался в нашей Java системе. Помню, что там был велосипед с BigInteger и какие-то не очень понятные (для меня, во всяком случае) проверки.