From ec97b07611620859b78dfa18f7f06060a467f8f0 Mon Sep 17 00:00:00 2001 From: loyoi <1019748371@qq.com> Date: Sat, 15 Nov 2025 15:36:04 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20node=20ts=20=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E7=A4=BA=E4=BE=8B=E4=BB=A3=E7=A0=81=20=E5=AE=8C?= =?UTF-8?q?=E5=96=84=20go=20=E8=AF=AD=E8=A8=80post=20=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- signature/golang/sign.go | 10 ++- signature/nodejs/package.json | 12 ++++ signature/nodejs/sign.ts | 117 ++++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 signature/nodejs/package.json create mode 100644 signature/nodejs/sign.ts diff --git a/signature/golang/sign.go b/signature/golang/sign.go index ee7df95..f776b9d 100644 --- a/signature/golang/sign.go +++ b/signature/golang/sign.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -178,5 +178,11 @@ func main() { // Post 请求例子 // query2 := make(url.Values) // query2.Set("Limit", "1") - // doRequest(http.MethodPost, query2, []byte(`Scope=System`)) + // jsonBody := map[string]string{ + // "req_key": "jimeng_t2i_v40", + // "prompt": "a photo of a cat", + // } + // bodyBytes, _ := json.Marshal(jsonBody) + // doRequest(http.MethodPost, query2, bodyBytes) + } diff --git a/signature/nodejs/package.json b/signature/nodejs/package.json new file mode 100644 index 0000000..9aa5f38 --- /dev/null +++ b/signature/nodejs/package.json @@ -0,0 +1,12 @@ +{ + "name": "火山", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/signature/nodejs/sign.ts b/signature/nodejs/sign.ts new file mode 100644 index 0000000..474b410 --- /dev/null +++ b/signature/nodejs/sign.ts @@ -0,0 +1,117 @@ +/** + * 火山引擎 HMAC-SHA256 签名示例 (TypeScript / Node.js fetch) + * 经过了测试,请求成功 + */ + +import crypto from "crypto"; +import querystring from "querystring"; + +const ACCESS_KEY_ID = ""; // 从访问控制申请 +const SECRET_ACCESS_KEY = "=="; + +const ADDR = "https://visual.volcengineapi.com"; +const PATH = "/"; // 请求路径 +const SERVICE = "cv"; +const REGION = "cn-north-1"; +const ACTION = "CVSync2AsyncSubmitTask"; +const VERSION = "2022-08-31"; + +function hmacSHA256(key: Buffer | string, content: string): Buffer { + return crypto.createHmac("sha256", key).update(content).digest(); +} + +function getSignedKey(secretKey: string, date: string, region: string, service: string): Buffer { + const kDate = hmacSHA256(Buffer.from(secretKey, "utf-8"), date); + const kRegion = hmacSHA256(kDate, region); + const kService = hmacSHA256(kRegion, service); + const kSigning = hmacSHA256(kService, "request"); + return kSigning; +} + +function hashSHA256(data: Buffer | string): Buffer { + return crypto.createHash("sha256").update(data).digest(); +} + +async function doRequest(method: "GET" | "POST", queries: Record, body: object | null) { + // ====== 1. 构建请求 ====== + queries["Action"] = ACTION; + queries["Version"] = VERSION; + const queryString = querystring.stringify(queries, "&", "=").replace(/\+/g, "%20"); + const requestAddr = `${ADDR}${PATH}?${queryString}`; + + const bodyBuffer = body ? Buffer.from(JSON.stringify(body), "utf-8") : Buffer.alloc(0); + + // ====== 2. 构建签名材料 ====== + const now = new Date(); + const isoDate = now.toISOString(); // 2024-06-27T15:04:05.000Z + const date = isoDate.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z"); // 20240627T150405Z + const authDate = date.substring(0, 8); + + const payload = hashSHA256(bodyBuffer).toString("hex"); + + // 签名 header 列表 + const host = new URL(ADDR).host; + const signedHeaders = ["host", "x-date", "x-content-sha256", "content-type"]; + const headerList = [`host:${host}`, `x-date:${date}`, `x-content-sha256:${payload}`, `content-type:application/json`]; + + const headerString = headerList.join("\n"); + + const canonicalString = [method, PATH, queryString, headerString + "\n", signedHeaders.join(";"), payload].join("\n"); + + const hashedCanonicalString = hashSHA256(canonicalString).toString("hex"); + + const credentialScope = `${authDate}/${REGION}/${SERVICE}/request`; + const signString = ["HMAC-SHA256", date, credentialScope, hashedCanonicalString].join("\n"); + + // ====== 3. 计算签名 ====== + const signedKey = getSignedKey(SECRET_ACCESS_KEY, authDate, REGION, SERVICE); + const signature = hmacSHA256(signedKey, signString).toString("hex"); + + const authorization = + `HMAC-SHA256 Credential=${ACCESS_KEY_ID}/${credentialScope}, ` + `SignedHeaders=${signedHeaders.join(";")}, Signature=${signature}`; + + // ====== 4. 发起请求 ====== + const headers: Record = { + Host: host, + "X-Date": date, + "X-Content-Sha256": payload, + "Content-Type": "application/json", + Authorization: authorization, + }; + + console.log("===== 请求信息 ====="); + console.log("URL:", requestAddr); + console.log("Method:", method); + console.log("Headers:", headers); + if (body) console.log("Body:", JSON.stringify(body)); + + const res = await fetch(requestAddr, { + method, + headers, + body: method === "POST" ? bodyBuffer : undefined, + }); + + const text = await res.text(); + console.log("===== 响应信息 ====="); + console.log("Status:", res.status); + console.log("Body:", text); + + if (res.status === 200) { + console.log("请求成功"); + } else { + console.log("请求失败"); + } +} + +// 入口 +(async () => { + const body = { + req_key: "jimeng_t2i_v40", + prompt: "a photo of a cat", + }; + + await doRequest("POST", {}, body); + + // GET 请求示例 + // await doRequest("GET", { Limit: "1", Scope: "Custom" }, null); +})(); From 3ffc2e358f23013f2200248e64ee801ea8e62620 Mon Sep 17 00:00:00 2001 From: loyoi <1019748371@qq.com> Date: Mon, 8 Dec 2025 22:22:42 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=B5=8F=E8=A7=88?= =?UTF-8?q?=E5=99=A8=E7=89=88=E6=9C=AC=E7=AD=BE=E5=90=8D=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- signature/nodejs/package.json | 2 +- signature/nodejs/sign-web.ts | 138 ++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 signature/nodejs/sign-web.ts diff --git a/signature/nodejs/package.json b/signature/nodejs/package.json index 9aa5f38..18c1454 100644 --- a/signature/nodejs/package.json +++ b/signature/nodejs/package.json @@ -1,5 +1,5 @@ { - "name": "火山", + "name": "huosan", "module": "index.ts", "type": "module", "private": true, diff --git a/signature/nodejs/sign-web.ts b/signature/nodejs/sign-web.ts new file mode 100644 index 0000000..a49ac00 --- /dev/null +++ b/signature/nodejs/sign-web.ts @@ -0,0 +1,138 @@ +// 火山引擎 HMAC-SHA256 签名示例(浏览器版) + +const ACCESS_KEY_ID = ""; // 从访问控制申请 +const SECRET_ACCESS_KEY = "=="; + +const ADDR = "https://visual.volcengineapi.com"; +const PATH = "/"; // 请求路径 +const SERVICE = "cv"; +const REGION = "cn-north-1"; +const ACTION = "CVSync2AsyncSubmitTask"; +const VERSION = "2022-08-31"; + +const encoder = new TextEncoder(); + +// ArrayBuffer -> hex 字符串 +function arrayBufferToHex(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + const hex: string[] = []; + for (let i = 0; i < bytes.length; i++) { + hex.push(bytes[i].toString(16).padStart(2, "0")); + } + return hex.join(""); +} + +// 浏览器版 HMAC-SHA256 +async function hmacSHA256(key: string | ArrayBuffer, content: string): Promise { + const keyBytes = typeof key === "string" ? encoder.encode(key) : new Uint8Array(key); + const cryptoKey = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); + const sig = await crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(content)); + return sig; // ArrayBuffer +} + +// 浏览器版 SHA256 +async function hashSHA256(data: string | ArrayBuffer): Promise { + const bytes = typeof data === "string" ? encoder.encode(data) : new Uint8Array(data); + return crypto.subtle.digest("SHA-256", bytes); +} + +// 派生签名 key:kDate -> kRegion -> kService -> kSigning +async function getSignedKey(secretKey: string, date: string, region: string, service: string): Promise { + const kDate = await hmacSHA256(secretKey, date); + const kRegion = await hmacSHA256(kDate, region); + const kService = await hmacSHA256(kRegion, service); + const kSigning = await hmacSHA256(kService, "request"); + return kSigning; +} + +async function doRequest(method: "GET" | "POST", queries: Record, body: object | null): Promise { + // ====== 1. 构建请求 ====== + const q: Record = { ...queries }; + q["Action"] = ACTION; + q["Version"] = VERSION; + + // 用 URLSearchParams 模拟 querystring.stringify,并保持 + -> %20 的行为 + const usp = new URLSearchParams(); + Object.entries(q).forEach(([k, v]) => usp.append(k, v)); + const queryString = usp.toString().replace(/\+/g, "%20"); + const requestAddr = `${ADDR}${PATH}?${queryString}`; + + const bodyString = body ? JSON.stringify(body) : ""; + + // ====== 2. 构建签名材料 ====== + const now = new Date(); + const isoDate = now.toISOString(); // 2024-06-27T15:04:05.000Z + const date = isoDate.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z"); // 20240627T150405Z + const authDate = date.substring(0, 8); + + const payloadBuf = await hashSHA256(bodyString); + const payload = arrayBufferToHex(payloadBuf); + + const host = new URL(ADDR).host; + + // 注意:浏览器里实际发送的 Host 头由浏览器自动加,fetch 里设置 "Host" 会被忽略。 + const signedHeaders = ["host", "x-date", "x-content-sha256", "content-type"]; + const headerList = [`host:${host}`, `x-date:${date}`, `x-content-sha256:${payload}`, `content-type:application/json`]; + const headerString = headerList.join("\n"); + + const canonicalString = [method, PATH, queryString, headerString + "\n", signedHeaders.join(";"), payload].join("\n"); + + const hashedCanonical = await hashSHA256(canonicalString); + const hashedCanonicalHex = arrayBufferToHex(hashedCanonical); + + const credentialScope = `${authDate}/${REGION}/${SERVICE}/request`; + const signString = ["HMAC-SHA256", date, credentialScope, hashedCanonicalHex].join("\n"); + + // ====== 3. 计算签名 ====== + const signedKey = await getSignedKey(SECRET_ACCESS_KEY, authDate, REGION, SERVICE); + const signatureBuf = await hmacSHA256(signedKey, signString); + const signature = arrayBufferToHex(signatureBuf); + + const authorization = + `HMAC-SHA256 Credential=${ACCESS_KEY_ID}/${credentialScope}, ` + `SignedHeaders=${signedHeaders.join(";")}, Signature=${signature}`; + + // ====== 4. 发起请求 ====== + const headers: Record = { + // Host 头浏览器会自动带上,这里不用设置 + "X-Date": date, + "X-Content-Sha256": payload, + "Content-Type": "application/json", + Authorization: authorization, + }; + + console.log("===== 请求信息 ====="); + console.log("URL:", requestAddr); + console.log("Method:", method); + console.log("Headers:", headers); + if (body) console.log("Body:", bodyString); + + const res = await fetch(requestAddr, { + method, + headers, + body: method === "POST" ? bodyString : undefined, + }); + + const text = await res.text(); + console.log("===== 响应信息 ====="); + console.log("Status:", res.status); + console.log("Body:", text); + + if (res.status === 200) { + console.log("请求成功"); + } else { + console.log("请求失败"); + } +} + +// ====== 浏览器入口示例 ====== +(async () => { + const body = { + req_key: "jimeng_t2i_v40", + prompt: "a photo of a cat", + }; + + await doRequest("POST", {}, body); + + // GET 请求示例 + // await doRequest("GET", { Limit: "1", Scope: "Custom" }, null); +})();