# 签名生成

{% 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 %}


---

# 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://apidoc.tokenpay.me/chinese/shuo-ming/qian-ming-sheng-cheng.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.
