# API 认证

> 🔐 **重要提示**：请严格按照本文档说明进行 API 认证与签名，确保安全通信。


## 必要头部参数

### 请求头参数

| 参数名  | 含义描述 | 类型  | 必填  |
|  --- | --- | --- | --- |
| `x-paykka-appid` | 调用方唯一标识 | String[1,64] | ✅ |
| `x-paykka-timestamp` | 时间戳，单位毫秒，与当前时间误差不超过 5 分钟 | String[1,11] | ✅ |
| `x-paykka-nonce` | 防重放随机数，每次请求唯一 | String[10,100] | ✅ |
| `x-paykka-sign` | 请求签名 | String[1,500] | ✅ |
| `x-paykka-sign-alg` | 签名类型，固定传 `SHA256_WITH_RSA` | String[1,10] | ✅ |


### 响应头参数

| 参数名  | 含义描述 | 类型  | 必填  |
|  --- | --- | --- | --- |
| `x-paykka-timestamp` | 时间戳 | String[1,11] | ✅ |
| `x-paykka-nonce` | 防重放随机数，每次响应唯一 | String[10,100] | ✅ |
| `x-paykka-sign` | 响应签名 | String[1,500] | ✅ |


## 示例

### 请求体

```json
{
    "merchant_id": "YOUR_MERCHANT_ID",
    "payment_type": "PURCHASE",
    "authorisation_type": "FINAL_AUTH",
    "capture_method": "AUTOMATIC",
    "trans_id": "t202311081113",
    "amount": 445,
    "currency": "EUR",
    "return_url": "https://www.baidu.com/returnUrl",
    "payment": {
        "payment_method": "BANKCARD",
        "store_payment_method": false,
        "token_usage": "CARD_ON_FILE",
        "shopper_reference": "user1234567890",
        "encrypted_card_no": "string",
        "encrypted_exp_year": "string",
        "encrypted_exp_month": "string",
        "encrypted_cvv": "string"
    }
}
```

### 响应体

```json
{
  "ret_code": "000000",
  "ret_msg": "Success",
  "data": {
    "merchant_id": "YOUR_MERCHANT_ID",
    "trans_id": "t202311081113",
    "order_id": "GW20598371023658327",
    "status": "AUTHORIZED",
    "authorisation_type": "FINAL_AUTH",
    "capture_method": "AUTOMATIC",
    "amount": 445,
    "currency": "EUR",
    "payment": {
      "payment_method": "BANKCARD"
    },
    "card_info": {
      "bin": "424242",
      "last4": "4242",
      "card_brand": "VISA"
    },
    "balances": {
      "authed_amount": 445,
      "captured_amount": 0,
      "able_to_capture_amount": 445,
      "voided_amount": 0,
      "able_to_void_amount": 445,
      "refunded_amount": 0,
      "able_to_refund_amount": 0
    }
  }
}
```

## API 地址

### 沙箱 API 地址

```bash
https://openapi-sandbox.paykka.com
```

### 生产 API 地址

```bash
https://openapi.paykka.com
https://openapi.eu.paykka.com
```

## 计算签名

### 添加依赖

在您的 Java 代码中引入依赖：

```xml
<dependency>
  <groupId>commons-codec</groupId>
  <artifactId>commons-codec</artifactId>
  <version>1.15</version>
</dependency>
```

### 生成签名

#### 请求签名

开发者需要使用自身的私钥对API URL、消息体等关键数据的组合进行 `SHA256_WITH_RSA` 签名。请求的签名信息通过HTTP头传递，没有携带签名或者签名验证不通过的请求，都不会被执行，并返回401 Unauthorized 。

#### 响应签名

对于签名验证成功的请求，会使用开放平台的平台私钥对响应进行签名。签名的信息包含在HTTP头部中，开发者应使用开放平台的公钥进行验签。

#### 回调通知签名

当调用开发者的接口时，开放平台会使用本平台私钥对回调请求进行签名。签名的方法同响应签名的方式一致，开发者必须使用开放平台的公钥验证回调的签名。

#### 构造签名串

签名串一共有五行，每一行为一个参数。结尾以\n（换行符，ASCII编码值为0x0A）结束。

