深入理解 PHP 项目关于 PATHINFO 使用问题

 Technique  comment

总览

在使用常用的 LNMP 架构时,NGINX 和 PHP 通信时都是使用的 FastCGI 模式,什么是 CGI,应该如何理解其原理。

CGI

CGI(Common Gateway Interface,通用网关接口)是一个 Web 服务器提供信息服务的标准接口。通过 CGI 接口,Web 服务器就能够获取客户端提交的信息,然后转交给服务器端的 CGI 程序处理,每接收一个请求就会 Fork 一个新进程用于处理,然后将处理后的结果返回给客户端,处理完毕后关闭进程。这个恼人的 Fork-and-Execute 模式被人诟病,为解决此问题 FastCGI 应运而生。

小贴士:此方式下服务器有多少请求,就会有多少 CGI 子进程,每次进程启动都会加载解释器,载入配置文件,连接服务器等初始化操作,因此导致此方式性能低下,并且当请求数量多时会大量占用系统资源。

FastCGI

字面上就能看出来和 CGI 的区别是什么,很明显就是“快”,现代的接口都是使用的 FastCGI,抛弃了 CGI,并且 FastCGI 支持分布式工作,Web 服务可以将请求转发至其他服务器的 FastCGI 进行处理。

小贴士:此方式下服务器在启动 FastCGI 时会初始化环境,根据 Web 服务不同加载不同模块(Apache 的 mod_fastcgi;NGINX 的 ngx_http_fastcgi_module;iiS 的 iaspi 模块;Lighttpd 的 mod_fastcgi 模块等),此模式下父进程只启动一次,处理完毕后也不退出,父进程 Fork 出一定数量子进程监听在系统端口上一直等待请求的传输。

比如在 NGINX 配置中会自动将以 PHP 为后缀的请求转发至后端 CGI 接口进行处理

location ~ \.php$ {
    root           html;
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    include        fastcgi_params;
}

PATHINFO

很多常见的 PHP 项目在不开启伪静态时都能看到请求的 URL 为类似形式,比如 https://www.vvave.net/index.php/admin/login.html 这种。

对于此类 URL 有两种理解方式:

实际上绝大多数情况都是后一种情况,后方的部分为传入的参数,此部分就是所谓的 PATHINFO,具体说明详见 CGI

由于 Apache 的默认配置文件开启了 PATHINFO 支持,Apache & PHP 的环境下 PATHINFO 格式的 URL 可以不出任何错误的执行正确路径的 PHP 程序并使用 PATHINFO 中的参数。而 NGINX 的默认配置中对 PATHINFO 的支持不完备,因此通过 fastcgi_split_path_info 参数进行实现。

深入

配置部分扩展

以 NGINX 的默认配置文件为例,默认的是对请求的 URL 进行正则匹配来决定这个请求是否要交给 FastCGI 来执行。

location ~ \.php$ {
    root           html;
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    include        fastcgi_params;
}

综上,此段配置就是为匹配到以 .php 为结尾的 uri 交给 FastCGI 处理。

匹配符 匹配规则 优先级
= 表示精确匹配 1
^~ 表示以某字符串开头 2
~ 表示区分大小写的正则匹配 3
~* 表示不区分大小写的正则匹配 4
!~ 表示区分大小写不匹配的正则 5
!~* 表示不区分大小写不匹配的正则 6
/ 通用匹配,任何请求都会匹配到 7

注意:关于标识符,此处引用之前写过的博客内容,如上表。~~* 正则匹配规则在匹配后会寻找更精准的 location 再次进行匹配。

举个例子:

URL 是否匹配
https://www.vvave.net/index.php 匹配
https://www.vvave.net/admin/index.php?refer=www.vvave.net&a=index 匹配
https://www.vvave.net/index.php/index/index 不匹配

可以看出仅为 index.php 结尾的请求才能被此条规则正确匹配,其他请求会被认为是服务器上的目录。

注意:【此处引用晶晶博客中的说明便于理解】对 PHP 来说 fastcgi_param 指令产生的参数配置转换成了超全局数组变量 $_SERVER 的键值对配置,示例中 fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name 就配置了一个 SCRIPT_FILENAMEfastcgi 参数,转换成 PHP 中的变量就是 $_SERVER['SCRIPT_FILENAME'] ,PHP 参考手册中对 $_SERVER['SCRIPT_FILENAME'] 参数说明为当前执行脚本的绝对路径。

官方自带的 fastcgi_params 内容


fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;
fastcgi_param  REQUEST_SCHEME     $scheme;
fastcgi_param  HTTPS              $https if_not_empty;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

小贴士:此端内容都是为传递的参数,比如 NGINX 的版本、远程端口、服务协议、协议版本等参数,传递给 PHP 用于显示服务器信息。

配置原理

理解了 PATHINFO 后即可对其进行深层的理解和使用,比如部分项目的 URL 就需要配合 PATHINFO 的正确配置才能使用,举个例子:比如使用 ThinkPHP 框架开发的德尚商城项目就是如此。如果不添加正确的 PATHINFO 配置就会导致部分链接持续报错。

## 德尚商城配置模板
server {
    listen       80;
    server_name  www.dsshop.com;

    location / {
        root   /data/projects/dsshopsingle/public;
        index  index.php;
        if (!-e $request_filename) {
            rewrite  ^(.*)$  /index.php?s=/$1  last;
            break;
        }
    }

    location ~ ^(.+\.php)(.*)$ {
        root           /data/projects/dsshopsingle/public;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_split_path_info  ^(.+\.php)(.*)$;
        fastcgi_param  PATH_INFO  $fastcgi_path_info;
        if (!-e $document_root$fastcgi_script_name) {
            return 404;
        }
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
}

指令理解

从上面的例子可以看到 fastcgi_split_path_info 实质上就是一个用于分割 URI 转换为参数以便正确传输给后端程序的一个指令。

此指令的参数为一个正则表达式,正则表达式必须要有两个捕获子组(可理解为匹配到内容),第一个子组会赋值给 $fastcgi_script_name 变量,第二个子组会赋值给 $fastcgi_path_info 变量。

也就是说在没有使用 PATHINFO 时,NGINX 的 $fastcgi_script_name 参数为 PHP 文件的路径(访问时的 URI),所以在实际项目中,一般不使用 NGINX 的自带配置文件中的 fastcgi_param ,而是如下的配置。

    # 首先将 URI 中例如 xx.php/xxx/xxx 部分匹配出来
    location ~ ^(.+\.php)(.*)$ {
        root           /projects/public/directory;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        # 使用命令将 URI 进行正则拆分并赋值给不同参数
        fastcgi_split_path_info  ^(.+\.php)(.*)$;
        # PHP 中要能读取到 PATHINFO 这个变量,就要通过 fastcgi_param 指令将 fastcgi_split_path_info 指令匹配到的 pathinfo 部分赋值给 PATH_INFO
        fastcgi_param  PATH_INFO  $fastcgi_path_info;
        # 在将这个请求的 URI 匹配完毕后,检查这个绝对地址的 PHP 文件是否存在
        if (!-e $document_root$fastcgi_script_name) {
            # 若不存在直接抛出 404
            return 404;
        }
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }

小结

综上最后部分的配置基本可以完整套用,可以处理绝大多数情况,如果没有理解其工作原理也不想启用 PHP 的 cgi.fix_pathinfo 功能(可能存在安全风险),那么推荐可以直接抛弃 NGINX ,直接使用 Apache 即可,不存在 PATHINFO 问题。

附录

参考链接

回复