API 身份验证(Authentication)

为了安全地调用 FuturePay API,所有请求都必须通过 API Key + 请求头校验 的方式进行身份验证。

1. 获取 API Key

每个商户都会分配一个唯一的 API Key,用于标识您的商户身份。

  • API Key 可在 管理平台 → 开发者信息 页面中获取
  • API Key 属于敏感信息,请妥善保管
  • 严禁将 API Key 暴露在前端、客户端或任何公开环境中

2. IP 白名单配置

为进一步保障接口安全,FuturePay 采用 IP 白名单机制

  • 管理平台 → 开发者 → IP 白名单 中配置允许访问 API 的服务器 IP

3. 请求头(HTTP Headers)要求

每一次 API 调用,必须在 HTTP 请求头中携带以下字段

Header 名称是否必填说明
AuthorizationAPI Key
MerchantId商户 ID,用于标识主商户
appId应用 ID,用于区分商户下的不同应用
curTime当前请求时间戳(毫秒)
subAccountNo商户子账户编号,用于区分该笔请求所属的子账户

4. 子账户说明(subAccountNo)

当商户启用了 子账户功能 时:

  • 可在请求头中传入 subAccountNo
  • 系统将根据该字段,将订单、资金及账务归属到对应的子账户
  • 若未传 subAccountNo,则默认归属于主商户账户

5. Authorization 示例

Authorization: sk_test_xxxxxxxxxxxxx
MerchantId: 1957982378327453696
appId: 156935
curTime: 1707012345678
subAccountNo: SA-001

6. 安全建议

  • 请定期轮换 API Key
  • 如发现 API Key 泄露,请立即在管理平台中重置
  • 不要在日志、前端代码或第三方工具中明文记录 API Key
👍

沙盒与实时 API

要开始处理通过真实货币流动进行的付款,必须激活您的帐户,并且您需要使用 api.futurepay.global 主机而不是带有实时凭证的沙盒。

实时 API: https://api.futurepay.global

收单沙盒 API:https://api.futurepay-develop.com

收付款沙盒 API:https://preapi.futurepay-develop.com

提供无效的凭证将导致 401 未授权的响应状态代码。

{
    "message": "Unauthorized"
}

步骤 1:准备待签名参数

假设我们有以下待签名的参数(JSON 格式):

{
    "reference": "9B6F974D3DB8436AA2B139551933FF08",
    "amount": {
        "currency": "USD",
        "value": 100
    },
    "countryCode": "CN",
    "origin": "fffmall.com",
    "paymentMethod": {
        "holderName": "John Doe",
        "shopperEmail": "[email protected]",
        "type": "alipaycn"
    },
    "lineItems": [
        {
            "description": "订单描述"
        }
    ],
    "returnUrl": "https://wallet.futurepay-develop.com/api/PayNotify/paymentSynchronous/business_merchant_id/1/order_id/2115",
    "shopperReference": "FP3d9bcb7a0cf84b80bfc814e1cc43c613"
}

步骤 2:移除不需要签名的参数

移除 lineItems 参数不需要参与签名计算。移除后,待签名的参数变为:

{
    "reference": "9B6F974D3DB8436AA2B139551933FF08",
    "amount": {
        "currency": "USD",
        "value": 100
    },
    "countryCode": "CN",
    "origin": "fffmall.com",
    "paymentMethod": {
        "holderName": "John Doe",
        "shopperEmail": "[email protected]",
        "type": "alipaycn"
    },
    "returnUrl": "https://wallet.futurepay-develop.com/api/PayNotify/paymentSynchronous/business_merchant_id/1/order_id/2115",
    "shopperReference": "FP3d9bcb7a0cf84b80bfc814e1cc43c613"
}

步骤 3:排序参数

接下来,参数会按字典顺序进行排序。排序规则是按照 ASCII 字符的值进行比较。参数排序后的顺序如下:

amount={"currency":"USD","value":100}
countryCode=CN
origin=fffmall.com
paymentMethod={"holderName":"John Doe","shopperEmail":"[email protected]","type":"alipaycn"}
reference=9B6F974D3DB8436AA2B139551933FF08
returnUrl=https://wallet.futurepay-develop.com/api/PayNotify/paymentSynchronous/business_merchant_id/1/order_id/2115
shopperReference=FP3d9bcb7a0cf84b80bfc814e1cc43c613

