> For the complete documentation index, see [llms.txt](https://apidoc.tokenpay.me/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://apidoc.tokenpay.me/chinese/shuo-ming/qian-ming-sheng-cheng.md).

# 签名生成

{% hint style="info" %}
说明

商户可以按照下述步骤生成请求的签名，平台会在收到请求后进行签名的验证。如果签名验证不通过，将会拒绝处理请求，并返回相应的[业务状态码](/chinese/cuo-wu-ma/ye-wu-zhuang-tai-ma.md)。
{% endhint %}

### 准备 <a href="#zhun-bei" id="zhun-bei"></a>

商户需要注册有商户号，并通过商户后台创建支付应用，获取应用 `APP_ID` 和 `APP_SECRECT`。

### 条件 <a href="#tiao-jian" id="tiao-jian"></a>

本文档所有 `POST` 方法的接口都需要验证签名，其他接口暂不需验证。

### 构造签名串 <a href="#gou-zao-qian-ming-chuan" id="gou-zao-qian-ming-chuan"></a>

签名串一共有四行，每一行为一个参数。行尾以 `\n`（换行符，ASCII 编码值为 0x0A）结束，最后一行不用加`\n`。如果参数本身以 `\n`结束，也需要附加一个`\n`。

```
URL\n
请求时间戳\n
请求随机串\n
请求报文主体
```

我们以[查询订单](/chinese/zhi-fu-ding-dan/cha-xun-ding-dan.md)接口为例:

第一步，获取请求的绝对 URL，并去除域名部分得到参与签名的 URL。如果请求中有查询参数，URL 末尾应附加有'?'和对应的查询字符串。

```
/v1/transaction/query
```

第二步，获取发起请求时的系统当前时间戳（毫秒），即格林威治时间 1970 年 01 月 01 日 00 时 00 分 00 秒起至现在的总秒数，作为请求时间戳。平台会拒绝处理很久之前发起的请求，请商户保持自身系统的时间准确。

```
1554208460000
```

第三步，生成一个 32 位`随机字符串`。

```
593BEC0C930BF1AFEB40B4A08C8FB242
```

第四步，获取请求中的请求报文主体（request body）。

```json
{
    "app_id": "8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd",
    "mch_id": "12345678",
    "transaction_id": "e98b30294xxxxxxxxxxxx97a9d9e09ce",
    "out_trade_no": "fb72xxxx-xxxx-xxxx-xxxx-xxxx8a7b52cb"
}
```

第五步，按照前述规则，构造的请求签名串为：

{% code overflow="wrap" %}

```
/v1/transaction/query\n
1554208460\n
593BEC0C930BF1AFEB40B4A08C8FB242\n
{"app_id":"8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd","mch_id":"12345678","transaction_id":"e98b30294xxxxxxxxxxxx97a9d9e09ce","out_trade_no":"fb72xxxx-xxxx-xxxx-xxxx-xxxx8a7b52cb"}
```

{% endcode %}

### 加密签名串 <a href="#jia-mi-qian-ming-chuan" id="jia-mi-qian-ming-chuan"></a>

下面以 javascript 加密过程为例:

{% code overflow="wrap" %}

```javascript
let cryptoJs = require("crypto-js");
let key = CryptoJS.enc.Utf8.parse("9db664697xxxxxxxxxxxx2d27a3c925c") //APP的密钥
//AES加密
let ciphertext = cryptoJs.AES.encrypt(签名串, key, {
  mode: CryptoJS.mode.ECB,
  padding: CryptoJS.pad.Pkcs7,
}).toString();
```

{% endcode %}

加密后示例：

{% code overflow="wrap" %}

```
QCwHvoBM9TJ2wokF8hhaoS34P0nkJpYMisBUizpOj5q/77I6+KFPVvFUCaaUiu+KFctisJFU1DfJdCHrLpJIx9CirX5ku3L9TMGihFcEG8MGoh2dwDvunH8JgJOVV9ClSkpXqjad4flSuYMoxPOZqPHr+ktOLZ3pPzs12BMqmbZVNIe+oOezTZsQ8xxxxRgOJzwU/AbouZSl2xto7DcYCjvNSnw7BkuzBFgTfxVXB3+R7e+1SpdeJajuCKGKvYMVTe7slS5j/4LQ4vcr1QqOPhpoemsOV92tPhgQ0iGw3GKpLIEOoDAwy2+ojzP5XERh
```

{% endcode %}

### 拼接签名信息 <a href="#pin-jie-qian-ming-xin-xi" id="pin-jie-qian-ming-xin-xi"></a>

签名信息的拼接格式如下：\
app\_id=`APP_ID`,mch\_id=`商户ID`,nonce\_str=`第三步生成的随机字符串`,timestamp=`第二步生成的时间戳`,signature=`加密签名串`

### 设置 HTTP 头 <a href="#she-zhi-http-tou" id="she-zhi-http-tou"></a>

本文档 API 通过 HTTP `Authorization` 头来传递签名。 `Authorization` 由`认证类型`和`签名信息`两个部分组成，目前`认证类型` 仅支持 `TTPAY-AES-256-ECB`

Authorization 头的示例如下：（注意，示例因为排版可能存在换行，实际数据应在一行）

{% code overflow="wrap" %}

```
Authorization: TTPAY-AES-256-ECB app_id=8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd,mch_id=12345678,nonce_str=593BEC0C930BF1AFEB40B4A08C8FB242,timestamp=1554208460,signature=QCwHvoBM9TJ2wokF8hhaoS34P0nkJpYMisBUizpOj5q/77I6+KFPVvFUCaaUiu+KFctisJFU1DfJdCHrLpJIx9CirX5ku3L9TMGihFcEG8MGoh2dwDvunH8JgJOVV9ClSkpXqjad4flSuYMoxPOZqPHr+ktOLZ3pPzs12BMqmbZVNIe+oOezTZsQ8xxxxRgOJzwU/AbouZSl2xto7DcYCjvNSnw7BkuzBFgTfxVXB3+R7e+1SpdeJajuCKGKvYMVTe7slS5j/4LQ4vcr1QqOPhpoemsOV92tPhgQ0iGw3GKpLIEOoDAwy2+ojzP5XERh"
```

{% endcode %}

最终我们可以组一个包含了签名的 HTTP 请求了。

{% code overflow="wrap" %}

```sh
$ curl https://api.tokenpay.me/v1/transaction/query -H "Content-Type: application/json" -H 'Authorization: TTPAY-AES-256-ECB app_id=8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd,mch_id=12345678,nonce_str=593BEC0C930BF1AFEB40B4A08C8FB242,timestamp=1554208460,signature=QCwHvoBM9TJ2wokF8hhaoS34P0nkJpYMisBUizpOj5q/77I6+KFPVvFUCaaUiu+KFctisJFU1DfJdCHrLpJIx9CirX5ku3L9TMGihFcEG8MGoh2dwDvunH8JgJOVV9ClSkpXqjad4flSuYMoxPOZqPHr+ktOLZ3pPzs12BMqmbZVNIe+oOezTZsQ8xxxxRgOJzwU/AbouZSl2xto7DcYCjvNSnw7BkuzBFgTfxVXB3+R7e+1SpdeJajuCKGKvYMVTe7slS5j/4LQ4vcr1QqOPhpoemsOV92tPhgQ0iGw3GKpLIEOoDAwy2+ojzP5XERh' -X POST -d '{"out_trade_no": "fb72xxxx-xxxx-xxxx-xxxx-xxxx8a7b52cb", "transaction_id":"e98b30294xxxxxxxxxxxx97a9d9e09ce", "app_id":"8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd", "mch_id":"12345678" }'
```

{% endcode %}

### 演示代码 <a href="#yan-shi-dai-ma" id="yan-shi-dai-ma"></a>

{% tabs %}
{% tab title="Java" %}

{% code overflow="wrap" %}

```java
package com.example.http;

import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.sun.istack.internal.NotNull;
import okhttp3.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.Base64;

public class HttpApplication {
    private static String SECRET = "AES";
    private static String CIPHER_ALGORITHM = "AES/ECB/PKCS7Padding";
    private static String schema = "TTPAY-AES-256-ECB ";

    public static void main(String[] args) throws Exception {
        String key = "xxxxxxxx";    // 应用密钥
        String reqURL = "/v1/transaction/query";    // 请求接口

        OkHttpClient client = new OkHttpClient();
        JSONObject params = new JSONObject();

        try {
            params.put("app_id", "xxxxxxxx");           // 应用ID
            params.put("mch_id", "xxxxxxxx");           // 商户ID
            params.put("transaction_id", "xxxxxxxx");   // 平台订单号
            params.put("out_trade_no", "xxxxxxxx");     // 商户订单号
        } catch (JSONException e) {
            e.printStackTrace();
        }

        Security.addProvider(new BouncyCastleProvider());

        // 签名
        String auth = auth(reqURL, params, key);

        RequestBody body = RequestBody.create(MediaType.parse("application/json;charset=utf-8"), params.toString());
        Request request = new Request.Builder()
        .header("Authorization", auth)
        .url(reqURL)
        .post(body)
        .build();

        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                System.out.println("http request error");
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                System.out.println(response.body().string());
            }
        });
    }

    /**
    * 签名
    * @param reqURL 请求URL
    * @param raw 报文主体
    * @param key 应用密钥
    * @return 签名信息
    */
    public static String auth(String reqURL, JSONObject raw, String key) throws Exception {
        HttpUrl parseReqURL = HttpUrl.parse(reqURL);
        String url = parseReqURL.encodedPath();
        String appID = raw.getString("app_id");
        String mchID = raw.getString("mch_id");
        long currentTimeMillis = System.currentTimeMillis();
        String timestamp = String.valueOf(currentTimeMillis);
        String nonceStr = randomString(32);
        String message = url + "\n" + timestamp + "\n" + nonceStr + "\n" + raw.toString();
        String aes256ECBPkcs7PaddingEncrypt = aes256ECBPkcs7PaddingEncrypt(message, key);

        return schema + "app_id=" + appID + ",mch_id=" + mchID + ",nonce_str=" + nonceStr + ",timestamp=" + timestamp + ",signature=" + aes256ECBPkcs7PaddingEncrypt;
    }

    /**
    * 生成随机字符串
    * @param len 字符长度
    * @return 随机字符串
    */
    public static String randomString(Integer ...len) {
        int e = len.length <= 0 ? 32 : len[0];
        String str = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
        int strLen = str.length();
        StringBuilder stringBuilder = new StringBuilder();

        for (int i = 0; i < e; i++) {
            double random = Math.random();
            int v = (int) Math.floor(random * strLen);
            char charAt = str.charAt(v);

            stringBuilder.append(charAt);
        }

        return stringBuilder.toString();
    }

    /**
    * AES加密
    * @param str 字符串
    * @param key 密钥
    * @return 加密字符串
    * @throws Exception 异常信息
    */
    public static String aes256ECBPkcs7PaddingEncrypt(String str, String key) throws Exception {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);

        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(keyBytes, SECRET));

        byte[] doFinal = cipher.doFinal(str.getBytes(StandardCharsets.UTF_8));

        return new String(Base64.getEncoder().encode(doFinal));
    }

    /**
    * AES解密
    * @param str 字符串
    * @param key 密钥
    * @return 解密字符串
    * @throws Exception 异常信息
    */
    public static String aes256ECBPkcs7PaddingDecrypt(String str, String key) throws Exception {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);

        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBytes, SECRET));

        byte[] doFinal = cipher.doFinal(Base64.getDecoder().decode(str));

        return new String(doFinal);
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="Go" %}

{% code overflow="wrap" %}

```go
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"math/rand"
	"net/http"
	"time"

	"github.com/forgoer/openssl"
)

