应循环调用 urldecode() 直至无法再解,同时用长度和正则防死循环;还原后需按原始编码(如 UTF-8/GBK)针对性转码 query value 部分;parse_url() 等函数必须在完全解码后调用;根治方法是在短链存储时避免对整个 URL 多余编码。
PHP 接收短链接重定向后的目标 URL 时,经常发现 %2520(空格)、%253D(等号)、%252F(斜杠)这类“套娃式编码”。本质是原始 URL 已被编码一次,又被短链服务再次编码 —— 比如用户分享的是 https://a.com?q=hello world,短链服务存入数据库前做了 urlencode(),生成短链后跳转时又对整个 URL 再做了一次 urlencode(),导致最终 PHP 收到的是 https%253A%252F%252Fa.com%253Fq%253Dhello%252Bworld。
还原的关键不是“解一次”,而是“解到没有 %[0-9A-Fa-f]{2} 可解为止”,但不能无限制循环(防恶意构造)。建议用 urldecode() 循环 + 长度判断:
function fully_decode_url($url) {
$prev = '';
while ($url !== $p
rev) {
$prev = $url;
$url = urldecode($url);
// 防止死循环:若解码后长度没变,且还含 %xx,说明已无效或损坏
if (strlen($url) === strlen($prev) && preg_match('/%[0-9A-Fa-f]{2}/', $url)) {
break;
}
}
return $url;
}
短链目标 URL 中的中文、日文等非 ASCII 字符,在原始编码时可能用 UTF-8,也可能用 GBK(尤其老系统或某些国内短链平台)。PHP 默认按字节解码 urldecode(),不自动识别编码。如果还原后是 ä½ å¥½ 这类乱码,大概率是原始 URL 用 UTF-8 编码,但你误以为是 GBK;反之亦然。
Location 响应头值mb_detect_encoding() 辅助判断不可靠,优先以来源为准mb_convert_encoding($decoded, 'UTF-8', 'GBK') 或 iconv('GBK', 'UTF-8//IGNORE', $decoded)
mb_convert_encoding(),只转 query string 的 value 部分(比如 q=xxx 中的 xxx),否则会破坏协议、域名、路径中的合法 ASCII 字符parse_url() 和 parse_str() 处理前必须确保 URL 已完全还原很多人直接对未还原的编码串调用 parse_url(),结果 parse_url('https%3A%2F%2Fexample.com%3Fx%3D%E4%BD%A0') 返回 ['scheme' => 'https%3A', 'host' => '%2F%2Fexample.com%3Fx%3D%E4%BD%A0'] —— 完全错乱。必须在调用前完成彻底解码。
还原后,再安全拆解:
$raw_url = $_GET['url'] ?? '';
$decoded = fully_decode_url($raw_url);
$parsed = parse_url($decoded);
if ($parsed === false || empty($parsed['scheme']) || empty($parsed['host'])) {
die('Invalid URL');
}
// 只对 query 部分解码并解析
if (!empty($parsed['query'])) {
parse_str($parsed['query'], $query_params);
// $query_params 现在是关联数组,value 已是原始字符串(如 ['q' => '你好'])
}
真正省事的做法,是在生成短链时就堵住源头。入库前不做多余编码:
filter_var($url, FILTER_VALIDATE_URL)),再直接存为原始字符串urlencode() 后存储;如需转义用于 SQL,用参数化查询或 mysqli_real_escape_string(),而非 URL 编码Location: $stored_url 即可,由浏览器自行处理编码/解码逻辑?target=' . urlencode($original_url) —— 这是正确用法双重编码问题看似是接收端的事,其实八成发生在生成端。还原只是补救,规范存储才是根治点。尤其当短链要支持带 hash(#)或复杂 query 的 URL 时,任意一层多编码都会让 parse_url() 失效或截断。