你真的了解如何将 Nginx 配置为 Web 服务器吗

目录
  1. 设置虚拟服务器
  2. listen
  3. server_name
  4. 配置 location
  5. URI 与 location 参数的匹配
  6. 静态文件和代理
  7. 使用变量(Variables)
  8. 返回特定状态码
  9. 错误处理
  10. 重写 URI
  11. 附录
  12. 常用正则
  13. 全局变量
  14. 参考

阅读之前,建议先阅读初识 Nginx。之后,我们来了解一下 Nginx 配置。

抽象来说,将 Nginx 配置为 Web 服务器就是定义处理哪些 URL 和如何处理这些 URL 对应的请求。具体来说,就是定义一些虚拟服务器(Virtual Servers),控制具有特定 IP 和域名的请求。

更具体的来说,Nginx 通过定义一系列 location 来控制对 URI 的选择。每一个 location 定义了对映射到自己的请求的处理场景:返回一个文件或者代理请求,或者根据不同的错误代码返回不同的错误页面。另外,根据 URI 的不同,请求也可以被重定向到其它 server 或者 location 。

设置虚拟服务器

虚拟服务器由 listen 和 server_name 两个指令共同确定。

listen

Nginx 配置文件至少包含一个 server 指令,用来定义虚拟服务器。当请求到来时,Nginx 会首先选择一个虚拟服务器来处理该请求。

虚拟服务器定义在 http 上下文中的 server 中:

http {
    server {
        # Server configuration
    }
}

注意:http 中可以定义多个 server

server 配置块使用 listen 指令监听本机 IP 和端口号(也可以是 Unix domain socket 路径),支持 IPv4、IPv6,IPv6 地址需要用方括号括起来:

server {
    listen 127.0.0.1:8080;  # IPv4 地址,8080 端口
    # listen [2001:3CA1:10F:1A:121B:0:0:10]:80;   # IPv6 地址,80 端口
    # listen [::]:80;  # 监听本机所有 IPv6 地址,80 端口
    # The rest of server configuration
}

上述配置,如果不写端口号,默认使用 80 端口;如果不写 IP,则监听本机所有 IP。

server_name

如果多个 server 的 listen IP 和端口号一模一样,Nginx 通过请求头中的 Host 字段(例如 Host: lufficc.com)与 server_name 定义的主机名进行比较,来选择合适的虚拟服务器处理请求。例如:

server {
    listen      80;
    server_name lufficc.com  www.lufficc.com;
    ...
}

server_name 的参数可以为:

  1. 完整的主机名,如:api.lufficc.com 。
  2. 含有通配符(含有 *),如:*.lufficc.com 或 api.* 。
  3. 正则表达式,以 ~ 开头。

通配符只能在开头或结尾,而且只能与一个 . 相邻。www.*.example.org 和 w*.example.org 均无效。但是,可以使用正则表达式匹配这些名称,例如 ~^www\..+\.example\.org$ 和 ~^w.*\.example\.org$。而且 * 可以匹配多个部分:名称 *.example.org 不仅匹配 www.example.org,还匹配 www.sub.example.org。

对于正则表达式:Nginx 使用的正则表达式与 Perl 编程语言(PCRE)使用的正则表达式兼容。要使用正则表达式,必须以 ~ 开头。

命名的正则表达式可以捕获变量,然后使用:

server {
    server_name   ~^(www\.)?(?<domain>.+)$;

    location / {
        root   /sites/$domain;
    }
}

小括号 () 之间匹配的内容,也可以在后面通过 $1 来引用,$2 表示的是前面第二个 () 里的内容。

因此上述内容也可写为:

server {
    server_name   ~^(www\.)?(.+)$;

    location / {
        root   /sites/$2;
    }
}

server_name 可以同时写多个参数,混用完整主机名和通配符:

server {
    listen      80;
    server_name api.lufficc.com  *.lufficc.com;
    ...
}

如果多个名称匹配 Host 头部,Nginx 采用下列顺序选择,找到即停:

  1. 完整的主机名,如 api.lufficc.com。
  2. 最长的,且以 * 开头的通配名,如:*.lufficc.com。
  3. 最长的,且以 * 结尾的通配名,如:api.*。
  4. 第一个匹配的正则表达式(按照配置文件中的顺序)。

即优先级:api.lufficc.com > *.lufficc.com > api.* > 正则。注意前两类比的是“最长”,与书写顺序无关;只有正则才看先后顺序。

