Skip to main content

API 签名说明

说明
  1. 上图为加签验签的原理,所描述场景为商家服务器发起PayMatrixPay开放接口请求的过程;
  2. 商家根据 api 签名组装规则和参数签名组装规则,将参数利用商户私钥进行加签生成数字签名;
  3. 商户自己保存私钥,公钥则通过PayMatrixPay商户站(配置商户公钥)功能保存到PayMatrixPay;
  4. PayMatrixPay开放接口的访问,需要签名和参数同时传过来,PayMatrixPay收到请求会利用商户保存到PayMatrixPay的公钥和接收到的明文参数进行签名验证;
  5. 验证通过后会执行业务逻辑,业务逻辑执行完成返回结果,结果由两部分组成,一部分是业务结果,另一部分则是在返回 head 中的签名参数;
  6. 返回 head 的签名参数是由业务返回结果构建后利用PayMatrixPay私钥进行加签;
  7. 商户收到开放返回结果后,利用明文返回参数和PayMatrixPay公钥(获取PayMatrixPay公钥)与返回的签名进行对比验签;

API 签名串组装规则

说明 接口签名规则主要用于保障接口请求的合法性与安全性。客户端和服务端通过对接口请求数据进行签名校验,确保数据在传输过程中未被篡改。 签名规则依据 HTTP 请求的相关信息,包括 HTTP 方法、请求路径、查询参数、请求体、时间戳及随机因子,生成签名数据。

签名生成规则

签名数据的生成流程按照以下顺序构建:
  1. HTTP 方法 请求的 HTTP 方法(如 GET、POST)必须包含在签名数据中,且转换为大写字母形式。
  2. 请求路径 请求的路径,即 URI 部分(不包括查询参数),必须包括在签名数据中。
  3. 查询参数(仅限 GET 请求) 所有 URL 中的查询参数(例如 ?key=value&param=value)必须按字典序(字母升序)排序后拼接成字符串,格式为 key1=value1&key2=value2,并添加到签名数据中。如果没有查询参数,则添加一个空行保持结构。
  4. 请求体(仅限 POST 请求) 对于 POST 请求,签名数据中必须包含请求体内容的 SHA-256 哈希值。请求体的格式为 JSON 格式,需要先进行格式化(根据字典顺序排序键),再进行 SHA-256 加密。
  5. 时间戳 请求头中必须包含时间戳,格式为 UNIX 时间戳(精确到毫秒)。时间戳需要添加到签名数据中。
  6. 随机因子 请求头中必须包含一个随机生成的字符串,作为防止重放攻击的随机因子。随机因子需添加到签名数据中。

签名数据构建顺序

所有待签名的数据按以下顺序进行拼接:
  1. HTTP 方法{HTTP_METHOD}
  2. 请求路径{REQUEST_URI}
  3. 查询参数(仅限 GET 请求):{QUERY_STRING}
  4. 请求体(仅限 POST 请求):{SHA256_OF_REQUEST_BODY}
  5. 时间戳{TIMESTAMP}
  6. 随机因子{NONCE}
每个部分后都有一个换行符(\n),如果某部分为空(例如没有查询参数或者请求体为空),则填入空行。 拼接格式示例
GET
/api/v1/example
key1=value1&key2=value2
sha256HexOfRequestBody
1633345678
randomNonce

签名生成步骤

  1. 获取 HTTP 方法 通过 request.getMethod() 获取 HTTP 请求方法,并转换为大写字母。
  2. 获取请求路径 通过 request.getRequestURI() 获取请求的路径(不包含查询参数)。
  3. 构建查询字符串 通过获取请求中的查询参数,按字典顺序排序并拼接成 key1=value1&key2=value2 格式。若无查询参数,则添加一个空行。
  4. 处理请求体 对于 POST 请求,从请求体中提取数据,使用 SHA-256 加密算法计算请求体的哈希值。请求体数据需要按照字典顺序对 JSON 键进行排序,并将排序后的结果进行哈希。

签名因子构造与合并

这是一个 POST 请求,请求参数放在请求的 Body 中,格式为 JSON 以下是一个业务参数的示例 JSON:
{
  "c": {
    "b": "11",
    "a": "10"
  },
  "a": "100",
  "b": [
    {
      "f": "3",
      "e": "2",
      "d": "1"
    },
    {
      "j": "6",
      "i": "5",
      "h": "4"
    }
  ]
}
这些参数是签名的构造因子,下面我们将说明如何对这些签名因子进行合并和排序。

合并签名因子

步骤 1:排序最外层参数

我们首先对最外层的参数按照字母正序进行排序,排序后的结果如下:
{
  "a": "100",
  "b": [
    {
      "f": "3",
      "e": "2",
      "d": "1"
    },
    {
      "j": "6",
      "i": "5",
      "h": "4"
    }
  ],
  "c": {
    "b": "11",
    "a": "10"
  }
}

步骤 2:对子对象进行排序

对于嵌套的子对象,我们也需要对它们进行排序。例如,子对象 c

排序前:

{
  "b": "11",
  "a": "10"
}

排序后:

{
  "a": "10",
  "b": "11"
}

步骤 3:对子对象数组进行排序

