从自增ID到Keyed混淆:如何防止订单号被枚举扫描
2026/9/12 23:56:42 网站建设 项目流程

最近做一个小产品,到了联调阶段,分享链接里的订单号还是当年图省事直接透出的自增主键:/share/1024/share/1025。同事提了一句“你这个数字是不是能数出我们一天有多少单”,我盯着 URL 想了很久,发现这确实不是危言耸听。

我并不是说自增主键是错的。数据库里用单调整数做主键,索引、关联、分页都非常省心。问题出在把同一个 ID 直接暴露到对外链接上。一个可预测、可遍历的 ID,不只让订单量可以被轻松推算,还会给那些没有做好越权校验的接口提供一个天然扫把。

搜了一圈常见做法:有人建议直接换 UUID,有人建议用 Hashids,有人建议把 ID 加密后再放出去。这些方案各自有道理,但经常被混在一起推荐,很少有人把“加密、哈希、混淆”之间的差异讲清楚。直到我注意到一个在 Hacker News 上展示的项目标题:

Show HN: Arxid – keyed, non-enumerable ID obfuscation

keyednon-enumerableobfuscation这三个词,几乎就把这类方案的本质写完了。这篇文章不是替某个库写 README,而是展开聊聊这个标题背后的工程问题:keyed ID 混淆解决的是什么,它和 UUID、哈希、加密有什么边界,以及把它接进生产环境前,你应该想清楚哪些事。

先给一个贯穿全文的判断:keyed ID 混淆的真正价值,不是“把 ID 藏起来”,而是在不改变内部主键、不让 ID 失去可逆性的前提下,把“可猜测、可枚举、可推断规模”这三个暴露在系统边界上的问题拆掉。它是一层很薄的磨砂玻璃,挡住的是顺手扫描和好奇心,挡不住下定决心攻击你的人。任何把混淆当作安全兜底的方案,都还没真正理解这层玻璃的厚度。

1. 为什么自增 ID 暴露在链接里,是一个真实问题

1.1 一个数字 ID 透露的信息,往往超出你的预期

先还原一个常见场景。你的数据库里有一张订单表,主键是自增整数,第 10001 条订单的详情页是:

https://example.com/order/10001

把数字换一换,就能访问第 10000 条、第 10002 条。如果接口没有校验“当前用户是否属于这条订单”,这就是一个标准的越权访问问题,通常被叫做 IDOR(Insecure Direct Object Reference)。

但更隐蔽的是另一个问题:就算权限校验做得滴水不漏,攻击者访问不到别人的订单内容,他仍然可以沿着数字区间做大量请求,通过返回码、响应时间、页面状态来探测一个事实——你的平台到底有多少条订单。

这个信息有多敏感,取决于业务。对一个小社区来说,知道你有 5000 个用户,和知道你有 5000 个用户、且每周还在以 3 位数增长,完全是两个量级的信息。后者已经可以作为商业决策输入了。订单量、用户量、发票号段、草稿数量,这些业务指标会通过一串自增 ID 悄悄写在 URL 上。

1.2 枚举扫描和越权访问不是一回事,但常常连在一起

很多团队会把“ID 暴露”和“越权访问”混为一谈,然后得出一个结论:只要把接口的权限校验做好,ID 露出来也没关系。

这个结论只对了一半。权限校验解决的是“你能不能访问别人的资源”,枚举扫描解决的是“你的平台存在多少资源,以及资源以什么顺序产生”。前者是访问控制问题,后者是信息泄露问题。两者可以同时存在,也可以单独存在。

举个例子:一个接口对每个订单都校验了归属权,攻击者拿不到别人的订单,但他可以通过连续请求/order/1/order/100000,统计出哪些 ID 存在、哪些不存在,从而恢复出你的业务数据规模。这种情况下权限校验是好的,可信息仍然漏了。

所以,如果你对外的资源引用用的是自增 ID,就算你自认权限已经写得很完整,也应该在“对外可见的 ID 形态”这件事上多想想。枚举本身就是一种攻击面。

1.3 关键变化:从“隐藏”到“不可枚举”

有人会说,那我把 ID 从 URL 里去掉不就行了?比如通过 POST 参数、通过短链服务跳转。这在某些场景可行,但大多数情况下 ID 就是资源定位信息的一部分,你需要给用户、给合作方、给前端一个稳定引用。既然不能隐藏,那就必须让这个引用不可被预测。

“不可枚举”这个词,换成大白话是:给你一个合法 ID,你推不出下一个;给你一段合法序列,你无法在合理时间内遍历出完整集合。它不要求别人“看不见”,只要求别人“看见了也没法扩展”。

要做到这件事,最简单的两种路径:一种是让 ID 变得足够随机且足够长,例如 UUID v4;另一种是让 ID 的生成依赖一个别人不知道的秘密,也就是 keyed 方案。Arxid 走的是后一条路,这也是它和“直接换 UUID”最大的分水岭。

2. 拆解 Arxid 标题里的三个关键词

2.1 keyed:机密性来自密钥,而不是隐藏算法

先看keyed。这个词的意思是:混淆过程依赖一个密钥,而不是依赖某个“别人不知道的算法”。

很多初学的人会下意识觉得,只要我的置换逻辑足够复杂,别人看不出规律就行。但工程世界里有一条铁律:算法永远会泄露。你的代码可能被开源,可能被反编译,可能从一次版本泄露里流出去。如果混淆的安全完全建立在“算法保密”上,一旦代码曝光,整个方案就归零了。

keyed 方案则不同。哪怕把全部源码拿给你,只要密钥不泄露,你仍然无法从一个合法 token 推算出另一个。密钥给人的感觉像是一把钥匙:锁的结构公开没关系,没有钥匙就是打不开。

在落地层面,“keyed”通常意味着几个具体设计约束:

  • 密钥必须独立管理,不能硬编码在前端代码里。
  • 编码和解码必须使用同一个密钥体系,并且要考虑多环境(开发、测试、生产)的 key 隔离。
  • 密钥要能轮换。设计 token 结构时最好预留版本位,让解码方能根据版本选择对应的 key。

2.2 non-enumerable:不是“看不见”,是“推不出下一个”

再看non-enumerable。这个词是这个方案的核心卖点。

很多团队的直觉是:“我只要把 ID 搞复杂一点,别人就猜不到了。”于是有人把 10001 转成十六进制得到2711,或者直接 base64 一下得到MTAwMDE=,以为这样就安全了

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询