B 模式签名规则(TOTP)¶
概述¶
B 模式是基于 TOTP(基于时间的一次性密码)算法的鉴权方式,通过时间戳和 URI 生成动态验证码,确保签名的时效性和安全性。
B 模式的签名是一个 6 位数字验证码,生成规则如下:
- 使用 HMAC-SHA1 算法
签名格式¶
参数说明¶
| 参数 | 说明 |
|---|---|
| secret | 客户端持有的鉴权密钥,必须在服务端 密钥 列表中 |
| uri | 请求路径(不包含参数,例如 https://test.com/api/order/list?id=1234 中的 /api/order/list) |
| time_step | 时间步长(秒),与平台中的 有效时间 一致 |
请求示例¶
节点鉴权¶
当节点服务器接收到客户端通过加密 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
常见问题排查¶
- 请求头名不一致:客户端 Header 名与
签名请求头名称不一致 - URI 不一致:客户端参与签名的 path 与服务端
请求路径不一致 - time_step 不一致:客户端仍用 30 秒,但服务端已改为其他值(或反之)
- 密钥不一致:客户端
secret不在服务端密钥列表中 - 系统时间偏差过大:客户端机器时间漂移导致验证码过期,建议开启 NTP