联享券
请求加解密教程
使用 P-256 ECDH、HKDF-SHA256 和 AES-256-GCM,在 HTTPS 内再次加密联享券的路径、Token、请求体与响应体。
每个会话使用客户端临时密钥与服务端当前公钥协商共享秘密。
按品牌、来源、会话、通道和方向派生独立密钥。
请求和响应分别认证加密,篡改会导致解密失败。
何时启用
生产服务端调用建议使用 required 模式。即使 TLS 在网关或代理处终止,业务参数仍只会在安全传输客户端和 IM 网关内以明文出现。联享券测试系统的“接口测试”页可以切换应用层加密,直接比较两种请求结果。
required 模式握手或解密失败时直接失败。建立过安全会话后,不允许因服务配置变化静默降级为明文。
下载服务端接入示例
示例与当前网关协议同版本,只能放在商家服务端项目中,不是 App、Android 或网页客户端代码,也不要提交 sop_ Token。
mkdir -p secure-transport
curl -fsSLO "$SITE/examples/secure-transport/index.js" \
--output-dir secure-transport
curl -fsSLO "$SITE/examples/secure-transport/client.js" \
--output-dir secure-transport需要 Node.js 20 或提供 Fetch、Web Crypto、btoa 和 atob 的等价运行时。下载的 client.js 会从同目录导入 index.js。应用策略不是 * 时,将 IM_REQUEST_ORIGIN 设置为白名单允许的完整来源;客户端会把它绑定到加密会话并发送 Origin 头。
Java ZIP 是可直接编译的商家服务端 Maven 项目,包含安全传输调用库、连接检查、发券示例和运行说明,需要 JDK 17+。
完整发券示例
import { SecureTransportClient } from './secure-transport/client.js'
const API_ORIGIN = 'https://api.example.com'
const client = new SecureTransportClient({
baseUrl: `${API_ORIGIN}/api/v1`,
mode: 'required',
pageOrigin: process.env.IM_REQUEST_ORIGIN || '',
timeoutMs: 15_000,
})
const externalIssueId = 'points_order_20260909_001'
const idempotencyKey = `issue:${externalIssueId}`
const result = await client.dispatch({
method: 'POST',
path: '/api/v1/open-platform/vouchers/issue',
query: {},
headers: {
accept: 'application/json',
authorization: `Bearer ${process.env.IM_OPEN_PLATFORM_TOKEN}`,
'content-type': 'application/json',
'idempotency-key': idempotencyKey,
},
body: {
template_id: process.env.IM_POINTS_TEMPLATE_ID,
external_issue_id: externalIssueId,
recipient_user_id: 'AUTHORIZED_SOCHAT_USER_ID',
},
})
if (!result.secure) throw new Error('安全传输未建立')
if (result.status < 200 || result.status >= 300 || result.body?.success === false) {
throw new Error(result.body?.message || `IM API ${result.status}`)
}
console.log(result.body.data)安全分发接口外层成功时 HTTP 状态为 200。真实业务状态在解密后的 result.status,统一业务响应在 result.body;两层都必须检查。
协议流程
- 1. Bootstrap GET /api/v1/secure/bootstrap,校验 v、suite、enabled、kid 和 serverPublicKey,并用 serverTime 校准本地时间。
- 2. Session 生成临时 P-256 密钥对,POST /api/v1/secure/session 发送公钥;保存 sid、salt、brand、origin 和 expiresAt。
- 3. Derive 执行 ECDH,再用 HKDF 分别派生 http/c2s 与 http/s2c 两把 256 位密钥。
- 4. Encrypt 为每次请求生成新的 rid 和 96 位 IV,把 method、path、query、headers、body 整体加密。
- 5. Dispatch POST /api/v1/secure/dispatch。服务端校验时间、会话、来源和 rid 防重放,解密后执行逻辑接口。
- 6. Decrypt 使用同一 rid 和 s2c AAD 验证并解密响应,再按内层 status 处理业务结果。
网络上看到的请求
POST /api/v1/secure/dispatch
Content-Type: application/vnd.sochat.secure+json
{
"v": 1,
"suite": "P256-HKDF-SHA256-A256GCM",
"kid": "2026-09",
"sid": "st_...",
"rid": "rid_...",
"ts": 1788912000000,
"iv": "base64url-96-bit-iv",
"ciphertext": "base64url-ciphertext-and-128-bit-tag"
}authorization、逻辑接口路径、幂等键和业务 JSON 都在 ciphertext 内。外层请求只携带协议字段和链路追踪允许头。
密码学参数
| 项目 | 规则 |
|---|---|
suite | P256-HKDF-SHA256-A256GCM |
HKDF info | soim|v1|{brand}|{origin 或 -}|{sid}|http|{c2s 或 s2c} |
AAD | soim|v1|http|{c2s|s2c}|{kid}|{sid}|{rid}|{ts} |
IV / tag | 每次随机 96 位 IV;128 位 GCM 认证标签随 ciphertext 编码。 |
encoding | 所有二进制字段使用无填充 base64url。 |
clock / session | 默认允许 60 秒时钟偏差,会话默认 600 秒;客户端在到期前 5 秒续期。 |
会话、重试与幂等
- SECURE_SESSION_EXPIRED 或 SECURE_SESSION_MISMATCH 时清理会话、重新协商并且只重试一次。
- 重试会生成新的 rid 和 IV,但业务 Idempotency-Key、external_issue_id 和请求体必须保持不变。
- 每个 rid 在会话内只能使用一次;重复发送同一密文会返回 REQUEST_REPLAYED。
- 并发请求各自保留会话密钥副本,完成后清零;不要把会话密钥写入日志或持久化。
常见安全传输错误
安全传输错误发生在业务接口之前。业务 4xx 会正常加密返回,并出现在解密后的 result.status 和 result.body。
SECURE_TRANSPORT_REQUIRED · 426网关要求应用层加密,但请求走了普通业务地址。REQUEST_TIMESTAMP_OUT_OF_RANGE客户端时间偏差过大;重新读取 bootstrap.serverTime。SECURE_SESSION_EXPIRED会话过期或服务端已清理;重新协商后重试一次。SECURE_SESSION_ORIGIN_MISMATCH创建会话和分发请求的 Origin 不一致。Node 服务端通常都使用空 origin。DECRYPTION_FAILED密钥、AAD、IV、密文或认证标签不匹配。REQUEST_REPLAYED同一 sid 与 rid 已经处理过,必须生成新的 rid。上线检查
- sop_ Token 只放在内层 authorization 头和可信服务端环境变量中。
- 逻辑 path 必须以 /api/v1/ 开头,不能包含 query、hash、反斜杠或 /secure/;查询参数放在 query 对象。
- 加密分发只支持 JSON。二进制、流式响应和大于服务端上限的信封应走对应直连接口。
- 记录 inner status、业务 requestId 和安全错误码,禁止记录 Token、券码、明文密钥、salt 或完整 ciphertext。
