短链接为什么会存在
随手复制的一条链接常常是一两百个字符,后面拖着一长串给机器看的 utm_* 与 ref 参数。它机器能用,人却几乎用不了:没法念给人手敲、塞不进有字数上限的短信、印成二维码后码点过密难以扫描、贴进聊天框还折成三行。
短链解决的就是这个长度与可用性问题:把任意长的 URL 换成一个极短、固定长度的代号,真正的长地址存在服务端一张表里,访问短码时再用 3xx 跳过去。它同时附带获得了 indirection 的全部好处(改址、统计、灰度),但首先解决的是长度这个最朴素的问题。
短链的本质就是一张「短码到长 URL」的映射表加一次跳转。短码极短所以能念、能记、能印、能扫;真实目标藏在服务端所以随时能改、能在跳转那一下记统计、能按渠道灰度。
1 · 自增 id 与 base62
短码怎么生成才能又短又不撞?最经典的做法是每来一个新链接,数据库给它一个自增 id,再把这个十进制 id 用 base62(0-9、a-z、A-Z 共 62 个字符)编码。base62 的进制更大,同样的数用更少的字符就能写完,而且自增天然不重复,不像随机短码要查重、也不像 hash
截断会撞。
容量算一下就知道为什么够用: 万、 亿、 亿、 万亿。也就是说 5 位 base62 能编到 ,6 位到 。
2 · 自增 id 的可枚举风险
base62 只是换了进制,是可逆的双射,拿到一条短码解回 id 之后,相邻的
就是另一条短链。于是只要从 1 一路加上去,就能把整库的目标 URL 全爬下来。即便短码本身不可逆,单调递增也会泄露规模与增速——看到 id 到了 5000 万就知道发了多少,采样两次还能算增长,这是经典的德国坦克问题。goo.gl 与早期 bit.ly
都被枚举过,暴露出大量本以为不公开的网盘分享。要害只有一句:短码的难猜当不了访问控制,它只负责寻址。
警示 · 图 2-1 右列那个可逆置换仅作示意。真实生产里它必须是密码学强的(格式保留加密 FPE,或一个小 Feistel 网络),否则像图里的线性置换,被采几个样本就能反推出规律。
3 · 让短码既不撞又不可枚举
三种主流方案都在保住「短」的前提下消除「可猜」。
| 方案 | 怎么做 | 权衡 |
|---|---|---|
| 随机短码 | 不用 id,直接随机生成 6 至 8 位 base62 | 最简单;但要查重防撞,或靠空间够大让撞概率可忽略 |
| 加密自增 id | 内部仍自增(不撞、好分库),对外只暴露 encrypt(id) 的结果 |
兼顾两者。Sqids 与 Hashids 只能阻止随手枚举、并不是加密,顺序可被还原 |
| 自增加随机校验位 | 短码是 base62(id) 再拼几位随机后缀 |
改动最小;但 id 段仍单调,规模与增速照样泄露,只挡住了「加减一猜邻居」 |
警示 · 凡是真正私密的目标(私人相册、未发布草稿、内部文档),不要拿短码的难猜当门禁。短码再随机也只是 security through obscurity,链接一旦泄露就全开。访问控制(登录、权限、过期、一次性 token)必须另做一层,短码只管寻址。这与分享入口那条 open redirect 原则是同一件事的两面:跳转本身不可信任输入。
4 · 长 URL 与短链的对照
| 维度 | 直接发原始长 URL | 发短链 |
|---|---|---|
| 字数受限渠道(短信 / 微博) | 占满甚至超限,拆多条 / 吃光字数 ✗ | 十几字符,正文随便写 ✓ |
| 二维码 | version 高、码点密,印小后难以识别 ✗ | version 低、稀疏,易扫描 ✓ |
| 口播 / 印刷 | 念不出来、折行、字号极小 ✗ | 一行大字、能逐字念 ✓ |
| 发出后改目标 | 已印 / 已发的改不了 ✗ | 改服务端映射表,全部跟随 ✓ |
| 来源统计 | utm 自带但落地页各算各的 ✗ | 跳转那一下统一记录 ✓ |
| 代价 | 无,但前几行的债都留着 | 多一跳、一张表,且依赖短链服务存活 |
警示 · 短链最大的代价是 link rot:真实地址被藏在某一家短链服务背后,这家一旦关停(Google 的 goo.gl 已下线,不少免费服务转付费墙后清库),所有印在海报、名片、书里的短链随即全部变成死链,且无法挽救——手里只有短码,原始 URL
在别人库里。重要的长期链接要么自建短链域名,要么直接用原始 URL。另外短链看不出真实目标,也常被钓鱼利用。
访问短码后跳过去那一下用的就是 3xx,短链多用 302 而非 301,因为目标常变且要统计点击。把短码表做成一个真实的 /s/:code 服务(命中、统计、查表、跳目标)是分享入口的题目。