短码必须用自增ID转62进制,禁用md5/uniqid;建表设AUTO_INCREMENT=1000防短码过短;路由只取PATH_INFO中短码并校验格式;跳转强制header+exit且优先查Redis缓存。

短码必须用自增 ID 转 62 进制,别碰 md5() 和 uniqid()
md5($url) 截前 6 位或 uniqid() 直接用,看似省事,实际踩坑率极高:
- 碰撞风险真实存在,两个不同长链可能生成相同短码
- 无法反查原始 URL,后续统计、编辑、失效都断链
- uniqid() 在毫秒级并发下极易重复,尤其容器或负载均衡环境
可靠做法是依赖数据库 AUTO_INCREMENT 主键 + 自定义 62 进制编码:
- 字符集固定为
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' - 函数封装成
idToShort($id),避免手写除法循环(易错、难维护) - 建表时设
AUTO_INCREMENT = 1000,防止 id=1 编码后变成单字符"1"
INSERT ... ON DUPLICATE KEY UPDATE 是防并发重复的唯一靠谱写法
“先 SELECT 再 INSERT” 在并发下必然失败——两个请求同时查不到记录,然后都插入,触发唯一索引冲突或脏数据。
正确姿势是:
- 给
long_url字段加唯一索引(可前缀索引:long_url(255),兼顾性能与长度) - 插入语句用:
INSERT INTO short_links (long_url, short_code) VALUES (?, ?) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id) - 随后调用
PDO::lastInsertId()或mysqli_insert_id()拿到真实 ID(命中 duplicate 时也返回已有行的 id) - 绝对不用
REPLACE INTO:它本质是DELETE + INSERT,有外键或触发器时行为不可控
路由只认 PATH_INFO 中的短码,拒绝任何 query 参数参与跳转
用户访问 /s/abc123 是合法路径;若允许 /s/abc123?to=https://evil.com 并直接跳转,就是开放重定向漏洞。
ThinkPHP 路由定义必须写死路径变量:
-
Route::get('/s/<hash>', 'Index/Short@redirect')->pattern(['hash' => '\w{4,8}']);</hash> - 控制器里只取
$this->request->param('hash'),不读$_GET,不拼接任何外部输入 - 校验短码格式:长度 4–8 位、仅含字母数字;查库失败,或
long_url不以http开头且不在内网白名单域名内,直接抛HttpException(404)
跳转必须 header() + exit,漏掉 exit 就等于放行后续逻辑
redirect($url, 302) 底层虽调用 header(),但 ThinkPHP 默认不会自动 exit。一旦后续代码继续执行,可能:
- 泄露调试信息(尤其 display_errors 开启时)
- 重复写日志、发通知、扣积分等副作用
- Redis 缓存被覆盖或误删
强制写法只有这一种:
立即学习“PHP免费学习笔记(深入)”;
-
header('Location: ' . $safeUrl); -
exit; - 别用
http_response_code(302)单独设状态码,和Location头冲突风险高
真正容易被忽略的,是短码生成和跳转之间那几毫秒——缓存未命中的瞬间,多个请求同时穿透到 DB,靠唯一索引和 ON DUPLICATE KEY UPDATE 挡住,不是靠运气。



