如果 Host 头部不匹配任何一个 server_name,Nginx 将请求路由到该端口的默认虚拟服务器。默认虚拟服务器是指:监听该端口的第一个 server,或者显式用 default_server 声明的那个:

server {
    listen      80 default_server;
    ...
}

下面这 6 个 server 监听同一个端口,换几个 Host 试试,看看 Nginx 是在哪一级停下来的:

server_name 选择顺序
  1. 1精确名
  2. 2最长 *. 通配
  3. 3最长 .* 通配
  4. 4第一个正则
  5. 5default_server
  1. Aserver_name api.lufficc.com;
  2. Bserver_name *.lufficc.com;
  3. Cserver_name *.dev.lufficc.com;
  4. Dserver_name api.*;
  5. Eserver_name ~^www\d+\.example\.net$;
  6. Fserver_name _;default_server

没有精确名;以 * 开头的通配名里,C 的 *.dev.lufficc.com 匹配且最长。

点选或输入 Host(端口会被忽略),优先级阶梯会依次亮起,停在第一个有匹配的一级。

配置 location

选定 server 之后,由 location 决定请求最终如何处理。

URI 与 location 参数的匹配

当选择好 server 之后,Nginx 会根据 URI 选择合适的 location 来决定代理请求或者返回文件。

location 指令接受两种类型的参数:

  1. 前缀字符串(路径名称)
  2. 正则表达式

对于前缀字符串参数,URI 必须严格地以它开头。例如对于 /some/path/ 参数,可以匹配 /some/path/document.html ,但是不匹配 /my-site/some/path,因为 /my-site/some/path 不以 /some/path/ 开头。写成配置就是:

location /some/path/ {
    ...
}

对于正则表达式,以 ~ 开头表示大小写敏感,以 ~* 开头表示大小写不敏感。注意路径中的 . 要写成 \.。例如一个匹配以 .html 或者 .htm 结尾的 URI 的 location:

location ~ \.html?$ {
    ...
}

正则表达式的优先级大于前缀字符串。

如果找到匹配的前缀字符串,仍继续搜索正则表达式,但如果前缀字符串以 ^~ 开头,则不再检查正则表达式。

具体的搜索匹配流程如下(以官方文档为准):

  1. 将 URI 与所有的前缀字符串进行比较。
  2. = 修饰符表明 URI 必须与前缀字符串相等(不是开始,而是相等),如果找到,则搜索停止。
  3. 在所有匹配的前缀字符串中,记住最长的那一个。
  4. 如果这个最长前缀以 ^~ 开头,则不再搜索正则表达式,直接使用它。
  5. 否则,按照配置文件中的顺序测试正则表达式。
  6. 找到第一个匹配的正则表达式后停止,使用它。
  7. 如果没有正则表达式匹配,使用第 3 步记住的前缀字符串对应的 location。

两个容易误解的地方:

  • ^~ 只有在它正好是最长前缀时才生效,一个更短的 ^~ 前缀拦不住正则。
  • 前缀之间只比长度、与书写顺序无关,正则之间则只看书写顺序、不比“哪个更具体”。

另外,匹配用的是规范化之后的 URI:不含 ? 后面的参数,%XX 已解码,. 和 .. 已解析,连续的 / 被合并为一个。

下面用官方文档里的例子把这个流程跑一遍。换一个 URI,看每个 location 被依次检查的结果:

location 匹配模拟器
配置
server {
  1. Alocation = /精确匹配跳过
  2. Blocation /前缀被更长前缀取代
  3. Clocation /documents/前缀记住为最长前缀
  4. Dlocation ^~ /images/前缀 · 命中最长则跳过正则跳过
  5. Elocation ~* \.(gif|jpg|jpeg)$正则 · 不区分大小写命中
}

/documents/1.jpg:最长前缀 C 不带 ^~,于是按顺序检查正则,E ~* \.(gif|jpg|jpeg)$ 第一个匹配,正则胜出。

选择一组配置,再点选或输入 URI:先比较精确与前缀 location(记住最长的),再按顺序试正则。

= 修饰符拥有最高的优先级。如网站首页访问频繁,我们可以专门定义一个 location 来减少搜索匹配次数(因为搜索到 = 修饰的匹配的 location 将停止搜索),提高速度:

location = / {
    ...
}

静态文件和代理

