# Webhook 订单回调验签

本文是 [Webhook 订单回调验签](/zh/appendix/demo/webbook-validation.md) 的 demo

{% hint style="info" %}
使用本文代码时，请务必将其中的合作伙伴账号相关信息替换为您自身的
{% endhint %}

{% tabs %}
{% tab title="Java" %}
{% code lineNumbers="true" %}

```java
package onramp.webhookvalidation;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;

/**
 * SignUtil
 *
 * @author FaTPay
 */
public class SignUtil {

    private static final Logger LOG = LoggerFactory.getLogger(SignUtil.class);

    /**
     * Signature algorithms
     */
    public static final String SIGNATURE_ALGORITHM = "SHA256withRSA";

    /**
     * Webhook public key
     */
    private static final String FATPAY_PUBLIC_KEY = "<FaTPay RSA PUBLIC KEY>";

    /**
     * Verifies the signature
     *
     * @param params
     * @param rsaPublic
     * @return
     */
    public static Boolean rsaVerify(Map<String, String> params, String rsaPublic, String presign, String sign) {
        try {
            rsaPublic = StringUtils.replace(rsaPublic, "-----BEGIN PRIVATE KEY-----", "");
            rsaPublic = StringUtils.replace(rsaPublic, "-----END PRIVATE KEY-----", "");
            return doVerify(presign + paramsToStr(params), sign, rsaPublic);
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * Verifies the signature
     *
     * @param content
     * @param sign
     * @param publicKey
     * @return
     */
    public static boolean doVerify(String content, String sign, String publicKey) {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            byte[] encodedKey = Base64.decodeBase64(publicKey);
            PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));

            Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
            signature.initVerify(pubKey);
            signature.update(content.getBytes(StandardCharsets.UTF_8));

            return signature.verify(Base64.decodeBase64(sign));
        } catch (Exception ex) {
            LOG.error("验签失败，content={}", content, ex);
            throw new IllegalArgumentException("rsa verify fail");
        }
    }

    /**
     * Converts parameters into ordered strings
     *
     * @param params
     * @return
     */
    private static String paramsToStr(Map<String, String> params) {
        StringBuilder param = new StringBuilder();
        List<String> keys = new ArrayList(params.keySet());
        // Sorts the specified keys
        Collections.sort(keys);
        int index = 0;
        Iterator<String> iterator = keys.iterator();
        while (iterator.hasNext()) {
            String key = iterator.next();
            String value = params.get(key);
            // Filter out null and empty values
            if ("x-fp-signature".equals(key) || "signature".equals(key) || "sign".equals(key) || StringUtils.isBlank(value)) {
                continue;
            }
            param.append(index == 0 ? "" : "&").append(key).append("=").append(value);
            index++;
        }
        return param.toString();
    }

    /**
     * Returns URI
     *
     * @param url
     * @return
     */
    public static URI getURI(String url) {
        if (!(StringUtils.startsWithIgnoreCase(url, "http://") || StringUtils
                .startsWithIgnoreCase(url, "https://"))) {
            url = "http://" + url;
        }
        try {
            URI uri = new URI(url);
            return uri;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    // ******************** WEBHOOK DEMO START ********************//
    @PostMapping("/webhook_sign_verify")
    public String webhookSignVerify(@RequestBody Map<String, String> requestBody, HttpServletRequest request) {
        String signature = request.getHeader("X-FP-Signature");

        // Converts header keys to lower case
        Map<String, String> headers = new HashMap<>();
        headers.put("x-fp-timestamp", request.getHeader("X-FP-Timestamp"));
        headers.put("x-fp-nonce", request.getHeader("X-FP-Nonce"));
        headers.put("x-fp-partner-id", request.getHeader("X-FP-Partner-Id"));

        Map<String, String> params = new HashMap<>();
        params.putAll(requestBody);
        params.putAll(headers);

        String url = request.getRequestURL().toString();
        URI uri = SignUtil.getURI(url);
        String presign = request.getMethod() + uri.getHost() + request.getRequestURI() + "?";
        Boolean verified = false;
        try {
            verified = SignUtil.rsaVerify(params, FATPAY_PUBLIC_KEY, presign, signature);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (!verified) {
            return "Signature error";
        }
        // do something
        return "success";
    }
    // ******************** WEBHOOK DEMO END ********************//
}
```

{% endcode %}
{% endtab %}

{% tab title="Node.js" %}
{% code lineNumbers="true" %}

```jsx
const crypto = require('crypto');

var text = "original text to be signed"
var publickey = "public key to verify sign"

// verify signature
var verifier = crypto.createVerify("RSA-SHA256");
verifier.update(text);
result = verifier.verify(publickey, signature, "base64");
console.log("verify result: " + result)
```

{% endcode %}
{% endtab %}
{% endtabs %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://dev.fatpay.org/zh/appendix/demo/webbook-validation.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