如果参数为空，也需要附加一个空白字符和\n。

如果参数本身以\n结束，也需要附加一个\n。

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

* 1、获取HTTP请求的方法（POST）
* 2、获取请求的URL部分：即请求的绝对URL，并去除域名部分得到参与签名的URL。如果请求中有查询参数，URL末尾应附加有'?'和对应的查询字符串。URL中的参数部分应该进行URLEncode编码，以便对中文等特殊符号进行处理。
* 3、获取发起请求时的系统当前时间戳（Unix Timestamp），作为请求时间戳，时间戳精确到毫秒。
* 4、生成一个32位请求随机串，推荐生成随机数算法如下：调用随机数函数生成，将得到的值转换为字符串。
* 5、获取请求中的请求报文主体（request body）。


示例

```
POST\n
/api/pay/demo?id=1537\n
1705544961000\n
326425780571035424362645\n
{"merch":"123"}
```

#### 计算签名值

1、使用商户私钥对签名串进行 `SHA256_WITH_RSA` 签名；
2、并对签名结果进行 `Base64编码 + URLEncode` 得到签名值。

Java
```java
private static final String FORMAT = "%s\n%s\n%s\n%s\n%s";

/**
 * 使用SHA256withRSA算法进行签名
 * 
 * @param String privateKey Base64编码的PKCS8格式私钥
 * @param Object req 请求对象
 * @return string URL编码后的Base64签名结果
 */
public static String signWithSha256Rsa(Object req, String privateKey) {
    String content = String.format(FORMAT,
            Optional.ofNullable(req.getMethod()).orElse(""),
            Optional.ofNullable(req.getUri()).orElse(""),
            req.getTimestamp(),
            Optional.ofNullable(req.getNonce()).orElse(""),
            Optional.ofNullable(req.getBody()).orElse(""));
    log.debug("签名串：{}", content);

    Signature signature = Signature.getInstance("SHA256withRSA");
    byte[] privateKeys = Base64Utils.decodeFromString(privateKey);
    
    PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeys);
    PrivateKey priKey = KeyFactory.getInstance("RSA").generatePrivate(pkcs8EncodedKeySpec);
    signature.initSign(priKey);
    signature.update(content.getBytes(StandardCharsets.UTF_8));
    byte[] signed = signature.sign();

    String result = Base64Utils.encodeToString(signed);
    String sign = URLEncoder.encode(result);
    log.info("生成签名：{}", sign);
    return sign;
}
```

PHP
```php
/**
 * 使用SHA256withRSA算法进行签名
 * 
 * @param string $privateKeyBase64 Base64编码的PKCS8格式私钥
 * @param string $data 要签名的数据
 * @return string URL编码后的Base64签名结果
 * @throws Exception
 */
function signWithSha256Rsa($privateKeyBase64, $data) {
    // Base64解码私钥
    $privateKeyPem = base64_decode($privateKeyBase64);
    
    if ($privateKeyPem === false) {
        throw new Exception("Base64解码私钥失败");
    }
    
    // 将DER格式的私钥转换为PEM格式
    $privateKeyPem = "-----BEGIN PRIVATE KEY-----\n" . 
                      chunk_split(base64_encode($privateKeyPem), 64, "\n") . 
                      "-----END PRIVATE KEY-----";
    
    // 从PEM格式加载私钥
    $privateKey = openssl_pkey_get_private($privateKeyPem);
    
    if ($privateKey === false) {
        throw new Exception("加载私钥失败: " . openssl_error_string());
    }
    
    // 使用SHA256算法进行签名
    $signature = '';
    $success = openssl_sign($data, $signature, $privateKey, OPENSSL_ALGO_SHA256);
    
    if (!$success) {
        throw new Exception("签名失败: " . openssl_error_string());
    }
    
    // PHP 8.0+ 会自动释放资源，无需手动调用 openssl_free_key()
    
    // Base64编码签名结果
    $signBase64 = base64_encode($signature);
    
    // URL编码
    $signUrlEncoded = urlencode($signBase64);
    
    return $signUrlEncoded;
}
```