const (
	SignatureMessageFormat = "%s\n%d\n%s\n%s" // 数字签名原文格式
	HeaderAuthorizationFormat = "%s app_id=%s,mch_id=%s,nonce_str=%s,timestamp=%d,signature=%s"
)

type QueryReq struct {
	AppID         string `json:"app_id"`
	MchID         string `json:"mch_id"`
	TransactionID string `json:"transaction_id,omitempty"`
	OutTradeNo    string `json:"out_trade_no,omitempty"`
}

func main() {

	appSecret := "**********"           // 应用密钥
	reqPath := "/v1/transaction/query"  // 请求接口路径
	queryReq := QueryReq{
		AppID:         "**********",    // 应用ID
		MchID:         "**********",    // 商户ID
		TransactionID: "**********",    // 平台订单号
		OutTradeNo:    "**********",    // 商户订单号
	}

	b, _ := json.Marshal(queryReq)

    // 签名
	authorization, err := GenerateAuthorizationHeader(queryReq.AppID, queryReq.MchID, reqPath, string(b), appSecret)
	if err != nil {
		panic(err)
	}

    // 请求接口
	url := "" + reqPath

	payload := bytes.NewBuffer(b)

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", authorization)

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(string(body))
}

// 签名
func GenerateAuthorizationHeader(appID, mchID, reqURL, signBody, appSecret string) (string, error) {
	nonceStr := RandomString(32)
	timestamp := time.Now().Unix()
	message := fmt.Sprintf(SignatureMessageFormat, reqURL, timestamp, nonceStr, signBody)

	signatureResult, err := AesECBEncrypt(message, appSecret)
	if err != nil {
		return "", err
	}
	authorization := fmt.Sprintf(
		HeaderAuthorizationFormat, getAuthorizationType(), appID,
		mchID, nonceStr, timestamp, signatureResult,
	)
	return authorization, nil
}

