|
```html nginx重写url 一个请求进来,URI 长得像 /article/2024/05/nginx-rewrite.html,但后端应用只想看到 /article.php?id=123。这种时候,nginx 的 rewrite 模块就是干这事的。它不改变浏览器地址栏,也不发 302 跳转,纯粹在服务器内部把 URI 拆了重组,再把请求交给正确的处理程序。 我见过不少配置,rewrite 写了一大堆,最后全被 location 的匹配顺序给搅乱了。nginx 处理请求先走 server 块里的 location,再执行里面的 rewrite。如果你把 rewrite 写在 server 顶层,它会在选 location 之前跑一遍。顺序错了,规则再对也白搭。 举个实际例子。假设有个老站点,所有 /old/xxx 都要永久跳到 /new/xxx。写法是: rewrite ^/old/(.*)$ /new/$1 permanent; 这里的 permanent 返回 301,浏览器地址栏会变。如果只想内部重写,不跳转,就把 permanent 换成 last 或者干脆不写 flag。但 last 和 break 的区别很多人搞混。last 会重新走一遍 location 匹配,break 则停在当前 location 里继续处理。我一般用 last 来处理内部重定向到另一个 location 的情况,用 break 来避免死循环。 正则捕获组是 rewrite 的肌肉。$1、$2 对应括号里的内容。比如把 /user/张三 重写成 /profile.php?name=张三: rewrite ^/user/([^/]+)$ /profile.php?name=$1 last; 注意 [^/]+ 比 .* 安全,它不会跨路径段匹配。有人图省事写 (.*),结果 /user/a/b 也匹配上了,$1 变成 a/b,后端拿到参数直接懵。 还有 if 指令。nginx 官方文档里有一句著名的“if is evil”,但有些场景不得不用。比如根据 $http_user_agent 重写移动端请求: if ($http_user_agent ~* "(android|iphone)") { rewrite ^/$ /m/ last; } 这里 ~* 表示不区分大小写匹配。if 块里尽量只放 rewrite 和 return,别塞 proxy_pass 之类的,否则容易出诡异问题。我踩过一次坑:在 if 里写 proxy_pass,结果请求头丢了,排查半天。 rewrite 和 try_files 经常配合使用。单页应用(SPA)需要把所有不存在的路径都指向 index.html。典型配置: location / { try_files $uri $uri/ /index.html; } 这里不需要 rewrite,try_files 更直接。但如果要把 /api/ 开头的请求重写到后端,同时保留原始 URI,就得用 rewrite 加 proxy_pass: location /api/ { rewrite ^/api/(.*)$ /$1 break; proxy_pass http://backend; } 注意 proxy_pass 后面没有斜杠,配合 break 才能把重写后的 URI 原样传给后端。如果 proxy_pass 带了路径,比如 http://backend/,那 nginx 会替换掉匹配的 location 部分,行为又不一样。这种细节不实际跑一遍很难记住。 rewrite 规则多了会拖慢性能。每条 rewrite 都要做正则匹配,请求量大的时候,能合并就合并,能用 location 精确匹配就别用正则。我见过一个配置里写了 30 多条 rewrite,全是 ^/a/(.*) 这种,其实用 location /a/ 加 alias 就能解决。 调试 rewrite 最直接的办法是看 nginx 的 error_log,把日志级别调到 notice 或 info,重写过程会打出来。或者用 return 200 "uri: $uri"; 临时输出重写后的 URI,比猜来猜去快得多。 最后说个容易忽略的点:rewrite 里的 $args 和 $query_string。默认情况下,重写后的 URI 如果没带 ?,原始查询参数会自动附加。但如果你在替换字符串里写了 ?,原始参数就被丢弃了。比如 rewrite ^/search /find? last; 会丢掉 ?q=nginx,得写成 rewrite ^/search /find?$query_string last; 才行。这个坑我至少见过五次。 ``` |