虾壳开放平台 API 接入文档

版本: v1.0 更新日期: 2026-06-25

目录


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 流程说明

  1. 用户打开应用:用户在虾壳客户端点击打开第三方应用,虾壳生成一次性 session_key(5分钟有效),通过 URL 参数传递给第三方应用
  2. 换取令牌:第三方应用后端用 session_key 调用换取令牌接口,获取 accessToken(1小时)和 refreshToken(2天)
  3. 访问业务接口:使用 accessToken 调用获取用户信息、消费扣款等接口
  4. 刷新令牌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)

验证步骤

  1. 取响应体原始 JSON 字符串,计算 bodySha256 = SHA256(responseBody)
  2. 用本地保存的 appSecret 计算 expectedSignature = HMAC-SHA256(bodySha256, appSecret)
  3. 对比响应 Header X-Response-SignatureexpectedSignature 是否一致

注意:验证时需使用响应体的原始 JSON 字节(UTF-8),不要格式化或添加空白字符。


5. 接口列表

5.1 换取令牌

用一次性 session_key 换取 accessTokenrefreshToken

请求

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 分钟,获取后应立即使用