背景

下面是开发中常见的一个请求接口,根据视频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

比上面优化了一些,但是还是不完善。连续数字的结果还是很多相似性。iURHURuUR。除了第一位不同,其他2位还是相同的。

黄金

继续优化,那能不能把映射字母表搞成多个呢?或者动态生成映射字母表。

我麻了,这里仅仅举个例子。

例如:

  • 尾数为0的用青铜阶段的ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  • 尾数为1的用白银阶段的ALPHABET = "Kf8G7RDV2AzhU1xeYyrTgZ9SkB6omlsFP4QiHuvWaC3JwLMdpX0cIONq5Ebjtn";

那么测试结果为:

  • 原始业务ID:20000,其结果为HUR
  • 原始业务ID:20001,其结果为Bc5

可以看到结果已经有那味了。

钻石

别瞎研究了,头都秃了,伸手党用现成的吧。

Hashids是一款非常小巧跨语言的开源库,可以将数字或者16进制字符串转为短且唯一不连续的字符串,Hashids是双向编码(支持encodedecode),比如,它可以将347之类的数字转换为yr8之类的字符串,也可以将yr8之类的字符串重新解码为347之类的数字。

特点是:

  1. 对非负整数都可以生成唯一短id;
  2. 可以设置不同的盐,具有保密性;
  3. 递增的输入产生的输出无法预测;
  4. 代码较短(大约350行),且不依赖于第三方库。

那么Hashids的原理是什么?Hashids的机制类似于十进制数字转换为16进制的映射,但是做了一点改动:

  1. 没有使用16进制,而是62进制(26个字母大小写+10个数字);
  2. 这个「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 码。这不是真正的加密算法。有些人一生致力于密码学,还有很多更合适的算法:bcryptmd5aessha1blowfish。这是一个完整的列表

这不是加密算法哈,不具有安全性。

参考

  • https://hashids.org/
  • https://github.com/yomorun/hashids-java
  • https://segmentfault.com/a/1190000039811710
Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