> ## Documentation Index
> Fetch the complete documentation index at: https://waffo.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook - 签名验证

> Webhook 请求的 RSA 签名验证方法与代码示例。

每个 Webhook 请求的 Header 都包含 `X-SIGNATURE`，该签名使用 Waffo 私钥生成。商户必须使用 Waffo 公钥来验证签名。

## 获取 Waffo 公钥

登录 **Merchant Portal** → **Integration** 菜单，即可查看和复制 Waffo 公钥。

<Note>
  需要 **Dev** 或 **Admin** 角色权限才能访问该页面。
</Note>

## 推荐方式：使用 SDK

SDK 的 `handleWebhook()` 方法会自动执行签名验证、事件解析、路由以及响应签名：

<CodeGroup>
  ```typescript Node.js theme={null}
  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);
  });
  ```

  ```go Go theme={null}
  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))
  }
  ```
</CodeGroup>

## 手动验证

如果你需要手动处理（不使用 SDK），验证步骤如下：

<Steps>
  <Step title="获取签名">
    从请求 Header 中获取 `X-SIGNATURE`。
  </Step>

  <Step title="获取原始 Body">
    获取原始请求 Body 字符串（不要先 JSON parse 再 stringify）。
  </Step>

  <Step title="验证签名">
    使用 Waffo 公钥 + SHA256WithRSA 验证签名。
  </Step>

  <Step title="处理事件">
    在签名验证通过后处理事件。
  </Step>

  <Step title="对响应签名">
    你必须在响应中设置 `X-SIGNATURE` Header（使用商户私钥对响应 body 进行签名）。
  </Step>
</Steps>

### 手动示例

<CodeGroup>
  ```typescript Node.js theme={null}
  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);
  });
  ```

  ```go Go theme={null}
  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)
  }
  ```
</CodeGroup>

## 注意事项

<Warning>
  * **你必须在处理事件之前验证签名**。不要先响应再验证。
  * **响应必须包含 `X-SIGNATURE` Header**，否则 Waffo 会将该次投递视为失败。
  * 使用原始请求 body 来验证签名；不要先 JSON parse 再 stringify。
  * SDK 提供了完整的 webhook 处理流水线。相比手动处理，推荐使用 SDK。
</Warning>
