内容简介:记录一个nginx rewrite的坑
nginx很重要也很常用的一个功能就是rewrite,经常被用于实现伪静态SEO、升级迁移等各类运维需求。
今天专门学习了一下相关的配置方法,着实被坑了一顿,因此记录在这里。
基础知识
关于nginx rewrite配置的原理和方法,可以参考 这篇博客 。
配置执行顺序
概括的来说,nginx收到请求后会找到对应vhost的server配置开始执行解析,其执行顺序如下:
- 执行server块的rewrite指令:当然也包括其他指令,在这个过程中会跳过location指令的解析。
-
执行location匹配:当server块内的指令解析完后,会开始解析所有的location指令,也就是获取它们的匹配规则(注意不是执行location内的代码),得到全部location的匹配规则后,会按照一个优先级开始对URI进行规则匹配:
- =前缀的指令严格匹配这个查询。如果找到,停止搜索。
- 所有剩下的常规字符串,字符串长的优先匹配。如果这个匹配使用^〜前缀,搜索停止。
- 正则表达式,在配置文件中定义的顺序进行逐一匹配。
- 如果第3条规则产生匹配的话,结果被使用。否则,使用第2条规则的结果。
- 执行选定的location中的rewrite指令:经过上面的步骤就选定location了,接下来做的就是执行这个location内的代码即可(与其他location再无瓜葛)。当然,location内的全部指令都会被执行,不仅仅是rewrite。
关于rewrite的last和break
概括的说,它们具有如下的相同点:
break和last并不会阻止代码继续向下执行,而是跳过后面其他的rewrite指令,相当于视而不见。
它们具有如下的区别:
last不仅会继续执行后面的代码,而且当执行完毕后会重置nginx的解析状态,以重写后的URI为准重新进行Nginx匹配,就像一个新的请求一样。而break与last相比,只是缺少了这个行为而已。
当然,rewrite可以不跟随break、last,这种情况下后面的代码也将继续执行,并且rewrite也一样会被执行。
实例演示
事实上,nginx rewrite的break和last行为还有它的”阴暗面”,网上没有资料说明这一块,下面一起看个例子。
log_format myformat 'is_rewrite=' $is_rewrite; server { listen 9999; rewrite_log on; access_log /Users/webroot/access.log myformat; error_log /Users/webroot/error.log notice; root /Users/webroot/; location /break/ { # to_test直接访问test目录 if ($uri ~ ^/break/to_test/(.*)) { rewrite ^/break/to_test/(.*) /$1 break; root /Users/webroot/test; } # to_last改写到last,重走匹配 rewrite ^/break/to_last/(.*) /last/$1 last; # uri被重写, 则设置一个变量 if ($uri !~ ^/break/(.*)) { set $is_rewrite 1; } } location /last/ { rewrite ^/last/(.*) /test/$1 last; } location /test/ { } }
这是我设计的一个场景。
磁盘上有2个目录,它们里面的x文件内容不同:
liangdong:Documents webroot$ cat break/x break liangdong:Documents webroot$ cat test/x test
然后,我们逐条看一下nginx配置的目的:
- log_format:配置了一个access log的格式,命名为myformat。
- server:一个vhost,监听在9999端口。
- rewrite_log:on,将rewrite模块的日志输出打开,便于调试。
- access_log:这个vhost的access log使用myformat格式。
- error_log:错误日志和rewrite日志都会写到这个文件。
- root:这个vhost的文档目录。
- location /break:如果访问/break/x将匹配该location指令。
- location /last:如果访问/last/x将匹配该location指令。
- location /test:如果访问/test/x将匹配该location指令。
这里回顾一下”配置执行顺序”部分讲解的流程,在URI匹配得到对应的location后,将执行location内的指令,并结束请求。
接下来,我们逐个location来讲解,分别看看匹配时它们会做什么。
- 请求:/test/x
- 匹配:location /test
- 执行:内部指令是空,那么location立即执行完成。因为在server层指定了root,所以nginx会去/Users/webroot/下查找test/x文件,返回内容为test。
- 请求:/last/x
- 匹配:location /last
- 执行:rewrite将/last/x重写到了/test/x,最后的last会导致nginx重新解析请求,新的URI是/test/x,这次解析将会匹配location /test,也就是执行上一条规则内的指令,请求最终结束。
- 请求:/break/x
- 匹配:location /break
- 执行:首先演示了if正则的用法,显然/break/x不匹配^/break/to_test/(.*)规则,因此不进入If。同样,接下来的rewrite指令^/break/to_last/(.*)也未能匹配,因此跳过继续执行。最后执行if语句,/break/x可以匹配^/break/(.*)因此不会进入if(这个if判断的是不等于!~)。至此,该location结束执行,也就是什么事情也没做,会使用server中的root作为查找路径,最终在/Users/webroot/的break/x路径下找到文件,输出为”break”。
- 请求:/break/to_test/x
- 匹配:location /break
-
执行:这次第一个If语句成功匹配,if内的rewrite可以成功执行,将/break/to_test/x重写至了/x,break将导致该rewrite之后的所有rewrite将被忽略,但代码将继续向下执行(这很重要),因此接下来的root指令会得到执行,root被直接指向了/Users/webroot/test目录,其覆盖了server中定义的默认root。接下来遇到第二个rewrite指令时将直接跳过(因为之前的break)。
按照规则,接下来的if语句应该可以得到执行,并且也应满足条件,is_rewrite变量将会被设置为1,然而现实却是if没有执行,”坑”来了!
网上的资料都会说break/last之后的rewrite指令不会被执行,但是为什么导致了if不执行呢?其实,这种说法是”以讹传讹”的错误结论,按照官方的说法break和last终止的是 ngx_http_rewrite_module模块的执行,这个模块负责了像if、set、return、rewrite这些语法的解析和执行,因此不仅仅是rewrite失效,后续出现的这些语法都会失效!
官方文档地址: http://nginx.org/en/docs/http/ngx_http_rewrite_module.html#break ,break的相关说明:
Stops processing the current set of ngx_http_rewrite_module
directives.
If a directive is specified inside the location , further processing of the request continues in this location.
注意,停止的是the current set of ngx_http_rewrite_module
的执行,而不是Stops processing。因此,指令将继续下来执行,而属于ngx_http_rewrite_module的指令将被跳过。
这里贴一下rewrite日志,看一下究竟。
首先是请求/break/x:
2017/06/12 17:55:14 [notice] 29778#0: *790 "^/break/to_test/(.*)" does not match "/break/x", client: 127.0.0.1, server: , request: "GET /break/x HTTP/1.1", host: "localhost:9999" 2017/06/12 17:55:14 [notice] 29778#0: *790 "^/break/to_last/(.*)" does not match "/break/x", client: 127.0.0.1, server: , request: "GET /break/x HTTP/1.1", host: "localhost:9999" 2017/06/12 17:55:14 [notice] 29778#0: *790 "^/break/(.*)" matches "/break/x", client: 127.0.0.1, server: , request: "GET /break/x HTTP/1.1", host: "localhost:9999"
第1行日志代表了第一个If和/break/to_test/(.*)匹配失败,第2行日志代表了第二个rewrite /break/to_last/(.*)匹配失败,第3行日志判断!~ ^/break/(.*)得到执行但条件并没有成立,这是因为前面的rewrite都没有匹配成功,last和break并没有生效,因此if指令可以被ngx_http_rewrite_module模块正常执行。
接下来请求/break/to_test/x:
2017/06/12 17:57:49 [notice] 29778#0: *792 "^/break/to_test/(.*)" matches "/break/to_test/x", client: 127.0.0.1, server: , request: "GET /break/to_test/x HTTP/1.1", host: "localhost:9999" 2017/06/12 17:57:49 [notice] 29778#0: *792 "^/break/to_test/(.*)" matches "/break/to_test/x", client: 127.0.0.1, server: , request: "GET /break/to_test/x HTTP/1.1", host: "localhost:9999" 2017/06/12 17:57:49 [notice] 29778#0: *792 rewritten data: "/x", args: "", client: 127.0.0.1, server: , request: "GET /break/to_test/x HTTP/1.1", host: "localhost:9999"
第1行日志代表了第一个If匹配成功,因此进入了if。第2行日志代表了if内的rewrite匹配成功(指定了break),因此第3行日志打印URI被改写为/x。接下来还有1个rewrite /break/to_last/(.*)并没有打印match日志,后续的if也没有打印,说明rewrite的break生效了,符合预期。
接下来请求/break/to_last/x:
2017/06/12 18:03:57 [notice] 29778#0: *794 "^/break/to_test/(.*)" does not match "/break/to_last/x", client: 127.0.0.1, server: , request: "GET /break/to_last/x HTTP/1.1", host: "localhost:9999" 2017/06/12 18:03:57 [notice] 29778#0: *794 "^/break/to_last/(.*)" matches "/break/to_last/x", client: 127.0.0.1, server: , request: "GET /break/to_last/x HTTP/1.1", host: "localhost:9999" 2017/06/12 18:03:57 [notice] 29778#0: *794 rewritten data: "/last/x", args: "", client: 127.0.0.1, server: , request: "GET /break/to_last/x HTTP/1.1", host: "localhost:9999" 2017/06/12 18:03:57 [notice] 29778#0: *794 "^/last/(.*)" matches "/last/x", client: 127.0.0.1, server: , request: "GET /break/to_last/x HTTP/1.1", host: "localhost:9999" 2017/06/12 18:03:57 [notice] 29778#0: *794 rewritten data: "/test/x", args: "", client: 127.0.0.1, server: , request: "GET /break/to_last/x HTTP/1.1", host: "localhost:9999"
第1行日志说明If /break/to_test/(.*)匹配失败,因此没有进入If。第2行日志说明rewrite /break/to_last/(.*)匹配成功(指定了last),因此第3行日志打印URI被改写为/last/x。代码继续向下执行到最后一个If,因为之前rewrite last的原因If将不会被执行,因此没有相关日志打印。location执行完成,因为last原因URI被Nginx重新执行解析,这次匹配了location /last/,其内部的rewrite直接将/last/x重写为/test/x,因为其last的原因又再次被nginx重新执行解析。最终,与location /test匹配,直接读取server中的root路径/Users/webroot下的/test/x文件,请求结束。
掌握了rewrite可以做很多事情,玩的愉快。
以上所述就是小编给大家介绍的《记录一个nginx rewrite的坑》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。