前言

作为一个成熟的框架,对于多种服务器环境,应该提供虚拟主机标准配置样例的,但 ThinkPHP(以下统称为 tp) 并没有这样做,而是在文档 架构 模块的 URL 访问 章节提了一下 tp 的 URL 重写。

[ Apache ]
httpd.conf 配置文件中加载了 mod_rewrite.so 模块
AllowOverride None 将 None 改为 All
把下面的内容保存为 .htaccess 文件放到应用入口文件的同级目录下
<IfModule mod_rewrite.c>
  Options +FollowSymlinks -Multiviews
  RewriteEngine On

  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L]
</IfModule>


[ IIS ]
如果你的服务器环境支持 ISAPI_Rewrite 的话,可以配置 httpd.ini 文件,添加下面的内容:

RewriteRule (.*)$ /index\.php\?s=$1 [I]
在 IIS 的高版本下面可以配置 web.Config,在中间添加 rewrite 节点:

<rewrite>
 <rules>
 <rule name="OrgPage" stopProcessing="true">
 <match url="^(.*)$" />
 <conditions logicalGrouping="MatchAll">
 <add input="{HTTP_HOST}" pattern="^(.*)$" />
 <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
 <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
 </conditions>
 <action type="Rewrite" url="index.php/{R:1}" />
 </rule>
 </rules>
 </rewrite>


[ Nginx ]
在 Nginx 低版本中,是不支持 PATHINFO 的,但是可以通过在 Nginx.conf 中配置转发规则实现:

location / { // …..省略部分代码
    if (!-e $request_filename) {
        rewrite  ^(.*)$  /index.php?s=/$1  last;
    }
}

其实内部是转发到了 ThinkPHP 提供的兼容 URL ,利用这种方式,可以解决其他不支持 PATHINFO 的 WEB 服务器环境。

对于主流的三个服务器,Apache 几乎不需要做什么配置,直接读取 public 下的 .htaccess 文件就好了。IIS 用得较少,暂不讨论。主要问题就是 Nginx 服务器了(其实理解了配置项的内容,也就没有问题了)。

解决方案

要解决配置文件改写的难题,就得理解配置文件内部配置项的目的和意义。类似于要读懂题目,才能解题的概念。

以下面的一个比较完整的配置文件做演示:

测试环境

CentOS Linux release 7.6.1810 (Core)
nginx version: nginx/1.14.0
PHP 7.3.3 (cli) (built: Apr  1 2019 18:16:04) ( NTS )

Nginx 配置详情

server {
    listen       80;
    server_name  thinkphp.lo;
    root /var/www;
    index  index.html index.htm index.php;
    error_page  404              /404.html;
    location = /404.html {
        return 404 'Sorry, File not Found!';
    }
    error_page  500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html; # windows用户替换这个目录
    }
    location / {
        try_files $uri @rewrite;
    }
    location @rewrite {
        set $static 0;
        if  ($uri ~ \.(css|js|jpg|jpeg|png|gif|ico|woff|eot|svg|css\.map|min\.map)$) {
            set $static 1;
        }
        if ($static = 0) {
            rewrite ^/(.*)$ /index.php?s=/$1;
        }
    }
    location ~ /Uploads/.*\.php$ {
        deny all;
    }
    location ~ \.php/ {
       if ($request_uri ~ ^(.+\.php)(/.+?)($|\?)) { }
       fastcgi_pass 127.0.0.1:9000;
       include fastcgi_params;
       fastcgi_param SCRIPT_NAME     $1;
       fastcgi_param PATH_INFO       $2;
       fastcgi_param SCRIPT_FILENAME $document_root$1;
    }
    location ~ \.php$ {
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
    location ~ /\.ht {
        deny  all;
    }
}

Nginx 配置详情 转自:老朱亲自写的,最完美ThinkPHP Nginx 配置文件 - ThinkPHP 框架

我们把内容拆分一下:

    listen       80;
    server_name  thinkphp.lo;
    root /var/www;
    index  index.html index.htm index.php; 

这部分是 Nginx 下所有的虚拟主机都要求存在的。

摘抄一个 nginx.conf 内推荐的简单配置:

    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

一点差别在于根目录和索引文件配置项的位置,后者把他们放到 location / {} 中,其实没多大差别。后面摘抄的简单配置在不需要 PHP 参与(简单 html 静态页面)的情况下直接就可以用了。

    error_page  404              /404.html;
    location = /404.html {
        return 404 'Sorry, File not Found!';
    }
    error_page  500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html; # windows用户替换这个目录
    }

这部分属于错误页面定义,对常见的 404、50x 错误,选择自定义错误页面,而不是展示服务器默认的页面。

    location ~ /Uploads/.*\.php$ {
        deny all;
    }
    ...
    location ~ /\.ht {
        deny  all;
    }

这部分也很好理解(大部分规则都会应用到具体访问路径或文件上,所以一般都是 location 加上定义的配置规则,然后内部是匹配规则的请求处理),就是当访问 Uploads 下的 php 文件时,拒绝访问(防止上传了恶意脚本然后被执行),后面就是禁止访问 htaccess 文件(猜的)。

