nginx 默认先解码uri再匹配location,导致含%2f、%2b、%28%29等编码的url匹配失败或转发错误;应使用前缀匹配或正则匹配编码后字符串,并通过proxy_pass原样转发原始编码路径。

Nginx 处理含特殊编码的 URL(如 %2F、%2B、%28%29)时,核心矛盾在于:它默认先解码 URI 再匹配 location,但转发给后端时又可能不还原编码,导致路径语义错乱或 400/404 错误。关键不是“阻止编码”,而是控制解码时机与转发完整性。
location 匹配本身依赖解码后的路径
Nginx 在匹配阶段会自动对请求 URI 做一次标准解码(RFC 3986),例如:
-
/static/a%28test%29.css→ 解码为/static/a(test).css后再参与匹配 - 这意味着
location ~ a\(test\)\.css$永远不会命中(因为括号已变成字面(,而非%28)
正确写法是:
- ✅ 使用普通前缀匹配:
location /static/ { root /var/www; }—— 它不关心括号是否编码,只要路径以/static/开头就生效 - ✅ 若需正则精确匹配编码字符:
location ~ a%28test%29\.css$(注意点号\.要转义) - ❌ 避免在正则中写未编码的特殊字符(如
\(test\)),除非你确认客户端发来的是未编码原始字符(极少见)
proxy_pass 转发必须保持原始编码
问题常出在这里:
-
location /api/ { proxy_pass http://backend; }→ Nginx 会把/api/foo%2Bbar解码成/api/foo+bar,再拼接到http://backend/,最终发http://backend/api/foo+bar(+被当空格) -
location /api/ { proxy_pass http://backend/; }→ Nginx 剪掉/api/,转发/foo%2Bbar,但若后端期望保留/api/前缀,这就错了
稳妥做法是显式传递完整路径:
- 用正则捕获原始
$request_uri(含 query string)或$uri(仅 path):location /api/ { if ($request_uri ~ ^/api/(.*)$) { proxy_pass http://backend/$1; } } - 或更推荐:关闭隐式拼接,让 Nginx 原样转发整个 URI:
location /api/ { proxy_pass http://backend; # 不加结尾 /,且 location 末尾有 / }这样
/api/v1%2Fusers会原样变成http://backend/api/v1%2Fusers
防止非法编码干扰匹配流程
Nginx 默认拒绝含无效字节(如 %gg)或双重编码(如 %252F)的请求,直接返回 400。若需放行合法编码:
- 确保没启用
underscores_in_headers on(它不相关,但常被误配) - 不要在
map或if中用$request_uri判断编码——它含 query string,而 location 只匹配 path - 日志里加
$request_uri和$uri对比,确认解码行为:log_format debug '$time_local | $request_uri | $uri | $status';
推荐分层处理策略
-
前端:尽量避免生成含
/、+、()的路径;用-或_替代(如v1-test.css) - Nginx:用前缀匹配兜底,正则仅用于必要场景;proxy_pass 避免隐式重写
- 后端:接收方应能解析标准百分号编码,不依赖 Nginx 二次解码
不复杂但容易忽略











