跳转至

B 模式签名规则(TOTP)

概述

B 模式是基于 TOTP(基于时间的一次性密码)算法的鉴权方式,通过时间戳和 URI 生成动态验证码,确保签名的时效性和安全性。
B 模式的签名是一个 6 位数字验证码,生成规则如下:

  • 使用 HMAC-SHA1 算法

签名格式

signature = 010203

参数说明

参数 说明
secret 客户端持有的鉴权密钥,必须在服务端 密钥 列表中
uri 请求路径(不包含参数,例如 https://test.com/api/order/list?id=1234 中的 /api/order/list
time_step 时间步长(秒),与平台中的 有效时间 一致

请求示例

GET /demo.js HTTP/1.1
Host: test.com
x-security-auth: 010203

节点鉴权

当节点服务器接收到客户端通过加密 URL 发出的请求时,生成验证码与请求 signature 中携带的验证码做比较:两值相同,鉴权通过,响应请求;
当两值不同时,将会再验证一次上一个时间窗口的验证码,两值相同,鉴权通过,响应请求;若与请求 signature 中携带的验证码也不相同,鉴权失败,返回 418。

JavaScript 实现示例

const crypto = require("crypto");

/**
 * Base64 URL 解码
 * @param {string} input Base64 URL 编码的字符串
 * @returns {Buffer} 解码后的 Buffer
 */
function decodeBase64Url(input) {
  const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
  const pad = "=".repeat((4 - (normalized.length % 4)) % 4);
  return Buffer.from(normalized + pad, "base64");
}

/**
 * 将数字转换为 8 字节 Buffer
 * @param {number} num 数字
 * @returns {Buffer} 8 字节 Buffer
 */
function int64ToBuffer(num) {
  const buf = Buffer.alloc(8);
  let n = BigInt(num);
  for (let i = 7; i >= 0; i--) {
    buf[i] = Number(n & 0xffn);
    n >>= 8n;
  }
  return buf;
}

/**
 * 生成 B 模式验证码
 * @param {object} options 选项
 * @param {string} options.secret 鉴权密钥
 * @param {string} options.uri 请求路径
 * @param {number} options.timeStep 时间步长,默认 30 秒
 * @param {number} options.now 当前时间戳,默认当前时间
 * @returns {string} 6 位数字验证码
 */
function generateCode({ secret, uri, timeStep = 30, now = Math.floor(Date.now() / 1000) }) {
  const counter = Math.floor(now / timeStep);
  const key = decodeBase64Url(secret);
  const msg = Buffer.concat([int64ToBuffer(counter), Buffer.from(uri || "", "utf8")]);
  const hmac = crypto.createHmac("sha1", key).update(msg).digest();

  const offset = hmac[19] & 0x0f;
  const binary =
    ((hmac[offset] & 0x7f) << 24) |
    (hmac[offset + 1] << 16) |
    (hmac[offset + 2] << 8) |
    hmac[offset + 3];

  return String(binary % 1_000_000).padStart(6, "0");
}

/**
 * 发起带鉴权的请求
 * @async
 */
async function requestWithAuth() {
  const secret = "HDA2G3TZIOUVKBWWAXX4UPAYWU";
  const uri = "/demo.js";
  const code = generateCode({ secret, uri, timeStep: 30 });

  const res = await fetch(`https://test.com${uri}`, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
      "x-security-auth": code, // 需与 signatureParameterName 一致
    }),
  });

  const body = await res.text();
  console.log(res.status, body);
}

失败返回

校验失败时,服务端返回:

  • HTTP 状态码:418

常见问题排查

  1. 请求头名不一致:客户端 Header 名与 签名请求头名称 不一致
  2. URI 不一致:客户端参与签名的 path 与服务端 请求路径 不一致
  3. time_step 不一致:客户端仍用 30 秒,但服务端已改为其他值(或反之)
  4. 密钥不一致:客户端 secret 不在服务端 密钥 列表中
  5. 系统时间偏差过大:客户端机器时间漂移导致验证码过期,建议开启 NTP