解析到了这里,剩下的都是核心部分了。核心部分主要分成两部分:URL 重写部分和 php 脚本解析部分。

URL 重写

    location / {
        try_files $uri @rewrite;
    }
    location @rewrite {
        set $static 0;
        if  ($uri ~ \.(css|js|jpg|jpeg|png|gif|ico|woff|eot|svg|css\.map|min\.map)$) {
            set $static 1;
        }
        if ($static = 0) {
            rewrite ^/(.*)$ /index.php?s=/$1;
        }
    }

这部分前面的很像一种格式 try_files $uri $uri/ /index.php$uri;。只不过因为内容较多,把重写部分放在了 {} 内部。尝试解读,就是如果 uri 部分包含 css|js|... 等后缀,即请求这些类型的文件时,不执行重写。或者反过来说,如果不是请求这些具体的资源文件时,就执行 URL 重写。

等等,我好想还见过另外一种写法的改写:

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

其实这两者效果一样的,只不过低版本的 nginx (具体低到什么版本我也不清楚)不支持 try_files 规则。

那么重写规则,到底重写了什么呢?这是关键。这就得说说 TP 框架的四种模式了(最新版本 TP 取消了普通模式,所以只剩下三种)

这些模式主要针对的是 TP 路由传递的方式来讲的,简单来讲,就是模块、控制器、方法名的传递方式。

普通模式
使用正常的 query_string 参数来传递路由信息。示例:http://域名/项目名/入口文件?m=模块名&a=方法名&键1=值1&键2=值2

PATHINFO 模式
使用 PATHINFO 虚拟路径来传递路由信息。示例:http://域名/项目名/入口文件/模块名/方法名/键1/值1/键2/值2

REWRITE 重写模式
在 PATHINFO 模式的基础上,去掉入口文件便于 SEO 优化。示例:http://域名/项目名/模块名/方法名/键1/值1/键2/值2

兼容模式
TP 框架自带的一种模式,主要兼容服务器不支持 PATHINFO、虚拟主机也不能自定义配置的情况。示例:http://域名/项目名/入口文件?s=模块名/方法名/键1/值1/键2/值2。可以看出来,与 PATHINFO 模式的主要区别在于 PATHINFO 部分的传入方式。本来如果服务器支持 PATHINFO 解析,或者可以自定义虚拟主机配置 PATHINFO 参数是没有问题,但如果这两种情况都不能满足的话,又想使用形似 PATHINFO 模式传递路由信息和其他参数,就将 PATHINFO 部分通过参数 s 传入框架,框架会自动识别参数的。

除了普通模式和框架自带的兼容模式以外,要实现其他两种模式都需要借助服务器的额外配置。

就 PATHINFO 模式而言,关键点在于服务器是否支持 PATHINFO 模式,如果支持的话(php.ini 中 cgi.fix_pathinfo 项设为 1),服务器会自动解析出 URL 中的 PATHINFO 部分内容,并将它放到 $_SERVER['pathinfo'] 参数中。官方文档里说关闭 PATHINFO 模式可以 当文件不存在时,阻止 Ng­inx 将请求发送到后端的 PHP-FPM 模块,从而避免恶意脚本注入的攻击。所以对于可能没有开启 PATHINFO 的情况,又有通过 fastcgi_split_path_info 来分离 pathinfo 参数的配置方法。

具体分离方法如下(一般放在 php 脚本解析部分):

    #设置PATH_INFO,注意fastcgi_split_path_info已经自动改写了fastcgi_script_name变量,
    #后面不需要再改写SCRIPT_FILENAME,SCRIPT_NAME环境变量,所以必须在加载fastcgi.conf之前设置
    fastcgi_split_path_info ^(.+\.php)(/.*)$;
    fastcgi_param PATH_INFO $fastcgi_path_info;

而在这个基础上的 REWRITE 重写模式,就是将 URL 中省略的入口文件 index.php 重写到 URL 中。

rewrite ^/(.*)$ /index.php/$1 last;

但如果服务器不支持 PATHINFO 模式,而自己去改写虚拟主机配置又太麻烦,或者一直调试不好的话(我经历过这样的情况),可以采用重写方式使用兼容模式。如下:

rewrite ^(.*)$ /index.php?s=/$1 last;

php 脚本解析

    location ~ \.php/ {
       if ($request_uri ~ ^(.+\.php)(/.+?)($|\?)) { }
       fastcgi_pass 127.0.0.1:9000;
       include fastcgi_params;
       fastcgi_param SCRIPT_NAME     $1;
       fastcgi_param PATH_INFO       $2;
       fastcgi_param SCRIPT_FILENAME $document_root$1;
    }

    location ~ \.php$ {
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

为什么会有两组针对 php 脚本解析的内容?其实这两组配置是不一样的,前者是针对有 PATHINFO 部分内容的 URL,解析内部的 PATHINFO;后者是针对访问内容只到入口文件没有 PATHINFO 的情况,这样直接解析就好。

有时我们也会定义错误日志文件和访问日志文件,一般放在 server 配置内部的末尾,如下:

    error_log  /var/log/nginx/ci.error.log;
    access_log  /var/log/nginx/ci.access.log;