实时接口对接
一、概述
平台 WebSocket 服务基于安全实时通信协议,支持事件订阅、推送和处理,适用于开发者事件(如用户登出、数据更新等)监听。 为确保接口安全性和数据完整性,接入流程采用 HMAC-SHA256 算法进行签名和验证,防止请求篡改和重放攻击。
二、接入准备
1. IP 白名单报备
为确保服务的合法性和安全性,同时便于监管部门进行监督和管理,请在接入平台前联系平台运营人员进行 IP 白名单报备。
报备信息包括:
- 服务商名称
- 联系人信息
- 服务器出口 IP 地址
- 预计调用频率
2. 获取访问凭证
联系平台运营人员获取以下访问凭证:
accessKeyId:用于标识服务商身份的唯一 IDaccessKeySecret:用于生成签名的密钥,请妥善保管
3. 环境准备
测试环境: ws://tech-test-api.jxszpt.com
生产环境: wss://tech-api.jxszpt.com
三、签名算法
签名生成步骤
- 准备签名参数:
accessKeyId、accessKeySecret、timestamp - 将参数按格式拼接:
${accessKeyId}-${accessKeySecret}-${timestamp} - 使用
HMAC-SHA256算法对拼接字符串进行签名 - 将签名结果转换为十六进制字符串
请求头设置
将签名信息添加到请求头中:
| Header 名称 | 描述 | 示例 |
|---|---|---|
X-AccessKeyId | 访问密钥 ID | your_access_key_id |
X-Signature | 生成的签名 | a1b2c3d4e5f6... |
X-Timestamp | 时间戳(毫秒) | 1692518400000 |
签名生成步骤
- 准备签名参数:
accessKeyId、accessKeySecret、timestamp - 将参数按格式拼接:
${accessKeyId}-${accessKeySecret}-${timestamp} - 使用
HMAC-SHA256算法对拼接字符串进行签名 - 将签名结果转换为十六进制字符串
语言特定签名生成示例:
Node.js
const crypto = require('crypto');
function generateSignature(accessKeyId, accessKeySecret) {
const timestamp = Date.now().toString();
const stringToSign = `${accessKeyId}-${accessKeySecret}-${timestamp}`;
const hmac = crypto.createHmac('sha256', accessKeySecret);
hmac.update(stringToSign);
const signature = hmac.digest('hex');
return { signature, timestamp };
}Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class SignatureGenerator {
private static String toHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
public static String[] generateSignature(String accessKeyId, String accessKeySecret) throws Exception {
String timestamp = String.valueOf(System.currentTimeMillis());
String stringToSign = accessKeyId + "-" + accessKeySecret + "-" + timestamp;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(accessKeySecret.getBytes("UTF-8"), "HmacSHA256"));
byte[] hmacBytes = mac.doFinal(stringToSign.getBytes("UTF-8"));
String signature = toHexString(hmacBytes);
return new String[]{signature, timestamp};
}
}Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"time"
)
func generateSignature(accessKeyId, accessKeySecret string) (string, string) {
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
stringToSign := accessKeyId + "-" + accessKeySecret + "-" + timestamp
mac := hmac.New(sha256.New, []byte(accessKeySecret))
mac.Write([]byte(stringToSign))
signature := hex.EncodeToString(mac.Sum(nil))
return signature, timestamp
}请求头设置
签名生成后,将以下参数放入 WebSocket 连接的请求头:
X-AccessKeyId:accessKeyId值,标识服务商身份。X-Signature:生成的 HMAC-SHA256 签名,用于验证请求完整性。X-Timestamp:timestamp值,用于防重放攻击。
在 WebSocket 连接中,请求头是传递认证信息的关键。每个语言的 WebSocket 客户端库有不同方式设置 headers,确保 X-AccessKeyId、X-Signature 和 X-Timestamp 正确包含。
Node.js (使用 socket.io-client)
socket.io-client 支持通过 extraHeaders 配置请求头,签名参数直接在连接时传递。示例:
const io = require('socket.io-client');
const crypto = require('crypto');
function connectWebSocket() {
const accessKeyId = process.env.TENANT_ACCESS_KEY_ID;
const accessKeySecret = process.env.TENANT_ACCESS_KEY_SECRET;
const timestamp = Date.now().toString();
const stringToSign = `${accessKeyId}-${accessKeySecret}-${timestamp}`;
const hmac = crypto.createHmac('sha256', accessKeySecret);
hmac.update(stringToSign);
const signature = hmac.digest('hex');
const socket = io('wss://tech-api.jxszpt.com', {
path: '/developer.event',
reconnection: true,
transportOptions: {
polling: {
extraHeaders: {
'X-AccessKeyId': accessKeyId,
'X-Signature': signature,
'X-Timestamp': timestamp
}
}
}
});
socket.on('connect', () => {
console.log('已连接到 developer.event 命名空间');
});
socket.on('disconnect', (reason) => {
console.warn(`连接断开: ${reason}`);
});
socket.on('portal.user.logout', (message) => {
console.log('接收到 portal.user.logout:', message);
});
socket.on('error', (error) => {
console.error('WebSocket 错误:', error);
});
return socket;
}
connectWebSocket();说明:
extraHeaders嵌套在transportOptions.polling中,因为socket.io初始使用 HTTP 轮询,后升级为 WebSocket。- 每次重连需重新生成
timestamp和signature,以避免时间戳过期。
Node.js (使用 ws)
ws 库在构造函数中直接支持 headers 参数。示例:
const WebSocket = require('ws');
const crypto = require('crypto');
function connectWebSocket() {
const accessKeyId = process.env.TENANT_ACCESS_KEY_ID;
const accessKeySecret = process.env.TENANT_ACCESS_KEY_SECRET;
const timestamp = Date.now().toString();
const stringToSign = `${accessKeyId}-${accessKeySecret}-${timestamp}`;
const hmac = crypto.createHmac('sha256', accessKeySecret);
hmac.update(stringToSign);
const signature = hmac.digest('hex');
const ws = new WebSocket('wss://tech-api.jxszpt.com/developer.event', {
headers: {
'X-AccessKeyId': accessKeyId,
'X-Signature': signature,
'X-Timestamp': timestamp
}
});
ws.on('open', () => {
console.log('已连接到 developer.event 命名空间');
});
ws.on('close', () => {
console.log('连接断开');
setTimeout(connectWebSocket, 5000); // 重连
});
ws.on('message', (data) => {
console.log('接收到消息:', data.toString());
});
ws.on('error', (error) => {
console.error('WebSocket 错误:', error);
});
}
connectWebSocket();说明:
headers直接在WebSocket构造函数中设置,适用于纯 WebSocket 协议。- 需手动实现重连逻辑,每次重连重新生成签名。
Java (使用 java-websocket)
java-websocket 允许在 WebSocketClient 构造函数中传入 headers。示例:
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
public class WebSocketConnector extends WebSocketClient {
public WebSocketConnector(URI serverUri, Map<String, String> headers) {
super(serverUri, headers);
}
@Override
public void onOpen(ServerHandshake handshakedata) {
System.out.println("已连接到 developer.event 命名空间");
}
@Override
public void onMessage(String message) {
System.out.println("接收到消息: " + message);
}
@Override
public void onClose(int code, String reason, boolean remote) {
System.out.println("连接断开: " + reason);
// 重连逻辑
}
@Override
public void onError(Exception ex) {
System.err.println("错误: " + ex.getMessage());
}
public static void main(String[] args) throws Exception {
String accessKeyId = System.getenv("TENANT_ACCESS_KEY_ID");
String accessKeySecret = System.getenv("TENANT_ACCESS_KEY_SECRET");
String timestamp = String.valueOf(System.currentTimeMillis());
String stringToSign = accessKeyId + "-" + accessKeySecret + "-" + timestamp;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(accessKeySecret.getBytes("UTF-8"), "HmacSHA256"));
byte[] hmacBytes = mac.doFinal(stringToSign.getBytes("UTF-8"));
String signature = bytesToHex(hmacBytes);
Map<String, String> headers = new HashMap<>();
headers.put("X-AccessKeyId", accessKeyId);
headers.put("X-Signature", signature);
headers.put("X-Timestamp", timestamp);
URI uri = new URI("wss://tech-api.jxszpt.com/developer.event");
WebSocketConnector client = new WebSocketConnector(uri, headers);
client.setConnectionLostTimeout(0); // 启用自动重连
client.connect();
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}说明:
- Headers 通过
Map传递给WebSocketClient构造函数。 setConnectionLostTimeout(0)启用自动重连,需重新生成签名。
Go (使用 gorilla/websocket)
gorilla/websocket 使用 http.Header 设置请求头。示例:
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"log"
"os"
"time"
"github.com/gorilla/websocket"
"net/http"
)
func main() {
accessKeyId := os.Getenv("TENANT_ACCESS_KEY_ID")
accessKeySecret := os.Getenv("TENANT_ACCESS_KEY_SECRET")
timestamp := time.Now().UnixMilli()
stringToSign := accessKeyId + "-" + accessKeySecret + "-" + strconv.FormatInt(timestamp, 10)
mac := hmac.New(sha256.New, []byte(accessKeySecret))
mac.Write([]byte(stringToSign))
signature := hex.EncodeToString(mac.Sum(nil))
url := "wss://tech-api.jxszpt.com/developer.event"
header := http.Header{}
header.Add("X-AccessKeyId", accessKeyId)
header.Add("X-Signature", signature)
header.Add("X-Timestamp", strconv.FormatInt(timestamp, 10))
conn, _, err := websocket.DefaultDialer.Dial(url, header)
if err != nil {
log.Fatal("连接错误:", err)
}
defer conn.Close()
log.Println("已连接到 developer.event 命名空间")
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Println("连接断开:", err)
// 重连
return
}
log.Printf("接收到消息: %s", message)
}
}说明:
http.Header用于设置认证 headers,直接传递给Dial。- 重连需重新生成
timestamp和signature。
请求头设置注意事项:
- 确保 headers 名称大小写正确(如
X-AccessKeyId)。 - 每次连接或重连时,重新生成
timestamp和signature。 - 某些库可能在 WebSocket 协议升级后忽略 headers,需验证库支持(如
socket.io使用extraHeaders)。 - 签名仅用于初始连接认证,事件消息无需签名。
五、服务端验签逻辑
验签步骤
- 参数提取:从请求头中提取
X-AccessKeyId、X-Signature、X-Timestamp - 时间戳验证:检查请求时间戳是否在当前时间的 5 分钟范围内
- 签名验证:使用相同算法重新生成签名,与接收到的签名进行比较
服务端验签示例(Node.js)
import crypto from 'crypto';
import express from 'express';
// 中间件:验证 API 签名
function validateSignature(req: express.Request, res: express.Response, next: express.NextFunction) {
try {
const accessKeyId = req.headers['x-accesskeyid'] as string;
const signature = req.headers['x-signature'] as string;
const timestamp = req.headers['x-timestamp'] as string;
if (!accessKeyId || !signature || !timestamp) {
return res.status(400).json({ error: '缺少必要的请求头参数' });
}
// 验证时间戳(5分钟内有效)
const now = Date.now();
const timestampNum = parseInt(timestamp, 10);
if (Math.abs(now - timestampNum) > 5 * 60 * 1000) {
return res.status(401).json({ error: '请求已过期' });
}
// 根据 accessKeyId 获取对应的 accessKeySecret(通常从数据库查询)
const accessKeySecret = getAccessKeySecret(accessKeyId);
if (!accessKeySecret) {
return res.status(401).json({ error: '无效的访问密钥' });
}
// 重新生成签名并验证
const stringToSign = `${accessKeyId}-${accessKeySecret}-${timestamp}`;
const hmac = crypto.createHmac('sha256', accessKeySecret);
hmac.update(stringToSign);
const expectedSignature = hmac.digest('hex');
if (signature !== expectedSignature) {
return res.status(401).json({ error: '签名验证失败' });
}
// 验证通过,继续处理请求
next();
} catch (error) {
return res.status(500).json({ error: '签名验证过程中发生错误' });
}
}
// 模拟获取密钥的函数(实际应用中应从数据库查询)
function getAccessKeySecret(accessKeyId: string): string | null {
const keyMap: { [key: string]: string } = {
'your_access_key_id': 'your_access_key_secret'
};
return keyMap[accessKeyId] || null;
}