// AES加密
func AesECBEncrypt(orig, key string) (string, error) {
	dst, _ := openssl.AesECBEncrypt([]byte(orig), []byte(key), openssl.PKCS7_PADDING)
	return base64.StdEncoding.EncodeToString(dst), nil
}

// AES解密
func AesECBDecrypt(crypted, key string) (string, error) {
	x := len(crypted) * 3 % 4
	switch {
	case x == 2:
		crypted += "=="
	case x == 1:
		crypted += "="
	}
	crytedByte, err := base64.StdEncoding.DecodeString(crypted)
	if err != nil {
		return "", err
	}
	origData, err := openssl.AesECBDecrypt(crytedByte, []byte(key), openssl.PKCS7_PADDING)
	if err != nil {
		return "", err
	}
	return string(origData), err
}

// 生成随机字符
func RandomString(length int) string {
	str := []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
	var result []byte
	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
	for i := 0; i < length; i++ {
		result = append(result, str[rnd.Intn(len(str))])
	}
	return string(result)
}

func getAuthorizationType() string {
	return "TTPAY-AES-256-ECB"
}
```

{% endcode %}
{% endtab %}

{% tab title="PHP" %}

{% code overflow="wrap" %}

```php
<?php
$key = "xxxxxxxx";                // 应用密钥
$APP_ID = "xxxxxxxx";             // 应用ID
$mchID = "xxxxxxxx";              // 商户ID
$transactionID = "xxxxxxxx";      // 平台订单号
$outTradeNo = "xxxxxxxx";         // 商户订单号
$url = "/v1/transaction/query";   // 接口路径
$timestamp = time()*1000;         // 当前时间戳
$nonce = generateStr(32);         // 32位随机字符串

