PAYATHON 2026

如何在 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 取决于支付服务端的协议。客户端单方面更换算法后,服务端将无法验证签名。

备注:内容仅供参考。