Open API RSA 模式使用指南
本文档旨在详细指导用户如何使用 RSA 模式调用共绩算力 Open API,并提供基于 Python 的实现示例。
1. 前置条件
在开始之前,请确保您已准备好以下环境和工具:
- Python 环境(可选):确保您的系统已安装 Python 3.6 或更高版本。
- 必要的 Python 库(可选):您需要安装 cryptography 库用于 RSA 加签,以及 requests 库用于发送 HTTP 请求。您可以使用 pip 进行安装:bash
pip install cryptography requests
- <b>OpenSSL 工具</b>:通常用于在本地生成 RSA 密钥对。大多数 Linux/macOS 系统自带,Windows 用户可能需要单独安装。
- <b>您的 RSA 密钥文件</b>:您需要一个 RSA 密钥文件(通常是 .key 或 .pem 格式),该密钥是您提供给平台所对应的密钥。
## 2. 获取并设置 RSA 密钥
使用 RSA 模式的第一步是生成 RSA 密钥对并在平台设置您的公钥。
### 2.1 获取 RSA 密钥对
1. <b>在本地生成 RSA 密钥对:</b>
- 打开终端或命令行工具(如 PowerShell, CMD, Bash)。
- 使用 OpenSSL 生成私钥文件(例如 2048 位):
```bash
openssl genrsa -out private.key 2048
- 从私钥中提取公钥文件:
```bash
openssl rsa -pubout -in private.key -out public.pem
- 执行完成后,您将得到 `private.key`(私钥,务必妥善保管,切勿泄露)和 `public.pem`(公钥,用于提供给平台)两个文件。
<img src="/assets/RYFebqFNooHLgjxpeG6cPmSYnix.png" src-width="217" src-height="55" align="center"/>
1. <b>在平台设置您的公钥:</b>
- 登录共绩算力平台。
- 进入 API 密钥管理页面 `https://console.suanli.cn/settings/key`。
- 点击“新建密钥”,选择“RSA 加验签模式”。
- <b>填写备注并提供您的 RSA 公钥。</b> 使用文本编辑器打开您本地生成的 `public.pem` 文件,复制从 `-----BEGIN PUBLIC KEY-----` 到 `-----END PUBLIC KEY-----` 的全部内容,粘贴到平台对应的输入框。
- 系统将生成并展示与您公钥匹配的 RSA 私钥 以及其他相关信息。
<b>重要:</b> 请务必立即妥善保管生成的私钥和相关信息,因为它们只会显示一次。一旦丢失,您将无法找回,只能重新生成新的密钥对。平台仅保存您的公钥用于验证签名。
<img src="/assets/FT8jbaVCIo7MuyxOThOc0dktn9d.png" src-width="1852" src-height="966" align="center"/>
### 2.2 三种 RSA 密钥格式介绍
在平台设置密钥时,可能会涉及不同的密钥格式。理解这些格式有助于您正确处理密钥文件:
- <b>RsaPkcs8Pem</b>
- 结构:符合 PKCS#8 标准的 RSA 密钥
- 格式:PEM 编码(含头部/尾部标签的文本文件,如 -----BEGIN PRIVATE KEY-----)
- 用途:通用性强,适用于配置文件、文件存储
- <b>RsaPkcs1Pem</b>
- 结构:符合 PKCS#1 标准的 RSA 密钥(原始 RSA 参数)
- 格式:PEM 编码(含 RSA 专属标签,如 -----BEGIN RSA PRIVATE KEY----- )
- 用途:兼容需要 PKCS#1 格式的旧系统
- <b>RsaPkcs8Base64</b>
- 结构:PKCS#8 标准密钥
- 格式:纯 Base64 字符串(无 PEM 标签)
- 用途:适合代码嵌入或 API 直接传输
### 2.3 RSA 密钥格式选型建议
- <b>首选 RsaPkcs8Pem</b>:平台通常默认此格式,通用性最佳,便于文件保管和配置。
- <b>特殊场景选其他</b>:
- 需对接传统系统 → RsaPkcs1Pem
- 需密钥值直接嵌入代码 → RsaPkcs8Base64
<b>注: 平台在您选择并生成密钥后,密钥内容只会展示一次,请务必及时、妥善保管。</b>
## 3. 使用 Python 脚本进行加签和调用
我们已经为您准备了一个 Python 脚本 `rsa_sign_util.py` 来帮助您实现加签和 API 调用过程。
### 3.1 脚本代码 (`rsa_sign_util.py`)
```py
import base64
import time
import json
import http.client
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from typing import Optional, Union, Tuple
# --- 加密函数 ---
def encrypt_data_with_public_key(public_key_path: str, data: bytes) -> Optional[str]:
"""
使用 RSA 公钥加密数据并进行 Base64 编码。
Args:
public_key_path: RSA 公钥文件路径。
data: 待加密的原始数据 (字节类型)。
Returns:
加密后并 Base64 编码的字符串,或 None 如果加密失败。
"""
try:
with open(public_key_path, "rb") as key_file:
# 尝试加载 PEM 格式的公钥
public_key = serialization.load_pem_public_key(
key_file.read(),
backend=default_backend()
)
except FileNotFoundError:
print(f"错误:公钥文件未找到:{public_key_path}")
return None
except Exception as e:
print(f"错误:加载公钥失败:{e}")
return None
try:
# 使用 RSA 公钥和 PKCS1v15 填充进行加密
ciphertext = public_key.encrypt(
data,
padding.PKCS1v15()
)
# 将加密结果进行 Base64 编码
encrypted_base64 = base64.b64encode(ciphertext).decode('utf-8')
return encrypted_base64
except Exception as e:
print(f"错误:数据加密失败:{e}")
return None
def sign_request(private_key_path: str, api_path: str, api_version: str, api_token: str, request_data: dict, method: str, public_key_path_for_encryption: Optional[str] = None) -> Tuple[Optional[str], Optional[int], Optional[str], Optional[str]]:
"""
生成待加签字符串并计算 RSA-SHA256 签名,可选对请求体进行 RSA 公钥加密。
Args:
private_key_path: RSA 私钥文件路径。
api_path: API 接口地址。
api_version: API 版本。
api_token: API 认证 Token。
request_data: 接口请求体 (Python 字典)。
method: HTTP 方法 (e.g., "GET", "POST").
public_key_path_for_encryption: 可选的 RSA 公钥文件路径,如果提供,则对 request_data 进行加密。
Returns:
包含签名字符串、时间戳、用于签名的 data_str 和实际发送的 payload 的元组。
如果签名或加密失败,返回 (None, None, None, None)。
"""
try:
# 读取私钥文件
with open(private_key_path, "rb") as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=None, # 如果私钥有密码,请在此提供
backend=default_backend()
)
except FileNotFoundError:
print(f"错误:私钥文件未找到:{private_key_path}")
return None, None, None, None
except Exception as e:
print(f"错误:加载私钥失败:{e}")
return None, None, None, None
timestamp_ms = int(time.time() * 1000)
data_to_sign = ""
request_payload = "" # 实际发送的请求体
if public_key_path_for_encryption:
# 如果提供了公钥路径,先加密请求数据
request_data_bytes = json.dumps(request_data, separators=(',', ':')).encode('utf-8')
encrypted_data_base64 = encrypt_data_with_public_key(public_key_path_for_encryption, request_data_bytes)
if encrypted_data_base64 is None:
# 加密失败
return None, None, None, None
# 加密后的 Base64 字符串用于签名
data_to_sign = encrypted_data_base64
# 实际发送的请求体就是这个加密后的 Base64 字符串
request_payload = encrypted_data_base64
else:
# 如果没有提供公钥路径,根据方法处理 request_data
if method.upper() == "GET" and not request_data:
data_to_sign = ""
request_payload = "" # GET 请求体为空
else:
# 使用 separators 参数生成紧凑 json,避免空格影响签名
data_str = json.dumps(request_data, separators=(',', ':'))
data_to_sign = data_str
# 实际发送的请求体也是这个 JSON 字符串
request_payload = data_str
# 生成待加签字符串:path\nversion\ntimestamp\ntoken\ndata
string_to_sign = f"{api_path}\n{api_version}\n{timestamp_ms}\n{api_token}\n{data_to_sign}"
# 使用 RSA 私钥和 SHA-256 算法进行签名
try:
# 待签名字符串需编码为字节类型
signature = private_key.sign(
string_to_sign.encode('utf-8'),
padding.PKCS1v15(), # 填充方式
hashes.SHA256() # 哈希算法
)
except Exception as e:
print(f"错误:签名计算失败:{e}")
return None, None, None, None
# 将签名结果进行 Base64 编码
sign_str_base64 = base64.b64encode(signature).decode('utf-8')
return sign_str_base64, timestamp_ms, data_to_sign, request_payload
# --- 示例用法 ---
# 请修改以下变量值:
YOUR_PRIVATE_KEY_FILE = "private.key" # 私钥文件路径
YOUR_PUBLIC_KEY_FILE = "public.pem" # 公钥文件路径 (用于加密,如果需要)
API_GATEWAY_HOST = "https://openapi.suanli.cn" # API 网关域名
TARGET_API_PATH = "/api/deployment/resource/search" # 目标 API 路径
API_VERSION = "1.0.0" # API 版本
YOUR_API_TOKEN = "084b3e01-5faa-4c4c-bac6-433dbdd83150-20250609144703" # API Token
API_REQUEST_DATA = {} # 接口请求体数据
HTTP_METHOD = "GET" # HTTP 方法
# 是否需要加密请求体 (True/False)
NEED_ENCRYPTION = True # 根据您的需求设置
# --- 执行签名和发送请求 ---
print("开始处理请求...")
public_key_path_for_encryption = YOUR_PUBLIC_KEY_FILE if NEED_ENCRYPTION else None
# 调用签名函数 (现在可以传入公钥路径进行加密)
sign_string, timestamp, data_for_signing_str, actual_request_payload = sign_request(
YOUR_PRIVATE_KEY_FILE,
TARGET_API_PATH,
API_VERSION,
YOUR_API_TOKEN,
API_REQUEST_DATA,
HTTP_METHOD,
public_key_path_for_encryption # 传入公钥路径
)
if sign_string and timestamp is not None and data_for_signing_str is not None and actual_request_payload is not None:
print("处理成功。")
print(f"用于签名的 data_str: '{data_for_signing_str}'")
print(f"计算得到的 Base64 签名字符串 (sign_str): {sign_string}")
print(f"使用的时间戳 (timestamp): {timestamp}")
print(f"构建的待加签原始字符串(供参考,请确保与平台逻辑一致):\n---BEGIN STRING TO SIGN---\n{TARGET_API_PATH}\n{API_VERSION}\n{timestamp}\n{YOUR_API_TOKEN}\n{data_for_signing_str}\n---END STRING TO SIGN---")
# 构建完整的请求 URL
request_url = API_GATEWAY_HOST + TARGET_API_PATH
# 构建请求头
headers = {
"version": API_VERSION,
"timestamp": str(timestamp), # timestamp 在 Header 中需要是字符串类型
}
# 添加 token 到 Header
if YOUR_API_TOKEN:
headers["token"] = YOUR_API_TOKEN
# 如果有 token (非简易模式),则添加 sign_str
if sign_string is not None: # 确保 sign_string 成功生成
headers["sign_str"] = sign_string
print(f"\n发送 API 请求至: {request_url}")
print(f"请求方法: {HTTP_METHOD}")
print(f"请求头: {json.dumps(headers, indent=2)}")
# 发送 HTTP 请求
try:
# 从 API_GATEWAY_HOST 中提取 host
host = API_GATEWAY_HOST.replace("https://", "")
conn = http.client.HTTPSConnection(host)
# http.client的request方法参数: method, url, body, headers
# GET请求 body为None或空字符串
# 实际发送的请求体根据 actual_request_payload 确定
payload_bytes = actual_request_payload.encode('utf-8') if actual_request_payload else b''
conn.request(HTTP_METHOD, TARGET_API_PATH, payload_bytes, headers)
res = conn.getresponse()
data = res.read()
print("\nAPI 响应:")
# 响应体是 bytes,需解码
decoded_data = data.decode("utf-8")
# 尝试解析并打印 JSON 响应
try:
print(json.dumps(json.loads(decoded_data), indent=2, ensure_ascii=False))
except json.JSONDecodeError:
print("响应体不是 JSON 格式:")
print(decoded_data)
conn.close()
except Exception as e:
print(f"\n发送请求时发生未知错误: {e}")
else:
print("\n签名生成失败或构建待加签字符串出错,或者加密失败,无法发送 API 请求。请检查错误信息。")
3.2 脚本使用说明
- 保存脚本: 确保您已将完整的 Python 代码保存为 rsa_sign_util.py 文件。
- 准备密钥文件:将您的 RSA 私钥文件(例如 private.key)和可选的公钥文件(例如 public.pem,如果需要加密)放在与 rsa_sign_util.py 相同的目录下,或者更新脚本中 YOUR_PRIVATE_KEY_FILE 和 YOUR_PUBLIC_KEY_FILE 的完整路径。
- 配置变量: 打开 rsa_sign_util.py 文件,找到 # --- 示例用法 --- 部分,根据您要调用的具体 API 修改以下变量的值:

YOUR_PRIVATE_KEY_FILE: 您的 RSA 私钥文件路径。 YOUR_PUBLIC_KEY_FILE: 可选:RSA 公钥文件路径 (用于加密请求体,如果需要)。 API_GATEWAY_HOST: API 网关域名。 TARGET_API_PATH: 您要调用的具体 API 接口路径。 API_VERSION: API 版本。 YOUR_API_TOKEN: 您的 API Token (如果不需要,设置为空字符串 "")。 API_REQUEST_DATA: 您要发送的接口请求体数据 (Python 字典格式)。 HTTP_METHOD: 您要使用的 HTTP 方法 ("GET" 或 "POST")。 NEED_ENCRYPTION: 是否需要加密请求体 (True/False)
- 运行脚本: 打开终端或命令行,切换到 rsa_sign_util.py 文件所在的目录,然后运行:python rsa_sign_util.py
3.3 响应结果说明
开始处理请求...
处理成功。
用于签名的 data_str: '{}'
计算得到的 Base64 签名字符串 (sign_str): ...
使用的时间戳 (timestamp): ...
构建的待加签原始字符串(供参考,请确保与平台逻辑一致):
---BEGIN STRING TO SIGN---
/api/deployment/resource/search
1.0.0
...
//**实际 token 字符串**
{}
---END STRING TO SIGN---
发送 API 请求至:https://openapi.suanli.cn/api/deployment/resource/search
请求方法:GET
请求头:{
"version": "1.0.0",
"timestamp": "...",
"token": "实际 token 字符串",
"sign_str": "..."
}
API 响应:
{
"code": 0,
"msg": "success",
"data": {
"device_categories": [
{
"device_name": "H20",
"region_map": {
"xingjiangf-1": {
"region": "xingjiangf-1",
"region_name": "新疆一区",
"mark": {
"resource": {
"device_name": "H20",
"region": "xingjiangf-1",
"gpu_name": "H20",
"gpu_count": 1,
"gpu_memory": 98304,
"memory": 196608,
"cpu_cores": 24
},
"region_name": null,
"mark": "ha8GDGEAORN3a9Hhu7X+W4Vtce1MVTeWkoGuBrgRNV0VuvbQPOAijLHAl\nL3THz4zxz17CQ2xan6Q2UrnIQ\n6pUPcLqpoLUCh5MaAR9kIet8llTG2oulHTzTR6DoHqgQoSFvtGpGD4Rh7S1F32ACv6i9o7EFy0RPUnKv\nTMUFPSo1heC27vZCm+ab5SZnpZiQ=="
},
"price": 2052,
"inventory": 0
},
"hebeif-1": {
"region": "hebeif-1",
"region_name": "河北一区",
"mark": {
"resource": {
"device_name": "H20",
"region": "hebeif-1",
"gpu_name": "H20",
"gpu_count": 1,
"gpu_memory": 98304,
"memory": 196608,
"cpu_cores": 24
},
"region_name": null,
"mark": "ha8GDGEAORN3a9Hhu7X+W4Vtce1MVTeWkoGuBrgRNV0ZtvTTM6x9yfvPw\nu+KYjci3wc3UkKcenSQvVqwc1\nfUVOgLm41GH3chMf1Qrz8UotFVU3Hn7xqS3D53GpC30EtfCecJ8SO4UhLT1UfiU2Oj9tokF2akCb1wba\ns+QFIKvwEvJgaT2WcPA1YY"
},
"price": 2052,
"inventory": 0
}
],
"regions": [
{
"region": "xingjiangf-1",
"region_name": "新疆一区",
"mark": {
"resource": {
"device_name": "H20",
"region": "xingjiangf-1",
"gpu_name": "H20",
"gpu_count": 1,
"gpu_memory": 98304,
"memory": 196608,
"cpu_cores": 24
},
"region_name": null,
"mark": "ha8GDGEAORN3a9Hhu7X+W4Vtce1MVTeWkoGuBrgRNV0VuvbQPOAijLHAl\nL3THz4zxz17CQ2xan6Q2UrnIQ\n6pUPcLqpoLUCh5MaAR9kIet8llTG2oulHTzTR6DoHqgQoSFvtGpGD4Rh7S1F32ACv6i9o7EFy0RPUnKv\nTMUFPSo1heC27vZCm+ab5SZnpZiQ=="
},
"price": 2052,
"inventory": 0
},
这个 JSON 数据是运行 rsa_sign_util.py
脚本后收到的 API 响应。
code
: 状态码。0
表示 API 请求成功。message
: 消息字符串。"success"
进一步确认了请求成功。result
: 包含实际数据的结果对象。total
: 表示找到的资源总数。resources
: 这是一个列表,包含了每种资源类型的详细信息。列表中的每个对象代表一种特定的设备配置(例如 "H20 x 8", "4090 x 8" 等)。device_name
: 设备的名称或配置。region_map
和regions
: 提供该资源在不同区域的可用性和详细信息。regions
是一个列表,每个元素代表一个区域的详情。region
: 区域代码。region_name
: 区域的中文名称(例如 "浙江一区", "福建一区")。mark
: 包含资源规格和标记的内部对象。price
: 该资源在该区域的价格。inventory
: 该资源在该区域的库存数量。gpu_name
: 使用的 GPU 型号(例如 "H20", "4090")。gpu_memory
: GPU 显存大小。gpu_count
: GPU 数量。memory
: 系统内存大小。cpu_cores
: CPU 核心数。4. API 接口说明
4.1 公共请求头
### 4.2 请求体 参考具体接口要求。如果请求体需要加密,请参考 4.5加密流程(先加密再计算加签字符串)。请求参考下面的示例。 ### 4.3 cURL 请求示例 ```bash列名
类型
是否必填
说明
实例值
version
String
是
API 版本
1.0.0
token
String
否
认证 token
sign_str
String(Base64)
否
签名字符串(参考加签流程)
timestamp
Integer
是
时间戳(毫秒)
1721299458423
curl --location --request POST 'https://{gateway-host}/{api-path}' \ --header 'token: a0e13fe1-5626-4c05-926b-20f586c69102-20240821144204' \ --header 'timestamp: 1724222524375' \ --header 'version: 1.0.0' \ --header 'sign_str: EowIBAAKCAQEArbNcSNSLjHzqOzrYL+7afEh5TI4hn1BCxsuzY02c1RMn24a2YEvpqCCVDxmgN/dcAdcCcvhO/2wDG389LuEkw+QhVPzdAE29bbzz+Gb/FDusVNo6tl8mbfd/XA53h3sOCekEMP2QCoPAoUO94wUWK5RpjsONf9Bs0Q6YUmL4TWqvPGmWjc/Y8winDtFzKN2his5PhbWlRiSkENGXSma6lr66BA/SduAY/Fl8YxEWThVkYsAurg0rEd83DilN4zp7hZf82Msjgp8kPm/SMHTEF2V2cOo82m12HyRJKuKS0L8WPVIwQmXJ6VN55ue+b96sryUs/WZyfiXTh0thoa9vqQIDAQABAoIBAQCsOMDQSUTPl27aKR7+b5FbVrRF7kpx3i9HUeLcG7DbJrIHHAspcTsLgrqoDR1pQC2OeXMpMP+KirqOAdtU5tAAFenijRBGY83kx0sSSHSyx/O28eTyu2ar84/oY0OqJZ0mwE1ykYXGlxlgC31zYLC5pt3+Oe/LAYlSwmjOjuhoQDMZoiCByIw6oDAVLIlZwTl8A/HAR1yacDTh6lps1vTZ5lBfIDpbn1gHb2GWqHu8q9oD9G9IEyWwwTE1IsNXVwBKEifjLubd2WfTWDmROVZZ9T4AXm/40/Eb6ALApwY5s7lBMIiapmcJZzlyELEukUMmYN7vHBjdMDOPK3tjLSKNA' \ --data-raw '{"task_id": 1}'
### 4.4 公共响应体
<table>
<colgroup>
<col/>
<col/>
<col/>
<col/>
<col/>
</colgroup>
<tbody>
<tr><td><p>列名</p></td><td><p>类型</p></td><td><p>不是 null</p></td><td><p>说明</p></td><td><p>实例值</p></td></tr>
<tr><td><p>code</p></td><td><p>String</p></td><td><p>是</p></td><td><p>响应 code</p></td><td><p>0000</p></td></tr>
<tr><td><p>message</p></td><td><p>String</p></td><td><p>否</p></td><td><p>响应说明</p></td><td><p>success</p></td></tr>
<tr><td><p>data</p></td><td><p>Object</p></td><td><p>否</p></td><td><p>数据体</p></td><td><p>{"task_name": "test_task1"}</p></td></tr>
</tbody>
</table>
### 4.5 加签流程
1. <b>生成待加签字符串</b>:
将以下参数按照顺序用换行符 `\n` 连接起来:
> `path`: 接口地址。GET 请求需要包含完整的 URL(含 query 参数)。
> `version`: 请求头中的接口版本。
> `timestamp`: 请求头中的时间戳(毫秒)。
> `token`: 请求头中的 token。
> `data`: 接口请求体。如果接口需要加密,则为加密后的 Base64 字符串。
格式示例:`path\nversion\ntimestamp\ntoken\ndata`
例如:
`/api/user/order/get_this_week_residue_withdrawal_count 1.0.0 1724222524375 a0e13fe1-5626-4c05-926b-20f586c69102-20240821144204 {"username":"test1","password":"password1"} `
<b>计算签名</b>:
使用 RSA-SHA256 签名算法对待签名字符串进行计算,得到签名结果(字节数组)。
<b>Base64 编码</b>:
将签名结果(字节数组)转换为 Base64 字符串。
<b>设置请求头</b>:
将生成的 Base64 签名字符串设置到请求头的 `sign_str` 字段。
### 4.6 加密流程
1. 根据 RSA 公钥对待发送的请求体进行 rsa_pubk_encrypt 算法加密。
2. 将加密后的字节数组转换为 Base64 字符串。
3. 将该 Base64 字符串作为实际的请求体发送。
4. 如果请求同时需要加密和加签 <b>必须先进行加密步骤,再进行加签步骤</b>(加签时使用的 data 即为加密后的 Base64 字符串)。