如何在 Java 中生成与 PHP hash 相同的 SHA256 签名
结论
这段 PHP 代码计算的是普通 SHA-256:
SHA256(data + serviceKey)
它并不是 HMAC-SHA256。Java 应使用 MessageDigest,按照与 PHP 相同的顺序拼接 data 和 serviceKey,计算 SHA-256 后输出小写十六进制字符串。
Java 实现
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public final class SignatureUtil {
private SignatureUtil() {
}
public static String createSignature(String data, String serviceKey) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] input = (data + serviceKey)
.getBytes(StandardCharsets.UTF_8);
byte[] hash = digest.digest(input);
StringBuilder result = new StringBuilder(hash.length * 2);
for (byte b : hash) {
result.append(String.format("%02x", b & 0xff));
}
return result.toString();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 is not supported", e);
}
}
}
调用方式:
String data = "prepared-order-data";
String serviceKey = "private-key";
String signature = SignatureUtil.createSignature(data, serviceKey);
System.out.println(signature);
这段 Java 代码对应以下 PHP 代码:
$signature = hash('sha256', $data . $serviceKey);
使用 Java 17 或更高版本时,可以通过 HexFormat 简化十六进制转换:
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
public static String createSignature(String data, String serviceKey) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(
(data + serviceKey).getBytes(StandardCharsets.UTF_8)
);
return HexFormat.of().formatHex(hash);
}
为什么 HmacSHA256 的结果不同
下面这段 Java 代码:
Mac.getInstance("HmacSHA256");
计算的是:
HMAC-SHA256(key, data)
PHP 示例计算的则是:
SHA256(data + key)
这是两种不同的算法。即使 data 和 key 完全相同,计算结果也不会相同。
只有当 PHP 代码使用以下写法时,Java 才应该使用 HmacSHA256:
hash_hmac('sha256', $data, $serviceKey);
当前 PHP 代码调用的是 hash(),所以 Java 应使用 MessageDigest。
仍然不一致时检查什么
改用 MessageDigest 后,如果结果仍然不同,问题通常出在参与哈希计算的原始字节上,而不是 SHA-256 的实现。
1. prepareData() 的结果必须一致
PHP 参与计算的不是原始 $orderData,而是:
$data = prepareData($orderData);
Java 必须完整复现 prepareData() 的处理逻辑,包括:
- 字段顺序
- 字段之间的分隔符
- 空字段和
null的处理 - 数字和金额格式
- 日期格式
- URL 编码或 JSON 序列化方式
- 是否去除首尾空白
- 大小写转换
- 特殊字符转义
例如,下面两个 JSON 字符串表达相同的数据,但会产生不同的 SHA-256:
{"amount":100,"currency":"CNY"}
{"currency":"CNY","amount":100}
签名校验比较的是字节序列,不会解析或理解其中的数据结构。
2. 字符编码必须一致
Java 示例明确使用 UTF-8:
.getBytes(StandardCharsets.UTF_8)
PHP 字符串本质上是字节序列。只有 PHP 中 $data 和 $serviceKey 的实际字节同样采用 UTF-8,两端的结果才会一致。
Java 中不要使用未指定字符集的写法:
text.getBytes()
它会依赖运行环境的默认字符集,在不同机器上可能产生不同结果。
3. 拼接顺序不能颠倒
PHP 使用:
$data . $serviceKey
对应的 Java 写法是:
data + serviceKey
不能改成:
serviceKey + data
也不要自行加入冒号、空格、换行或其他分隔符。
4. 检查不可见字符
以下内容都会改变签名:
- 字符串末尾的换行
\n与\r\n的差异- 前导或尾随空格
- UTF-8 BOM
- 制表符
- 全角和半角字符
- Unicode 组合形式不同但显示相同的字符
排查时,应比较两端真正参与计算的字节,而不能只靠肉眼查看字符串。Java 可以打印输入字节的十六进制值:
byte[] input = (data + serviceKey).getBytes(StandardCharsets.UTF_8);
System.out.println(HexFormat.of().formatHex(input));
PHP 可以用下面的代码查看同一输入的十六进制值:
echo bin2hex($data . $serviceKey);
只要两端输出的输入十六进制完全相同,使用 SHA-256 得到的结果就一定相同。
关于安全性的注意事项
如果支付接口规定使用 SHA256(data + serviceKey),实现时就必须严格遵守协议,不能自行替换算法。
这种方式与标准 HMAC 不同。设计新协议时,通常更适合使用:
HMAC-SHA256(serviceKey, data)
能否改用 HMAC 取决于支付服务端的协议。客户端单方面更换算法后,服务端将无法验证签名。
备注:内容仅供参考。