Node.js
```javascript
const crypto = require('crypto');

/**
 * 使用 SHA256withRSA 算法进行签名（Node.js 内置 crypto，无需额外依赖）
 *
 * @param {string} privateKeyBase64 Base64 编码的 PKCS8 格式私钥（DER）
 * @param {string} data 要签名的数据（签名串）
 * @returns {string} URL 编码后的 Base64 签名结果
 */
function signWithSha256Rsa(privateKeyBase64, data) {
  const keyDer = Buffer.from(privateKeyBase64, 'base64');
  const privateKey = crypto.createPrivateKey({
    key: keyDer,
    format: 'der',
    type: 'pkcs8',
  });
  const sign = crypto.createSign('RSA-SHA256');
  sign.update(data, 'utf8');
  const signature = sign.sign(privateKey);
  const signBase64 = signature.toString('base64');
  return encodeURIComponent(signBase64);
}
```

Python
```python
import base64
from urllib.parse import quote

from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding


def sign_with_sha256_rsa(private_key_base64: str, data: str) -> str:
    """
    使用 SHA256withRSA 算法进行签名。

    需要安装依赖: pip install cryptography

    :param private_key_base64: Base64 编码的 PKCS8 格式私钥（DER）
    :param data: 要签名的数据（签名串）
    :return: URL 编码后的 Base64 签名结果
    """
    key_der = base64.b64decode(private_key_base64)
    private_key = serialization.load_der_private_key(key_der, password=None)
    signature = private_key.sign(
        data.encode("utf-8"),
        padding.PKCS1v15(),
        hashes.SHA256(),
    )
    sign_base64 = base64.b64encode(signature).decode("ascii")
    return quote(sign_base64, safe="")
```

### 设置请求头

1. 设置请求头 `x-paykka-appid`、`x-paykka-nonce`、`x-paykka-timestamp`、`x-paykka-sign-alg`，并确保参数正确
2. 将签名设置到请求头 `x-paykka-sign` 中，并确保签名原始串参数和请求体 `requestBody` 完全一致


### 请求示例

```bash
curl 'https://openapi-sandbox.paykka.com/v3/payments/acq' -X POST \
 -H 'Content-Type: application/json'\
-H 'Content-Type: application/json' \
-H 'x-paykka-appid: 978594372956732' \
-H 'x-paykka-nonce: 4326048250346354435' \
-H 'x-paykka-sign: Mif3gh48xxxxxxxxx' \
-H 'x-paykka-sign-alg: SHA256_WITH_RSA' \
-H 'x-paykka-timestamp: 1757387467986' \
-d '{"merchant_id": "YOUR_MERCHANT_ID"}'
```

## 验证签名

> ⚠️ **注意**: 验证签名时使用的公钥是 PayKKa 公钥


Java
```java
private static final String FORMAT = "%s\n%s\n%s\n%s\n%s";

/**
 * 使用SHA256withRSA算法进行验签
 * 
 * @param String publicKey Base64编码的X509格式公钥
 * @param Object req 请求对象
 * @param Object resp 响应对象
 * @return boolean 验签结果，true表示验签成功，false表示验签失败
 */
public static boolean verifyWithSha256Rsa(Object req, Object resp, String publicKey) {
    String content = String.format(FORMAT,
          Optional.ofNullable(req.getMethod()).orElse(""),
          Optional.ofNullable(req.getUri()).orElse(""),
          resp.getTimestamp(),
          Optional.ofNullable(resp.getNonce()).orElse(""),
          Optional.ofNullable(resp.getBody()).orElse(""));
    log.debug("签名串：{}", content);

    Signature signature = Signature.getInstance("SHA256withRSA");
    byte[] publicKeys = Base64Utils.decodeFromString(publicKey);

    X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeys);
    PublicKey pubKey = KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec);
    signature.initVerify(pubKey);
    signature.update(content.getBytes(StandardCharsets.UTF_8));

    String channelSignature = URLDecoder.decode("signatureDataFromPayKKa", StandardCharsets.UTF_8);
    boolean verified = signature.verify(Base64Utils.decodeFromString(channelSignature));
    return verified;
}
```