location 也定义了如何处理匹配的请求:返回静态文件 或者 交给代理服务器处理。下面的例子中,第一个 location 返回 /data 目录中的静态文件,第二个 location 则将请求传递给 https://lufficc.com 域名的服务器处理:

server {
    location /images/ {
        root /data;
    }

    location / {
        proxy_pass https://lufficc.com;
    }
}

root 指令定义了静态文件的根目录,并且和 URI 拼接形成最终的本地文件路径。如请求 /images/example.png,则拼接后返回本地服务器文件 /data/images/example.png。

proxy_pass 指令将请求传递到 URL 指向的代理服务器,然后将来自代理服务器的响应转发给客户端。在上面的示例中,所有不以 /images/ 开头的 URI 的请求都将传递给代理服务器处理。

比如我当年把 proxy_pass 设置为 https://www.baidu.com,那么访问 search.lufficc.com 就会得到和百度首页一样的响应(页面),连搜索功能都和百度没差别(这个演示站点现在已经下线了),配置如下:

server{
      listen 80;
      server_name search.lufficc.com;
      location / {
              proxy_pass https://www.baidu.com;
      }
}

注意:代理到别人的 HTTPS 站点时,Nginx 默认不发送 SNI,很多 CDN 会因此拒绝握手,通常还需要加上 proxy_ssl_server_name on;。

使用变量(Variables)

你可以使用变量来使 Nginx 在不同的请求下采用不同的处理方式。变量是在运行时计算的,用作指令的参数。变量由 $ 开头的符号表示。变量基于 Nginx 的状态定义信息,例如当前处理的请求的属性。

有很多预定义变量,例如核心的 HTTP 变量,你也可以使用 set、map 和 geo 指令定义自定义变量。大多数变量在运行时计算,并包含与特定请求相关的信息。例如,$remote_addr 包含客户端 IP 地址,$uri 保存当前 URI 值。

一些常用的变量如下:

变量名称作用
$uri请求中的当前 URI(不带请求参数),它可以通过内部重定向或者 index 指令进行修改,$uri 不包含主机名,如 /foo/bar.html。
$arg_name请求参数 name 的值,如 ?name=lufficc 时为 lufficc
$hostname运行 Nginx 的机器的主机名
$args请求行中的参数(查询字符串)
$query_string同 $args
$request完整的原始请求行,如 GET /foo?x=1 HTTP/1.1
$request_uri包含请求参数的原始 URI,它无法修改,不包含主机名,如:/cnphp/test.php?arg=freemouse。
……

一个简单的应用就是从 http 重定向到 https 时带上路径信息,其中 $request_uri 即上表中的原始 URI:

server{
       ...
       return      301 https://lufficc.com$request_uri;
       ...
}

返回特定状态码

如果你的网站上的一些资源永久移除了,最快最简洁的方法就是使用 return 指令直接返回:

location /wrong/url {
    return 404;
}

return 的第一个参数是响应代码。可选的第二个参数可以是重定向(对应于代码 301、302、303、307 和 308)的 URL,或在响应正文中返回的文本。例如:

location /permanently/moved/url {
    return 301 http://www.example.com/moved/here;
}

return 指令可以包含在 location 和 server 上下文中。放在 location 里:

server{
      location / {
              return 404;
      }
}

或者放在 server 里:

server{
      ...
      return 404;
      location / {
          ...            
      }
}

错误处理

error_page 命令可以配置特定错误码的错误页面,或者重定向到其他的页面。下面的示例将在 404 错误发生时返回 /404.html 页面。

error_page 404 /404.html;

error_page 命令定义了如何处理错误,因此不会直接返回,而 return 确实会立即返回。当代理服务器或者 Nginx 处理时产生相应的错误代码,均会返回相应的错误页面。

在下面的示例中,当 Nginx 找不到页面时,它将使用代码 301 替换代码 404,并将客户端重定向到 http://example.com/new/path.html:

location /old/path.html {
    error_page 404 =301 http://example.com/new/path.html;
}

此配置很有用,比如当客户端仍尝试用旧的 URI 访问页面时,301 代码通知浏览器页面已永久移除,并且需要自动替换为返回的新地址。

重写 URI

rewrite 指令可以多次修改请求的 URI。rewrite 的第一个参数是 URI 需要匹配的正则表达式,第二个参数是将要替换的 URI。第三个参数可选,是一个标志:last、break 决定是否继续重写,redirect(302)、permanent(301)则直接返回重定向。例如:

