请求签名(Request Signature)是 网络请求中用于 "身份验证" 和 "防篡改" 的安全机制
1. 技术原理
后端要求前端在发送请求时,附加一个 "签名串"(通常放在请求头或参数中)。签名由 请求参数、时间戳、随机串、密钥 等按固定规则组合加密生成,后端接收后用相同规则验证签名:
若参数被篡改、签名不匹配 → 直接拒绝请求;
结合时间戳可防止 "过期请求重放"(比如签名有效期 5 分钟)。
2. 核心要素(缺一不可)
密钥(secretKey):前后端约定的 "暗号",仅双方知晓,不可泄露;
时间戳(timestamp):防止请求被重复使用(后端校验时间差,如 ±300s);
随机串(nonce):增加签名唯一性,防止字典攻击;
排序 + 加密:参数按字典序排序(避免顺序影响签名),再用 MD5/SHA256 等哈希算法加密。
javascript
// 前端配置
const config = {
baseUrl: "http://localhost:3000",
secretKey: "my_secure_secret_2025",
apiPath: "/api/order"
};
crypto=require("crypto")
// 1. 生成 32 位加密安全随机串(X-Nonce)
function generateNonce() {
return Array.from(crypto.getRandomValues(new Uint8Array(16)),
b => b.toString(16).padStart(2, "0")
).join("");
}
// 2. 构造规范查询串(key 升序 + URI 编码)
function buildQueryStr(params = {}) {
return Object.keys(params)
.sort()
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join("&");
}
// 3. 生成 HMAC-SHA256 签名(核心步骤)
async function generateSignature(method, uri, params, body, nonce, timestamp) {
// 按规则拼接待签名字符串
const queryStr = buildQueryStr(params);
const bodyStr = JSON.stringify(body);
const signStr = [
method.toUpperCase(), // 方法大写(统一格式)
uri, // 接口路径(如 /api/order)
queryStr, // 查询串(空则为 "")
bodyStr, // 请求体(空则为 "{}")
nonce, // 随机串
timestamp.toString() // 时间戳(转字符串)
].join("\n"); // 用换行符分隔
// 浏览器 Web Crypto API 计算 HMAC-SHA256
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(config.secretKey),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signatureBuffer = await crypto.subtle.sign("HMAC", key, encoder.encode(signStr));
// 转为十六进制字符串(后端需相同格式)
return Array.from(new Uint8Array(signatureBuffer))
.map(b => b.toString(16).padStart(2, "0"))
.join("");
}
// 4. 发送带签名的请求(最终调用)
async function sendSignedRequest() {
try {
// 准备请求参数
const method = "POST";
const params = { page: 1, size: 10 }; // URL 查询参数
const body = { userId: "user_888", goodsId: "goods_123", amount: 99 }; // 请求体
const nonce = generateNonce(); // 生成随机串(如:"a3f8d2e7c9b0456789abcdef01234567")
const timestamp = Date.now(); // 生成时间戳(如:1731688800000)
// 生成签名(核心结果)
const signature = await generateSignature(
method, config.apiPath, params, body, nonce, timestamp
);
console.log("生成的签名:", signature); // 示例输出:"7e2a9d3f8b4e6c5a0d1e2f3g4h5i6j7k8l9m0n1o2p3q4r5s6t7u8v9w0x1y2z3"
// 拼接请求 URL(含查询参数)
const queryStr = buildQueryStr(params);
const url = `${config.baseUrl}${config.apiPath}${queryStr ? `?${queryStr}` : ""}`;
// 发送请求(带签名头)
const response = await fetch(url, {
method,
headers: {
"Content-Type": "application/json",
"X-Timestamp": timestamp.toString(), // 时间戳头
"X-Nonce": nonce, // 随机串头
"X-Signature": signature // 签名头
},
body: JSON.stringify(body)
});
const result = await response.json();
console.log("请求结果:", result); // 打印后端响应
return result;
} catch (error) {
console.error("请求失败:", error);
}
}
// 执行请求(启动前端流程)
sendSignedRequest();
javascript
const express = require("express");
const cors = require("cors");
const redis = require("redis");
const crypto = require("crypto");
const app = express();
// 后端配置(与前端完全一致)
const config = {
secretKey: "my_secure_secret_2025",
timeOffset: 300000, // 5分钟时间戳误差(ms)
redisUrl: "redis://127.0.0.1:6379",
port: 3000
};
// 初始化 Redis 客户端(缓存 nonce 防重复请求)
const redisClient = redis.createClient({ url: config.redisUrl });
redisClient.connect().catch(err => console.error("Redis 连接失败:", err));
// 中间件:解析 JSON 请求体 + 跨域支持
app.use(cors());
app.use(express.json());
// 1. 构造规范查询串(与前端完全一致的逻辑)
function buildQueryStr(params = {}) {
return Object.keys(params)
.sort()
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join("");
}
// 2. 签名验证中间件(核心步骤,对应验证流程 1-6)
async function verifySignature(req, res, next) {
try {
// 步骤1:从请求头取出关键信息(X-Timestamp/X-Nonce/X-Signature)
const { "x-timestamp": timestamp, "x-nonce": nonce, "x-signature": signature } = req.headers;
if (!timestamp || !nonce || !signature) {
return res.status(401).json({ code: -1, msg: "缺少签名相关请求头" });
}
// 步骤2:验证时间戳有效性(防重放攻击)
const currentTime = Date.now();
if (Math.abs(currentTime - Number(timestamp)) > config.timeOffset) {
return res.status(401).json({ code: -1, msg: "时间戳无效(请求已过期或未来时间)" });
}
// 步骤3:验证随机串唯一性(防重复请求)
const nonceKey = `sign_nonce:${nonce}`;
const nonceExists = await redisClient.get(nonceKey);
if (nonceExists) {
return res.status(401).json({ code: -1, msg: "重复请求(nonce 已使用)" });
}
// 步骤4:按前端规则拼接待签名字符串
const method = req.method.toUpperCase();
const uri = req.path; // 接口路径(如 /api/order)
const queryStr = buildQueryStr(req.query); // URL 查询参数
const bodyStr = JSON.stringify(req.body); // 请求体(与前端 JSON.stringify 一致)
const signStr = [method, uri, queryStr, bodyStr, nonce, timestamp].join("\n");
console.log("后端拼接的待签名字符串:", signStr);
// 步骤5:使用相同 SecretKey 计算预期签名
const expectedSignature = crypto
.createHmac("sha256", config.secretKey)
.update(signStr, "utf8")
.digest("hex"); // 转为十六进制字符串(与前端格式一致)
console.log("后端计算的预期签名:", expectedSignature);
console.log("前端传递的签名:", signature);
// 步骤6:对比签名,一致则通过
if (signature !== expectedSignature) {
return res.status(401).json({ code: -1, msg: "签名验证失败(参数篡改或密钥不一致)" });
}
// 缓存 nonce(设置过期时间,与时间戳误差一致)
await redisClient.setEx(nonceKey, config.timeOffset / 1000, "used");
// 验证通过,进入业务逻辑
next();
} catch (error) {
console.error("签名验证异常:", error);
res.status(500).json({ code: -1, msg: "服务器内部错误" });
}
}
// 3. 业务接口(需签名验证才能访问)
app.post("/api/order", verifySignature, (req, res) => {
// 这里是你的业务逻辑(如创建订单、查询数据等)
const { userId, goodsId, amount } = req.body;
const { page, size } = req.query;
// 返回成功响应
res.json({
code: 0,
msg: "请求成功(签名验证通过)",
data: {
orderId: `order_${Date.now()}`, // 生成随机订单号
userId,
goodsId,
amount,
page: Number(page),
size: Number(size),
status: "success"
}
});
});
// 启动后端服务
app.listen(config.port, () => {
console.log(`后端服务启动:http://localhost:${config.port}`);
console.log("请确保 Redis 已启动(默认端口 6379)");
});