Skip to content
共绩算力文档中心

Open API RSA 模式使用指南

本文档指导开发者使用 RSA 签名模式调用共绩算力 Open API。RSA 模式在 Token 鉴权基础上增加请求签名,适用于生产环境及对安全性要求较高的集成场景。

阅读本文档前,建议先了解 [Open API 接入指南] 中的基础概念与 Token 模式。

客户端 共绩算力 Open API
│ │
│ 1. 本地持有 RSA 私钥(签名) │
│ 2. 平台保存 RSA 公钥(验签) │
│ │
│ 构建待签名字符串 → RSA-SHA256 签名 │
│ ─────────── HTTP Request ──────────────► │
│ Header: token, version, timestamp, │
│ sign_str │
│ Body: JSON 或加密后的 Base64 │
│ │
│ ◄────────── HTTP Response ─────────────── │
│ { code, msg, data } │

对比项

Token 模式

RSA 签名模式

鉴权方式

Header 携带 token

Header 携带 token + sign_str

私钥

不需要

客户端本地持有,用于签名

公钥

不需要

上传至平台,用于验签

请求体加密

不支持

部分接口支持(见 Apifox 文档)

适用环境

开发/测试

生产环境

项目

说明

API 基址

https://openapi.suanli.cn

签名算法

RSA-SHA256,PKCS#1 v1.5 填充

签名编码

Base64

时间戳

毫秒级 Unix 时间戳(13 位)

接口版本(version)

1.0.0(以各接口文档为准)

密钥管理

https://console.suanli.cn/settings/key


以下流程可在约 15 分钟内完成 RSA 模式首次调用。

Terminal window
openssl genrsa -out private.key 2048
openssl rsa -pubout -in private.key -out public.pem
  1. 登录控制台 → API 密钥管理
  2. 新建密钥 → 选择「RSA 加验签模式」
  3. 粘贴 public.pem 完整内容 → 生成并保存 API Token 和平台返回的 RSA 公钥
  1. 第 4 节 规则构建待签名字符串
  2. 使用私钥 RSA-SHA256 签名并 Base64 编码
  3. 在 Header 中携带 tokenversiontimestampsign_str

确认 HTTP 200 且响应体 code0000。若失败,参见 第 7 节调试与排错


推荐使用 OpenSSL 生成 2048 位 RSA 密钥对:

Terminal window
openssl genrsa -out private.key 2048
openssl rsa -pubout -in private.key -out public.pem
openssl genrsa -aes256 -out private.key 2048

文件

用途

保管要求

private.key

客户端签名

严格保密,禁止上传至平台或代码仓库

public.pem

平台验签

上传至控制台,可公开

  1. 访问 https://console.suanli.cn/settings/key
  2. 点击「新建密钥」→ 选择「RSA 加验签模式」
  3. 填写备注,粘贴公钥(从 -----BEGIN PUBLIC KEY----------END PUBLIC KEY----- 的完整内容)
  4. 点击生成,立即复制并保存平台返回的 API Token 和 RSA 公钥

⚠️ API Token 仅在创建时展示一次。平台仅保存公钥用于验签,私钥由您自行保管。

格式名称

结构标准

编码形式

推荐场景

RsaPkcs8Pem(推荐)

PKCS#8

PEM(含 BEGIN/END 标签)

通用场景,文件存储

RsaPkcs1Pem

PKCS#1

PEM(RSA 专属标签)

对接传统 PKCS#1 系统

RsaPkcs8Base64

PKCS#8

纯 Base64 字符串

代码内嵌、API 传输

默认推荐使用 RsaPkcs8Pem 格式。


Header

类型

必填

说明

示例

token

string

平台分配的 API Token

your-api-token-here

version

string

接口版本号

1.0.0

timestamp

string

毫秒级 Unix 时间戳

1770194570564

sign_str

string

RSA-SHA256 签名(Base64)

(动态生成)

Content-Type

string

固定为 application/json

application/json

  1. 构建待签名字符串(5 个字段,以换行符 \n 连接)
  2. 使用 RSA 私钥 + SHA-256 + PKCS1v15 填充进行签名
  3. 对签名结果进行 Base64 编码
  4. 将结果写入 Header sign_str
{path}\n{version}\n{timestamp}\n{token}\n{body}

