trade.qfex.com 后,必须在 1 分钟内完成认证,否则服务器会关闭连接。认证方式为 HMAC-SHA256 签名或有效 JWT 令牌。
工作方式
HMAC
- 连接
wss://trade.qfex.com?api_key=YOUR_PUBLIC_KEY。 - 生成密码学安全随机 nonce(十六进制编码,最长 100 个字符),并获取当前 Unix 时间戳。
- 构造字符串
${nonce}:${unix_ts},使用私密密钥计算 HMAC-SHA256,再将结果编码为十六进制签名。 - 发送包含下述
hmac对象的认证载荷。nonce 在 15 分钟窗口内必须唯一。
{
"type": "auth",
"params": {
"hmac": {
"public_key": "qfex_pub_xxxxx",
"nonce": "c0ffee...",
"unix_ts": 1760545414,
"signature": "5f2e..."
},
"account_id": "11111111-1111-1111-1111-111111111111"
}
}
JWT
对于 JWT 会话,包括通过 QFEX OAuth 获取的令牌,连接时将 JWT 作为查询参数:wss://trade.qfex.com/?jwt=URL_ENCODED_JWT
{
"type": "auth",
"params": {
"jwt": "eyJhbGciOiJFUzI1NiIsImtpZCI6ImNiYzVmZWNmLTlhODItNGFlNy04NDFkLTBkMTdjMjUzMWM3OCIsInR5cCI6IkpXVCJ9...",
"account_id": "11111111-1111-1111-1111-111111111111"
}
}
hmac 或 jwt 旁添加可选字段 account_id。省略时,连接使用主账户。
Builder 归因
要将交易手续费归因给开发者,在同一认证载荷中添加可选的builder_code。该代码为开发者在开发者设置或通过 Builder Codes API创建的 UUID。
{
"type": "auth",
"params": {
"jwt": "USER_JWT",
"builder_code": "11111111-1111-4111-8111-111111111111"
}
}
params.builder_code 传递代码。完整 OAuth 至交易流程见开发者集成指南。
示例代码
# Python (websocket-client)
# pip install websocket-client
import hashlib
import hmac
import json
import secrets
import time
import websocket
PUB_KEY = "qfex_pub_xxxxx"
SECRET_KEY = "qfex_secret_yyyyyy"
def build_auth_message():
nonce = secrets.token_hex(16)
unix_ts = int(time.time())
payload = f"{nonce}:{unix_ts}".encode()
signature = hmac.new(SECRET_KEY.encode(), payload, hashlib.sha256).hexdigest()
return {
"type": "auth",
"params": {
"hmac": {
"public_key": PUB_KEY,
"nonce": nonce,
"unix_ts": unix_ts,
"signature": signature,
}
},
}
def on_open(ws):
ws.send(json.dumps(build_auth_message()))
def on_message(_, message):
print("Message:", message)
def on_error(_, error):
print("Error:", error)
def on_close(_, code, reason):
print("Closed:", code, reason)
ws = websocket.WebSocketApp(
"wss://trade.qfex.com?api_key=" + PUB_KEY,
on_open=on_open,
on_message=on_message,
on_error=on_error,
on_close=on_close,
)
ws.run_forever()
// Node.js (ws)
// npm i ws
import crypto from "crypto";
import WebSocket from "ws";
const pubKey = "qfex_pub_xxxxx";
const secretKey = "qfex_secret_yyyyyy";
function buildAuthMessage() {
const nonce = crypto.randomBytes(16).toString("hex");
const unixTs = Math.floor(Date.now() / 1000);
const payload = `${nonce}:${unixTs}`;
const signature = crypto
.createHmac("sha256", secretKey)
.update(payload)
.digest("hex");
return {
type: "auth",
params: {
hmac: {
public_key: pubKey,
nonce,
unix_ts: unixTs,
signature,
},
},
};
}
const ws = new WebSocket(`wss://trade.qfex.com?api_key=${pubKey}`);
ws.on("open", () => {
ws.send(JSON.stringify(buildAuthMessage()));
});
ws.on("message", (msg) => {
console.log("Message:", msg.toString());
});
ws.on("error", (err) => console.error("WS error:", err));
ws.on("close", (code, reason) => {
console.log("Closed:", code, reason?.toString() ?? "");
});
// Go (gorilla/websocket)
// go get github.com/gorilla/websocket
package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"log"
"os"
"os/signal"
"time"
"github.com/gorilla/websocket"
)
func main() {
pubKey := "qfex_pub_xxxxx"
secretKey := []byte("qfex_secret_yyyyyy")
c, _, err := websocket.DefaultDialer.Dial("wss://trade.qfex.com?api_key="+pubKey, nil)
if err != nil {
log.Fatal("dial:", err)
}
defer c.Close()
nonceBytes := make([]byte, 16)
if _, err := rand.Read(nonceBytes); err != nil {
log.Fatal("nonce:", err)
}
nonce := hex.EncodeToString(nonceBytes)
unixTs := time.Now().Unix()
message := fmt.Sprintf("%s:%d", nonce, unixTs)
h := hmac.New(sha256.New, secretKey)
h.Write([]byte(message))
signature := hex.EncodeToString(h.Sum(nil))
auth := map[string]any{
"type": "auth",
"params": map[string]any{
"hmac": map[string]any{
"public_key": pubKey,
"nonce": nonce,
"unix_ts": unixTs,
"signature": signature,
},
},
}
if err := c.WriteJSON(auth); err != nil {
log.Fatal("write auth:", err)
}
go func() {
for {
_, message, err := c.ReadMessage()
if err != nil {
log.Println("read:", err)
return
}
fmt.Printf("Message: %s\n", message)
}
}()
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
<-interrupt
}
// Java (OkHttp WebSocket)
// Gradle: implementation("com.squareup.okhttp3:okhttp:4.12.0")
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.concurrent.TimeUnit;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import okhttp3.*;
public class AuthWs {
private static final String PUB_KEY = "qfex_pub_xxxxx";
private static final String SECRET_KEY = "qfex_secret_yyyyyy";
public static void main(String[] args) {
OkHttpClient client = new OkHttpClient.Builder()
.pingInterval(20, TimeUnit.SECONDS)
.build();
Request request = new Request.Builder()
.url("wss://trade.qfex.com?api_key=" + PUB_KEY)
.build();
WebSocketListener listener = new WebSocketListener() {
@Override public void onOpen(WebSocket ws, Response response) {
ws.send(buildAuthPayload());
}
@Override public void onMessage(WebSocket ws, String text) {
System.out.println("Message: " + text);
}
@Override public void onFailure(WebSocket ws, Throwable t, Response r) {
System.err.println("WS error: " + t.getMessage());
}
};
client.newWebSocket(request, listener);
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
private static String buildAuthPayload() {
SecureRandom random = new SecureRandom();
byte[] nonceBytes = new byte[16];
random.nextBytes(nonceBytes);
String nonce = bytesToHex(nonceBytes);
long unixTs = System.currentTimeMillis() / 1000;
String data = nonce + ":" + unixTs;
String signature;
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(keySpec);
signature = bytesToHex(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
throw new IllegalStateException("Unable to sign payload", e);
}
return String.format(
"{\"type\":\"auth\",\"params\":{\"hmac\":{\"public_key\":\"%s\",\"nonce\":\"%s\",\"unix_ts\":%d,\"signature\":\"%s\"}}}",
PUB_KEY, nonce, unixTs, signature);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
响应示例
{ "type": "auth", "result": "success" }
客户端成功认证后,服务器通过定期发送 WebSocket ping 帧形式的心跳消息保持连接。