对于子对象数组,我们还需要对数组中的每个对象按字母顺序进行排序。例如,子对象 b

排序前:

[
  {
    "f": "3",
    "e": "2",
    "d": "1"
  },
  {
    "j": "6",
    "i": "5",
    "h": "4"
  }
]

排序后:

[
  {
    "d": "1",
    "e": "2",
    "f": "3"
  },
  {
    "h": "4",
    "i": "5",
    "j": "6"
  }
]

步骤 4:递归排序所有子对象

整个 JSON 对象在排序后的结果如下,注意每层子对象都经过了排序,这是一个递归的过程:
{
  "a": "100",
  "b": [
    {
      "d": "1",
      "e": "2",
      "f": "3"
    },
    {
      "h": "4",
      "i": "5",
      "j": "6"
    }
  ],
  "c": {
    "a": "10",
    "b": "11"
  }
}

步骤 5:构建签名因子字符串

合并并排序后的参数最终构成的签名因子字符串为:
a=100&d=1&e=2&f=3&h=4&i=5&j=6&a=10&b=11

关键规则总结

  • 按字母正序:所有参数都按字母顺序排列。
  • 逐层排序:先排序最外层对象,然后逐层对嵌套的对象进行排序。
  • 子对象数组排序:数组内的对象也需要按字母顺序进行排序。
按照以上步骤生成的签名因子可以确保在验证时准确无误。
  1. 添加时间戳和随机因子 时间戳和随机因子需要在请求中包含,并添加到待签名数据的末尾。
  2. 构建待签名字符串 所有字段按顺序拼接,形成最终的待签名字符串。
  3. 签名生成 使用生成的待签名字符串通过 RSA 或其他加密算法进行签名,生成签名数据。

签名示例

假设以下是一个 GET 请求的示例:
  • HTTP 方法GET
  • 请求路径/api/v1/example
  • 查询参数key1=value1&key2=value2
  • 请求体:无(GET 请求没有请求体)
  • 时间戳1633345678
  • 随机因子randomNonce
待签名字符串为:
GET
/api/v1/example
key1=value1&key2=value2

1633345678
randomNonce

签名验证规则

服务器端收到请求后,使用相同的规则和密钥生成签名,并与请求头中的签名进行比对。若签名匹配,则说明请求有效;否则,返回签名验证失败。

安全注意事项

  • 时间戳有效性:时间戳防止重放攻击,通常需要在签名有效期内才能使用,超过有效期的签名请求应视为无效。
  • 随机因子:随机因子是为了防止重放攻击,每次请求应该使用不同的随机因子。
  • 加密密钥管理:确保密钥安全,不应暴露在客户端。客户端和服务端都必须妥善管理密钥。

代码实现解析

以下是签名生成的 Java 实现核心部分的代码解析:
    public static String buildDataToSign(HttpServletRequest request, String timestamp, String nonce) {
        StringBuilder sb = new StringBuilder();

        // 1. HTTP方法(必须)
        sb.append(request.getMethod().toUpperCase()).append("\n");

        // 2. 请求路径(必须)
        sb.append(request.getRequestURI()).append("\n");

        // 3. 查询参数(GET请求必须)
        String queryString = buildSortedQueryString(request);
        if (!queryString.isEmpty()) {
            sb.append(queryString).append("\n");
        } else {
            sb.append("\n"); // 空行保持结构
        }

        // 4. 请求体(POST/PUT必须)
        String requestBody = ServletUtils.getPostData(request);
        if (StringUtils.isNotBlank(requestBody)) {
            String formatBody = formatRequestBody(requestBody);
            String sha256HexStr = DigestUtils.sha256Hex(formatBody);
            sb.append(sha256HexStr).append("\n");
        } else {
            sb.append("\n"); // 空行保持结构
        }

        // 5. 时间戳和随机因子(必须)
        sb.append(timestamp).append("\n");
        sb.append(nonce);

        return sb.toString();
    }

    /**
     * 构建排序后的查询字符串
     */
    private static String buildSortedQueryString(HttpServletRequest request) {
        Map<String, String> params = new TreeMap<>();

        Enumeration<String> paramNames = request.getParameterNames();
        while (paramNames.hasMoreElements()) {
            String paramName = paramNames.nextElement();
            String paramValue = request.getParameter(paramName);
            params.put(paramName, paramValue);
        }

        if (params.isEmpty()) {
            return "";
        }

        return params.entrySet().stream()
                .map(entry -> entry.getKey() + "=" + entry.getValue())
                .collect(Collectors.joining("&"));
    }

    /**
     * 将 post json data 进行格式
     *
     * @param requestBody
     * @return
     */
    public static String formatRequestBody(String requestBody) {
        Map<String, Object> data = JacksonUtils.parseObject(requestBody, new TypeReference<Map<String, Object>>() {
        });
        StringBuffer content = new StringBuffer();
        append(content, data);
        return content.toString();
    }

    private static void append(StringBuffer content, Map<String, Object> sourceObj) {
        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);
                    }
                }
            } else if (value instanceof Map) {
                append(content, (Map<String, Object>) value);
            } 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("&");
                }
                content.append(key);
                content.append("=");
                content.append(value);
            }
        }
    }