字段

说明

注意事项

path

接口路径

/ 开头;GET 请求含 Query 时需将完整 path?query 纳入签名

version

接口版本

与 Header version 一致,通常为 1.0.0

timestamp

时间戳

与 Header timestamp 一致,毫秒级

token

API Token

与 Header token 一致

body

请求体

GET 或无 Body 时为 empty string "";加密接口使用加密后的 Base64 字符串

GET 请求(无 Body):

path = /api/deployment/resource/search
version = 1.0.0
timestamp = 1770194570564
token = your-api-token-here
body = (空字符串)
待签名字符串:
/api/deployment/resource/search
1.0.0
1770194570564
your-api-token-here

POST 请求(含 Body):

path = /api/deployment/task/create
version = 1.0.0
timestamp = 1770194570564
token = your-api-token-here
body = {"task_name":"test-task","points":1}
待签名字符串:
/api/deployment/task/create
1.0.0
1770194570564
your-api-token-here
{"task_name":"test-task","points":1}

部分接口(如「创建任务」)支持请求体 RSA 加密。是否需要加密以接口文档中「是否需要加密」字段为准。

加密算法规范:

项目

说明

加密公钥

平台提供的 RSA 公钥(创建密钥时返回),非您上传的公钥

加密算法

RSA,PKCS#1 v1.5 填充

分段长度

2048 位密钥每段最多 244 字节(明文)

输出格式

各分段密文拼接后,整体 Base64 编码

JSON 序列化

紧凑格式,无多余空格:separators=(',', ':')

  1. 将请求体 dict 序列化为紧凑 JSON 字符串
  2. 按 244 字节分段,使用平台公钥逐段 RSA 加密
  3. 拼接所有密文块,进行 Base64 编码 → 得到 encrypted_body
  4. encrypted_body 作为 HTTP 请求体发送
  5. 签名时 body 字段填入 encrypted_body先加密,后签名

注意:加密公钥(平台公钥)与签名私钥(您的私钥)是两套不同用途的密钥,请勿混用。

┌──────────────────────────────────────────────────┐
│ 1. 准备请求体(JSON 或空) │
│ 2. 如需加密 → RSA 公钥加密 → Base64 │
│ 3. 构建待签名字符串 path\nversion\n...\nbody │
│ 4. RSA-SHA256 签名 → Base64 → sign_str │
│ 5. 设置 Header 并发送 HTTP 请求 │
└──────────────────────────────────────────────────┘

以「创建任务」接口为例,假设原始 JSON 为:

{"task_name":"test-task","points":1}

加密后 HTTP 请求体变为 Base64 字符串(示意):

xK7g3fP2mN8vQ1wR5tY9zA...(加密后的 Base64,非原始 JSON)

签名时 body 字段使用加密后的 Base64 字符串

path = /api/deployment/task/create
version = 1.0.0
timestamp = 1770194570564
token = your-api-token-here
body = xK7g3fP2mN8vQ1wR5tY9zA...(加密后的 Base64)
待签名字符串:
/api/deployment/task/create
1.0.0
1770194570564
your-api-token-here
xK7g3fP2mN8vQ1wR5tY9zA...(加密后的 Base64)

POST /api/deployment/task/detail HTTP/1.1
Host: openapi.suanli.cn
Content-Type: application/json
token: your-api-token-here
version: 1.0.0
timestamp: 1770194570564
sign_str: EowIBAAKCAQEA...(Base64 签名)
{"task_id": 1}
Terminal window
curl -X POST "https://openapi.suanli.cn/api/deployment/task/detail" \
-H "Content-Type: application/json" \
-H "token: your-api-token-here" \
-H "version: 1.0.0" \
-H "timestamp: 1770194570564" \
-H "sign_str: YOUR_BASE64_SIGNATURE" \
-d '{"task_id": 1}'
{
"code": 0000,
"msg": "success",
"data": { }
}

字段

类型

说明

code

number

0000 表示成功,非 0000 表示失败

msg

string

结果描述

data

object

业务数据,结构因接口而异

注意:部分历史文档中 code 写作字符串 "0000",请以实际接口返回及 Apifox 文档为准。

