从零开发短视频电商 隐藏业务ID以及缩短业务链接
下面是开发中常见的一个请求接口,根据视频ID获取视频详情。在实际使用的时候,大部分情况下,业务ID具有递增属性,那么实际调用情况可能如下:看到问题了吗?虽然这样使用很简单有效,但是却向用户展示了业务ID,由于这种业务ID是可猜测、可枚举的,通过这种业务ID,别有用心的人可以计算出表中的数据,爬虫也很容易爬取所有数据。那么我们就需要一个功能来隐藏我们的业务ID,怎么实现呢?首先你的数据库业务ID肯定
背景
下面是开发中常见的一个请求接口,根据视频ID获取视频详情。
GET xxx/api/v1/videos/{id}
在实际使用的时候,大部分情况下,业务ID具有递增属性,那么实际调用情况可能如下:
GET xxx/api/v1/videos/10
GET xxx/api/v1/videos/11
...
GET xxx/api/v1/videos/102
看到问题了吗?虽然这样使用很简单有效,但是却向用户展示了业务ID,由于这种业务ID是可猜测、可枚举的,通过这种业务ID,别有用心的人可以计算出表中的数据,爬虫也很容易爬取所有数据。
那么我们就需要一个功能来隐藏我们的业务ID,怎么实现呢?首先你的数据库业务ID肯定是业务递增Long型,这个点是不会变的,应该没有人用UUID这种当主键吧,那么只需要把业务ID转换成无规则的字符串即可。
例如:
GET xxx/api/v1/videos/10
GET xxx/api/v1/videos/102
转换成
GET xxx/api/v1/videos/Glk0f
GET xxx/api/v1/videos/zk2B9
这里也可以借鉴下其他平台,如下面的B站和油管,都没有直接暴露业务ID。
- B站:https://www.bilibili.com/video/BV1mT4y167Uj
- 油管:https://www.youtube.com/watch?v=zc3UQQVgQ187
实现
青铜
接着上面分析,业务ID转换成无规则的字符串?如何做呢?我回想起之前做的工作这么久了,你应该知道的短链接架构设计,里面有 62进制和10进制的互转实现。
private String ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
// 10进制转62进制
private static String encoding(long num) {
if(num < 1)
throw new IllegalArgumentException("num must be greater than 0.");
StringBuilder sb = new StringBuilder();
for (; num > 0; num /= 62) {
sb.append(ALPHABET.charAt((int) (num % 62)));
}
return sb.toString();
}
// 62进制转10进制
private static long decoding(String str) {
if(str==null || str.trim().length() == 0 ){
throw new IllegalArgumentException("str must not be empty.");
}
long result = 0;
for (int i = 0; i < str.length(); i++) {
result += ALPHABET.indexOf(str.charAt(i)) * Math.pow(62, i);
}
return result;
}
例如:
- 原始业务ID:
19999,其62进制为zc5 - 原始业务ID:
20000,其62进制为Ac5 - 原始业务ID:
20001,其62进制为Bc5
是可以实现我们的需求,但是也存在一个问题,一般的小白可能看不懂你这个业务ID了,但是懂点代码的还是能看出来的,很容易就能将 62 进制转换为 10 进制,只能过滤一些小白。
白银
那怎么办呢?
我们把这个映射字母表给他随机打乱,例如上面的ALPHABET
private String ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
经过洗牌算法之后的映射字母表
private String ALPHABET = "Kf8G7RDV2AzhU1xeYyrTgZ9SkB6omlsFP4QiHuvWaC3JwLMdpX0cIONq5Ebjtn";
还是上面的测试数据,结果如下:
- 原始业务ID:
19999,其结果为iUR - 原始业务ID:
20000,其结果为HUR - 原始业务ID:
20001,其结果为uUR
比上面优化了一些,但是还是不完善。连续数字的结果还是很多相似性。iUR、HUR、uUR。除了第一位不同,其他2位还是相同的。
黄金
继续优化,那能不能把映射字母表搞成多个呢?或者动态生成映射字母表。
我麻了,这里仅仅举个例子。
例如:
- 尾数为0的用青铜阶段的
ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - 尾数为1的用白银阶段的
ALPHABET = "Kf8G7RDV2AzhU1xeYyrTgZ9SkB6omlsFP4QiHuvWaC3JwLMdpX0cIONq5Ebjtn";
那么测试结果为:
- 原始业务ID:
20000,其结果为HUR。 - 原始业务ID:
20001,其结果为Bc5。
可以看到结果已经有那味了。
钻石
别瞎研究了,头都秃了,伸手党用现成的吧。
Hashids是一款非常小巧跨语言的开源库,可以将数字或者16进制字符串转为短且唯一不连续的字符串,Hashids是双向编码(支持encode和decode),比如,它可以将347之类的数字转换为yr8之类的字符串,也可以将yr8之类的字符串重新解码为347之类的数字。
特点是:
- 对非负整数都可以生成唯一短id;
- 可以设置不同的盐,具有保密性;
- 递增的输入产生的输出无法预测;
- 代码较短(大约350行),且不依赖于第三方库。
那么Hashids的原理是什么?Hashids的机制类似于十进制数字转换为16进制的映射,但是做了一点改动:
- 没有使用16进制,而是62进制(26个字母大小写+10个数字);
- 这个「62进制」的映射通过加盐而做了扰动。
项目使用
1.增加依赖
<dependency>
<groupId>org.hashids</groupId>
<artifactId>hashids</artifactId>
<version>1.0.3</version>
</dependency>
2.编解码
// 数字
Hashids hashids = new Hashids("salt", MIN_HASH_LENGTH);
String encryptString = hashids.encode(347L);
long[] decrypedNumbers = hashids.decode("Y5bAyr8dLO4");
// 字符串 必须是16进制字符串
String encryptString = hashids.encodeHex(HexUtil.encodeHexStr("this is a string"));
String decrypedNumbers = hashids.decodeHex("1prnZLrKPlS5EEe61reMCNzkJXP");
3.测试
结果为:
- 原始业务ID:
20000,其结果为Ob9P。 - 原始业务ID:
20001,其结果为XJX1。
注意!!!它不能用于以下场景
- 不要尝试对负数进行编码。它行不通。该库目前仅支持正数和零。
- 不要编码字符串。我们已经收到了几个添加此功能的请求——“添加起来似乎很容易”。出于安全目的,我们不会添加此功能,这样做会鼓励人们对敏感数据(例如密码)进行编码。这是错误的工具。
- 不要对敏感数据进行编码。这包括敏感整数,例如数字密码或 PIN 码。这不是真正的加密算法。有些人一生致力于密码学,还有很多更合适的算法:bcrypt、md5、aes、sha1、blowfish。这是一个完整的列表。
这不是加密算法哈,不具有安全性。
参考
- https://hashids.org/
- https://github.com/yomorun/hashids-java
- https://segmentfault.com/a/1190000039811710
更多推荐



所有评论(0)