如何编写 nginx+PHP 配置文件

配置 nginx + PHP 的文章网上一搜一大把,很多教程通常都是把很大的篇幅给了如何下载源代码,如何编译等内容,对配置文件的编写一笔带过,一般就是给一个现成的配置文件了事。由于Linux发行版众多,而且时间跨度非常大,所以网上搜到的配置文件有不少都存在过时甚至错误的内容,有的放到现在的环境下根本无法运行,有的则会留下一些隐患。

其实,对于绝大多数普通用户来说,使用包管理工具来安装nginx + PHP已经可以满足需求,不必自己下载源代码包进行编译,但是对于配置文件的编写却会有一些五花八门的需求。因此,学习nginx的配置文件编写才是整个过程中的重中之重,直接复制粘贴教程里的现成配置代码通常后患无穷。

笔者这里就简单说说该如何去编写一份正确的 nginx + PHP 配置文件。

我们就以一份我所使用的配置文件作为例子进行讲解。配置见下:

server {
	listen 80;
	listen [::]:80;
	server_name example.com www.example.com;

	access_log /var/log/nginx/example.com_access.log;
	error_log /var/log/nginx/example.com_error.log;

	root /var/www/html/example;
	index index.html index.htm index.php;

	location / {
		try_files $uri $uri/ /index.php?q=$uri&$args;
	}

	location ~ [^/]\.php(/|$) {
		fastcgi_split_path_info ^(.+?\.php)(/.*)$;
		set $path_info $fastcgi_path_info;

		try_files $fastcgi_script_name =404;

		fastcgi_pass unix:/run/php/php7.0-fpm.sock;
		fastcgi_param PATH_INFO $path_info;
		include fastcgi.conf;
	}

	location ~ /\.ht {
		deny all;
	}
}

配置文件的结构

首先我们看一下配置文件的结构,配置文件是由server块和若干个location块构成的,所有的location块都包含在server块中。一个server块用来定义一个虚拟服务器,一个location块用来表示对一类URI的处理动作。这里不理解location块的意义不要紧,我们下面还会详细讲。nginx的变量继承关系是从外到内的,也就是说在server块中定义的量,在该server块里面的location块中同样有效。因此我们要在server块中定义各location块中共同的部分,比如根目录,index文件等信息。

公共信息部分

我们先看第一段,这一段是虚拟服务器的基本信息:

listen 80;
listen [::]:80;
server_name example.com www.example.com;

这一段中,listen定义的是监听端口和IP,如果不指定IP的话,默认监听0.0.0.0,也就是说本机所有IP都可以访问,当然也可以指定具体的IP。“[::]”表示的IPv6的“0.0.0.0”,也就是说,该虚拟服务器同时在IPv6上监听,且监听本机所有IPv6地址。

server_name定义的是该虚拟服务器绑定的域名,当有多个虚拟服务器都监听80端口时,需要通过域名来具体区分究竟是访问哪个虚拟服务器。nginx可以为一个虚拟服务器绑定多个域名,不同域名用空格分开。

再看接下来的日志部分:

access_log /var/log/nginx/example.com_access.log;
error_log /var/log/nginx/example.com_error.log;

这里定义了访问日志和错误日志的位置。一般情况下,各Linux发行版中的nginx附带的主配置文件nginx.conf中已经定义了访问日志和错误日志的地址,但是这个日志会把所有的虚拟服务器的日志混在一起。如果虚拟服务器比较多的话,还是推荐分开定义日志位置,这样每个站点的日志有独立的文件,方便查看。

如果不需要日志的话,可以用下面的方式关掉日志:

access_log off;
error_log off;

之后便是一些和文档相关的定义:

root /var/www/html/example;
index index.html index.htm index.php;

root定义的是虚拟服务器的根目录,index定义的是在访问一个目录地址(即URI结尾为“/”)时默认访问的文件。可以通过index指定多个文件作为默认文件,nginx在处理请求时会按顺序寻找默认文件。比如在这个例子当中,当我们访问http://example.com/时,nginx会先寻找有没有index.html,如果没有的话会继续向后寻找。注意:index指令会引起“内部跳转”。不知道什么是“内部跳转”也无所谓,下面会讲到。

重点——location块

剩下的部分就都是location块了,我们可以发现每个location块都是下面这种形式:

