从加密到“一机一密”签名方案的改进与实践

前言

在移动端、IoT设备与后端通信的场景中,如何安全、高效地验证设备身份(鉴权)是架构设计的核心。本文将记录我们从最初的“加密模式”演进到高性能“一机一密签名模式”的过程,并深入探讨其中的技术细节与架构权衡

为什么放弃加密,改用签名

在早期方案中,我们尝试直接对敏感信息进行AES对称加密

  • 做法:客户端将SN + Timestamp通过AES加密发给服务端,服务端解密验证
  • 痛点
    1. 性能开销大:解密运算对CPU的消耗远高于哈希运算,在高并发下容易成为瓶颈。
    2. 抗攻击能力弱(拒绝成本高):面对大规模DDoS攻击,服务端必须对每个垃圾请求执行解密才能判断其非法,极易导致 CPU 挂起
    3. 调试困难Header中全是加密乱码,排查问题时无法直观看到设备号和时间

改进思路: 采用签名机制。即:明文传输信息 + 摘要校验

核心架构:一机一密

为了防止“一处泄露,全线崩溃”,我们摒弃了全局共享密钥,采用“一机一密”

1. 密钥的生成与分发

  • 生成逻辑RealKey = SHA256("固定随机字符串" + 序列号)。利用SHA-256作为“转换器”,将短字符转换为标准的256位随机密钥
  • 激活接口:设备首次启动时,调用HTTPS激活接口,服务端下发该RealKey,客户端保存于本地安全区域

2. 存储策略:性能与灵活性的权衡

我们最终选择了 “存储型(三级缓存)” 方案,而非“实时计算型”:

  • 三级缓存架构本地缓存 + Redis + MySQL
  • 为什么不选实时计算? 虽然计算不占存储,但无法实现“精细化撤销”。如果某个设备密钥泄露,计算型方案必须通过复杂的黑名单逻辑拦截;而存储型方案只需删除对应的Key即可。

签名算法实现:从 SHA-256 到 HMAC

在拼接签名串时,我们做了两项重要改进:

1. 保护全量数据

不仅拼接 snts,还将 Method、URI、Body 纳入签名范围

str = "sn="+sn+"&ts="+ts+"&method=POST&uri=/api/pay&body="+md5(requestBody)
这彻底杜绝了攻击者拦截 Header 后篡改 Body 的风险

2. 引入 HMAC 标准

不直接使用 SHA256(str + key),而是采用 HMAC-SHA256

  • 原因HMAC 在内部做了两次哈希处理,能有效抵御长度扩展攻击(Length Extension Attack),是目前工业界身份验证的标准结构

后端鉴权流:高并发与一致性

服务端通过 AOP 拦截器实现无侵入鉴权,流程如下:

1. 拦截与取 Key(漏斗模型)

  1. L1(本地内存):微秒级读取
  2. L2(Redis):毫秒级读取,回填 L1
  3. L3(DB):兜底读取,回填 RedisL1

2. 验证逻辑

  1. 快速拒绝:检查 ts 是否过期、sn 是否存在 ⬅️ 盲点1:Redis缓存三剑客问题
  2. 签名比对:服务端计算 mySign,与 Header 传来的 sign 进行常量时间比对

⬆️盲点2

华生发现了盲点:既然要先检查 sn 是否存在/黑名单,不是符合计算型的特征吗?为啥还要用存储型呢?

3. 数据一致性处理

当我们需要“撤销密钥”或“更换密钥”时,会遇到三级缓存同步问题

  • 我们的选择旁路缓存 (Cache Aside) + TTL
  • 写操作:先更新 DB -> 再删除 Redis
  • 本地缓存处理:设置 1~5 分钟的短 TTL,利用过期策略实现“最终一致性
  • 结论:在密钥撤销场景下,没必要引入 MQCanal延时双删。密钥变更极低频,TTL 带来的几分钟延迟在业务上是完全可控的

何时需要更高级的“非对称签名”?

“非对称签名”参考HTTPS的数字签名

如果场景涉及 第三方对接(不信任对方数据库)极高法律效应(不可抵赖性),请考虑使用 RSA/ECDSA。对于企业内部 APPIoT 闭环生态,本文的 HMAC + 一机一密 方案在性能、开发成本和安全性之间达到了最优平衡

⬅️盲点1解析

缓存穿透问题是会出现的,因为Redis中存的key物理永不过期,所以不会出现缓存击穿的问题,但是有本地缓存带来的“微击穿

防穿透:我们在 AOP 层引入了 布隆过滤器。只有合法的序列号才能进入缓存查询流,非法 SN 连缓存都碰不到

防击穿:在本地缓存查询处加一个互斥锁,确保同一台设备只有一个线程去查Redis

⬆️盲点2解析

既然第一步””快速拒绝””(黑名单)都要查 Redis,说明网络 IO 已经发生了,那为什么不顺便在这一步用计算型 Key,反而要选择 存储型 方案呢?

1. “封禁设备”与“更换密钥”的区别(核心原因)

计算型 + 黑名单

  • 密钥是写死的(SHA256(MasterSecret + SN)

  • 如果设备A的密钥泄露了,你只能把设备 ASN 关进小黑屋(黑名单)

  • 没法给设备A换一个新密钥。只要它从小黑屋出来,它的密钥还是原来那个泄露掉的密钥

有人觉得MasterSecret可以是随版本变化的,然后可以用Nacos来管理,但不可能为了把设备A放出小黑屋,就强制所有设备更新到最新版本,显然很不合理

存储型

  • 密钥是随机生成的
  • 如果设备 A 的密钥泄露了,你可以直接生成一个新的随机密钥覆盖掉旧的
  • 优势:设备 A 依然可以正常工作,只是它的“通行证”换了

2. 缓存利用率的差异

存储型方案

  1. 先看本地缓存(L1

  2. 如果有 Key,直接拿来验签。(0 网络 IO)

  3. 验证通过,直接放行

注意: 在这种情况下,我们不需要查 Redis 黑名单。因为“Key 在不在”本身就代表了权限。如果设备被禁,我们只需要清理DBRedis和本地缓存,它下一次进来时由于拿不到Key,自然就被“快速拒绝”了

计算型方案

  1. 计算 Key
  2. 必须Redis 查一下这个 SN 在不在黑名单里
  3. 如果为了省这步 IO 也在本地存黑名单,那你就要面临**“双重状态同步”**(既要同步 MasterSecret 变化,又要同步黑名单变化),复杂度反而更高

3. 安全性权衡(极端情况)

计算型

  • 全公司的安全都系于那一个 MasterSecret
  • 如果黑客攻破了后端服务器,拿到了这个字符串,他可以瞬间批量算出全球所有设备的私密密钥

存储型

  • 数据库里存的是成千上万个随机密钥
  • 即便黑客攻破了一台服务器,拿到了部分缓存或数据库,他也只能控制这一批设备,无法通过某个公式推导出未泄露设备的密钥

4. 逻辑简明程度

存储型逻辑

  • 鉴权只需要问一个问题:“这个 SN 对应的密钥是什么?” ⬅️ Redis缓存三剑客问题
  • 拿到了就验签,拿不到就拒绝,逻辑极其线性

计算型逻辑

  • 鉴权需要问两个问题:“根据公式算的密钥对吗?”“它在黑名单里吗?”
  • 这增加了 逻辑耦合

5. 什么情况“计算型”最佳

如果你的设备量达到了亿级(如共享单车、智能电表),且你根本不打算提供“更换密钥”的功能