news 2026/4/16 15:04:28

用 Hashids 优雅解决 C 端自增 ID 暴露问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 Hashids 优雅解决 C 端自增 ID 暴露问题

为什么 C 端系统不能直接暴露自增 ID?

在后端系统中,我们习惯使用数据库自增 ID,并习惯性的直接返回给C端交互使用,例如:

登录接口在登录成功后返回的用户基础信息
{
"userId": 100,
"username": "tom",
"gender": 1,
"birthday": "2000-10-12 00:00:00"
}

URL上直接有自增ID:GET /api/user/100

可被枚举

只要有人发现这是自增 ID,例如:GET /api/user/100,自然可以被枚举

/api/user/101

/api/user/102

/api/user/103

......

发现了问题没:

  • 可以轻松遍历接口、爬虫可以批量扫库
  • 哪怕你有登录态、鉴权,只要权限校验有一个点没兜住,后果极其严重:表数据被批量抓取,隐私数据被遍历、爬光
  • 这类攻击成本极低,甚至不算“攻击”

越权

越权的风险被无限放大,你“以为”你做了鉴权,其实不一定,现实情况往往是:

  • 接口 A 做了用户校验
  • 接口 B 忘了
  • 新接口临时加的,校验漏了
  • 某个内部接口被误暴露

一旦 ID 是可预测的:

  • 攻击者只需要找到 一个没校验的入口
  • 就可以“横向移动”访问所有数据

例如,URL中存在自增ID,在C端非常典型的场景是用户分享链接给朋友,如果朋友修改URL中的ID,就会跳转到本不属于自己能看到的数据内容。

业务信息全暴露

通过 ID 就能看穿你的业务信息,例如:

  • 📈订单量增长速度

  • 👥用户规模

  • ⏱️业务峰值时段

  • 🧮是否删过数据(ID 是否断层)

这种在C端用户看来没有意义的数据,如果让用户“看不懂”的 ID,反而更专业。

  • 👉 纯数字 ID,看起来像“内部系统”

  • 👉混淆 ID, 更像“产品设计的一部分”

解决方案

根据以上问题,我们期望有这样一种解决方案可以混淆自增ID

  • 唯一不可重复:数据量内都必须唯一,不能重复
  • 支持可逆:ID可以编码为一个看不出规律的串,也可以解码为原ID,不影响数据库ID字段
  • 高效生成与解析:生成、验证的算法必须保证效率,不能占用太多系统资源
  • 不可预测与安全:无规则混淆,规律性不能很明显,不能轻易被人猜测到,防止爆刷
  • 工程成本低:不改表、不迁数据

常见但不够优雅的解决方案

  • UUID
    • 字符串过长
    • URL、二维码不友好
    • 调试体验差
  • Snowflake / Base64
    • 仍然可能暴露时间信息
    • 前后端实现不统一
  • AES / RSA 加密 ID
    • 性能与复杂度成本高
    • 对“只是隐藏 ID”来说属于过度设计

这个解决方案就是Hashids。

Hashids的核心功能:把一个或多个整数(int / long)转换成一个不可预测、可逆的短字符串。

基本属性

  • 输入是整数(支持long型),输出是字符串(只包含:a-z A-Z 0-9,无其他特殊字符)
  • 可以自定义编码字符
  • 可逆,但不可猜
  • 支持多个ID编码为一个字符串
  • 可控制最小长度,不支持“最长长度”限制,实际长度是不固定的,随输入数字大小变化

典型用途:

  • 数据库自增 ID ,对外展示用字符串,防止 ID 枚举

  • 短链接 / 邀请码 / 兑换码

  • URL / 小程序参数更友好

  • 将多个数字(数组)进行混淆,防止参数被篡改

注意:它是“混淆(obfuscation)”,不是“加密(encryption)”,不能作为密码学类的场景使用。

Hashids内部原理

编码(encdoe)流程:

原始数字 --> 打乱字符表(依赖 salt) --> 选取 guard / separator --> 进制转换(base-N) --> 按规则拼接 --> 输出字符串

