Flutter 中如何生成 Montypay 的有效 hash
结论
Montypay 通常不是将参数拼接后直接计算大写 MD5,而是按下面的顺序生成 hash:
SHA1(MD5(UPPERCASE(orderNumber + orderAmount + orderCurrency + orderDescription + merchantPass)))
具体步骤如下:
- 按规定顺序拼接字段,不添加任何分隔符。
- 将拼接后的整个字符串转为大写。
- 计算 MD5,得到小写十六进制字符串。
- 将这个 MD5 字符串作为输入,再计算 SHA-1。
- 最终提交小写的 SHA-1 十六进制字符串,长度通常为 40。
当前代码只计算了 MD5,还将结果转成了大写,所以无法通过服务端校验。
Flutter 实现
使用 Dart 的 crypto 包:
dependencies:
crypto: ^3.0.0
生成 hash:
import 'dart:convert';
import 'package:crypto/crypto.dart';
String generateMontypayHash({
required String orderNumber,
required String orderAmount,
required String orderCurrency,
required String orderDescription,
required String merchantPass,
}) {
final source = (
orderNumber +
orderAmount +
orderCurrency +
orderDescription +
merchantPass
).toUpperCase();
// md5.convert(...).toString() 返回小写十六进制字符串
final md5String = md5.convert(utf8.encode(source)).toString();
// SHA-1 的输入是 MD5 的十六进制文本,不是 MD5 原始字节
return sha1.convert(utf8.encode(md5String)).toString();
}
调用方式:
final hashString = generateMontypayHash(
orderNumber: 'order-1234',
orderAmount: '0.19',
orderCurrency: 'usd',
orderDescription: 'Important gift',
merchantPass: 'merchantPass',
);
print(hashString);
将 hashString 原样放入请求的 hash 字段,不要再次调用 toUpperCase()。
对应的计算过程
同样的逻辑也可以写成:
final input =
'$orderNumber$orderAmount$orderCurrency$orderDescription$merchantPass'
.toUpperCase();
final md5Hex = md5.convert(utf8.encode(input)).toString();
final hashString = sha1.convert(utf8.encode(md5Hex)).toString();
这里最容易忽略的是,第二次哈希接收的是 MD5 的十六进制字符串,例如:
e10adc3949ba59abbe56e057f20f883e
它接收的不是 md5.convert(...).bytes 返回的 16 个原始字节。两种输入计算出的 SHA-1 完全不同。
如何核对 Postman 的 Pre-request Script
在 Postman 中打开对应的请求或 Collection,进入 Scripts → Pre-request。脚本通常类似下面这样:
const source = (
orderNumber +
orderAmount +
orderCurrency +
orderDescription +
merchantPass
).toUpperCase();
const md5Hash = CryptoJS.MD5(source).toString();
const hash = CryptoJS.SHA1(md5Hash).toString();
pm.variables.set("hash", hash);
可以临时加入以下日志,确认真正参与计算的值:
console.log("source:", source);
console.log("md5Hash:", md5Hash);
console.log("hash:", hash);
打开 Postman Console 后重新发送请求,再将输出的 source、MD5 和最终 hash 与 Flutter 的结果逐项比较。不要只复制 Postman 最终生成的 hash。订单号、金额或描述发生变化时,hash 也需要重新计算。
如果 Collection 使用环境变量,还需要检查脚本实际取得的值:
console.log(pm.variables.replaceIn("{{order_number}}"));
console.log(pm.variables.replaceIn("{{merchant_pass}}"));
Postman 变量可能来自 Environment、Collection 或请求级变量。界面显示的初始值不一定是脚本当前读取的值。
仍然校验失败时重点检查
即使算法没有问题,下面这些差异仍会导致 Hash is not valid:
- 字段顺序必须与 Pre-request Script 完全一致。
orderAmount必须使用请求中发送的原始文本。0.19、0.190和0.1900会生成不同的 hash。- 不要先将金额转换为
double,再重新格式化。 - 币种、描述和订单号中的大小写、空格及标点必须与请求正文一致。
- 不要额外执行
trim(),除非 Postman 脚本也进行了相同处理。 merchantPass必须是商户密钥或 merchant password,不能用merchant_key、登录密码或示例字符串代替。- 测试环境和生产环境通常使用不同的商户凭据。
- 拼接内容中不能加入
&、=、空格、换行或其他分隔符。 - 确认请求字段没有被表单编码、JSON 序列化或其他中间逻辑改动。
排查时,最好让 Postman 和 Flutter 同时输出拼接后的 source、中间结果 md5Hex 以及最终的 SHA-1。三个阶段的输入完全一致时,最终 hash 也应该一致。如果当前 Montypay Collection 的 Pre-request Script 与上述公式不同,应以 Collection 实际采用的字段顺序和转换规则为准。
备注:内容仅供参考。
