HMAC-SHA256 签名认证规范
如何通过密码学 HMAC-SHA256 签名算法对 SmallPict API 请求进行安全鉴权。
为了确保安全性,SmallPict API 全面采用 密码学 HMAC-SHA256 请求签名认证机制。
此标准在传输层提供三大核心安全保障:
- 载荷完整性 (Payload Integrity): 请求路径与 Body 内容在传输途中无法被恶意篡改。
- 防重放攻击 (Anti-Replay Attack): 严格的时间戳漂移窗口阻断被截获请求的重复利用。
- 零密钥泄露风险 (Zero Secret Exposure): 您的 Secret Key 永不在公网明文传输。
必需的 HTTP 请求头
每个受保护的 API 调用都必须在 HTTP Request Header 中包含以下 3 个字段:
HTTP
X-API-Key: sp_sdk_9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0bX-Timestamp: 1716301234X-Signature: 3a9f8b2c4d6e8a0f1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d1e3f5a7b9c1d3e5f7a| 请求头字段 | 数据类型 | 描述与取值规则 |
|---|---|---|
X-API-Key | String | 客户公开凭证(如 sp_sdk_..., sp_wp_..., sp_test_...) |
X-Timestamp | Integer String | 发起请求时的 Unix 时间戳(秒级整数字符串) |
X-Signature | Hex String | 使用 Secret Key 计算得到的 64 位十六进制 HMAC-SHA256 签名 |
签名生成算法
步骤 1:构建规范待签名字符串 (String-To-Sign)
使用换行符 (\n) 将 HTTP 方法、请求路径、时间戳与 Body 哈希依次拼接:
TEXT
{HTTP_METHOD}\n{PATH}\n{TIMESTAMP}\n{BODY_SHA256_HEX}{HTTP_METHOD}:大写的请求动词(如POST、GET、DELETE)。{PATH}:不带 Query 参数的精确请求 URI(如/v1/optimize、/v1/quota)。{TIMESTAMP}:与请求头X-Timestamp完全一致的当前时间戳。{BODY_SHA256_HEX}:请求 Body 原始字节的 SHA-256 十六进制哈希。对于GET请求或无 Body 请求,请使用空字符串的哈希值:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855。
步骤 2:计算 HMAC-SHA256 哈希
TEXT
Signature = hex(HMAC_SHA256(SecretKey, StringToSign))官方多语言签名代码实现
Node.js (TypeScript)
TypeScript
import crypto from 'node:crypto';
export function signRequest( method: string, path: string, secretKey: string, bodyString: string = '') { const timestamp = Math.floor(Date.now() / 1000).toString(); const bodyHash = crypto.createHash('sha256').update(bodyString).digest('hex'); const stringToSign = `${method.toUpperCase()}\n${path}\n${timestamp}\n${bodyHash}`; const signature = crypto .createHmac('sha256', secretKey) .update(stringToSign) .digest('hex');
return { timestamp, signature };}Python 3
Python
import hmacimport hashlibimport time
def sign_request(method: str, path: str, secret_key: str, body_bytes: bytes = b"") -> tuple[str, str]: timestamp = str(int(time.time())) body_hash = hashlib.sha256(body_bytes).hexdigest() string_to_sign = f"{method.upper()}\n{path}\n{timestamp}\n{body_hash}" signature = hmac.new( secret_key.encode("utf-8"), string_to_sign.encode("utf-8"), hashlib.sha256 ).hexdigest() return timestamp, signatureGo
Go
package main
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "strconv" "time")
func SignRequest(method, path, secretKey string, body []byte) (timestamp string, signature string) { ts := strconv.FormatInt(time.Now().Unix(), 10) h := sha256.Sum256(body) bodyHash := hex.EncodeToString(h[:]) stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s", method, path, ts, bodyHash)
mac := hmac.New(sha256.New, []byte(secretKey)) mac.Write([]byte(stringToSign)) return ts, hex.EncodeToString(mac.Sum(nil))}时间戳漂移校验窗口 (±300 秒)
为防范重放攻击,SmallPict 网关强制校验客户端时间戳与网关服务器标准时间的误差:
凡满足 |server_time - X-Timestamp| > 300 秒的请求,一律返回 401 Unauthorized (ERR_TIMESTAMP_DRIFT)。请确保发起请求的服务器开启了 NTP 时间自动校准。