37 lines
1.6 KiB
TypeScript
37 lines
1.6 KiB
TypeScript
// src/lib/crypto.ts — 前端 JS 请求签名(反爬一层,非加密)
|
||
//
|
||
// 仅做 HMAC-SHA256 计算,由 api.ts 的公开请求原语附带 X-Sign / X-Sign-Ts / X-Sign-Nonce 头,
|
||
// 供后端 middleware.ClientSign 校验「请求大概率来自真前端」。
|
||
//
|
||
// 安全说明:secret 必须出现在前端 bundle 才能签名(对浏览器可见),故只是「提高成本」,
|
||
// 不能作为唯一防线;真正有效需配合后端按 IP 限流。生产请改用随机长串并同步两端。
|
||
|
||
const CLIENT_SIGN_SECRET = "dev-client-sign-secret-change-me"
|
||
const CLIENT_SIGN_TTL = 30 // 秒,签名有效期(与后端 client_sign.ttl_seconds 一致)
|
||
|
||
// 计算 HMAC-SHA256 签名。
|
||
// canonical = METHOD + "\n" + Path + "\n" + RawQuery + "\n" + ts + "\n" + nonce
|
||
// 与后端 middleware.ClientSign 的拼接方式严格一致(换行符分隔,顺序固定)。
|
||
export async function clientSign(
|
||
method: string,
|
||
fullPath: string,
|
||
rawQuery: string,
|
||
): Promise<{ sig: string; ts: string; nonce: string }> {
|
||
const ts = String(Math.floor(Date.now() / 1000))
|
||
const nonce = Math.random().toString(36).slice(2) + Date.now().toString(36)
|
||
const msg = [method, fullPath, rawQuery, ts, nonce].join("\n")
|
||
const enc = new TextEncoder()
|
||
const key = await crypto.subtle.importKey(
|
||
"raw",
|
||
enc.encode(CLIENT_SIGN_SECRET),
|
||
{ name: "HMAC", hash: "SHA-256" },
|
||
false,
|
||
["sign"],
|
||
)
|
||
const buf = await crypto.subtle.sign("HMAC", key, enc.encode(msg))
|
||
const sig = Array.from(new Uint8Array(buf))
|
||
.map((b) => b.toString(16).padStart(2, "0"))
|
||
.join("")
|
||
return { sig, ts, nonce }
|
||
}
|