location [ = | ~ | ~* | ^~ ] uri { ... }

其中,“[ = | ~ | ~* | ^~ ] uri”表示URI的匹配规则,大括号里的内容表示对匹配到的URI的处理方式。就像上文所说的那样——“一个location块用来表示对一类URI的处理动作”。location块的匹配规则中带有“~”修饰符的都是以正则表达式表示的匹配关系。除“~”修饰符外,还有“=、~*、^~”修饰符,都有不同的含义,具体见下表:

“=”表示精确匹配,只有完全匹配才有效。
“~”表示通过正则表达式匹配,对大小写敏感。
“~*”表示通过过正则表达式匹配,对大小写不敏感。
“^~”表示通过前缀进行匹配,并且优先级在正则表达式之前。

当nginx处理URI时,带有这四个修饰符的规则会被优先匹配,如果匹配不到,会对无修饰符的规则进行匹配。无修饰符的规则则是按照URI的前缀进行匹配,例如“/”就是匹配所有http://example.com/下面的请求,例如/index.htm或者/foo/index.htm。

所有的匹配,包括有修饰符和无修饰符的匹配规则,都是按照最大匹配原则进行匹配,也就是说,假设同时存在“/”和“/foo/”两个规则的话,“/foo/index.htm”会匹配到后者上。

location的URI匹配问题是一个比较复杂的问题,更多的说明和例子见这里:http://nginx.org/en/docs/http/ngx_http_core_module.html#location

我们先来看第一个location块:

location / {
		try_files $uri $uri/ /index.php?q=$uri&$args;
	}

匹配规则是“/”,代表匹配http://example.com/下面的所有请求,且是长度最短的常规前缀匹配,因此综合nginx自身的修饰符优先级规则和最大匹配优先规则,该规则的优先级最低。

规则内部只有try_files … 一条语句,try_files用来顺序判断所列文件是否存在,并返回第一个存在的文件。try_files的前几个参数都是用来尝试寻找的文件,而最后一个参数则是无法寻找到前面那些文件而返回的结果。例如,将最后一个参数指定为“=404”则会返回HTTP 404错误代码。需要特别说明的是,try_files的参数中,除最后一个参数外,都只作用在当前块中,均不会产生“内部跳转”。

大家应该会想起来前面也提到过“内部跳转”的事,这里就简单解释一下这个内部跳转。简单来说,内部跳转就是一个会触发location URI匹配的跳转,但是却不是通过返回HTTP 301/302给客户端来实现的。

我们来看看能触发内部跳转和不能触发内部跳转的区别:

假设我现在在用WordPress,permalink都是“http://example.com/nginx-php-configuration-tutorial/”这种形式的,我需要将URI重定向至/index.php?q=$uri&$args来处理这种permalink。

如果我的try_files写成下面这样:

try_files $uri $uri/ /index.php?q=$uri&$args =404;

大家会发现返回的永远是HTTP 404,因为try_files只有最后一个参数可以产生内部跳转,其他的会被当作要寻找的文件,所以会在当前位置寻找文件“/index.php?q=$uri&$args”,而这个文件显然是不存在,自然就会返回HTTP 404。

如果我们把最后的“=404”删掉,“/index.php?q=$uri&$args”成为了最后一个参数,就会发生内部跳转,从而触发处理php文件的location块,此时我们的permalink就会被正常处理了。

关于try_files,更多细节请见这里:http://nginx.org/en/docs/http/ngx_http_core_module.html#try_files

刚才说了很多关于try_files的事情,我们再来看第二个location块:

location ~ [^/]\.php(/|$) {
		fastcgi_split_path_info ^(.+?\.php)(/.*)$;;
		set $path_info $fastcgi_path_info;

		try_files $fastcgi_script_name =404;

		fastcgi_pass unix:/run/php/php7.0-fpm.sock;
		fastcgi_param PATH_INFO $path_info;
		include fastcgi.conf;
	}

这个块的作用就是处理php文件。我们先从匹配规则来看,规则以“~”开头,表明它是一条以正则表达表示的匹配规则。“[^/]\.php(/|$)”是一条正则表达式,表示的是匹配所有结尾为“.php”和“.php/”的URI,例如“http://example.com/index.php”和http://example.com/index.php/foo/bar/”,后面这种我在用ThinkPHP的时候曾经用过,所以我个人认为对这种URI的兼容也是有必要的。

