跳到主要内容

Webhook 签名

Webhook 签名可用于验证收到的 Webhook 通知的真实性和完整性。通过校验 Webhook 签名,商户可以防止恶意方向 Webhook 端点发送被篡改的数据。

配置

配置 Webhook 订阅时,可以生成一个用于签名 Webhook 内容的密钥。请前往 Webhook 订阅 仪表盘,选择订阅,点击 菜单,选择 生成密钥。生成后,可在表格中复制密钥用于 Webhook 校验。

注意:生成或轮换密钥后,Webhook 通知中的签名最多可能需要 30 秒才会出现。

签名校验

启用密钥后,每条 Webhook 消息会携带以下 HTTP 头:

  • timestamp:用于生成签名的 UNIX 秒级时间戳。
  • timezone:时区。
  • signature:所有有效密钥对应的签名,逗号分隔。

校验 Webhook 内容的步骤:

  1. 使用 Webhook 订阅密钥计算 RSA 值。
  2. 确认计算结果与签名头中的至少一个值匹配。
  3. (可选)校验时间戳是否过旧,以防止重放攻击。

重要: 提取 payload 时,需使用收到的原始字符串,包括所有格式、空格和换行。如用 JSON 库解析,请务必使用原始值。

SDK 支持

EFundFlow SDK 支持 Webhook 签名校验。示例:

JAVA# 示例

import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.math.BigDecimal;

public class WebhookSignatureVerifier {

/**
* 验证 Webhook 签名
* @param payload 原始请求体字符串(body)
* @param signature header中的signature字段(Base64字符串)
* @param publicKey 平台提供的公钥(PEM去头尾后的Base64字符串)
* @return true=验签通过,false=验签失败
*/
public static boolean verifySignature(String payload, String signature, String publicKey) {
try {
// 1. Generate signature string using the same method as SignUtil
String signContent = generateSignatureString(payload);

// 2. Restore public key
byte[] keyBytes = Base64.getDecoder().decode(publicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(keySpec);

// 3. Build Signature instance using SHA1withRSA (same as SignUtil)
Signature sig = Signature.getInstance("SHA1withRSA");
sig.initVerify(pubKey);
sig.update(signContent.getBytes(StandardCharsets.UTF_8));

// 4. Verify signature
byte[] signatureBytes = Base64.getDecoder().decode(signature);
return sig.verify(signatureBytes);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

public static String generateSignatureString(String jsonStr) throws Exception {
Map<String, Object> payment = JSON.parseObject(jsonStr);
StringBuffer content = new StringBuffer();
append(content, payment, "&", "=");
return content.toString();
}

private static void append(StringBuffer content, Map<String, Object> sourceObj, String kvPairSeparator, String kvSeparator) {
if (sourceObj == null) {
return;
}
Map<String, Object> obj = sourceObj;
if (obj.keySet().size() == 0) {
return;
}
List<String> keyList = new ArrayList<String>(obj.keySet().size());
for (String key : obj.keySet()) {
keyList.add(key);
}
Collections.sort(keyList);
for (String key : keyList) {
Object value = obj.get(key);
if (value instanceof List) {
for (int i = 0; i < ((List<?>) value).size(); i++) {
Object item = ((List<?>) value).get(i);
if (item instanceof Map) {
append(content, (Map<String, Object>) item, kvPairSeparator, kvSeparator);
}
}
} else if (value instanceof Map) {
append(content, (Map<String, Object>) value, kvPairSeparator, kvSeparator);
} else if (value instanceof String
|| value instanceof Float
|| value instanceof Double
|| value instanceof Integer
|| value instanceof Long
|| value instanceof BigDecimal
|| value instanceof Boolean) {
if (content.length() > 0) {
content.append(kvPairSeparator);
}
content.append(key);
content.append(kvSeparator);
content.append(value);
}
}
}

// 示例用法
public static void main(String[] args) {
String payload = "{...}"; // 原始body字符串
String signature = "Base64SignatureStringFromHeader";
String publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A..."; // PEM去头尾后的Base64

boolean valid = verifySignature(payload, signature, publicKey);
System.out.println("验签结果: " + valid);
}
}

SDK 支持 C#、Go、Java、PHP、Python 和 TypeScript。

密钥轮换

可在 Webhook 订阅页面轮换密钥。轮换期间,系统会同时发送新旧密钥的签名。轮换期结束后,仅使用新密钥。

轮换步骤:

  1. 在 Webhook 订阅页面通过 轮换密钥… 菜单生成新密钥。
  2. 更新 Webhook 端点以使用新密钥。
  3. 验证 Webhook 已用新密钥处理。
  4. 等待轮换期结束,旧密钥失效。

注意:轮换后新签名最多可能需要 30 秒才会出现在通知中。

Powered by Docusaurus