尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

C端自增ID暴露隐患解析:Hashids短ID混淆方案与落地实践

发布时间:2026/9/9 12:02:05

资讯中心
01
ARTICLE

C端自增ID暴露隐患解析:Hashids短ID混淆方案与落地实践

C端自增ID暴露隐患解析:Hashids短ID混淆方案与落地实践
开发C端项目这么多年有一个问题几乎每个后端都会撞上数据库里那一串连号的自增ID直接被塞进了URL、接口响应和前端页面。刚开始觉得没什么无非是/order/100001、/user/123456这种形式后来在安全问题和技术改造上栽了几次跟头才意识到这个看似不起眼的细节其实值得认真对待。我最后的解法是用Hashids这类带盐编码库给ID套一层壳在不改数据库、不动索引的前提下把对外暴露的ID全部换成不可猜测的短串。这篇文章就把这个思路完整拆一遍讲清楚为什么自增ID在C端是个隐患、我为什么在众多方案里选了Hashids、它背后的原理是什么以及完整的落地步骤和踩坑记录给正在纠结ID暴露问题的同学一个可以直接参考的实践方案。1. 问题剖析C端自增ID暴露的隐患到底在哪里1.1 自增ID为什么在C端这么普遍自增ID几乎是互联网后端最流行的主键方案没有之一。原因很简单MySQL的InnoDB引擎对主键有聚簇索引自增ID是顺序写入B树的分裂和页填充都处于最优状态插入性能好索引空间也小。相比UUID这种随机散列的主键自增ID在数据量大、写入频繁的业务里优势非常明显这也是DBA和架构师默认首选它的原因。但问题在于自增ID是连续的、可预测的。C端产品一旦把ID直接暴露给用户等于把业务增长情况、订单规模、用户数量全部摊开在别人面前。拿电商举例用户下完单看到/order/10001顺手改成/order/10002如果接口没做权限校验就能看到别的用户的订单信息。即使校验做得好这种连续ID也会诱导人去遍历无形中增加了接口的恶意请求量。我见过很多团队会把自增ID当成“内部细节”觉得没人会闲到去逐个改URL。这个想法在早期产品上可能没大问题等用户量上来之后各种爬虫、脚本、恶意遍历都会盯上这些连续的ID到那时候再改就麻烦了。前端页面、收藏链接、App缓存、第三方回调到处都散落着老的数字ID改起来牵一发动全身。1.2 暴露ID会引发哪些具体风险把ID暴露在C端实际风险可以分成几个层次来看。越权访问风险这是最直接的。如果ID是连续的攻击者不需要任何技术只需要把URL里的数字改一下就能尝试访问别人的资源。订单详情、个人资料、发票信息、优惠券只要能猜到ID就能去试探。就算后端做了鉴权也不能指望每个接口都严格校验总会有疏漏的地方。业务数据泄露风险。通过观察ID的变化范围可以估算出平台每天产生的订单量、注册用户量、资源增长速率。这些数据对竞品分析来说非常有价值。比如一个拼车平台用两个时间点分别注册两个账号根据用户ID差值就能算出这期间的注册量再结合活动时间点基本就能反推出拉新效果。增加恶意攻击面。连续ID天然适合自动化工具遍历。批量注册、批量薅羊毛、批量爬取公开内容这些操作的前提就是ID可预测。把ID换成不可枚举的随机串虽然不能完全阻止恶意请求但至少让自动化脚本的成本提高了一大截。SEO和隐私层面的意外问题。有些C端产品会生成带ID的分享链接连续ID会被搜索引擎收录很多不相关的页面还会导致重复内容问题。另外把自增ID暴露在页面源码、接口返回里也会让用户信息在不知不觉中被关联。1.3 一个真实案例的还原有一次我负责的活动系统上线活动报名接口返回的是报名记录的ID前端拿到之后跳转到详情页URL长这样/signup/detail/88621。上线当天就有人写了个脚本从1到10万把接口扫了一遍直接把所有报名用户的姓名、手机号、报名信息全拉走了。这个事情当时挺严重的最后排查下来接口确实做了登录校验但只校验了“是否登录”没有校验“这条报名记录是不是当前用户的”。这个案例让我深刻认识到一件事连续ID是越权漏洞的放大器。如果当初ID是不可枚举的就算接口鉴权有疏漏攻击者也无法轻易遍历数据风险等级会低很多。后来我把所有C端对外ID都换成了Hashids再配合权限校验即便某个接口偶尔漏了鉴权外人也无法在有限时间内批量拉取数据。2. 方案选型为什么我最终选了 Hashids2.1 绕不开的4个替代方案对比在处理ID暴露问题时网上常见的方案大致有四种我逐一对比过各有优劣但最后还是选了Hashids。方案一UUID替换主键。UUID本身不具备规律性暴露出去不存在枚举问题。但代价非常大主键从INT/BIGINT变成CHAR(36)存储空间膨胀好几倍索引性能下降InnoDB聚簇索引的随机插入还会导致页分裂严重。大数据量的表如果已经用自增ID建好了迁移成本更是难以接受。而且UUID本身并不是一个“友好”的ID放在URL里太长。方案二雪花IDSnowflake或其他分布式ID。64位整型不连续但可预测通过ID的组成结构可以推断出时间戳和机器编号部分场景下仍然存在信息泄露问题。而且它同样要改数据库字段和所有写入逻辑对存量系统工程量巨大。方案三Base64编码原始ID。这算是最偷懒的做法把数字转成字母串就完事。Base64是可逆的而且没有任何密钥谁拿到都能秒解。123456编码后是某种规则符号写个十几行代码就能还原出数字这跟直接暴露ID没什么本质区别。方案四数据库新增映射表。维护一张hash_id和原始ID的映射表代价是要多查一次库、多维护一份数据还要处理映射生成的并发问题比较适合老系统改造时的过渡方案但从长期看增加了系统复杂度。这几种方案对比下来Hashids的优势很明显不改变主键类型、不改数据库结构、代码侵入极小而且自带盐值混淆同一个数字在不同盐值下编码结果完全不同能有效防止被猜解。2.2 Hashids 的定位与适用边界Hashids从名字就能看出来它的定位是“生成适合展示给用户的短ID”而不是一个加密工具。它的本质是带盐的、可逆的编码算法和加密算法是两个维度的东西。Hashids适合的场景是解决ID被枚举、被猜测的问题让对外URL更干净让爬虫和遍历脚本的成本更高。它适合订单号、用户ID、资源ID、活动ID这类业务标识。它不适合的场景有两类。第一类是真正需要高安全等级的数据比如支付流水、提现凭证、内部权限标识这些场景应该使用正规的加密方案或数字签名而不是靠Hashids去保护。第二类是数据本身敏感、就算编码了也不能暴露的场景比如手机号、身份证号这些任何时候都不应该出现在URL或者可逆编码里。我见过一些团队把Hashids当成万能钥匙所有接口参数都用它编码实际上这是有风险的。Hashids的官方文档也反复强调它不是一个安全加密库应该把它定位成“防枚举混淆层”真正的安全措施权限校验、鉴权、限流一个都不能少。3. 原理拆解Hashids 是怎么把数字“变”成短串的3.1 它不是哈希是带盐编码很多同学第一次接触Hashids会误以为它和MD5、SHA256一样是单向散列其实差别非常大。哈希是单向的不可逆Hashids是可逆的它专门提供了decode方法只要知道盐和算法就能把编码后的字符串还原成原始数字。Hashids内部做的事情可以简单理解为输入一串非负整数算法先对它们做混合运算把数字间的相互影响打散然后映射到自定义字母表上再通过进制转换得到一串看起来像随机单词的字符串。整个过程引入了盐Salt和字母表Alphabet作为输入所以同样的数字使用不同的盐会得到完全不同的编码结果。因为它可逆、又有盐值保护所以非常适合“对外展示不可猜、对内解析方便”的场景。从产品视角看它和短链接的思路有点像都是把一个长数字映射成短串但Hashids不需要额外维护映射关系这点比短链服务轻量得多。3.2 三个关键参数的决定方式盐值Salt是Hashids安全性的核心。盐相当于编码的密钥必须妥善保管。实际项目中我会用一个至少16位的随机字符串最好包含大小写字母、数字和特殊符号并且越没规律越好。有一点必须注意盐值一旦配置到生产环境就不要再随意修改了否则所有历史编码都无法解码。这个我后面会详细讲踩坑经历。最小长度Min Hash Length决定编码后字符串的最短长度。默认是0意味着数字小时编码结果会很短比如1个字符数字变大后会自动变长。实际项目中我一般设成8~10太短了容易被枚举太长了又失去短ID的意义。需要说明的是这个长度是“最小”长度不是固定长度当原始数字非常大时输出仍然可能超过这个值。字母表Alphabet决定编码用什么字符集。默认是大小写字母加数字一共60多个字符。如果ID会出现在URL里建议去掉URL特殊字符比如-、_如果担心用户输错或者视觉混淆可以把容易混淆的字符剔除比如0O、1lI。我看到有一些团队为了让ID更短会使用自定义字母表这个完全可行只要保证字母表长度不少于16个字符即可这是Hashids库的强制要求。3.3 多ID联合编码与解码还原Hashids还支持把多个数字编码成一个字符串解码后返回一个数组。这个特性在处理复合主键、多参数场景时非常有用。举个例子某个资源属于某个用户URL里需要同时带上用户ID和资源ID传统做法是/resource/123/456用Hashids可以编码成一个串/resource/kQ8xMv2z更简洁也更难猜。不过要注意解码后返回的一定是数组即使是单个数字编码也要记得取数组第一个元素。这点在不同语言的实现里略有区别但在使用时都要处理数组下标问题避免出现类型转换异常。另外Hashids只能编码非负整数这是它的硬性限制。对于负数、浮点数是无法编码的。好在自增ID天然是正整数所以这个限制在实际使用中并不构成障碍。如果你有其它类型的ID需要混淆比如13位的时间戳虽然可以编码但要意识到编码后的字符串会非常长反而达不到短ID的效果。4. 实操落地从接口层接入到历史数据兼容4.1 不同语言环境下的最小可用代码Hashids官方提供了很多语言版本的实现包括JavaScript、Python、Java、Go、C#、PHP等。我实际用过的几个语言写出来给同学们参考。Node.js版本使用的是hashids库const Hashids require(hashids/cjs); const hashids new Hashids(your-random-salt-here, 8, abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890); // 编码 const id 100001; const hash hashids.encode(id); console.log(hash); // 输出类似 Qk5d8m2x // 解码 const decoded hashids.decode(hash); console.log(decoded); // [100001]Python版本使用hashids库from hashids import Hashids hashids Hashids(saltyour-random-salt-here, min_length8, alphabetabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890) id 100001 hash hashids.encode(id) print(hash) # 输出类似 Qk5d8m2x decoded hashids.decode(hash) print(decoded) # (100001,)Java版本使用org.hashids:hashidsHashids hashids new Hashids(your-random-salt-here, 8); String hash hashids.encode(100001L); long[] numbers hashids.decode(hash); // numbers[0] 100001L整体来说各语言的API设计高度一致基本就是new Hashids(salt, minLength, alphabet)然后调encode和decode。在引入时注意选对包名和版本就行了。4.2 接入路由、API 与前端展示层Hashids最常见的接入方式是在接口层做统一编解码不用改动数据库结构。我的习惯是在出参阶段把原始ID编码成Hashids字符串在入参阶段把Hashids字符串解码回原始ID再进入业务逻辑。举例来说一个订单详情接口改造前返回{ orderId: 100001, status: paid, amount: 19900 }改造后返回{ orderId: Qk5d8m2x, status: paid, amount: 19900 }前端拿到的是处理后的ID详情页跳转URL也变成/order/Qk5d8m2x。后端收到这个参数后做一次解码还原成100001再去数据库查询。整个业务逻辑完全不用动只增加了一层编解码函数。这里有几个细节要注意。第一解码失败的情况要单独处理比如客户端传了一个非法的hash串decode返回的是空数组这时应该直接返回参数错误而不是继续走查询逻辑。第二编码后的ID建议在接口层统一处理不要散落在业务代码里否则会出现有的接口返回数字、有的接口返回字符串的混乱情况。第三前端不要拿到hash后再自己解码所有解码都必须在服务端完成否则盐值就泄露了。另外如果项目使用了Swagger或者API文档管理工具记得把ID字段的类型标注成string并备注为“Hashed ID”避免前端同学误以为还是数字类型去相加或比较。4.3 老数据与历史链接的过渡方案线上系统改造最头疼的都是兼容问题。已经发出的订单短信、邮件通知、收藏夹里的链接都是老的数字ID不可能让用户手动改更不可能说作废就作废。我的经验是做一个双格式兼容的过渡方案。服务端解析入参时先判断参数是纯数字还是Hashids格式。如果是纯数字就当成老ID直接使用同时可以在响应里把新的Hashids ID下发如果是Hashids格式就解码成数字ID再使用。这样新旧接口可以并行老链接长期有效新链接逐步推广。把这个方案落地核心就是一个通用解析函数function parseIdParam(input) { // 已经是纯数字说明是旧格式 if (/^\d$/.test(input)) { return parseInt(input, 10); } // 尝试按 hashids 解码 const decoded hashids.decode(input); if (decoded.length 0) { return decoded[0]; } throw new Error(invalid id format); }这套过渡方案上线后可以持续几个月甚至永远保留。我个人的建议是不要轻易下线老格式兼容因为很多外部系统、搜索引擎、用户收藏的链接生命周期远比我们预想的要长。强行下线老格式最终接投诉的还是自己。4.4 方案改造后的一次压测记录有同学担心引入Hashids会带来性能损耗担心解码、编码成为接口的瓶颈。我的看法是完全多虑了但为了严谨我专门做过一次压测对比。测的是Java版本机器是普通的4核8G云主机。用JMeter分两组压测一组是原生的自增ID查询接口一组是加了Hashids编解码的接口。每组压测10万次请求。结果两组接口的TPS差距在1%以内平均响应时间的差值在个位数毫秒以内基本可以忽略不计。Hashids的单次编解码操作耗时在微秒级而一个接口的数据库查询、网络传输、JSON序列化动辄就是几十毫秒这点额外开销根本排不上号。这也是我敢在核心接口直接引入Hashids的原因。它不是那种让人提心吊胆的“重型”操作更像是一层轻量级的转换函数放在接口层非常合适。5. 踩坑实录这些坑我基本都踩过5.1 换了盐用户收藏过的链接全部失效第一次使用Hashids时我对盐值的理解不够深觉得盐就是一个配置项随手上线了一个测试用的盐后来觉得不够随机顺手换了一个新的盐值重新部署。结果当天就收到一堆用户反馈说收藏的链接打不开了、历史订单详情页报错。排查了一圈才发现链接里的Hashids是用旧盐编码的新盐解码后变成空数组接口直接报参数错误。从那以后我立了一个规矩盐值一旦上了生产就永远不要改哪怕你觉得它不够随机、不够长也绝对不要在生产环境更换。如果实在不放心就在上线前充分测试确定最终盐值后再发布。另外盐值不要写死在代码里应该放进配置中心或环境变量方便独立管理同时也好审计。5.2 解码返回空数组排查了半小时还有一次前端同学反馈某个详情页打开是空白的我看日志发现接口返回了参数错误原因就是解码返回了空数组。单独在本地写了个测试用例解码又能正常解析后来才发现是前端传参时把URL里的hash值大小写改变了。Hashids默认字母表是大小写敏感的Qk5d8m2x和qk5d8m2x解码结果完全不同。这类问题在高并发下容易踩最好在接入时做统一处理。我的做法是在解析函数里先trim一下去掉首尾空格同时约定前端传递hash时不修改大小写。如果想让ID大小写不敏感可以在生成Hashids时指定只含小写字母的字母表输出全是小写这样用户输入大写的也能兼容处理但代价是字符集变小同等长度下能表达的ID空间会小一些。5.3 把Hashids当加密用结果数据全裸奔这是我看到过最危险的误用。有些团队觉得Hashids编码后的ID没法还原于是就把用户手机号、身份证号、支付单号也用Hashids编码后塞进URL以为这样就安全了。实际上Hashids是可逆编码不是加密而且算法本身是公开的只要拿到盐值任何编码都能被还原。更严重的是盐值一旦泄露所有历史数据编码瞬间失去意义这种风险和加密方案完全不在一个级别。我的原则是Hashids只用来处理不敏感但需要防枚举的业务标识比如订单ID、用户ID、活动ID。对于任何涉及个人隐私的数据一律走正规加密或者干脆不暴露。Hashids是防君子的门锁不是防窃贼的保险柜这个认知一定要有。5.4 常见问题速查表问题现象可能原因处理方案解码结果为空数组盐值不一致、输入被修改、不是Hashids生成的内容先检查盐值再核对输入串是否完整、大小写是否正确编码后的字符串太长原始数字太大或最小长度设置偏大适当减小最小长度或使用更大的字符集字母表接口报Number类型溢出JS处理超过安全范围的整数使用字符串或Long类型传递避免number精度丢失同一个ID每次编码结果不同配置的盐值或字母表不稳定固定配置确保所有实例统一不要从外部动态传入部分历史链接失效换过盐值或改过字母表长期保留旧盐解码逻辑或提供双盐兼容方案数据库字段存不下hash字符串字段长度设计不够提前确认hash最大长度预留足够varchar空间最后再分享一个我自己用得很顺的小技巧如果既想保留自增ID的查询性能又不想让外部看到连续规律可以在给外部返回数据时把Hashids编码的结果缓存到Redis里设置一定的过期时间。这样就算同一个ID被频繁访问编解码也只发生一次剩余的请求直接走缓存。配合本地缓存使用性能损耗几乎可以忽略不计。我实际做下来整体改造成本很低收益却很显著如果你也在被C端ID暴露问题困扰这个方法值得直接试一下。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。