4. 基于HMAC的一次性令牌设备认证

基于HMAC的一次性令牌设备认证 学习卡片

HMAC 一次性令牌的认证模型

本方案的核心是:设备与服务端共享一个预置密钥 K,设备在每次连接时用当前时间戳 ts 和随机数 nonce 构造消息,计算 HMAC-SHA256(K, deviceId | ts | nonce) 作为一次性令牌。服务端用同一密钥重算并比对,同时检查时间戳窗口与 nonce 是否已使用,从而同时实现身份认证与防重放。

设备端 预置密钥 K deviceId = "sensor-042" 时间戳 ts Unix 秒级,如 1718000000 随机数 nonce 16 字节随机,每次不同 HMAC-SHA256 计算 token = HMAC(K, msg) 发送认证请求 {deviceId, ts, nonce, token} 服务端 接收请求 {deviceId, ts, nonce, token} 时间窗口检查 |now - ts| ≤ 30s Nonce 去重检查 Redis SETNX nonce HMAC 重算比对 expected = HMAC(K, msg) 返回认证结果 200 OK / 401 Unauthorized ① 发送认证请求 ② 返回认证结果 重放攻击拦截 同一 nonce 二次提交 → 拒绝 过期 ts 提交 → 拒绝

令牌生成与校验的关键实现

设备端使用预置密钥对 deviceId|ts|nonce 拼接串计算 HMAC-SHA256,输出十六进制字符串作为令牌。服务端收到请求后,先检查时间戳是否在允许窗口内(建议 ±30 秒),再检查 nonce 是否已被使用(可用 Redis 的 SETNX 配合过期时间实现),最后用相同密钥重算 HMAC 并做常量时间比较。

import hmac, hashlib, time, os, json

# 设备端:生成一次性令牌
def generate_token(device_id: str, secret: bytes) -> dict:
    ts = int(time.time())
    nonce = os.urandom(16).hex()
    msg = f"{device_id}|{ts}|{nonce}".encode()
    token = hmac.new(secret, msg, hashlib.sha256).hexdigest()
    return {"deviceId": device_id, "ts": ts, "nonce": nonce, "token": token}

# 服务端:校验令牌
def verify_token(payload: dict, secret: bytes, used_nonces: set, window: int = 30) -> bool:
    device_id = payload["deviceId"]
    ts = payload["ts"]
    nonce = payload["nonce"]
    token = payload["token"]

    # 1. 时间窗口检查
    if abs(int(time.time()) - ts) > window:
        return False

    # 2. Nonce 去重检查
    if nonce in used_nonces:
        return False

    # 3. HMAC 重算比对(常量时间比较)
    msg = f"{device_id}|{ts}|{nonce}".encode()
    expected = hmac.new(secret, msg, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, token):
        return False

    used_nonces.add(nonce)
    return True

参数选择与安全边界

参数推荐值说明
时间窗口±30 秒需考虑设备时钟漂移与网络延迟,过小易误拒,过大增加重放窗口
Nonce 长度16 字节(128 位)随机数空间足够大,碰撞概率可忽略
Nonce 存储过期2 × 时间窗口确保过期 nonce 自动清理,避免内存无限增长
HMAC 算法SHA-256输出 256 位,抗碰撞与抗长度扩展攻击
密钥长度≥ 32 字节与 SHA-256 分组长度匹配,保证 HMAC 安全强度
设备端时钟必须保持同步(建议 NTP),否则时间窗口校验会失败。若设备无法保证时钟精度,可改用服务端下发挑战值的方式(见后续案例),但本方案在时钟可控的局域网或蜂窝物联网场景中足够有效。
服务端应使用 hmac.compare_digest 而非 == 比较令牌,避免时序侧信道攻击。Nonce 去重集合建议使用 Redis 并设置 TTL,TTL 设为时间窗口的 2 倍即可。