> For the complete documentation index, see [llms.txt](https://docs.musepay.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.musepay.io/cn/ji-cheng/authentication.md).

# 身份验证

配置 RSA 凭据并为 MusePay API 请求生成签名。

MusePay 使用 RSA 签名验证 API 请求。您需要自行生成并保管私钥，然后将对应的公钥上传至 MusePay。MusePay 不需要、也不会获取您的私钥。

## 生成 RSA 密钥对

生成 PKCS #8 PEM 格式的 2048 位 RSA 私钥，然后导出对应的公钥：

```bash
openssl genpkey -algorithm RSA -out muse_secret.key -pkeyopt rsa_keygen_bits:2048
openssl pkey -in muse_secret.key -pubout -out muse_public.key
```

私钥文件格式如下。示例中间内容已经掩码处理，并非可用私钥：

```
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSj...已省略...AQAB
-----END PRIVATE KEY-----
```

{% hint style="danger" %}
请安全保管 `muse_secret.key`。切勿上传或发送给 MusePay，也不要将其提交到代码仓库、输出到日志或嵌入客户端代码。
{% endhint %}

打开 [Partner Portal](https://partner.musepay.io)，进入 **API Key** 页签，上传 `muse_public.key` 的内容以配置合作伙伴公钥。MusePay 使用该公钥验证您的 API 请求签名。

上传前，请删除 `-----BEGIN PUBLIC KEY-----` 和 `-----END PUBLIC KEY-----` 标记行，并去除所有换行。仅上传剩余的 Base64 公钥内容，确保其为完整的一行。

<figure><img src="https://3284514181-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FXNWFeaFjBSxNdDRnZTvM%2Fuploads%2Fgit-blob-7e55746d6e616e348aeffd754916b811abe1f802%2Fimage%20(15).png?alt=media" alt="Partner Portal API Key 页面，显示 Partner ID 和公钥配置"><figcaption><p>在 Partner Portal 配置公钥并下载 MusePay 公钥。</p></figcaption></figure>

同时下载并妥善保存 MusePay 公钥。验证签名的 [Webhook 通知](/cn/ji-cheng/webhook.md)时，应使用 MusePay 公钥，而不是您的合作伙伴公钥。

## 身份验证必填字段

每个 API 请求的 Body 都必须包含以下字段。生成签名前，还需要将接口特有字段放入同一个 Body。

| 名称           | 类型     | 说明                                         |
| ------------ | ------ | ------------------------------------------ |
| `partner_id` | String | Partner Portal 中显示的合作伙伴 ID。                |
| `sign_type`  | String | 固定值：`RSA`。                                 |
| `timestamp`  | String | 当前 10 位 Unix 秒级时间戳。                        |
| `nonce`      | String | 本次请求的唯一随机值。每次请求都应生成新的 nonce；60 秒内重复使用将被拒绝。 |
| `sign`       | String | 按下述方式生成并经过 Base64 编码的 RSA 签名。              |

共享请求字段的完整说明请参阅[通用参数](/cn/api-can-kao/api-reference/common-parameters.md)。

## 生成签名

请使用最终发送给 MusePay 的准确请求值：

1. 移除 `sign` 字段。
2. 排除值为 `null` 或空字符串的字段。
3. 按字段名称区分大小写地进行升序排列。
4. 将每个字段格式化为 `key=value`，再使用 `&` 连接。不要对该规范字符串进行 URL 编码。
5. 将规范字符串编码为 UTF-8，并使用您的 RSA 私钥和 `SHA1WithRSA` 算法生成签名。
6. 对生成的签名进行 Base64 编码，并将结果作为 `sign` 发送。

{% hint style="warning" %}
当前 MusePay API 固定使用 `SHA1WithRSA`。除非 MusePay 确认您的集成已经迁移，否则请勿替换为其他签名算法。
{% endhint %}

### 请求示例

未签名的请求 Body：

```json
{
  "partner_id": "200001",
  "request_id": "202609110001",
  "sign_type": "RSA",
  "timestamp": "1789056000",
  "nonce": "5K8264ILTKCH16CQ2502SI8ZNMTM67VS"
}
```

生成的规范字符串为：

```
nonce=5K8264ILTKCH16CQ2502SI8ZNMTM67VS&partner_id=200001&request_id=202609110001&sign_type=RSA&timestamp=1789056000
```

签名完成后，将经过 Base64 编码的结果加入请求 Body：

```json
{
  "partner_id": "200001",
  "request_id": "202609110001",
  "sign_type": "RSA",
  "timestamp": "1789056000",
  "nonce": "5K8264ILTKCH16CQ2502SI8ZNMTM67VS",
  "sign": "<Base64 编码的 RSA 签名>"
}
```

## 代码示例

以下示例从 `muse_secret.key` 文件读取私钥。请将该文件存放在代码仓库之外，并严格限制访问权限。

<details>

<summary>Java</summary>

```java
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

static String canonicalize(Map<String, ?> params) {
    return params.entrySet().stream()
        .filter(entry -> !"sign".equals(entry.getKey()))
        .filter(entry -> entry.getValue() != null)
        .filter(entry -> !String.valueOf(entry.getValue()).isEmpty())
        .sorted(Map.Entry.comparingByKey())
        .map(entry -> entry.getKey() + "=" + entry.getValue())
        .collect(Collectors.joining("&"));
}

static PrivateKey loadPrivateKey(Path path) throws Exception {
    String pem = Files.readString(path, StandardCharsets.UTF_8)
        .replace("-----BEGIN PRIVATE KEY-----", "")
        .replace("-----END PRIVATE KEY-----", "")
        .replaceAll("\\s", "");
    byte[] keyBytes = Base64.getDecoder().decode(pem);
    return KeyFactory.getInstance("RSA")
        .generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
}

static String sign(String content, PrivateKey privateKey) throws Exception {
    Signature signer = Signature.getInstance("SHA1WithRSA");
    signer.initSign(privateKey);
    signer.update(content.getBytes(StandardCharsets.UTF_8));
    return Base64.getEncoder().encodeToString(signer.sign());
}

Map<String, Object> requestBody = new HashMap<>();
requestBody.put("partner_id", "200001");
requestBody.put("request_id", "202609110001");
requestBody.put("sign_type", "RSA");
requestBody.put("timestamp", String.valueOf(Instant.now().getEpochSecond()));
requestBody.put("nonce", UUID.randomUUID().toString().replace("-", ""));

PrivateKey privateKey = loadPrivateKey(Path.of("muse_secret.key"));
requestBody.put("sign", sign(canonicalize(requestBody), privateKey));
```

</details>

<details>

<summary>JavaScript（Node.js）</summary>

```javascript
import { readFileSync } from 'node:fs'
import { randomUUID, sign } from 'node:crypto'

function canonicalize(params) {
  return Object.entries(params)
    .filter(([key, value]) => key !== 'sign' && value !== null && value !== undefined && value !== '')
    .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
    .map(([key, value]) => `${key}=${value}`)
    .join('&')
}

const requestBody = {
  partner_id: '200001',
  request_id: '202609110001',
  sign_type: 'RSA',
  timestamp: Math.floor(Date.now() / 1000).toString(),
  nonce: randomUUID().replaceAll('-', '')
}

const privateKey = readFileSync('muse_secret.key', 'utf8')
const content = canonicalize(requestBody)
requestBody.sign = sign('RSA-SHA1', Buffer.from(content, 'utf8'), privateKey).toString('base64')
```

</details>

<details>

<summary>PHP</summary>

```php
<?php
$requestBody = [
    "partner_id" => "200001",
    "request_id" => "202609110001",
    "sign_type" => "RSA",
    "timestamp" => (string) time(),
    "nonce" => bin2hex(random_bytes(16))
];

$signingFields = array_filter(
    $requestBody,
    fn($value, $key) => $key !== "sign" && $value !== null && $value !== "",
    ARRAY_FILTER_USE_BOTH
);
ksort($signingFields, SORT_STRING);

$pairs = [];
foreach ($signingFields as $key => $value) {
    $pairs[] = $key . "=" . $value;
}
$content = implode("&", $pairs);

$privateKeyPem = file_get_contents("muse_secret.key");
if ($privateKeyPem === false) {
    throw new RuntimeException("Unable to read the private key");
}

if (!openssl_sign($content, $signature, $privateKeyPem, OPENSSL_ALGO_SHA1)) {
    throw new RuntimeException("Unable to sign the request");
}

$requestBody["sign"] = base64_encode($signature);
```

</details>

## 签名错误排查

如果 MusePay 拒绝签名，请确认：

* Partner Portal 中上传的公钥与本次签名使用的私钥相匹配；
* 规范字符串包含除 `sign` 外的所有非空请求字段；
* 字段名称和值与最终请求 Body 完全一致；
* 字段已按名称排序，且未进行 URL 编码；
* 内容使用 UTF-8 编码，并通过 `SHA1WithRSA` 签名；
* `timestamp` 是 10 位 Unix 秒级时间戳；
* 每个请求都生成了新的 `nonce`。