加密请求的 HTTP Body 为 Base64 字符串(非 JSON 对象),签名同样基于加密后的 Body:

POST /api/deployment/task/create HTTP/1.1
Host: openapi.suanli.cn
Content-Type: application/json
token: your-api-token-here
version: 1.0.0
timestamp: 1770194570564
sign_str: EowIBAAKCAQEA...(Base64 签名)
xK7g3fP2mN8vQ1wR5tY9zA...(加密后的 Base64 请求体)
Terminal window
curl -X POST "https://openapi.suanli.cn/api/deployment/task/create" \
-H "Content-Type: application/json" \
-H "token: your-api-token-here" \
-H "version: 1.0.0" \
-H "timestamp: 1770194570564" \
-H "sign_str: YOUR_BASE64_SIGNATURE" \
-d "xK7g3fP2mN8vQ1wR5tY9zA..."

Python:

Terminal window
pip install cryptography requests

Java: JDK 11+(内置 java.net.httpjava.security

Go: Go 1.18+(标准库 crypto/rsa

import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
def gen_sign(private_key, path, version, timestamp, token, body=""):
"""生成 RSA-SHA256 签名"""
sign_content = f"{path}\n{version}\n{timestamp}\n{token}\n{body}"
signature = private_key.sign(
sign_content.encode("utf-8"),
padding.PKCS1v15(),
hashes.SHA256(),
)
return base64.b64encode(signature).decode("utf-8")
import base64
import json
from cryptography.hazmat.primitives.asymmetric import padding
def encrypt_body(platform_public_key, json_data: dict) -> str:
"""使用平台公钥加密请求体,返回 Base64 字符串"""
json_str = json.dumps(json_data, separators=(",", ":"), ensure_ascii=False)
data_bytes = json_str.encode("utf-8")
encrypted_chunks = []
chunk_size = 244 # RSA-2048 + PKCS1v15 单段最大明文长度
for start in range(0, len(data_bytes), chunk_size):
chunk = data_bytes[start : start + chunk_size]
encrypted_chunks.append(
platform_public_key.encrypt(chunk, padding.PKCS1v15())
)
return base64.b64encode(b"".join(encrypted_chunks)).decode("utf-8")

6.4 GET 请求示例(Python,无加密)

Section titled “6.4 GET 请求示例(Python,无加密)”
import time
import requests
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
BASE_URL = "https://openapi.suanli.cn"
API_TOKEN = "your-api-token-here"
PRIVATE_KEY_PATH = "./private.key"
API_VERSION = "1.0.0"
with open(PRIVATE_KEY_PATH, "rb") as f:
private_key = serialization.load_pem_private_key(
f.read(), password=None, backend=default_backend()
)
path = "/api/deployment/resource/search"
timestamp = str(int(time.time() * 1000))
body = ""
sign_str = gen_sign(private_key, path, API_VERSION, timestamp, API_TOKEN, body)
headers = {
"Content-Type": "application/json",
"token": API_TOKEN,
"version": API_VERSION,
"timestamp": timestamp,
"sign_str": sign_str,
}
response = requests.get(BASE_URL + path, headers=headers)
print(response.json())

适用于 Apifox 标注「是否需要加密 = 是」的接口(如创建任务)。

import time
import requests
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
BASE_URL = "https://openapi.suanli.cn"
API_TOKEN = "your-api-token-here"
PRIVATE_KEY_PATH = "./private.key" # 您的私钥,用于签名
PLATFORM_PUBLIC_KEY_PATH = "./platform_pub.pem" # 平台公钥,用于加密
API_VERSION = "1.0.0"
with open(PRIVATE_KEY_PATH, "rb") as f:
private_key = serialization.load_pem_private_key(
f.read(), password=None, backend=default_backend()
)
with open(PLATFORM_PUBLIC_KEY_PATH, "rb") as f:
platform_public_key = serialization.load_pem_public_key(
f.read(), backend=default_backend()
)
path = "/api/storage/encrypt/check"
timestamp = str(int(time.time() * 1000))
json_data = {
"storage_name": "demo-oss",
"juicefs_region_id": 3,
"bucket": "demo-bucket",
"dir": "dataset",
"endpoint": "oss-cn-beijing.aliyuncs.com",
"access_key": "access-key",
"secret_key": "secret-key"
}
encrypted_body = encrypt_body(platform_public_key, json_data)
sign_str = gen_sign(
private_key, path, API_VERSION, timestamp, API_TOKEN, encrypted_body
)
headers = {
"Content-Type": "application/json",
"token": API_TOKEN,
"version": API_VERSION,
"timestamp": timestamp,
"sign_str": sign_str,
}
response = requests.post(
BASE_URL + path,
headers=headers,
data=encrypted_body, # 发送加密后的 Base64 字符串
)
print(response.json())
import javax.crypto.Cipher;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
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.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class EncryptedPostExample {
private static final String BASE_URL = "https://openapi.suanli.cn";
private static final String API_TOKEN = "your-api-token-here";
private static final String API_VERSION = "1.0.0";
private static final int CHUNK_SIZE = 244;
public static void main(String[] args) throws Exception {
PrivateKey privateKey = loadPrivateKey("./private.key");
PublicKey platformPublicKey = loadPublicKey("./platform_pub.pem");
String path = "/api/deployment/task/create";
String timestamp = String.valueOf(System.currentTimeMillis());
String json = "{\"task_name\":\"test-task\",\"points\":1}";
// 1. 加密
String encryptedBody = encryptBody(platformPublicKey, json);
// 2. 签名(基于加密后的 body)
String signStr = genSign(privateKey, path, API_VERSION, timestamp, API_TOKEN, encryptedBody);
// 3. 发送
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + path))
.header("Content-Type", "application/json")
.header("token", API_TOKEN)
.header("version", API_VERSION)
.header("timestamp", timestamp)
.header("sign_str", signStr)
.POST(HttpRequest.BodyPublishers.ofString(encryptedBody))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
static String encryptBody(PublicKey publicKey, String plainText) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] data = plainText.getBytes(StandardCharsets.UTF_8);
java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream();
for (int i = 0; i < data.length; i += CHUNK_SIZE) {
int end = Math.min(i + CHUNK_SIZE, data.length);
output.write(cipher.doFinal(data, i, end - i));
}
return Base64.getEncoder().encodeToString(output.toByteArray());
}
static String genSign(PrivateKey privateKey, String path, String version,
String timestamp, String token, String body) throws Exception {
String content = path + "\n" + version + "\n" + timestamp + "\n" + token + "\n" + body;
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(privateKey);
sig.update(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(sig.sign());
}
static PrivateKey loadPrivateKey(String path) throws Exception {
String pem = Files.readString(Path.of(path))
.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 PublicKey loadPublicKey(String path) throws Exception {
String pem = Files.readString(Path.of(path))
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] keyBytes = Base64.getDecoder().decode(pem);
return KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(keyBytes));
}
}

