Nginx loaction优先级探究

本文通过配置实例来探究Nginx中location的优先级,在Nginx官方文档中,有优先级的说明,我们通过实例对此进行验证,加深理解。

实验声明:
return指令
该指令可以直接返回一个状态码及文本信息,由于是实验环境,没有必要建立真实的目录来对比location的命中,也免去了查看日志等繁琐的步骤。
因此,此实验均使用return指令来返回结果,用到的指令格式为return code text

实验环境
本文中使用的server_name采用的域名test.local.lyz810.com为真实域名,绑定IP为127.0.0.1,所以读者可以完全复制到本机实验。另外,*.local.lyz810.com域名做了泛解析,全部解析到127.0.0.1,读者可以任意选用该域名的子域名进行各种实验。

location类型

  1. 精确匹配:=
  2. 正则大小写敏感:~
  3. 正则大小写不敏感:~*
  4. 前缀匹配(高优先级):^~
  5. 前缀匹配:/

location优先级
以下是官网中给出的优先级:

To find location matching a given request, nginx first checks locations defined using the prefix strings (prefix locations). Among them, the location with the longest matching prefix is selected and remembered. Then regular expressions are checked, in the order of their appearance in the configuration file. The search of regular expressions terminates on the first match, and the corresponding configuration is used. If no match with a regular expression is found then the configuration of the prefix location remembered earlier is used.

翻译:
Nginx首先检查使用前缀定义的location,会选择并记住一个最长匹配的location作为候选。
检查正则表达式,根据出现在配置文件中的顺序。
遇到第一个匹配的正则表达式就停止搜索。
如果没有匹配的正则表达式,就使用最开始候选的location。

由此可以看出,正则表达式优先级最高,多个正则表达式按从上到下就先匹配到的为准,其次是前缀location。
注意,上面是普通前缀与正则之间的比较,所有location中精确匹配优先级最高。

实验一
目的:验证精确匹配优先级最高
原理:让所有类型的location出现在一个server中,并将精确匹配放到最后,消除顺序可能带来的影响,观察是否命中精确匹配location
配置:

server {
  server_name test.local.lyz810.com;
  default_type text/plain;

  location /test123 {
    return 200 prefix;
  }
  
  location ~ "/test\d{3}$" {
    return 200 regular;
  }

  location =/test123 {
    return 200 exact;
  }
}

结果:访问/test123,返回exact.说明精确匹配优先级最高

实验二
目的:验证正则的优先级高于前缀
原理:访问一个同时满足前缀和正则的页面,观察返回结果。为了排除顺序的影响,提供两个不同顺序。
配置:

server {
  server_name test.local.lyz810.com;
  default_type text/plain;

  location /test123 {
    return 200 prefix;
  }
  
  location ~ "/test\d{3}" {
    return 200 regular;
  }

  location ~ "/welcome\d{3}$" {
    return 200 regular;
  }

  location /welcome123 {
    return 200 prefix;
  }
}

结果:访问/test123和/welcome123都返回regular,说明正则优先级高于前缀,并且与正则和前缀出现的顺序无关。

实验三
目的:验证前缀匹配只与匹配长度有关,与顺序无关。
原理:通过不同顺序的前缀匹配,观察输出结果。
配置:

server {
  server_name test.local.lyz810.com;
  default_type text/plain;

  location /test123 {
    return 200 test123;
  }
  
  location /test1234 {
    return 200 test1234;
  }

  location /welcome1234 {
    return 200 welcome1234;
  }

  location /welcome123 {
    return 200 welcome123;
  }
}

结果:访问/welcome1234和/test1234分别返回welcome1234和test1234,与顺序无关,只与匹配长度有关。

实验四
目的:比较高优先级前缀(^~)与正则的优先级
原理:将正则location置于最前,高优先级前缀location置于后面,通过观察输出结果,比较优先级。
配置:

server {
  server_name test.local.lyz810.com;
  default_type text/plain;

  location ~ "/test\d{3}" {
    return 200 regular;
  }

  location ^~ /test123 {
    return 200 prefix;
  }
}

结果:访问/test123返回prefix,证明^~优先级高于正则

实验五
目的:一个配置文件了解location优先级
配置:

server {
  server_name test.local.lyz810.com;
  default_type text/plain;

  location =/test12345 {
    return 200 =/test12345;
  }

  location /test12 {
    return 200 /test12;
  }

  location /test1234 {
    return 200 /test1234;
  }

  location ^~ /test123 {
    return 200 "^~/test123";
  }

  location ~ "/test\d{3}$" {
    return 200 "~/test\d{3}";
  }

  location ~ "/test\d{4}$" {
    return 200 "~/test\d{4}";
  }

  location ~ "/test\d*" {
    return 200 "~/test\d*";
  }
}

结果:
1.访问/test12返回~/test\d*,匹配项/test12、/test\d*,正则优先级高,故命中正则。
2.访问/test123返回^~/test123,匹配项/test12、^~/test123、~/test\d{3}、~/test\d*,先对前缀进行比较,发现有2个前缀/test12和^~/test123,后者长,用后者,然后发现前缀匹配带^~,不再查找正则,故命中^~/test123。
3.访问/test1234返回~/test\d{4},匹配项/test12、/test1234、^~/test123、~/test\d{4}、~/test\d*,先对前缀进行比较,发现有3个前缀/test12、/test1234、^~/test123,根据匹配长度,选中/test1234,再对正则进行匹配,~/test\d{4}是第一个匹配的正则,而之前选出的前缀中不带^~,故命中第一个匹配的正则。
4.访问/test12345返回=/test12345,匹配项=/test12345、/test12、/test1234、^~/test123、~/test\d*,有精确匹配存在,其他的都靠边站。
5.访问/test123456返回~/test\d*,匹配项/test12、/test1234、^~/test123、~/test\d*,前缀选择/test1234,无^~,命中第一个(唯一一个)匹配的正则表达式。
6.访问/welcome/test1234返回~/test\d{4},匹配项~/test\d{4}、~/test\d*,正则可以匹配path的一部分,而前缀只能从头开始匹配。
7.访问/test12345?params=1返回=/test12345,location匹配的是path部分,不包括后面的queryString。

总结
location匹配的算法描述如下(实验的出的结论,非源码中的算法):
1.令pathname=url路径部分,findPrefix=””
2.令prefixList=所有前缀location,regularList=所有正则location,exactList=所有精确匹配location
3.遍历exactList,查找location =pathname,如果找到,则命中,算法结束
4.遍历prefixList,如果匹配pathname则与findPrefix的长度进行比较
4.1如果长度大于findPrefix,令findPrefix=当前location
4.2如果长度小于findPrefix,不做任何操作,继续遍历
4.3如果长度等于findPrefix,在配置载入的时候,会报location重复的错误(如同时存在/123和^~/123),没有该分支
5.判断findPrefix是否带^~前缀,如果带,命中findPrefix,算法结束
6.遍历regularList,如果匹配pathname,命中当前location,算法结束
7.如果findPrefix不为空,命中findPrefix,算法结束
8.输出404

解决openssl升级到1.1.0后shadowsocks服务报错问题

本文适用于解决openssl升级到1.1.0以上版本,导致shadowsocks2.8.2启动报undefined symbol: EVP_CIPHER_CTX_cleanup错误。

最近将openssl升级到了1.1.0b版本,编译之后shadowsocks无法启动,报错如下:

