鉴权
每个请求都需要四个请求头。密钥既是身份标识,也是签名密钥,不会在请求中明文传输签名以外的内容。
请求头
| 字段 | 类型 | 说明 | |
|---|---|---|---|
| X-Api-Key | string | 必填 | 控制台生成的 API Key,同时用作签名密钥 |
| X-Api-Ts | string | 必填 | Unix 毫秒时间戳,与服务器时间相差不超过 5 分钟 |
| X-Api-Nonce | string | 必填 | 16~64 字符随机串,每次请求必须不同 |
| X-Api-Sign | string | 必填 | Base64(HMAC-SHA256(签名原文, API Key)) |
签名原文
五段以换行符连接,顺序固定。注意 PATH 含 /api 前缀,带 query 时一并计入且不重排。
1METHOD大写,如 GET / POST
2PATH[?QUERY]含 /api,query 原样
3X-Api-Ts毫秒时间戳
4X-Api-Nonce随机串
5SHA256_HEX(body)无 body 即空串的摘要
五段以换行符连接后,签名 = Base64(HMAC-SHA256(该原文, API Key)),放入 X-Api-Sign。
一个真实例子
五段(换行分隔)
GET
/api/open/v1/tasks?from=0&size=5
1758153600000
9f2c1a7b4e6d8035
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855最后一段是空 body 的 SHA-256——所有 GET 请求都是这个固定值,可以直接抄。
规则
- 时间戳与服务器时间相差不得超过 5 分钟(双向)。请确保机器时钟已同步。
- nonce 为 16~64 字符的随机串,每个请求都必须不同。重复使用会被拒绝,这是防重放机制。
- JSON 请求体参与签名(取其 SHA-256 十六进制)。GET 请求与文件直传使用空字符串的 SHA-256。
- query 参数按原始顺序计入,不排序、不解码。直接用你实际请求的 URL 即可。
示例代码
# cURL 本身不能算 HMAC,先用 shell 生成签名再发请求
API_KEY="YOUR_API_KEY"
URL="https://api.geeknums.io/api/open/v1/account"
METHOD="GET"
BODY=''
PATH_Q=$(printf '%s' "$URL" | sed -E 's#^https?://[^/]+##')
TS=$(( $(date +%s) * 1000 ))
NONCE=$(head -c16 /dev/urandom | xxd -p)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')
SIGN_BASE=$(printf '%s\n%s\n%s\n%s\n%s' "$METHOD" "$PATH_Q" "$TS" "$NONCE" "$BODY_HASH")
SIGN=$(printf '%s' "$SIGN_BASE" | openssl dgst -sha256 -hmac "$API_KEY" -binary | base64)
curl -X "$METHOD" "$URL" \
-H "X-Api-Key: $API_KEY" \
-H "X-Api-Ts: $TS" \
-H "X-Api-Nonce: $NONCE" \
-H "X-Api-Sign: $SIGN"import base64, hashlib, hmac, time, uuid, json
import urllib.request
from urllib.parse import urlsplit
API_KEY = "YOUR_API_KEY"
url = "https://api.geeknums.io/api/open/v1/account"
method = "GET"
body = ""
parts = urlsplit(url)
path = parts.path + (("?" + parts.query) if parts.query else "")
ts = str(int(time.time() * 1000))
nonce = uuid.uuid4().hex
sign_base = "\n".join([method, path, ts, nonce,
hashlib.sha256(body.encode()).hexdigest()])
sign = base64.b64encode(
hmac.new(API_KEY.encode(), sign_base.encode(), hashlib.sha256).digest()
).decode()
headers = {
"X-Api-Key": API_KEY,
"X-Api-Ts": ts,
"X-Api-Nonce": nonce,
"X-Api-Sign": sign,
}
req = urllib.request.Request(url, method=method,
data=body.encode() if body else None, headers=headers)
with urllib.request.urlopen(req) as resp:
print(resp.status, resp.read().decode())import crypto from "node:crypto";
const API_KEY = "YOUR_API_KEY";
const url = "https://api.geeknums.io/api/open/v1/account";
const method = "GET";
const body = "";
const u = new URL(url);
const path = u.pathname + u.search;
const ts = String(Date.now());
const nonce = crypto.randomBytes(16).toString("hex");
const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
const signBase = [method, path, ts, nonce, bodyHash].join("\n");
const sign = crypto.createHmac("sha256", API_KEY).update(signBase).digest("base64");
const headers = {
"X-Api-Key": API_KEY,
"X-Api-Ts": ts,
"X-Api-Nonce": nonce,
"X-Api-Sign": sign,
};
const res = await fetch(url, { method, headers });
console.log(res.status, await res.text());import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.UUID;
public class OpenApiExample {
public static void main(String[] args) throws Exception {
String apiKey = "YOUR_API_KEY";
String url = "https://api.geeknums.io/api/open/v1/account";
String method = "GET";
String body = "";
URI uri = URI.create(url);
String path = uri.getRawPath() + (uri.getRawQuery() == null ? "" : "?" + uri.getRawQuery());
String ts = String.valueOf(System.currentTimeMillis());
String nonce = UUID.randomUUID().toString().replace("-", "");
MessageDigest sha = MessageDigest.getInstance("SHA-256");
StringBuilder hex = new StringBuilder();
for (byte b : sha.digest(body.getBytes(StandardCharsets.UTF_8))) {
hex.append(String.format("%02x", b));
}
String signBase = String.join("\n", method, path, ts, nonce, hex.toString());
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(apiKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String sign = Base64.getEncoder().encodeToString(
mac.doFinal(signBase.getBytes(StandardCharsets.UTF_8)));
HttpRequest.Builder builder = HttpRequest.newBuilder(uri)
.header("X-Api-Key", apiKey)
.header("X-Api-Ts", ts)
.header("X-Api-Nonce", nonce)
.header("X-Api-Sign", sign)
.method(method, HttpRequest.BodyPublishers.noBody());
HttpResponse<String> res = HttpClient.newHttpClient()
.send(builder.build(), HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode() + " " + res.body());
}
}package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
func main() {
apiKey := "YOUR_API_KEY"
rawURL := "https://api.geeknums.io/api/open/v1/account"
method := "GET"
body := ""
u, _ := url.Parse(rawURL)
path := u.EscapedPath()
if u.RawQuery != "" {
path += "?" + u.RawQuery
}
ts := fmt.Sprintf("%d", time.Now().UnixMilli())
nb := make([]byte, 16)
rand.Read(nb)
nonce := hex.EncodeToString(nb)
sum := sha256.Sum256([]byte(body))
signBase := strings.Join([]string{method, path, ts, nonce, hex.EncodeToString(sum[:])}, "\n")
mac := hmac.New(sha256.New, []byte(apiKey))
mac.Write([]byte(signBase))
sign := base64.StdEncoding.EncodeToString(mac.Sum(nil))
req, _ := http.NewRequest(method, rawURL, strings.NewReader(body))
req.Header.Set("X-Api-Key", apiKey)
req.Header.Set("X-Api-Ts", ts)
req.Header.Set("X-Api-Nonce", nonce)
req.Header.Set("X-Api-Sign", sign)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))
}