news 2026/6/10 20:39:02

用 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/6/9 22:21:48

基于Springboot+Vue的校园设备维护报修系统源码文档部署文档代码讲解等

课题介绍 本课题旨在设计并实现一套基于SpringBootVue的前后端分离校园设备维护报修系统&#xff0c;解决校园内设备故障报修流程繁琐、维修进度不透明、设备信息管理混乱、维修资源调配不合理等问题。系统采用SpringBoot作为后端核心框架&#xff0c;结合MyBatis-Plus简化数据…

作者头像 李华
网站建设 2026/6/10 11:40:40

基于Springboot+Vue的校园家教信息平台的设计源码文档部署文档代码讲解等

课题介绍 本课题旨在设计并实现一套基于SpringBootVue的前后端分离校园家教信息平台&#xff0c;解决校园内家教需求与供给信息不对称、交易流程不规范、信息审核不严格等问题。系统采用SpringBoot作为后端核心框架&#xff0c;结合MyBatis-Plus简化数据操作&#xff0c;搭配Vu…

作者头像 李华
网站建设 2026/6/10 11:35:17

基于Springboot+Vue的校园信息共享系统源码文档部署文档代码讲解等

课题介绍 本课题旨在设计并实现一套基于SpringBootVue的前后端分离校园信息共享系统&#xff0c;解决校园内各类信息分散杂乱、传播效率低、信息审核不规范、师生获取精准信息不便等问题。系统采用SpringBoot作为后端核心框架&#xff0c;结合MyBatis-Plus简化数据操作&#xf…

作者头像 李华
网站建设 2026/6/10 11:39:11

使用 Zensical 快速搭建静态博客网站(类似Hugo、Hexo)

从零到一&#xff0c;快速搭建你的 Zensical 博客 Zensical 官方网站: https://zensical.org/ Zensical 官方文档: https://zensical.org/docs/ 第一步&#xff1a;环境准备 检查 Python 版本 Zensical 需要 Python 3.8 或更高版本。首先检查你的 Python 版本&#xff1a; …

作者头像 李华
网站建设 2026/6/10 11:36:04

大数据毕设项目:基于Hadoop的某篮球队各个球员数据分析系统的设计与实现(源码+文档,讲解、调试运行,定制等)

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

作者头像 李华
网站建设 2026/6/10 11:38:05

基于opencv与深度学习Deeplab舌苔分割检测代码及教程 深度学习图像分割 舌苔分割图像数据集

深度学习Deeplab舌苔分割检测代码及教程 引言 舌苔分割是中医诊断中的一个重要环节&#xff0c;通过对舌苔的分析可以辅助医生了解患者的健康状况。近年来&#xff0c;深度学习技术在医学图像处理领域取得了显著进展&#xff0c;Deeplab系列模型因其卓越的分割性能而被广泛应…

作者头像 李华