步骤 4:格式化参数

然后,将每个参数以 key=value 的形式连接起来,并使用 & 符号连接所有的参数。这里的格式化结果如下:

amount={"currency":"USD","value":100}&countryCode=CN&origin=fffmall.com&paymentMethod={"holderName":"John Doe","shopperEmail":"[email protected]","type":"alipaycn"}&reference=9B6F974D3DB8436AA2B139551933FF08&returnUrl=https://wallet.futurepay-develop.com/api/PayNotify/paymentSynchronous/business_merchant_id/1/order_id/2115&shopperReference=FP3d9bcb7a0cf84b80bfc814e1cc43c613

步骤 5:附加认证密钥

接下来,将API 密钥附加到格式化后的字符串的末尾。假设认证密钥为 "secret123",那么待签名的字符串变为:

amount={"currency":"USD","value":100}&countryCode=CN&origin=fffmall.com&paymentMethod={"holderName":"John Doe","shopperEmail":"[email protected]","type":"alipaycn"}&reference=9B6F974D3DB8436AA2B139551933FF08&returnUrl=https://wallet.futurepay-develop.com/api/PayNotify/paymentSynchronous/business_merchant_id/1/order_id/2115&shopperReference=FP3d9bcb7a0cf84b80bfc814e1cc43c613secret123

步骤 6:计算 SHA-256 哈希值

最后,待签名的字符串通过 SHA-256 哈希算法生成签名。生成的签名为:

f6e21134b5fbb06d6b668115d9370d0efa9b7c4dff2b56cb5c0670dbafa1510d

步骤 7:将签名结果放到 HTTP 请求头的 Authorization

POST /v1/payment-charges HTTP/1.1
Host: api.futurepay-develop.com
Content-Type: application/json
Authorization: f6e21134b5fbb06d6b668115d9370d0efa9b7c4dff2b56cb5c0670dbafa1510d
appId: 1801233382194151424
merchantId:1760141409517584384
curTime:2024-01-01 14:24:24

{..}

签名方法

package com.futurebank.pojo.utils;

import com.fasterxml.jackson.databind.ObjectMapper;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.*;

public class SingUtils {

    public static String getSign(Map<String, Object> params, String authKey) throws Exception {

        Map<String, Object> sortMap = new HashMap<>();
        sortMap.putAll(params);
        sortMap.remove("lineItems");

        // 排序参数并附加认证密钥
        String param = sortAndFormatParams(sortMap) + authKey;
        // 返回SHA-256哈希值
        return sha256(param);
    }

