X-SIGNATURE,该签名使用 Waffo 私钥生成。商户必须使用 Waffo 公钥来验证签名。
获取 Waffo 公钥
登录 Merchant Portal → Integration 菜单,即可查看和复制 Waffo 公钥。需要 Dev 或 Admin 角色权限才能访问该页面。
推荐方式:使用 SDK
SDK 的handleWebhook() 方法会自动执行签名验证、事件解析、路由以及响应签名:
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const body = req.body.toString();
const signature = req.headers['x-signature'] as string;
const result = await waffo.webhook().handleWebhook(body, signature);
res.setHeader('X-SIGNATURE', result.responseSignature);
res.setHeader('Content-Type', 'application/json');
res.status(200).send(result.responseBody);
});
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
signature := r.Header.Get("X-SIGNATURE")
result := handler.HandleWebhook(string(body), signature)
w.Header().Set("X-SIGNATURE", result.ResponseSignature)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
w.Write([]byte(result.ResponseBody))
}
手动验证
如果你需要手动处理(不使用 SDK),验证步骤如下:1
获取签名
从请求 Header 中获取
X-SIGNATURE。2
获取原始 Body
获取原始请求 Body 字符串(不要先 JSON parse 再 stringify)。
3
验证签名
使用 Waffo 公钥 + SHA256WithRSA 验证签名。
4
处理事件
在签名验证通过后处理事件。
5
对响应签名
你必须在响应中设置
X-SIGNATURE Header(使用商户私钥对响应 body 进行签名)。手动示例
import { createVerify, createSign } from 'crypto';
function verifyWaffoSignature(body: string, signature: string): boolean {
const verify = createVerify('SHA256');
verify.update(body);
return verify.verify(process.env.WAFFO_PUBLIC_KEY!, signature, 'base64');
}
function signResponse(responseBody: string): string {
const sign = createSign('SHA256');
sign.update(responseBody);
return sign.sign(process.env.MERCHANT_PRIVATE_KEY!, 'base64');
}
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const body = req.body.toString();
const signature = req.headers['x-signature'] as string;
if (!verifyWaffoSignature(body, signature)) {
const failedBody = JSON.stringify({ message: 'failed' });
res.setHeader('X-SIGNATURE', signResponse(failedBody));
return res.status(200).send(failedBody);
}
// 处理事件...
const successBody = JSON.stringify({ message: 'success' });
res.setHeader('X-SIGNATURE', signResponse(successBody));
res.status(200).send(successBody);
});
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"encoding/json"
"io"
"net/http"
"os"
)
// 验证 Waffo 签名
func verifyWaffoSignature(body string, signature string) bool {
pubKeyPEM := []byte(os.Getenv("WAFFO_PUBLIC_KEY"))
block, _ := pem.Decode(pubKeyPEM)
pubKey, _ := x509.ParsePKIXPublicKey(block.Bytes)
sig, _ := base64.StdEncoding.DecodeString(signature)
hash := sha256.Sum256([]byte(body))
err := rsa.VerifyPKCS1v15(pubKey.(*rsa.PublicKey), crypto.SHA256, hash[:], sig)
return err == nil
}
// 签名商户响应
func signResponse(responseBody string) string {
privKeyPEM := []byte(os.Getenv("MERCHANT_PRIVATE_KEY"))
block, _ := pem.Decode(privKeyPEM)
privKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes)
hash := sha256.Sum256([]byte(responseBody))
sig, _ := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA256, hash[:])
return base64.StdEncoding.EncodeToString(sig)
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
signature := r.Header.Get("X-SIGNATURE")
if !verifyWaffoSignature(string(body), signature) {
failedBody, _ := json.Marshal(map[string]string{"message": "failed"})
w.Header().Set("X-SIGNATURE", signResponse(string(failedBody)))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
w.Write(failedBody)
return
}
// 处理事件...
successBody, _ := json.Marshal(map[string]string{"message": "success"})
w.Header().Set("X-SIGNATURE", signResponse(string(successBody)))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
w.Write(successBody)
}
注意事项
- 你必须在处理事件之前验证签名。不要先响应再验证。
- 响应必须包含
X-SIGNATUREHeader,否则 Waffo 会将该次投递视为失败。 - 使用原始请求 body 来验证签名;不要先 JSON parse 再 stringify。
- SDK 提供了完整的 webhook 处理流水线。相比手动处理,推荐使用 SDK。