核心组成元素

  • 核心组成元素
    • 字符表(alphabet):指定那些字符是输出的结果集
    • 默认字符集:abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890
    • 你也可以自定义(例如只用大小写 + 数字,排除 0/O/I/l),注意字符不能重复。
    • 字符集长度 = 进制 base N
  • salt
    • 决定字符表如何被洗牌
    • 同一个 ID,在不同 salt 下,结果完全不同
    • 不保存 salt,就无法反解
  • separators & guards
    • Hashids 会从 alphabet 中分出两类特殊字符:separators,分隔多个数字。guards,控制最小长度、增强不可预测性。
    • 这一步主要是为了:避免输出模式过于规则,同时支持编码多个数字的场景(encode(1,2,3))

怎么实现可逆性的?

  • 字符表洗牌(consistent shuffle)
  • 在相同 alphabet + salt下,编码同一ID,结果永远相同

查看代码

for i from alphabet.length-1 downTo 1: j = (salt_char_code + i + previous) % i swap(alphabet[i], alphabet[j])

怎么转换为字符串的?

  • 假设字符表(alphabet)的长度为62
  • 数字先模62,得到的余数作为下标从字符表中取得一个字符
  • 再除62,直到数字小于0时停止
  • 得到一个字符串

如何支持同时编码多个数字?

例如:encode(1, 2, 3)

1 → abc
2 → k9
3 → z

中间用 separator(也是来自 alphabet,但经过专门筛选)隔开,最终输出:abcXk9Yz

怎么保证最小长度?

  • 在头尾插入 guard

  • 再次洗牌 alphabet

  • 重复直到满足长度,这一步是伪随机填充,不影响 decode。

decode的工作流程?

  • 去掉 guards

  • 用 separators 切分

  • 复原 alphabet 洗牌

  • 每一段做 base-N → long

decode失败怎么处理?

  • salt 不一致 → decode 失败或得到错误值
  • alphabet 不一致 → decode 失败

代码实现

终于到了激动人心的代码实现环节,撸起袖子,敲键盘。

在pom中导入依赖

<dependency> <groupId>org.hashids</groupId> <artifactId>hashids</artifactId> <version>1.0.3</version> </dependency>

简单用法