    public static String sha256(String str) {
        String encodeStr = "";
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] encodedhash = digest.digest(str.getBytes(StandardCharsets.UTF_8));
            encodeStr = bytesToHex(encodedhash);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("algorithm not supported");
        }
        return encodeStr;
    }


    public static String sortAndFormatParams(Map<String, Object> params) throws Exception {
        // 创建排序的 TreeMap
        Map<String, Object> sortedMap = new TreeMap<>(params);
        StringBuilder sb = new StringBuilder();
        ObjectMapper mapper = new ObjectMapper();

        // 遍历排序后的Map
        for (Map.Entry<String, Object> entry : sortedMap.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();

            if (value == null) {
                continue;
            }

            String valueStr;
            if (value instanceof Map) {
                // 对嵌套的Map进行递归排序和格式化
                valueStr = mapper.writeValueAsString(sortAndFormatNestedMap((Map<String, Object>) value));
            } else if (value instanceof List) {
                // 对List进行排序和格式化
                valueStr = mapper.writeValueAsString(sortAndFormatList((List<Object>) value));
            } else {
                valueStr = objectToString(value);
            }

            // 将键值对添加到StringBuilder中
            sb.append(key).append("=").append(valueStr).append("&");
        }

        // 移除最后一个 '&' 字符
        if (sb.length() > 0) {
            sb.setLength(sb.length() - 1);
        }

        return sb.toString();
    }

    // 排序嵌套的Map对象的方法
    private static Map<String, Object> sortAndFormatNestedMap(Map<String, Object> nestedMap) {
        Map<String, Object> sortedNestedMap = new TreeMap<>();
        for (Map.Entry<String, Object> entry : nestedMap.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();

            if (value instanceof Map) {
                // 对嵌套的Map进行递归排序
                sortedNestedMap.put(key, sortAndFormatNestedMap((Map<String, Object>) value));
            } else if (value instanceof List) {
                // 对List进行递归排序
                sortedNestedMap.put(key, sortAndFormatList((List<Object>) value));
            } else {
                sortedNestedMap.put(key, value);
            }
        }
        return sortedNestedMap;
    }

    private static List<Object> sortAndFormatList(List<Object> list) {
        List<Object> sortedList = new ArrayList<>();
        for (Object item : list) {
            if (item instanceof Map) {
                sortedList.add(sortAndFormatNestedMap((Map<String, Object>) item));
            } else if (item instanceof List) {
                sortedList.add(sortAndFormatList((List<Object>) item));
            } else {
                sortedList.add(item);
            }
        }
        return sortedList;
    }

    // 将对象转换为字符串的方法
    private static String objectToString(Object obj) {
        if (obj instanceof BigDecimal) {
            return ((BigDecimal) obj).toPlainString();
        } else if (obj instanceof Double) {
            return obj.toString();
        } else if (obj instanceof Integer) {
            return obj.toString();
        } else if (obj instanceof String) {
            return (String) obj;
        } else {
            return obj.toString();
        }
    }

    // 将字节数组转换为十六进制字符串
    private static String bytesToHex(byte[] hash) {
        StringBuilder hexString = new StringBuilder(2 * hash.length);
        for (byte b : hash) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) {
                hexString.append('0');
            }
            hexString.append(hex);
        }
        return hexString.toString();
    }
}
<?php

class SignUtils {

    public static function getSign(array $params, string $authKey): string {
        // 移除 lineItems
        if (isset($params['lineItems'])) {
            unset($params['lineItems']);
        }

        // 排序参数并附加认证密钥
        $param = self::sortAndFormatParams($params) . $authKey;

        // 返回SHA-256哈希值
        return self::sha256($param);
    }

    public static function sha256(string $str): string {
        return hash('sha256', $str);
    }

    public static function sortAndFormatParams(array $params): string {
        // 使用 ksort 按键排序
        ksort($params);

        $result = [];

        foreach ($params as $key => $value) {
            if ($value === null) {
                continue;
            }

            if (is_array($value)) {
                if (self::isAssocArray($value)) {
                    // 递归处理关联数组
                    $valueStr = json_encode(self::sortAndFormatNestedMap($value), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
                } else {
                    // 处理列表
                    $valueStr = json_encode(self::sortAndFormatList($value), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
                }
            } else {
                $valueStr = self::objectToString($value);
            }

            $result[] = "$key=$valueStr";
        }

        // 使用 & 连接键值对
        return implode('&', $result);
    }

    private static function sortAndFormatNestedMap(array $nestedMap): array {
        $sortedNestedMap = [];
        ksort($nestedMap);

        foreach ($nestedMap as $key => $value) {
            if (is_array($value)) {
                if (self::isAssocArray($value)) {
                    $sortedNestedMap[$key] = self::sortAndFormatNestedMap($value);
                } else {
                    $sortedNestedMap[$key] = self::sortAndFormatList($value);
                }
            } else {
                $sortedNestedMap[$key] = $value;
            }
        }

        return $sortedNestedMap;
    }

    private static function sortAndFormatList(array $list): array {
        $sortedList = [];

        foreach ($list as $item) {
            if (is_array($item)) {
                if (self::isAssocArray($item)) {
                    $sortedList[] = self::sortAndFormatNestedMap($item);
                } else {
                    $sortedList[] = self::sortAndFormatList($item);
                }
            } else {
                $sortedList[] = $item;
            }
        }

        return $sortedList;
    }

    private static function objectToString($obj): string {
        if (is_float($obj)) {
            return number_format($obj, 8, '.', ''); // 避免科学计数法
        } elseif (is_int($obj) || is_string($obj)) {
            return (string)$obj;
        } else {
            return json_encode($obj);
        }
    }

    private static function isAssocArray(array $arr): bool {
        // 判断是否是关联数组
        return array_keys($arr) !== range(0, count($arr) - 1);
    }
}