短链接系统设计

1,894 阅读5分钟

一 短链接介绍

image.png

举个栗子:

有时会收到和上面差不多的营销短信,e9q.cn/CeCKrn这个就是短连接。

他是怎么生成的呢?

可以网上找一个短域名服务商,例如urlc.cn

image.png

现在我的掘金技术社区的地址是这个:https://juejin.cn/user/26044011644071/posts (45个字符)

我通过的短链接服务https://www.urlc.cn/可以将上面的地址转成http://k8i.cn/b6lQY(19个字符)

为什么要生成短连接
  • 看个连接

https://search.jd.com/Search?keyword=%E4%BB%8E%E4%BD%A0%E7%9A%84%E5%85%A8%E4%B8%96%E7%95%8C%E8%B7%AF%E8%BF%87&enc=utf8&suggest=2.def.0.SAK7|MIXTAG_SAK7R,SAK7_M_AM_L5361,SAK7_M_COL_L15275,SAK7_S_AM_R,SAK7_SC_PD_R,SAK7_SS_PM_LC|G*SAK7_M_COL_L15275&wq=%E7%A9%BF%E8%BF%87&pvid=955b57b84dee41f3b1d0b33f13db34fe

这是个电商的链接,非常长。

一般单条短信的字数限制的70字。

上面的连接要作为营销链接,可能要发送好几条。用户也不会习惯去拼接着写字符串再访问这个连接。

  • 怎么办呢 我们可以将原来的长链接转成较短的链接。

二 短链接设计

我们先回到生成好的短链上http://k8i.cn/b6lQY

虽然这个链接看起来有点奇怪,但他终究还是一个链接,从URL的特征我们可以分出:

  • k8i.cn是域名
  • b6lQY是参数。

短链接的原理其实就是:

  • 将长链接通过一定的手段生成一个短链接
  • 访问短链接时实际访问的是短链接服务器,然后根据短链接的参数找回对应的长链接
  • 重定向跳转

image.png

通过上面的分析我们可以知道的是,我们实际核心要做的是怎么从b6lQY类似这样的参数找到对应的完整URL:https://juejin.cn/user/26044011644071/posts

脑子第一时间想到的是:能不能通过一个压缩算法将https://juejin.cn/user/26044011644071/posts压缩更小的字符?

  • md5加密链接再截取指定长度作为短链path
url = hashlib.new('md5', bytes(link, "utf-8")).hexdigest()[0:5]

这种方式会有个问题就是哈希冲突,因为你无法加密后保证截取的五个字符是否是一样的。

  • 优化方案
    • ID自增后,转成62进制(10个数字+26小写英文字母+26大写英文字母),在DB保存映射关系,生成短链接

    • 5位数的短地址数量931,151,402.

    • 6位数的短地址数量558亿 过程描述

    • 1 长链接进入

    • 2 签发ID 0-500亿 eg: id = 13333333

    • 3 将ID转换成 62进制 eg: 62进制转换后 path=3A3n

    • 4 短链path值为62进制的3A3n

    • 5 返回短链http://ddz.com/3A3n

image.png

id 0              62进制 0           url http://aaa.com
id 62             62进制 Z           url http://bbb.com
id 14332343333    62进制 4FSsxjh     url http://ccc.com
​

这样就可以解决hash冲突问题了。

新问题:信息安全

假设当前PATH=F4VNe0自增后依次为PATH=F4VNe1 PATH=F4VNe2 PATH=F4VNe3 ... ... PATH=F4VNex PATH=F4VNey PATH=F4VNez ... ... PATH=F4VNeX PATH=F4VNeY PATH=F4VNeZ

自增的ID可能比较容易猜出来规律,从而造成数据泄露。

场景:AB是付费短连接服务商,因为A服务商的短连接是自增的,被B供应商猜到了。通过自增的短连接访问到了客户真实的地址,可以采用合理的营销方式撬走你的客户。所以短链也有加密需求。

压缩算法优化

  • 固定长度path6位,所以id区间[931,151,402, 558亿]之间自增

  • 62进制 右移一位 通过62进制生成的path=4VNe5F,右移一位以后path >> path=F4VNe5

  • 在指定位置插入

    • 链接md5第一位=e,插入path的第2个位置,path=Fe4VNe5

部分代码

main.html

// 全部代码在gitee上,底下有git地址
<div id="container" class="center-justify">
    <div class="sea-container">
        <form action="/" method="post">
            <input type="text" name="url" class="blue-input">

            <input type="submit" value="生成" class="blue-button">
        </form>
        <p id="selectedId">端地址:{{url}}</p>
    </div>
</div>

index.html

//页面跳转
<script>window.location.href="{{url}}";</script>

script.py

def binary_conversion(num, bc_start, bc_end):
    # num位数字,bc_start为num的进制数,bc_end为目标进制数,取值为2-62
    a = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
         'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
         'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
         'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
    nx = str(num)
    b1 = list(nx)
    b2 = []
    for i in b1:
        for i1 in range(0, 62):
            if a[i1] == i:
                b2 = b2 + [i1]
                if i1 > bc_start:
                    print(i, "错误定义")
    b2.reverse()
    n1 = 0
    n2 = 1
    for i in b2:
        n1 = n1 + int(i) * (pow(bc_start, n2 - 1))
        n2 = n2 + 1
    n = n1
    b = []
    while True:
        s = n // bc_end
        y = n % bc_end
        b = b + [y]
        if s == 0:
            break
        n = s
    b.reverse()
    bd = ""
    for i in b:
        bd = bd + a[i]
    return bd


def is_http(url):
    # 判断网址的合法性
    result = re.findall(
        r"(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*,]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)|([a-zA-Z]+.\w+.+[a-zA-Z0-9/_]+)",
        url)
    return True if result else False


def left_str(s):
    lens = len(s)
    return s[lens - 2:] + s[0:lens - 2]  # 左移两位


def right_str(s):
    return s[2:] + s[:2]  # 右移2位


def get_url_number():
    # 签发url的id
    try:
        num = res_obj.get("url_number")
        num = int(num) + 1  # 62进制,每次加1 可用地址500多亿
        res_obj.set("url_number", num)
    except:
        num = 1000000000
        res_obj.set("url_number", num)

    return num

view.py

@app.route('/', methods=["GET", "POST"])
def index():
    url = "ddz.imibi.cn/"
    if request.method == "POST":
        link = request.form.get("url")
        if len(link) > 300:
            url = "地址长度超过要求"
        # 是不是网址判断
        elif not is_http(link):
            url = "请输入正确的网址"
        else:
            num = get_url_number()  # 签发编号
            num = binary_conversion(num, 10, 62)  # 转换成62进制
            # 随机产生url_md5字符插入位置
            n = random.randint(0, 5)
            url_md5 = hashlib.new('md5', bytes(link, "utf-8")).hexdigest()[-1:]
            num = num[:n] + url_md5 + num[n:]  # 拼接上路由的md5加密信息
            num = left_str(num)  # 混淆左移两位
            url = num
            res_obj.delete(url)
            res_obj.hset(url, link, "默认")
            url = "ddz.imibi.cn/" + url
    context = {
        "display": "block",
        "url": url
    }
    return render_template('main.html', **context)

测试一下

修改本机hosts文件

文件路径

C:\Windows\System32\drivers\etc\hosts

结尾增加

127.0.0.1       ddz.com

image.png

启动本地服务 --host=127.0.0.1 --port=80

ddz.com通过DNS解析会映射到本机IP.

image.png

通过生成的短连接成功访问目标链接 image.png

参考文章