PHP
```php
/**
 * 使用SHA256withRSA算法进行验签
 * 
 * @param string $publicKeyBase64 Base64编码的X509格式公钥
 * @param string $data 原始数据
 * @param string $signatureUrlEncoded URL编码后的Base64签名
 * @return bool 验签结果，true表示验签成功，false表示验签失败
 * @throws Exception
 */
function verifyWithSha256Rsa($publicKeyBase64, $data, $signatureUrlEncoded) {
    // URL解码签名
    $signatureBase64 = urldecode($signatureUrlEncoded);
    
    if ($signatureBase64 === false) {
        throw new Exception("URL解码签名失败");
    }
    
    // Base64解码签名
    $signature = base64_decode($signatureBase64);
    
    if ($signature === false) {
        throw new Exception("Base64解码签名失败");
    }
    
    // Base64解码公钥
    $publicKeyDer = base64_decode($publicKeyBase64);
    
    if ($publicKeyDer === false) {
        throw new Exception("Base64解码公钥失败");
    }
    
    // 将DER格式的公钥转换为PEM格式
    // X509格式的公钥需要添加PEM头尾
    $publicKeyPem = "-----BEGIN PUBLIC KEY-----\n" . 
                     chunk_split(base64_encode($publicKeyDer), 64, "\n") . 
                     "-----END PUBLIC KEY-----";
    
    // 从PEM格式加载公钥
    $publicKey = openssl_pkey_get_public($publicKeyPem);
    
    if ($publicKey === false) {
        throw new Exception("加载公钥失败: " . openssl_error_string());
    }
    
    // 使用SHA256算法进行验签
    $result = openssl_verify($data, $signature, $publicKey, OPENSSL_ALGO_SHA256);
    
    // PHP 8.0+ 会自动释放资源，无需手动调用 openssl_free_key()
    
    if ($result === 1) {
        return true; // 验签成功
    } elseif ($result === 0) {
        return false; // 验签失败
    } else {
        throw new Exception("验签过程出错: " . openssl_error_string());
    }
}
```

Node.js
```javascript
const crypto = require('crypto');

/**
 * 使用 SHA256withRSA 算法进行验签（Node.js 内置 crypto）
 *
 * @param {string} publicKeyBase64 Base64 编码的 X509 公钥（DER，SubjectPublicKeyInfo，与 Java X509EncodedKeySpec 一致）
 * @param {string} data 原始数据（签名串）
 * @param {string} signatureUrlEncoded 响应头 x-paykka-sign 等 URL 编码后的 Base64 签名
 * @returns {boolean} true 验签成功，false 验签失败
 */
function verifyWithSha256Rsa(publicKeyBase64, data, signatureUrlEncoded) {
  // 与 Java URLDecoder.decode 行为对齐：+ 视为空格后再按 URI 组件解码
  const channelSignature = decodeURIComponent(
    signatureUrlEncoded.replace(/\+/g, ' ')
  );
  const signature = Buffer.from(channelSignature, 'base64');
  const keyDer = Buffer.from(publicKeyBase64, 'base64');
  const publicKey = crypto.createPublicKey({
    key: keyDer,
    format: 'der',
    type: 'spki',
  });
  const verify = crypto.createVerify('RSA-SHA256');
  verify.update(data, 'utf8');
  return verify.verify(publicKey, signature);
}
```

Python
```python
import base64
from urllib.parse import unquote_plus

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding


def verify_with_sha256_rsa(
    public_key_base64: str, data: str, signature_url_encoded: str
) -> bool:
    """
    使用 SHA256withRSA 算法进行验签。

    需要安装依赖: pip install cryptography

    :param public_key_base64: Base64 编码的 X509 公钥（DER，与 Java X509EncodedKeySpec 一致）
    :param data: 原始数据（签名串）
    :param signature_url_encoded: URL 编码后的 Base64 签名
    :return: True 验签成功，False 验签失败
    """
    signature_base64 = unquote_plus(signature_url_encoded)
    signature = base64.b64decode(signature_base64)
    key_der = base64.b64decode(public_key_base64)
    public_key = serialization.load_der_public_key(key_der)
    try:
        public_key.verify(
            signature,
            data.encode("utf-8"),
            padding.PKCS1v15(),
            hashes.SHA256(),
        )
        return True
    except InvalidSignature:
        return False
```