4. 基于HMAC的一次性令牌设备认证
HMAC 一次性令牌的认证模型
本方案的核心是:设备与服务端共享一个预置密钥 K,设备在每次连接时用当前时间戳 ts 和随机数 nonce 构造消息,计算 HMAC-SHA256(K, deviceId | ts | nonce) 作为一次性令牌。服务端用同一密钥重算并比对,同时检查时间戳窗口与 nonce 是否已使用,从而同时实现身份认证与防重放。
令牌生成与校验的关键实现
设备端使用预置密钥对 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 倍即可。