你在其他教程里,会看到另一种简单的匹配规则写法“\.php$”,这种不能匹配到我上面所说的第二种URI,如果对第二种URI无需求的话,也可以使用这种匹配方式。

块中的第一行,fastcgi_split_path_info用来定义一个正则表达式,该正则表达式可以将URI分离成$fastcgi_path_info和$fastcgi_script_name,前者是脚本所在的目录,后者是脚本文件名,这些都是fastcgi必须的参数。第二行的“set …”则用来将$fastcgi_path_info存储在$path_info中,因为接下来的try_files会清空$fastcgi_path_info。如果你的匹配规则使用的是简单的形式的话,那么这里的正则表达式也可以写成“^(.+\.php)(/.+)$”,两个都可以。

关于为什么try_files会清空$fastcgi_path_info,请看这里:https://trac.nginx.org/nginx/ticket/321

接下来的try_files作用和前面一样,不再赘述。

“fastcgi_pass”是将php脚步交给php-fpm处理,这里用的是UNIX socket,如果用的是TCP port的话,需要写成下面的形式:

fastcgi_pass 127.0.0.1:9000;

究竟是写成哪种形式,和PHP-FPM的配置有关,可以查看PHP-FPM的www.conf来判断,其中定义的listen就是监听的TCP port或者UNIX socket。

如果太懒的话,还有一种更简单粗暴的方式,在PHP-FPM已经启动的情况下,直接用netstat,如果发现有程序在监听127.0.0.1:9000的话,那就可以用TCP port方式,如果有程序在监听php7.0-fpm.sock的话,就是UNIX socket方式。

“fastcgi_param”用来设置fastcgi的参数,这里设定的是PATH_INFO,用的就是刚才的$path_info。

“include fastcgi.conf”是加载fastcgi.conf,该文件中保存着更多的fastcgi参数。在有些教程里,这里是这么写的:

include fastcgi_params;

还有的教程是这么写的:

include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/foo$fastcgi_script_name;

除上面两种,还有的教程是这么写的:

include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

其实上面的写法都对。如果大家查看fastcgi_params会发现它的内容和fastcgi.conf的内容几乎一样,在有的发行版中,它和fastcgi.conf完全一样,在其他发行版中,fastcgi_params会比fastcgi.conf少一条SCRIPT_FILENAME。

所以上面的写法其实就是不同时代,不同发行版的写法。早年的fastcgi_params中没有定义SCRIPT_FILENAME,所以就有人手写一条“fastcgi_param SCRIPT_FILENAME /var/www/foo$fastcgi_script_name;”,但是这种写法实际上是“硬编码”,一旦环境变动就要进行修改,于是大家统一成“fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;”,这样适应性就强了很多。为了方便大家,后来nginx中直接添加了fastcgi.conf,其实就是fastcgi_params加上后面那条定义SCRIPT_FILENAME的语句,为了兼容性,原来的fastcgi_params依然保留了下来。

但是有些发行版,例如Ubuntu 16.04,将fastcgi_params改成和fastcgi.conf一模一样的了,这样即使用“include fastcgi_params;”也是没问题的。

下面说说最后一个location块:

location ~ /\.ht {
		deny all;
	}

这个块的作用就是禁止对一切.htaccess文件的访问,目的就是为了提升在nginx和apache共享目录时的安全性,避免通过nginx读取到apache需要用的.htaccess。如果你并不使用apache的话,这个块也就没有存在的意义。

测试

在test.php文件中写入下面的代码:

<?php var_export($_SERVER)?>

将该文件放置于根目录下,通过浏览器访问下面的地址:

/test.php
/test.php/
/test.php/foo
/test.php/foo/
/test.php/foo/bar.php
/test.php/foo/bar.php?v=1

如果访问时不出现HTTP 404,且返回的各参数都符合预期,证明nginx + php配置正确。

CC BY-NC 4.0 本作品使用基于以下许可授权:Creative Commons Attribution-NonCommercial 4.0 International License.

《如何编写 nginx+PHP 配置文件》上有1条评论

发表评论

邮箱地址不会被公开。 必填项已用*标注