记录一个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行为还有它的”阴暗面”,网上没有资料说明这一块,下面一起看个例子。

这是我设计的一个场景。

磁盘上有2个目录,它们里面的x文件内容不同:

然后,我们逐条看一下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:

第1行日志代表了第一个If和/break/to_test/(.*)匹配失败,第2行日志代表了第二个rewrite /break/to_last/(.*)匹配失败,第3行日志判断!~ ^/break/(.*)得到执行但条件并没有成立,这是因为前面的rewrite都没有匹配成功,last和break并没有生效,因此if指令可以被ngx_http_rewrite_module模块正常执行。


接下来请求/break/to_test/x:

第1行日志代表了第一个If匹配成功,因此进入了if。第2行日志代表了if内的rewrite匹配成功(指定了break),因此第3行日志打印URI被改写为/x。接下来还有1个rewrite /break/to_last/(.*)并没有打印match日志,后续的if也没有打印,说明rewrite的break生效了,符合预期。

接下来请求/break/to_last/x:

第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可以做很多事情,玩的愉快。

 

 

 

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~