Skip to content
控制台

Open API RSA 模式使用指南

🥑

本文档旨在详细指导用户如何使用 RSA 模式调用共绩算力 Open API,并提供基于 Python 的实现示例。

1. 前置条件

在开始之前,请确保您已准备好以下环境和工具:

  • Python 环境(可选):确保您的系统已安装 Python 3.6 或更高版本。
  • 必要的 Python 库(可选):您需要安装 cryptography 库用于 RSA 加签,以及 requests 库用于发送 HTTP 请求。您可以使用 pip 进行安装:pip install cryptography requests
  • OpenSSL 工具:通常用于在本地生成 RSA 密钥对。大多数 Linux/macOS 系统自带,Windows 用户可能需要单独安装。
  • 您的 RSA 密钥文件:您需要一个 RSA 密钥文件(通常是 .key 或 .pem 格式),该密钥是您提供给平台所对应的密钥。

2. 获取并设置 RSA 密钥

使用 RSA 模式的第一步是生成 RSA 密钥对并在平台设置您的公钥。

2.1 获取 RSA 密钥对

  1. 在本地生成 RSA 密钥对:
    • 打开终端或命令行工具(如 PowerShell, CMD, Bash)。
    • 使用 OpenSSL 生成私钥文件(例如 2048 位):openssl genrsa -out private.key 2048
    • 从私钥中提取公钥文件:openssl rsa -pubout -in private.key -out public.pem
    • 执行完成后,您将得到 private.key(私钥,务必妥善保管,切勿泄露)和 public.pem(公钥,用于提供给平台)两个文件。
  1. 在平台设置您的公钥:
    • 登录共绩算力平台。
    • 进入 API 密钥管理页面 https://console.suanli.cn/settings/key
    • 点击“新建密钥”,选择“RSA 加验签模式”。
    • 填写备注并提供您的 RSA 公钥。 使用文本编辑器打开您本地生成的 public.pem 文件,复制从 -----BEGIN PUBLIC KEY----------END PUBLIC KEY----- 的全部内容,粘贴到平台对应的输入框。
    • 系统将生成并展示与您公钥匹配的 RSA 私钥 以及其他相关信息。

重要: 请务必立即妥善保管生成的私钥和相关信息,因为它们只会显示一次。一旦丢失,您将无法找回,只能重新生成新的密钥对。平台仅保存您的公钥用于验证签名。

2.2 三种 RSA 密钥格式介绍

在平台设置密钥时,可能会涉及不同的密钥格式。理解这些格式有助于您正确处理密钥文件:

  • RsaPkcs8Pem

    • 结构:符合 PKCS#8 标准的 RSA 密钥
    • 格式:PEM 编码(含头部/尾部标签的文本文件,如 -----BEGIN PRIVATE KEY-----)
    • 用途:通用性强,适用于配置文件、文件存储
  • RsaPkcs1Pem

    • 结构:符合 PKCS#1 标准的 RSA 密钥(原始 RSA 参数)
    • 格式:PEM 编码(含 RSA 专属标签,如 -----BEGIN RSA PRIVATE KEY----- )
    • 用途:兼容需要 PKCS#1 格式的旧系统
  • RsaPkcs8Base64

    • 结构:PKCS#8 标准密钥
    • 格式:纯 Base64 字符串(无 PEM 标签)
    • 用途:适合代码嵌入或 API 直接传输

2.3 RSA 密钥格式选型建议

  • 首选 RsaPkcs8Pem:平台通常默认此格式,通用性最佳,便于文件保管和配置。
  • 特殊场景选其他
    • 需对接传统系统 → RsaPkcs1Pem
    • 需密钥值直接嵌入代码 → RsaPkcs8Base64

注:平台在您选择并生成密钥后,密钥内容只会展示一次,请务必及时、妥善保管。

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 脚本使用说明

  1. 保存脚本: 确保您已将完整的 Python 代码保存为 rsa_sign_util.py 文件。
  2. 准备密钥文件:将您的 RSA 私钥文件(例如 private.key)和可选的公钥文件(例如 public.pem,如果需要加密)放在与 rsa_sign_util.py 相同的目录下,或者更新脚本中 YOUR_PRIVATE_KEY_FILE 和 YOUR_PUBLIC_KEY_FILE 的完整路径。
  3. 配置变量: 打开 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)

  1. 运行脚本: 打开终端或命令行,切换到 rsa_sign_util.py 文件所在的目录,然后运行:python rsa_sign_util.py

3.3 响应结果说明

json
开始处理请求...
处理成功。
用于签名的 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_mapregions: 提供该资源在不同区域的可用性和详细信息。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 公共请求头

    列名

    类型

    是否必填

    说明

    实例值

    version

    String

    API 版本

    1.0.0

    token

    String

    认证 token

    sign_str

    String(Base64)

    签名字符串(参考加签流程)

    timestamp

    Integer

    时间戳(毫秒)

    1721299458423

    ### 4.2 请求体 参考具体接口要求。如果请求体需要加密,请参考 4.5 加密流程(先加密再计算加签字符串)。请求参考下面的示例。 ### 4.3 cURL 请求示例 ```bash

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>{&quot;task_name&quot;: &quot;test_task1&quot;}</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"}    `
        1. <b>计算签名</b>:
    使用 RSA-SHA256 签名算法对待签名字符串进行计算,得到签名结果(字节数组)。
    1. <b>Base64 编码</b>:
    将签名结果(字节数组)转换为 Base64 字符串。
    1. <b>设置请求头</b>:
    将生成的 Base64 签名字符串设置到请求头的 `sign_str` 字段。
        ### 4.6 加密流程
    1. 根据 RSA 公钥对待发送的请求体进行 rsa_pubk_encrypt 算法加密。
    2. 将加密后的字节数组转换为 Base64 字符串。
    3. 将该 Base64 字符串作为实际的请求体发送。
    4. 如果请求同时需要加密和加签 <b>必须先进行加密步骤,再进行加签步骤</b>(加签时使用的 data 即为加密后的 Base64 字符串)。