IT牛人博客聚合网站(www.udpwork.com)用到了 Apache 的 mod_rewrite 模块进行 URL 重写. 但是, 在使用过程中曾经出现过一个比较诡异的问题. 开始认为是重写规则设置得不对, 后来才发现, 是"%2F"导致 Apache 直接返回 404 错误.
比如浏览查看某个标签下的文章列表的链接为
http://www.udpwork.com/tag/Linux
在重写之前的链接是
http://www.udpwork.com/?tag=Linux
在 PHP 脚本中用如下代码重写 URL
$url = '/tag/' . urlencode($tag);
相应的 Apache mod_rewrite 配置为
RewriteCond %{REQUEST_URI} ^/tag/.*$ RewriteRule ^/tag/(.*)$ ?tag=$1 [L]
不过, 当 tag 中包含斜杠'/'时, 出现了问题, 服务器提示"404 - Not Found". 比如 tag 是 Unix/Linux, 生成的链接是
http://www.udpwork.com/tag/Unix%2FLinux
斜杠'/'被转义成'%2F', 那么最终的还原后的链接应该是
http://www.udpwork.com/?tag=Unix%2FLinux
直接访问这个未重写过的 URL, 是完全正常的. 但 Apache 一直报 404 错误. 后来才发现, 原来 Apache 有一个配置项"AllowEncodedSlashes", 默认是"Off", 也就是不允许请求路径(上例是 /tag/Unix%2FLinux)中包含编码后的斜杠'/'(在某些平台是反斜杠'\'). 这个选项的的相应代码在 mod_rewrite 模块被执行之前
// request.c AP_DECLARE(int) ap_process_request_internal(request_rec *r){ if (d->allow_encoded_slashes) { access_status = ap_unescape_url_keep2f(r->parsed_uri.path); } else { access_status = ap_unescape_url(r->parsed_uri.path); } } // util.c AP_DECLARE(int) ap_unescape_url(char *url){ if (IS_SLASH(*x) || *x == '\0') badpath = 1; ... else if (badpath) return HTTP_NOT_FOUND; }
默认不允许包含"%2F". 如果请求路径中包含了, 那么 ap_unescape_url() 函数认为是无效的路径, 直接返回 HTTP_NOT_FOUND, 最终浏览器得到的是"404 - Not Found"出错页面. 当然可以通过修改 Apache 配置来解决这个问题, 不过, 在 PHP 脚本中解决更通用
$url = '/tag/' . urlencode(str_replace('/', '%2F', $tag));
这样, 斜杠被两次转义, 变为"/tag/Unix%252FLinux". Apache 接收到请求后, 进行一次解码, 得到"/tag/Unix%2FLinux", 以参数"tag=Unix%2FLinux"交给 PHP 脚本处理, PHP 自己再将请求参数进行一次解码, $_GET['tag'] 的值就是"Unix/Linux"了.