location /users/ {
    rewrite ^/users/(.*)$ /show?user=$1 break;
}

你可以在 server 和 location 上下文中包括多个 rewrite 指令。Nginx 按照它们出现的顺序一个一个地执行指令。当选择 server 时,server 中的 rewrite 指令将执行一次。

在 Nginx 处理一组 rewrite 指令之后,它根据新的 URI 选择 location(也就是把上面的匹配流程再走一遍)。如果所选 location 仍旧包含 rewrite 指令,它们将依次执行。如果 URI 被其中某条改写了,则在处理完所有定义的 rewrite 指令后,搜索新的 location。

以下示例将 rewrite 指令与 return 指令结合使用:

server {
    ...
    rewrite ^(/download/.*)/media/(.*)\..*$ $1/mp3/$2.mp3 last;
    rewrite ^(/download/.*)/audio/(.*)\..*$ $1/mp3/$2.ra  last;
    return  403;
    ...
}

诸如 /download/some/media/file 的 URI 被改为 /download/some/mp3/file.mp3。由于 last 标志,后续指令(第二个 rewrite 指令和 return 指令)被跳过,但 Nginx 继续以更改后的 URI 处理请求。

类似地,诸如 /download/some/audio/file 的 URI 被替换为 /download/some/mp3/file.ra。如果 URI 不匹配任何 rewrite 指令,Nginx 将 403 错误代码返回给客户端。

last 与 break 的区别是:

  • last :在当前 server 或 location 上下文中停止执行 rewrite 指令,但是 Nginx 继续搜索与重写的 URI 匹配的 location,并应用新 location 中的任何 rewrite 指令(这意味着 URI 可能再次改变)。
  • break :停止当前上下文中 rewrite 指令的处理,并取消搜索与新 URI 匹配的 location。不会执行新 location中的 rewrite 指令。

附录

下面是写配置时常查的两份清单。

常用正则

  • . :匹配除换行符以外的任意字符
  • ? :重复 0 次或 1 次
  • + :重复 1 次或更多次
  • * :重复 0 次或更多次
  • \d :匹配数字
  • ^ :匹配字符串的开始
  • $ :匹配字符串的结束
  • {n} :重复 n 次
  • {n,} :重复 n 次或更多次
  • [c] :匹配单个字符 c
  • [a-z] :匹配 a-z 小写字母的任意一个

全局变量

  • $args :这个变量等于请求行中的参数,同 $query_string
  • $content_length :请求头中的 Content-Length 字段。
  • $content_type :请求头中的 Content-Type 字段。
  • $document_root :当前请求在 root 或 alias 指令中指定的值。
  • $host :依次取请求行中的主机名、请求头 Host 字段、匹配到的 server_name。
  • $http_user_agent :客户端 User-Agent 信息
  • $http_cookie :客户端 Cookie 信息
  • $limit_rate :这个变量可以限制连接速率。
  • $request_method :客户端请求的方法,通常为 GET 或 POST。
  • $remote_addr :客户端的 IP 地址。
  • $remote_port :客户端的端口。
  • $remote_user :已经经过 Basic 认证的用户名。
  • $request_filename :当前请求的文件路径,由 root 或 alias 指令与请求 URI 生成。
  • $scheme :请求的协议(http 或 https)。
  • $server_protocol :请求使用的协议版本,如 HTTP/1.1、HTTP/2.0。
  • $server_addr :服务器地址,在完成一次系统调用后可以确定这个值。
  • $server_name :服务器名称。
  • $server_port :请求到达服务器的端口号。
  • $request_uri :包含请求参数的原始 URI,不包含主机名,如:/foo/bar.php?arg=baz。
  • $uri :不带请求参数的当前 URI,$uri 不包含主机名,如 /foo/bar.html。
  • $document_uri :与 $uri 相同。

例如请求 http://localhost:88/test1/test2/test.php(假设 root /var/www/html;):

变量值
$hostlocalhost
$server_port88
$request_uri/test1/test2/test.php
$document_uri/test1/test2/test.php
$document_root/var/www/html
$request_filename/var/www/html/test1/test2/test.php

参考

  1. NGINX Admin Guide:Configuring NGINX as a Web Server
  2. nginx 文档:location 指令
  3. nginx 文档:Server names
  4. nginx 文档:ngx_http_rewrite_module