最近做一个小产品,到了联调阶段,分享链接里的订单号还是当年图省事直接透出的自增主键:/share/1024、/share/1025。同事提了一句“你这个数字是不是能数出我们一天有多少单”,我盯着 URL 想了很久,发现这确实不是危言耸听。
我并不是说自增主键是错的。数据库里用单调整数做主键,索引、关联、分页都非常省心。问题出在把同一个 ID 直接暴露到对外链接上。一个可预测、可遍历的 ID,不只让订单量可以被轻松推算,还会给那些没有做好越权校验的接口提供一个天然扫把。
搜了一圈常见做法:有人建议直接换 UUID,有人建议用 Hashids,有人建议把 ID 加密后再放出去。这些方案各自有道理,但经常被混在一起推荐,很少有人把“加密、哈希、混淆”之间的差异讲清楚。直到我注意到一个在 Hacker News 上展示的项目标题:
Show HN: Arxid – keyed, non-enumerable ID obfuscationkeyed、non-enumerable、obfuscation这三个词,几乎就把这类方案的本质写完了。这篇文章不是替某个库写 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=,以为这样就安全了