Java 加密注意:分段加密后拼接所有密文字节块,再整体 Base64 编码,与 Python 示例逻辑一致。

package main
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
const (
baseURL = "https://openapi.suanli.cn"
apiToken = "your-api-token-here"
apiVersion = "1.0.0"
chunkSize = 244
)
func main() {
privateKey := loadPrivateKey("./private.key")
platformPublicKey := loadPublicKey("./platform_pub.pem")
path := "/api/deployment/task/create"
timestamp := fmt.Sprintf("%d", time.Now().UnixMilli())
jsonData := `{"task_name":"test-task","points":1}`
// 1. 加密
encryptedBody := encryptBody(platformPublicKey, jsonData)
// 2. 签名
signStr := genSign(privateKey, path, apiVersion, timestamp, apiToken, encryptedBody)
// 3. 发送
req, _ := http.NewRequest(http.MethodPost, baseURL+path, strings.NewReader(encryptedBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("token", apiToken)
req.Header.Set("version", apiVersion)
req.Header.Set("timestamp", timestamp)
req.Header.Set("sign_str", signStr)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
func encryptBody(pub *rsa.PublicKey, plain string) string {
data := []byte(plain)
var encrypted []byte
for i := 0; i < len(data); i += chunkSize {
end := i + chunkSize
if end > len(data) {
end = len(data)
}
chunk, _ := rsa.EncryptPKCS1v15(rand.Reader, pub, data[i:end])
encrypted = append(encrypted, chunk...)
}
return base64.StdEncoding.EncodeToString(encrypted)
}
func genSign(priv *rsa.PrivateKey, path, version, timestamp, token, body string) string {
content := path + "\n" + version + "\n" + timestamp + "\n" + token + "\n" + body
sig, _ := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, []byte(content))
return base64.StdEncoding.EncodeToString(sig)
}
func loadPrivateKey(path string) *rsa.PrivateKey {
data, _ := os.ReadFile(path)
block, _ := pem.Decode(data)
key, _ := x509.ParsePKCS8PrivateKey(block.Bytes)
return key.(*rsa.PrivateKey)
}
func loadPublicKey(path string) *rsa.PublicKey {
data, _ := os.ReadFile(path)
block, _ := pem.Decode(data)
pub, _ := x509.ParsePKIXPublicKey(block.Bytes)
return pub.(*rsa.PublicKey)
}

Go 签名注意genSign 需 import "crypto" 包;平台签名算法为 RSA-SHA256,对原始待签名字符串进行 SHA-256 哈希后签名。

检查项

正确做法

常见错误

timestamp

毫秒级 13 位数字

使用秒级 10 位时间戳

version

"1.0.0" 字符串

使用整数 1 或平台版本 v1.2.0

path

/ 开头,GET 含 query 时一并签名

遗漏 query 参数

body

无 Body 时用 "",非 null/{}

GET 请求错误填入 {}

签名顺序

path → version → timestamp → token → body

字段顺序错误或分隔符非 \n

加密顺序

先加密 body,再用加密结果签名

先签名后加密

服务器时间

与 NTP 同步,偏差 < 5 分钟

本地时间严重偏移

签名验证失败

打印待签名字符串逐字段比对;确认 body 与最终发送内容一致;确认私钥与平台公钥为同一密钥对。

私钥加载失败

检查 PEM 格式与文件路径;如有密码保护需传入 password 参数。

时间戳过期

同步系统时间;每次请求重新生成 timestamp,勿复用旧值。

加密后签名仍失败

确认 sign 使用的是加密后的 Base64 字符串,而非原始 JSON。

sign_content = f"{uri}\n{version}\n{timestamp}\n{token}\n{body}"
print("--- BEGIN STRING TO SIGN ---")
print(repr(sign_content))
print("--- END STRING TO SIGN ---")
print(f"sign_str: {sign_str}")

实践

说明

私钥隔离

私钥仅存于安全存储(KMS/密钥管理服务),禁止写入代码仓库

环境分离

开发、测试、生产使用独立密钥对

最小暴露

文档与示例一律使用占位符 Token,禁止泄露真实密钥

定期轮换

建议每 90 天轮换密钥对,旧密钥及时在控制台吊销

传输安全

仅通过 HTTPS 调用 API

权限控制

团队场景下仅主账号/管理员可管理密钥


需要。RSA 模式在 token 鉴权基础上增加 sign_str 签名,二者缺一不可。

Q2:GET 请求的 Query 参数是否参与签名?

Section titled “Q2:GET 请求的 Query 参数是否参与签名?”

是。GET 请求应将 path?key=value 完整路径作为 path 字段参与签名,顺序与编码需与实际请求 URL 一致。

body 字段使用空字符串 "",不要使用 null{} 或空格。

使用平台提供的公钥加密请求体;您上传的公钥仅用于平台验证您的签名。

Q5:能否在 Token 模式和 RSA 模式之间切换?

Section titled “Q5:能否在 Token 模式和 RSA 模式之间切换?”

不能混用。创建密钥时选定的模式决定了调用方式,需使用对应密钥重新集成。

无法恢复。需重新生成密钥对、在控制台创建新密钥,并更新所有集成方配置。


文档

说明

链接

Open API 接入指南

概述、Token 模式、快速开始

Open API 接入指南

Open API 接口文档

接口参数、是否加密/加签

https://s.apifox.cn/6aa360d3-d8f2-471e-b841-3a35c33a7b7c

API 密钥管理

控制台密钥创建与管理

https://console.suanli.cn/settings/key

团队账号常见问题

密钥管理权限说明

https://suanli.cn/docs/platform/team-account/bpcxwi8vviqxylk677tcpfwqnkf/