当前位置: 首页 > news >正文

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

   在 C 端系统中,直接对外暴露数据库自增 ID 往往会带来数据枚举、越权访问等安全隐患。本文将从实际业务场景出发,分析自增 ID 暴露的问题本质,并介绍一种基于 Hashids 的可逆 ID 混淆方案。通过 Hashids,我们可以在不改变数据库结构的前提下,实现对外 ID 的安全化与美观化,兼顾安全性、性能与工程可落地性。

本文主要内容:

  • 为什么 C 端系统不能直接暴露自增 ID
  • 解决方案是什么
  • hashids有那些功能
  • hashids的代码实现是什么

为什么 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) % iswap(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;@PostConstructpublic 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
查看代码
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;/*** 自定义参与编码的字符** 字符集规则:* 至少 16 个字符* 不允许重复字符** 使用场景* 避免 0/O、l/1 混淆* 只允许大写字母**/
@Service
public class HashidsBaseCharService {@Value("${hashids.salt}")private String salt;@Value("${hashids.min-length}")private int minLength;@Value("${hashids.base-char}")private String baseChar;private Hashids hashids = null;@PostConstructpublic void init() {hashids = new Hashids(salt, minLength, baseChar);}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;}}

结尾

在文章最后,尝试对自己提问一下问题,来检验你是否真正了解到了本文的核心内容。

  1. C 端自增 ID 暴露会有那些问题?
  2. hashids的核心特性?为什么可以作为混淆自增id的解决方案?
  3. hashids的功能有那些?
  4. 代码怎么写?

本文结束。

http://www.jsqmd.com/news/342915/

相关文章:

  • ue metahuman自动绑定
  • 全球股市估值与脑机接口在情绪管理中的应用
  • MegaFlow:大模型时代Agent训练的分布式编排系统详解
  • 大模型RAG管道优化:“过度设计“的邻居扩展策略是否真的有效?
  • 华为OD技术面真题 - Mysql相关 - 4
  • 把90!输出到屏幕上(二)
  • Agent产品经理10大高频问题详解,附专业回答模板,建议收藏!
  • 电子抑振控制实验中MATLAB+示波器的用法-PART-RIGOL-电磁制振
  • 【学习笔记】【算法】线段树进阶
  • AI大模型应用开发从入门到精通:2026大模型应用开发最全学习路线
  • 全国镍材优质厂家有哪些?优先选哪些维度筛选? - 非研科技
  • 小红书美妆推广服务商:传声港新媒体平台美妆行业精准营销白皮书 - 速递信息
  • 大数据时代下 Kafka 的核心原理深度剖析
  • 江浙沪蟹粉专家常见问题解答(2026最新) - 速递信息
  • 振动下机械臂鲁棒快控制-EXP-振动控制-机械臂
  • 【课程设计/毕业设计】基于Django+大数据爬虫的短视频推荐系统的设计与实现基于django+大数据平台的短视频推荐系统设计与实现【附源码、数据库、万字文档】
  • AI原生语音识别避坑指南:常见问题与解决方案
  • 2026年口碑好的全自动离心机,拉袋离心机,平板离心机厂家优质供应商榜单 - 品牌鉴赏师
  • 计算机大数据毕设实战-基于django+大数据平台的食物营养成分分析与推荐系统的设计与实现基于Django打造食物营养数据可视化分析系统【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • AI元人文:哪吒——认知异化时代的元神话与行动纲领
  • 例说FPGA:可直接用于工程项目的第一手经验【2.7】
  • 普通人学习大模型应该怎么学?_大模型的挑战与优势?
  • MySQL dump and pump
  • 大数据毕设项目:基于django+大数据平台的短视频推荐系统设计与实现(源码+文档,讲解、调试运行,定制等)
  • 编程的未来:从复杂到简单的转变
  • P4999 烦人的数学作业
  • 【毕业设计】基于django+大数据平台的短视频推荐系统设计与实现(源码+文档+远程调试,全bao定制等)
  • K8s 部署Doris 高可用集群 - 指南
  • 大数据领域的创新应用案例
  • 大数据连接池配置:结构化数据访问优化