import org.hashids.Hashids; import org.springframework.stereotype.Service; import java.util.Arrays; @Service public class HashidsService { //Bean单例,不存在线程安全问题 private final Hashids hashids = new Hashids(); public String encode(int code) { return hashids.encode(code); } public String encode(long code) { return hashids.encode(code); } public long decode(String decoded) { long[] decodes = hashids.decode(decoded); if (decodes.length == 0) { throw new IllegalArgumentException("非法ID"); } return decodes[0]; } public String encodeArr(int[] codes) { long[] codeArr = Arrays.stream(codes).asLongStream().toArray(); return hashids.encode(codeArr); } public String encodeArr(long[] codes) { return hashids.encode(codes); } public long[] decodeArr(String decoded) { long[] decodes = hashids.decode(decoded); if (decodes.length == 0) { throw new IllegalArgumentException("非法ID"); } return decodes; } }

加盐

加盐混淆

  • 防止别人用同样库解你 ID
  • salt 一旦上线 绝对不能改
# application.yml hashids: salt: kjsdfiaosudkskldjfa #混淆用的盐 min-length: 8 #最小长度

查看代码

package com.ks.demo.uc.hashids; import org.hashids.Hashids; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.util.Arrays; /** * 加盐混淆 * * salt的作用 * 防止别人用同样库解你 ID * salt 一旦上线 绝对不能改 */ @Service public class HashidsSaltService { @Value("${hashids.salt}") private String salt; @Value("${hashids.min-length}") private int minLength; //new在@Value注入之前 //解决方案:后构造器,在构造器的入参使用@Value,使用@ConfigurationProperties单独注入 //private Hashids hashidsSalt = new Hashids(salt); private Hashids hashidsSalt = null; private Hashids hashidsMinLen = null; @PostConstruct public void init() { hashidsSalt = new Hashids(salt); hashidsMinLen = new Hashids(salt, minLength); } public String encode(int code) { return hashidsSalt.encode(code); } public String encode(long code) { return hashidsSalt.encode(code); } public long decode(String decoded) { long[] decodes = hashidsSalt.decode(decoded); if (decodes.length == 0) { throw new IllegalArgumentException("非法ID"); } return decodes[0]; } public String encodeArr(int[] codes) { long[] codeArr = Arrays.stream(codes).asLongStream().toArray(); return hashidsSalt.encode(codeArr); } public String encodeArr(long[] codes) { return hashidsSalt.encode(codes); } public long[] decodeArr(String decoded) { long[] decodes = hashidsSalt.decode(decoded); if (decodes.length == 0) { throw new IllegalArgumentException("非法ID"); } return decodes; } public String encodeMinLen(int code) { return hashidsMinLen.encode(code); } public String encodeMinLen(long code) { return hashidsMinLen.encode(code); } public long decodeMinLen(String decoded) { long[] decodes = hashidsMinLen.decode(decoded); if (decodes.length == 0) { throw new IllegalArgumentException("非法ID"); } return decodes[0]; } public String encodeMinLenArr(int[] codes) { long[] codeArr = Arrays.stream(codes).asLongStream().toArray(); return hashidsMinLen.encode(codeArr); } public String encodeMinLenArr(long[] codes) { return hashidsMinLen.encode(codes); } public long[] decodeMinLenArr(String decoded) { long[] decodes = hashidsMinLen.decode(decoded); if (decodes.length == 0) { throw new IllegalArgumentException("非法ID"); } return decodes; } }

自定义参与编码的字符

字符集规则:

  • 至少 16 个字符
  • 不允许重复字符
# application.yml hashids: salt: kjsdfiaosudkskldjfa #混淆用的盐 min-length: 8 #最小长度 #至少 16 个字符,不允许重复字符 #参与编码的字符,可以剔除调0/O/o,1/I/l等字符,增强可读性 base-char: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 9:21:38

大数据毕设选题推荐:基于Hadoop的某篮球队各个球员数据分析系统的设计与实现【附源码、mysql、文档、调试+代码讲解+全bao等】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/4/16 11:13:50

jsp二手车评估检测平台系统90i84程序+源码+数据库+调试部署+开发环境

本系统&#xff08;程序源码数据库调试部署开发环境&#xff09;带论文文档1万字以上&#xff0c;文末可获取&#xff0c;系统界面在最后面。 系统程序文件列表 用户,检测师傅,汽车品牌,二手车信息,师傅信息,预约信息,取消预约,完成订单,关于我们 开题报告内容 一、研究背景…

作者头像 李华
网站建设 2026/4/16 12:34:16

例说FPGA:可直接用于工程项目的第一手经验【3.0】

12.ycbcr2rgb.v模块代码解析 该模块的内部功能框图如图12-40所示。YCrCb输入视频流经过该模块内部缓存排序、乘累积运算(放大256倍)、加法运算以及溢出与缩小(256倍)处理后,输出RGB视频流。 在视频图像显示、处理时,采用的颜色空间主要有RGB和YCrCb两种。RGB基于三基色…

作者头像 李华
网站建设 2026/4/16 2:18:48

甜椒叶病害数据集

1.数据集分为训练集和测试集2.训练集如下所示第一个文件夹是细菌斑叶&#xff08;449张&#xff09;第二个是健康叶子&#xff08;4014张&#xff09;测试集细菌斑叶 11张健康叶子10张

作者头像 李华
网站建设 2026/4/16 10:53:25

如何用Agentic AI 提升客户服务质量?提示工程架构师的5个技巧

如何用Agentic AI 提升客户服务质量?提示工程架构师的5个技巧 关键词 Agentic AI、客户服务质量、提示工程架构师、技巧、自然语言处理 摘要 本文聚焦于如何运用Agentic AI提升客户服务质量,为提示工程架构师提供五个实用技巧。首先介绍了Agentic AI和客户服务领域的背景…

作者头像 李华
网站建设 2026/4/16 11:01:45

例说FPGA:可直接用于工程项目的第一手经验【2.7】

11.4 IP核配置——FIFO配置 本实例例化了3个FIFO,即video_ctrl.v模块例化的video_fifo、ddr_avl_bridge.v模块例化的rdfifo_for_sdram和wrfifo_for_sdram,关于它们的基本配置,说明如下。 1.视频时域变换FIFO配置(video_fifo) 基本的配置请参考工程实例8的FIFO配置说明,…

作者头像 李华