最近做了一个需求,就是利用HMAC-SHA256 对接口进行签名,签名的流程是
将生成随机字符串、时间戳、待签名的请求数据,排序后,利用HMAC-SHA256结合 AppSercet 生成签名,放在请求的 header 里面
1、随机生成字符串
ts
/**
* 生成随机字符串
* @param length 字符串长度,默认32
* @returns 随机字符串
*/
export function generateNonce(length: number = 32): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let nonce = '';
for (let i = 0; i < length; i++) {
nonce += chars.charAt(Math.floor(Math.random() * chars.length));
}
return nonce;
}
2、 生成时间戳
javascript
/**
* 生成时间戳(毫秒)
* @returns 时间戳字符串
*/
export function generateTimestamp(): string {
return Date.now().toString();
}
3、对请求体进行排序
ts
/**
* 对data进行排序
* @param obj body
* @returns 排序后的对象
*/
export function canonicalString(obj: any): string {
if (obj === null || typeof obj !== "object") return JSON.stringify(obj);
if (Array.isArray(obj)) {
return "[" + obj.map(canonicalString).join(",") + "]";
}
// 对象:先过滤掉 undefined 的字段,再排序
const keys = Object.keys(obj)
.filter((key) => obj[key] !== undefined) // 过滤掉 undefined
.sort();
const kv = keys.map((k) => `"${k}":${canonicalString(obj[k])}`);
return "{" + kv.join(",") + "}";
}
4、 将时间戳、随机字符串、请求体拼接
ts
/**
* 生成API请求签名
* @param body 请求体(对象或null)
* @param timestamp 时间戳(可选,不提供则自动生成)
* @param nonce 随机串(可选,不提供则自动生成)
* @returns Promise<{signature: string, timestamp: string, nonce: string}>
*/
export async function generateSignature(
body: any = null,
timestamp?: string,
nonce?: string
): Promise<{
signature: string;
timestamp: string;
nonce: string;
}> {
// 生成时间戳和随机串
const ts = timestamp || generateTimestamp();
const n = nonce || generateNonce();
// 构建待签名字符串
let payload = canonicalString(data)
// 这里与后端约定好拼接的规则,不一定是这个
const signString = [payload, ts, n, API_CONFIG.APP02].join('&');
// 利用 APP_SECERT 计算签名
const signature = await calculateHMAC(signString, API_CONFIG.APP_SECERT);
return {
signature,
timestamp: ts,
nonce: n,
};
}
5、计算签名
在这里是碰到一个问题,最开始的写法是使用 Web Crypto API, 在线上(http)会有问题,本地 localhost 问题, 原因是 crypto.title 为 false, 所以后续的逻辑走不下去,原因是这个仅支持 localhost 和 https,所以换了一种方案,使用crypto-js 来计算签名,这是使用纯 js 实现的,与浏览器 API 没有关系,支持 http/https
ts
/**
* 使用 HMAC-SHA256 计算签名
* @param message 待签名字符串
* @param secret 密钥
* @returns Promise<string> 签名字符串(hex格式)
*/
export async function calculateHMAC(
message: string,
secret: string
): Promise<string> {
// 使用 crypto-js 计算 HMAC-SHA256
const hash = CryptoJS.HmacSHA256(message, secret);
const hashHex = hash.toString(CryptoJS.enc.Hex);
return hashHex
// // 使用 Web Crypto API
// const encoder = new TextEncoder();
// const keyData = encoder.encode(secret);
// const messageData = encoder.encode(message);
// alert(`keyData-${keyData}`)
// alert(`messageData-${messageData}`)
// alert(`Before importKey, crypto.subtle exists: ${!!crypto.subtle}`);
// // 导入密钥
// let cryptoKey;
// try {
// cryptoKey = await crypto.subtle.importKey(
// "raw",
// keyData,
// {
// name: "HMAC",
// hash: "SHA-256",
// },
// false,
// ["sign"]
// );
// alert(`After importKey`);
// } catch (error: any) {
// alert(`importKey error: ${error.message}`);
// throw error;
// }
// // 计算签名
// const signature = await crypto.subtle.sign(
// 'HMAC',
// cryptoKey,
// messageData
// );
// alert(`signature-${signature}`)
// // 转换为十六进制字符串
// const hashArray = Array.from(new Uint8Array(signature));
// alert(`hashArray-${hashArray}`)
// const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
// alert(`hashHex-${hashHex}`)
// return hashHex;
}
6、 为请求添加 header 参数
ts
/**
* 为请求添加签名Header
* @param headers 现有的请求头对象
* @param body 请求体
* @returns Promise<Record<string, string>> 包含签名信息的请求头
*/
export async function addSignatureHeaders(
headers: Record<string, string> = {},
body: any = null
): Promise<Record<string, string>> {
const { signature, timestamp, nonce } = await generateSignature(body);
return {
...headers,
'X-App-Id': API_CONFIG.APP_ID,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
};
}
现在我要弄清楚,为什么要签名,其实就是为了告诉请求服务器是自己人发的,防止篡改数据,防止伪装数据。密钥前后端约定好, 签名用在无 access_token 的场景中,通常是发送验证码等场景
SHA256是一种哈希算法,转换成一串固定的 64 位十六进制字符串(叫 "哈希值")。它的特点是 "不可逆"------ 只能从内容算出哈希值,没法从哈希值反推回内容;而且内容只要改一个字符,哈希值就会完全不同。
HMAC 是SHA256的基础上还要加上一个 "只有你和对方知道的密钥"。这样一来,就算别人拿到原始内容,没有密钥也算不出正确的哈希值,安全性比单纯的 SHA256 更高。