请求签名(Request Signature)

请求签名(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)");
});
相关推荐
Cocktail_py4 小时前
JS如何调用wasm
开发语言·javascript·wasm
Jonathan Star5 小时前
基于 **Three.js** 开发的 3D 炮弹发射特效系统
javascript·数码相机·3d
Heo5 小时前
原型理解从入门到精通
前端·javascript·后端
Heo5 小时前
通用会话控制方案
前端·javascript·后端
Heo5 小时前
跨域问题解决方案汇总
前端·javascript·后端
shmily麻瓜小菜鸡5 小时前
Element Plus 的 <el-table> 怎么点击请求后端接口 tableData 进行排序而不是网络断开之后还可以自己排序
前端·javascript·vue.js
二川bro6 小时前
第38节:WebGL 2.0与Three.js新特性
开发语言·javascript·webgl
xiaoxue..6 小时前
深入理解 JavaScript 异步编程:从单线程到 Promise 的完整指南
前端·javascript·面试·node.js
倚肆6 小时前
HTMLElement 与MouseEvent 事件对象属性详解
前端·javascript