Traceback (most recent call last):
  File "/usr/bin/ssserver", line 9, in 
    load_entry_point('shadowsocks==2.8.2', 'console_scripts', 'ssserver')()
  File "/usr/lib/python2.7/site-packages/shadowsocks/server.py", line 34, in main
    config = shell.get_config(False)
  File "/usr/lib/python2.7/site-packages/shadowsocks/shell.py", line 262, in get_config
    check_config(config, is_local)
  File "/usr/lib/python2.7/site-packages/shadowsocks/shell.py", line 124, in check_config
    encrypt.try_cipher(config['password'], config['method'])
  File "/usr/lib/python2.7/site-packages/shadowsocks/encrypt.py", line 44, in try_cipher
    Encryptor(key, method)
  File "/usr/lib/python2.7/site-packages/shadowsocks/encrypt.py", line 83, in __init__
    random_string(self._method_info[1]))
  File "/usr/lib/python2.7/site-packages/shadowsocks/encrypt.py", line 109, in get_cipher
    return m[2](method, key, iv, op)
  File "/usr/lib/python2.7/site-packages/shadowsocks/crypto/openssl.py", line 76, in __init__
    load_openssl()
  File "/usr/lib/python2.7/site-packages/shadowsocks/crypto/openssl.py", line 52, in load_openssl
    libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,)
  File "/usr/lib64/python2.7/ctypes/__init__.py", line 373, in __getattr__
    func = self.__getitem__(name)
  File "/usr/lib64/python2.7/ctypes/__init__.py", line 378, in __getitem__
    func = self._FuncPtr((name_or_ordinal, self))
AttributeError: /usr/local/ssl/lib/libcrypto.so.1.1: undefined symbol: EVP_CIPHER_CTX_cleanup
shadowsocks start failed

这个问题是由于在openssl1.1.0版本中,废弃了EVP_CIPHER_CTX_cleanup函数,如官网中所说:

EVP_CIPHER_CTX was made opaque in OpenSSL 1.1.0. As a result, EVP_CIPHER_CTX_reset() appeared and EVP_CIPHER_CTX_cleanup() disappeared. EVP_CIPHER_CTX_init() remains as an alias for EVP_CIPHER_CTX_reset().

EVP_CIPHER_CTX_reset函数替代了EVP_CIPHER_CTX_cleanup函数
EVP_CIPHER_CTX_reset函数说明:

EVP_CIPHER_CTX_reset() clears all information from a cipher context and free up any allocated memory associate with it, except the ctx itself. This function should be called anytime ctx is to be reused for another EVP_CipherInit() / EVP_CipherUpdate() / EVP_CipherFinal() series of calls.

EVP_CIPHER_CTX_cleanup函数说明:

EVP_CIPHER_CTX_cleanup() clears all information from a cipher context and free up any allocated memory associate with it. It should be called after all operations using a cipher are complete so sensitive information does not remain in memory.

可以看出,二者功能基本上相同,都是释放内存,只是应该调用的时机稍有不同,所以用reset代替cleanup问题不大。

修改方法:

  1. 用vi打开文件:vi /usr/lib/python2.7/site-packages/shadowsocks/crypto/openssl.py
  2. 跳转到52行(shadowsocks2.8.2版本,其他版本搜索一下cleanup)
  3. 进入编辑模式
  4. 将第52行libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,)
    改为libcrypto.EVP_CIPHER_CTX_reset.argtypes = (c_void_p,)
  5. 再次搜索cleanup(全文件共2处,此处位于111行),将libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx)
    改为libcrypto.EVP_CIPHER_CTX_reset(self._ctx)
  6. 保存并推出
  7. 启动shadowsocks服务:service shadowsocks start

之后我们就可以继续愉快的、科学的畅游互联网了。
提示:openssl1.1.0目前兼容性很不好,大部分的软件都不支持
目前支持的有nginx-1.11.5、curl-7.50.3
不支持的有PHP-7.0.12、openssh-7.3p1
所以如果决定使用openssl1.1.0需要考虑很多兼容问题,必须保留1.0.2或1.0.1(不推荐,存在一些已知漏洞,最重要的是如果服务器要开http2,由于新版chrome必须使用ALPN的限制,只有1.0.2版本支持ALPN,所以必须升级到1.0.2)版本以便编译其他程序。