// 生成随机字符串
function generateStr($length) {
  $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  $res ='';

  for ( $i = 0; $i < $length; $i++ ) {
    $res .= $chars[ mt_rand(0, strlen($chars) - 1) ];
  }

  return $res;
}

// 报文主体
$body = '{"app_id":"'.$APP_ID.'","mch_id":"'.$mchID.'","transaction_id":"'.$transactionID.'","out_trade_no":"'.$outTradeNo.'"}';

// 构造签名串
$message = $url."\n".$timestamp."\n".$nonce."\n".$body;

// 加密签名串
$cipher = "aes-256-ecb";
$sign = openssl_encrypt($message, $cipher, $key);

// 拼接签名信息
$schema = 'TTPAY-AES-256-ECB';
$authorization = sprintf('%s app_id=%s,mch_id=%s,nonce_str=%s,timestamp=%d,signature=%s', $schema, $APP_ID, $mchID, $nonce, $timestamp, $sign);

// 设置HTTP头
$header[] = "Accept: application/json";
$header[] = "Content-Type: application/json";
$header[] = "Authorization: $authorization";

// 请求接口
$curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
curl_setopt($curl, CURLOPT_URL, "".$url);
curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$result = curl_exec($curl);
curl_close($curl);

var_export($result);
```

{% endcode %}

<br>
{% endtab %}

{% tab title="JavaScript" %}

{% code overflow="wrap" %}

```javascript
let cryptoJs = require("crypto-js");

let jsonObj = JSON.parse(pm.request.body.raw); //把提交上来的body字符串转成对象
let jsonStr = JSON.stringify(jsonObj)//重新把对象字符串，目的是压缩一下，避免body里面的换行影响签名
let appID = jsonObj.app_id;
let mchID = jsonObj.mch_id;

// 生成当前时间戳
let timestamp = Math.round(new Date() / 1000).toString()

//定义随机字符串
var nonceStr = randomString(); // 随机字符串
function randomString(e) {
    e = e || 32;
    var t = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678",
    a = t.length,
    n = "";
    for (i = 0; i < e; i++) n += t.charAt(Math.floor(Math.random() * a));
    return n
}

//获取完整路径的path
var reqURL = "/" + pm.request.url.path.join("/");

//拼接签名明文
message = reqURL + "\n" + timestamp + "\n" + nonceStr + "\n" + jsonStr

let key = CryptoJS.enc.Utf8.parse("********************************") // 应用密钥
//AES加密
let ciphertext = cryptoJs.AES.encrypt(message, key, {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7,
  }).toString();

//拼接放在Header的authorization，TTPAY-{加密方式},目前仅支持AES-256-ECB
let authorization = 'TTPAY-AES-256-ECB app_id=' + appID + ',mch_id=' + mchID + ',nonce_str=' + nonceStr + ',timestamp=' + timestamp + ',signature=' + ciphertext

pm.request.headers.upsert({ key: "authorization", value: authorization })
```

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