虾壳开放平台 API 接入文档
版本: v1.0 更新日期: 2026-06-25
目录
- 1. 概述
- 2. 接入准备
- 3. 认证流程
- 4. 签名机制
- 5. 接口列表
- 5.1 换取令牌
- 5.2 刷新令牌
- 5.3 获取用户信息
- 5.4 消费扣款
- 6. 错误码
- 7. 签名示例代码
- 8. 最佳实践
1. 概述
虾壳开放平台(OpenAPI)提供了一组标准化接口,供第三方应用后端与虾壳平台进行安全对接。通过 OpenAPI,第三方应用可以:
- 获取用户信息 — 在用户授权后获取其基本资料和钱包余额
- 消费扣款 — 以幂等方式扣除用户余额,用于应用内消费
所有 OpenAPI 接口均通过 HMAC-SHA256 签名机制保证请求的完整性和防重放,确保通信安全。
2. 接入准备
2.1 获取凭证
接入前,第三方应用需要在虾壳管理后台完成注册,获取以下凭证:
| 参数 | 说明 | 示例 |
|---|---|---|
appKey |
应用标识,全局唯一 | my-app-2024 |
appSecret |
应用密钥,用于签名计算 | abc123def456... |
⚠️ 安全提醒:
appSecret必须妥善保管,仅限服务端使用,禁止泄露到前端或客户端。
2.2 基础 URL
https://www.sharkai.club/api
3. 认证流程
3.1 完整时序
用户(虾壳客户端) 第三方应用后端 虾壳平台
│ │ │
│ POST /app/open/{appId} │ │
│───────────────────────────>│ │
│ 返回 session_key │ │
│<───────────────────────────│ │
│ │ │
│ 将 session_key 传递给 │ │
│ 第三方应用(URL参数) │ │
│───────────────────────────>│ │
│ │ POST /openapi/auth/token │
│ │ { sessionKey } │
│ │──────────────────────────>│
│ │ HMAC签名验证 ✓ │
│ │ 返回 accessToken │
│ │ + refreshToken │
│ │<──────────────────────────│
│ │ │
│ │ GET /openapi/user/info │
│ │ X-Access-Token: xxx │
│ │──────────────────────────>│
│ │ 返回用户信息+余额 │
│ │<──────────────────────────│
│ │ │
│ │ POST /openapi/consumption/ │
│ │ deduct { amount, ... } │
│ │──────────────────────────>│
│ │ 返回扣款结果 │
│ │<──────────────────────────│
3.2 流程说明
- 用户打开应用:用户在虾壳客户端点击打开第三方应用,虾壳生成一次性
session_key(5分钟有效),通过 URL 参数传递给第三方应用 - 换取令牌:第三方应用后端用
session_key调用换取令牌接口,获取accessToken(1小时)和refreshToken(2天) - 访问业务接口:使用
accessToken调用获取用户信息、消费扣款等接口 - 刷新令牌:
accessToken过期前,使用refreshToken轮换获取新令牌
3.3 获取 session_key
session_key 由虾壳用户端发起,第三方开发者需了解其产生过程以正确接收:
虾壳用户端调用(由虾壳前端自动完成,第三方无需实现):
POST /app/open/{appId}
Authorization: Bearer {用户JWT}
响应:
{
"code": 200,
"message": "success",
"data": {
"sessionKey": "550e8400-e29b-41d4-a716-446655440000",
"openUrl": "https://your-app.com/entry?session_key=550e8400-e29b-41d4-a716-446655440000"
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
sessionKey |
string | 一次性会话密钥(5分钟有效) |
openUrl |
string | 应用打开链接(将 session_key 拼接到应用配置的链接后) |
第三方接收方式:
虾壳前端会跳转到 openUrl,第三方应用的前端页面从 URL 中提取 session_key 参数,然后传递给后端,由后端调用 POST /openapi/auth/token 完成换令牌。
https://your-app.com/entry?session_key=550e8400-e29b-41d4-a716-446655440000
↑
第三方前端从此处提取 session_key
⚠️
session_key有效期仅 5 分钟,且一次性使用。第三方前端获取后应立即传给后端换令牌,避免超时失效。
3.4 令牌有效期
| 令牌 | 有效期 | 特性 |
|---|---|---|
session_key |
5 分钟 | 一次性使用,换取后立即失效 |
accessToken |
1 小时(3600秒) | 可多次使用,过期后需刷新 |
refreshToken |
2 天(172800秒) | 一次性使用,轮换后旧令牌立即失效 |
4. 签名机制
所有 /openapi/** 请求均需进行 HMAC-SHA256 签名验证。
4.1 必需 Header
| Header | 说明 | 示例 |
|---|---|---|
X-App-Key |
应用标识 | my-app-2024 |
X-Timestamp |
毫秒时间戳 | 1718083200000 |
X-Nonce |
随机字符串(防重放) | a1b2c3d4e5f6 |
X-Signature |
HMAC-SHA256 签名 | e8f9a0b1c2d3... |
4.2 签名算法
第一步:计算请求体 SHA256
bodySha256 = SHA256(requestBody)
- GET 请求无 body,使用空字符串的 SHA256:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 - POST 请求使用请求体 JSON 的 UTF-8 字节计算
第二步:构建规范化字符串
canonicalString = "{method}\n{path}\n{timestamp}\n{nonce}\n{bodySha256}"
各字段说明:
| 字段 | 取值 | 示例 |
|---|---|---|
method |
HTTP 方法(大写) | POST |
path |
请求路径(不含域名) | /openapi/auth/token |
timestamp |
与 Header 中一致的毫秒时间戳 | 1718083200000 |
nonce |
与 Header 中一致的随机串 | a1b2c3d4e5f6 |
bodySha256 |
第一步计算的 SHA256 | e3b0c44... |
第三步:计算签名
signature = HMAC-SHA256(canonicalString, appSecret)
返回十六进制小写字符串。
4.3 签名示例
假设:
- appSecret = test-app-secret-123456
- method = POST
- path = /openapi/auth/token
- timestamp = 1718083200000
- nonce = a1b2c3d4e5f6
- 请求体为 {"sessionKey":"550e8400-e29b-41d4-a716-446655440000"}
bodySha256 = SHA256('{"sessionKey":"550e8400-e29b-41d4-a716-446655440000"}')
canonicalString = "POST\n/openapi/auth/token\n1718083200000\na1b2c3d4e5f6\n{bodySha256}"
signature = HMAC-SHA256(canonicalString, "test-app-secret-123456")
4.4 空 Body 处理规则
GET 请求无请求体时,bodySha256 使用空字符串的 SHA256 常量值:
bodySha256 = SHA256("") = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
4.5 响应签名验证
所有 OpenAPI 接口的响应均包含 X-Response-Signature Header,第三方应用可验证响应体完整性。
签名规则:
X-Response-Signature = HMAC-SHA256(SHA256(responseBody), appSecret)
验证步骤:
- 取响应体原始 JSON 字符串,计算
bodySha256 = SHA256(responseBody) - 用本地保存的
appSecret计算expectedSignature = HMAC-SHA256(bodySha256, appSecret) - 对比响应 Header
X-Response-Signature与expectedSignature是否一致
注意:验证时需使用响应体的原始 JSON 字节(UTF-8),不要格式化或添加空白字符。
5. 接口列表
5.1 换取令牌
用一次性 session_key 换取 accessToken 和 refreshToken。
请求
POST /openapi/auth/token
Header
| 参数 | 必填 | 说明 |
|---|---|---|
X-App-Key |
是 | 应用标识 |
X-Timestamp |
是 | 毫秒时间戳 |
X-Nonce |
是 | 随机字符串 |
X-Signature |
是 | HMAC-SHA256 签名 |
Content-Type |
是 | application/json |
请求体
{
"sessionKey": "550e8400-e29b-41d4-a716-446655440000"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
sessionKey |
string | 是 | 临时会话密钥(用户打开应用时获取) |
响应
{
"code": 200,
"message": "success",
"data": {
"accessToken": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"expiresIn": 3600,
"refreshToken": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
"refreshExpiresIn": 172800
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
data.accessToken |
string | 访问令牌,用于后续业务接口 |
data.expiresIn |
int | accessToken 有效期(秒) |
data.refreshToken |
string | 刷新令牌(一次性,用于获取新 accessToken) |
data.refreshExpiresIn |
int | refreshToken 有效期(秒) |
5.2 刷新令牌
用 refreshToken 轮换获取新的双令牌。旧 refreshToken 一次性使用,轮换后立即失效。
请求
POST /openapi/auth/refresh
Header
| 参数 | 必填 | 说明 |
|---|---|---|
X-App-Key |
是 | 应用标识 |
X-Timestamp |
是 | 毫秒时间戳 |
X-Nonce |
是 | 随机字符串 |
X-Signature |
是 | HMAC-SHA256 签名 |
Content-Type |
是 | application/json |
请求体
{
"refreshToken": "f9e8d7c6-b5a4-3210-fedc-ba0987654321"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
refreshToken |
string | 是 | 刷新令牌 |
响应
{
"code": 200,
"message": "success",
"data": {
"accessToken": "new-access-token-uuid",
"expiresIn": 3600,
"refreshToken": "new-refresh-token-uuid",
"refreshExpiresIn": 172800
}
}
响应结构与 5.1 换取令牌 相同。
5.3 获取用户信息
用 accessToken 获取用户基本资料和钱包余额。
请求
GET /openapi/user/info
Header
| 参数 | 必填 | 说明 |
|---|---|---|
X-App-Key |
是 | 应用标识 |
X-Timestamp |
是 | 毫秒时间戳 |
X-Nonce |
是 | 随机字符串 |
X-Signature |
是 | HMAC-SHA256 签名 |
X-Access-Token |
是 | 访问令牌 |
请求体
无(GET 请求)
响应
{
"code": 200,
"message": "success",
"data": {
"userId": 100,
"nickname": "小明",
"avatar": "https://example.com/avatar.jpg",
"balance": 100.0000
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
data.userId |
long | 用户ID |
data.nickname |
string | 用户昵称 |
data.avatar |
string | 头像 URL |
data.balance |
decimal | 钱包余额(当地货币) |
5.4 消费扣款
用 accessToken 扣除用户余额。支持幂等:传入相同的 appOrderNo 不会重复扣款。
请求
POST /openapi/consumption/deduct
Header
| 参数 | 必填 | 说明 |
|---|---|---|
X-App-Key |
是 | 应用标识 |
X-Timestamp |
是 | 毫秒时间戳 |
X-Nonce |
是 | 随机字符串 |
X-Signature |
是 | HMAC-SHA256 签名 |
X-Access-Token |
是 | 访问令牌 |
Content-Type |
是 | application/json |
请求体
{
"amount": 10.00,
"appOrderNo": "order-20240601-001",
"remark": "GPT-4对话"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
amount |
decimal | 是 | 消费金额(当地货币),必须大于 0 |
appOrderNo |
string | 否 | 应用侧订单号(幂等键),建议传入 |
remark |
string | 否 | 备注 |
响应
{
"code": 200,
"message": "success",
"data": {
"orderNo": "CON20240601000001",
"appOrderNo": "order-20240601-001",
"balanceBefore": 100.0000,
"amount": 10.0000,
"balanceAfter": 90.0000
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
data.orderNo |
string | 平台消费记录号 |
data.appOrderNo |
string | 应用侧订单号(与请求中传入的 appOrderNo 一致) |
data.balanceBefore |
decimal | 扣款前余额 |
data.amount |
decimal | 本次消费金额 |
data.balanceAfter |
decimal | 扣款后余额 |
6. 错误码
6.1 响应格式
{
"code": 40509,
"message": "app.signature.invalid",
"data": null
}
| 字段 | 说明 |
|---|---|
code |
错误码(200 表示成功) |
message |
错误描述 |
data |
错误时为 null |
6.2 OpenAPI 错误码表
| 错误码 | message | 说明 | 处理建议 |
|---|---|---|---|
| 40501 | app.not_found |
应用不存在 | 检查 appKey 是否正确 |
| 40502 | app.offline |
应用已下线 | 联系平台管理员 |
| 40503 | app.session.invalid |
session_key 无效 | 重新获取 session_key |
| 40504 | app.session.expired |
session_key 已过期 | session_key 有效期 5 分钟,请尽快使用 |
| 40505 | app.session.used |
session_key 已使用 | session_key 为一次性令牌,不可重复使用 |
| 40506 | app.access_token.invalid |
accessToken 无效或过期 | 使用 refreshToken 刷新令牌 |
| 40507 | app.balance.insufficient |
用户余额不足 | 提示用户充值 |
| 40508 | app.consumption.amount_invalid |
消费金额无效 | 检查金额是否大于 0 |
| 40509 | app.signature.invalid |
签名验证失败 | 检查签名算法和 appSecret |
| 40510 | app.request.expired |
请求已过期 | 检查时间戳,确保在 5 分钟窗口内 |
| 40511 | app.request.replay |
请求重放(nonce 重复) | 每次请求使用不同的 nonce |
6.3 通用错误码
| 错误码 | message | 说明 |
|---|---|---|
| 200 | success |
成功 |
| 40106 | user.disabled |
用户已禁用 |
| 500 | — | 服务器内部错误 |
7. 签名示例代码
7.1 Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.UUID;
public class OpenApiSigner {
/**
* 计算请求签名
*
* @param method HTTP方法(GET/POST)
* @param path 请求路径
* @param appSecret 应用密钥
* @param body 请求体(GET传null)
* @return SignResult 包含签名所需的所有Header
*/
public static SignResult sign(String method, String path, String appSecret, String body) {
String timestamp = String.valueOf(System.currentTimeMillis());
String nonce = UUID.randomUUID().toString().replace("-", "");
// 1. 计算请求体SHA256
String bodySha256 = sha256Hex(body != null ? body : "");
// 2. 构建规范化字符串
String canonicalString = method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + bodySha256;
// 3. HMAC-SHA256签名
String signature = hmacSha256(canonicalString, appSecret);
return new SignResult(timestamp, nonce, signature);
}
private static String hmacSha256(String data, String key) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(keySpec);
byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash);
} catch (Exception e) {
throw new RuntimeException("签名失败", e);
}
}
private static String sha256Hex(String data) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash);
} catch (Exception e) {
throw new RuntimeException("SHA256计算失败", e);
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
}
/** 签名结果 */
public record SignResult(String timestamp, String nonce, String signature) {}
}
使用示例:
String appSecret = "your-app-secret";
String body = "{\"sessionKey\":\"550e8400-e29b-41d4-a716-446655440000\"}";
OpenApiSigner.SignResult result = OpenApiSigner.sign("POST", "/openapi/auth/token", appSecret, body);
// 设置请求Header
// X-Timestamp: result.timestamp()
// X-Nonce: result.nonce()
// X-Signature: result.signature()
7.2 Python
import hmac
import hashlib
import time
import uuid
def sign_request(method: str, path: str, app_secret: str, body: str | None = None) -> dict:
"""计算 OpenAPI 请求签名
Args:
method: HTTP方法(GET/POST)
path: 请求路径
app_secret: 应用密钥
body: 请求体(GET请求传None)
Returns:
包含 timestamp, nonce, signature 的字典
"""
timestamp = str(int(time.time() * 1000))
nonce = uuid.uuid4().hex
# 1. 计算请求体SHA256
body_bytes = (body or "").encode("utf-8")
body_sha256 = hashlib.sha256(body_bytes).hexdigest()
# 2. 构建规范化字符串
canonical_string = f"{method}\n{path}\n{timestamp}\n{nonce}\n{body_sha256}"
# 3. HMAC-SHA256签名
signature = hmac.new(
app_secret.encode("utf-8"),
canonical_string.encode("utf-8"),
hashlib.sha256
).hexdigest()
return {
"timestamp": timestamp,
"nonce": nonce,
"signature": signature
}
使用示例:
import requests
app_key = "your-app-key"
app_secret = "your-app-secret"
body = '{"sessionKey":"550e8400-e29b-41d4-a716-446655440000"}'
sign_result = sign_request("POST", "/openapi/auth/token", app_secret, body)
headers = {
"X-App-Key": app_key,
"X-Timestamp": sign_result["timestamp"],
"X-Nonce": sign_result["nonce"],
"X-Signature": sign_result["signature"],
"Content-Type": "application/json"
}
response = requests.post(
"https://your-domain.com/openapi/auth/token",
headers=headers,
data=body
)
print(response.json())
7.3 Node.js
const crypto = require('crypto');
/**
* 计算请求签名
* @param {string} method - HTTP方法(GET/POST)
* @param {string} path - 请求路径
* @param {string} appSecret - 应用密钥
* @param {string|null} body - 请求体(GET请求传null)
* @returns {{timestamp: string, nonce: string, signature: string}}
*/
function signRequest(method, path, appSecret, body = null) {
const timestamp = Date.now().toString();
const nonce = crypto.randomUUID().replace(/-/g, '');
// 1. 计算请求体SHA256
const bodySha256 = crypto
.createHash('sha256')
.update(body || '')
.digest('hex');
// 2. 构建规范化字符串
const canonicalString = [method, path, timestamp, nonce, bodySha256].join('\n');
// 3. HMAC-SHA256签名
const signature = crypto
.createHmac('sha256', appSecret)
.update(canonicalString)
.digest('hex');
return { timestamp, nonce, signature };
}
使用示例:
const https = require('https');
const appKey = 'your-app-key';
const appSecret = 'your-app-secret';
const body = JSON.stringify({ sessionKey: '550e8400-e29b-41d4-a716-446655440000' });
const { timestamp, nonce, signature } = signRequest('POST', '/openapi/auth/token', appSecret, body);
const options = {
hostname: 'your-domain.com',
path: '/openapi/auth/token',
method: 'POST',
headers: {
'X-App-Key': appKey,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
'Content-Type': 'application/json'
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => console.log(JSON.parse(data)));
});
req.write(body);
req.end();
8. 最佳实践
8.1 令牌管理
- 提前刷新:在
accessToken过期前(建议剩余 5 分钟时)主动使用refreshToken刷新,避免接口调用中断 - 安全存储:令牌应存储在服务端,禁止传递到前端或嵌入客户端代码
- 并发控制:
refreshToken为一次性使用,并发刷新可能导致其中一个失败,建议加分布式锁或由单一服务负责刷新
8.2 幂等处理
- 消费扣款接口支持幂等:传入相同的
appOrderNo不会重复扣款 - 强烈建议每次扣款都传入
appOrderNo,避免网络超时重试导致的重复扣款 appOrderNo由第三方应用生成,需保证全局唯一(建议格式:{应用前缀}-{日期}-{序号})
8.3 错误重试
| 场景 | 建议 |
|---|---|
| 签名失败(40509) | 检查签名算法,不重试 |
| 请求过期(40510) | 校准服务器时间,使用新时间戳重试 |
| 请求重放(40511) | 更换新的 nonce 重试 |
| 令牌无效(40506) | 使用 refreshToken 刷新后重试 |
| 余额不足(40507) | 提示用户充值,不重试 |
| 网络超时 | 检查扣款结果后再决定是否重试(结合 appOrderNo 幂等) |
8.4 安全建议
appSecret仅限服务端存储,禁止泄露到前端- 使用 HTTPS 协议通信
- 每次请求使用不同的
nonce,推荐使用 UUID session_key有效期仅 5 分钟,获取后应立即使用