萌将春秋ol官方手游
13.28MB · 2026-02-07
在 C 端系统中,直接对外暴露数据库自增 ID 往往会带来数据枚举、越权访问等安全隐患。本文将从实际业务场景出发,分析自增 ID 暴露的问题本质,并介绍一种基于 Hashids 的可逆 ID 混淆方案。通过 Hashids,我们可以在不改变数据库结构的前提下,实现对外 ID 的安全化与美观化,兼顾安全性、性能与工程可落地性。
本文主要内容:
在后端系统中,我们习惯使用数据库自增 ID,并习惯性的直接返回给C端交互使用,例如:
只要有人发现这是自增 ID, 例如:GET /api/user/100,自然可以被枚举
发现了问题没:
越权的风险被无限放大,你“以为”你做了鉴权,其实不一定,现实情况往往是:
一旦 ID 是可预测的:
例如,URL中存在自增ID,在C端非常典型的场景是用户分享链接给朋友,如果朋友修改URL中的ID,就会跳转到本不属于自己能看到的数据内容。
通过 ID 就能看穿你的业务信息,例如:
这种在C端用户看来没有意义的数据,如果让用户“看不懂”的 ID,反而更专业。
根据以上问题,我们期望有这样一种解决方案可以混淆自增ID
常见但不够优雅的解决方案
UUID
Snowflake / Base64
AES / RSA 加密 ID
这个解决方案就是Hashids。
Hashids的核心功能:把一个或多个整数(int / long)转换成一个不可预测、可逆的短字符串。
基本属性
典型用途:
注意: 它是“混淆(obfuscation)”,不是“加密(encryption)”,不能作为密码学类的场景使用。
编码(encdoe)流程:
核心组成元素
核心组成元素
salt
separators & guards
怎么实现可逆性的?
for i from alphabet.length-1 downTo 1:
j = (salt_char_code + i + previous) % i
swap(alphabet[i], alphabet[j])
怎么转换为字符串的?
如何支持同时编码多个数字?
例如:encode(1, 2, 3)
中间用 separator(也是来自 alphabet,但经过专门筛选)隔开,最终输出:abcXk9Yz
怎么保证最小长度?
decode的工作流程?
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;
}
}
加盐混淆
# 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;
}
}
字符集规则:
# 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;
@PostConstruct
public 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;
}
}
在文章最后,尝试对自己提问一下问题,来检验你是否真正了解到了本文的核心内容。
本文结束。