写在前面的话
我们前面已经谈了编译安装,基本语法,日志处理,location 匹配,root / alias 的不同效果。这里我们主要谈谈 rewrite(重写)功能,顺便说说 nginx 中自带的变量。在谈日志格式的时候我们已经聊了一些,这里做个补充。
指令:rewrite
rewrite 的实现依赖于我们编译的时候的 PCRE 库,我们可以通过 rewrite 功能实现将 URL 重写的功能。
总的来说,rewrite 能够实现以下:
1. 用户请求到达某个 server ,如果满足 server 内部的 rewrite 的正则匹配,那么 rewrite 将会对用户请求 URI 重写。
2. 重写完成后直接在该 server 内部去匹配 location。
3. 当匹配到 location 后,如果 location 内部又有 rewrite,那执行 rewrite 后再次在这个 server 内部去匹配 location,直到请求返回。
4. 当然这个过程不是无限的,nginx 对于这样的跳转就支持 10 次,如果过多甚至死循环,则会报 500 错误。
基本语法格式:
rewrite regex replacement [flag];
说明:
这里的使用 regex 匹配 URI,并将匹配到的 URI 替换成新的 URI(replacement)。如果有多个 rewrite,执行顺序是从上到下依次执行,匹配到一个后匹配并不会终止,会继续匹配下去,直到返回最后一个匹配为止。如果想中途终止,则需要设置 flag 参数。
当然上面说的都是重写 URI,如果 placement 中包含了任何协议相关,如:http:// 和 https://,则请求就直接返回 302 重定向终止了。
当然,浏览器在接收到 30x 的状态码后,会再度根据这个返回去请求 rewrite 之后的地址,最终得到所要想要的结果。如果不是 30x 的状态码,则属于 nginx 内部跳转,浏览器不需要再度发起请求。
在 rewrite 中有 4 个 flag 参数:
last | 停止所有 rewrite 相关指令,然后使用新的 URI 进行 location 匹配。 |
break | 停止所有 rewrite 相关指令, 和 last 不同的是,last 接着继续使用新的 URI 匹配 location。 而 break 则是直接使用当前的 URI 进行请求处理,能避免重复 rewrite,last 一般在 server,break 一般在 location。 |
redirect | URI 中不包含协议如 https://,但依然希望它返回 30x,让浏览器二次请求然后获取到结果就需要 redirect。 |
permanent | 和 redirect 类似,但是直接返回 301 永久重定向。 |
在 rewrite 中常用的正则:
. | 匹配换行符以外任意字符 |
? | 重复0次或者1次 |
+ | 重复1次或多次 |
* | 重复0次或者多次 |
\d | 匹配数字 |
^ | 匹配开始 |
$ | 匹配结束 |
{n} | 重复 n 次 |
{n,} | 重复 n 次或更多次 |
[c] | 匹配单个字符c |
[a-z] | 匹配 a-z 任意一个小写字母 |
使用 () 可以将匹配内容括起来,后面使用 $1 来引用,当然,第二个 () 就是 $2。
rewrite 示例:
示例1:直接跳转到其它 URL,但是将参数带过去
在 vhosts 下面新建:rewrite-demo.conf
server {
listen 8083;
server_name localhost;
rewrite_log on;
rewrite ^/(.*) https://www.ezops.com/$1 permanent;
error_log /data/logs/nginx/rewrite-error.log;
access_log /data/logs/nginx/rewrite-access.log mylog;
}
我们这里开启 rewrite log,这样定向错误会记录到 error_log 中。配置完成后重载 nginx,访问:
结果如下:
示例2:测试 last 和 break,修改刚刚的配置,这里我们用到 nginx 自带变量 uri
server {
listen ;
server_name localhost;
rewrite_log on;
rewrite ^/(.*) /hello/$ last;
error_log /data/logs/nginx/rewrite-error.log;
access_log /data/logs/nginx/rewrite-access.log mylog; location ^~ /hello {
echo "URI 1: $uri";
rewrite ^/hello/(.*) /world/$ last;
echo "URI 2: $uri";
} location ^~ /world {
echo "URI 3: $uri";
}
}
重载访问:
此时把 location 中 last 改为 break 测试:
可以发现:
如果 rewrite 是 last 作 flag 并不影响接下来继续去匹配 location,且该 location 下面就执行了 rewrite 操作,其它的都没有执行到。
但是当 break 作 flag 时,rewrite 就终止于目前的这个 location 了,在完成重写 URI 之后就开始执行该 location 下面的其他操作了。
示例3:参数后面 ? 的作用测试,修改配置,我们用到另外一个变量 args
server {
listen 8083;
server_name localhost;
rewrite_log on;
error_log /data/logs/nginx/rewrite-error.log;
access_log /data/logs/nginx/rewrite-access.log mylog; location ^~ /hello {
rewrite ^/(.*) /world/?from=$1 break;
echo "URI: $uri";
echo "ARG: $args";
}
}
访问结果如下:
修改 rewrite 配置,添加 ?:rewrite ^/(.*) /world/?from=$1? break; 重载访问:
可以发现:
如果 replacement 中包含参数,那默认旧 URI 中的请求参数也会拼接到 replacement 后面作为新的 URI,如果不希望这样,只需在 replacement 的后面加上 ?。
指令:set
我们一直都在说,nginx 为我们提供了很多的内部变量,但是有些时候这些变量并不能满足我们的需求,我们需要其它的一些自定变量来协助我们完成一个比较复杂的需求。set 就是这样一个指定,用来定义属于我们自己的变量,它的基本语法如下:
set $variable value;
举个例子,我们在 vhosts 下新增配置:set-demo.conf
server {
listen 8084;
server_name localhost;
set $STEP 1; location / {
set $STEP $STEP-2;
echo $STEP;
}
}
重载配置,访问测试:
指令:if 和 try_files
在 nginx 中,我们也可以像在其它编程语言一样添加逻辑判断,其中就有 if 和 try_files,if 一般在旧版中使用,但是新版中并不影响。语法格式:
if (判断条件) {...}
判断只能在 server 和 location 中使用。
1. 当判断条件只是一个变量的时候,只有该变量的值为空或者 0 的时候才为 false。
2. 变量可以通过 = 或者 != 来判断,如:$var = 123。
3. 判断条件里面也可以是一个正则匹配,如:$var ~ regex。
4. 其它的一些文件,目录校验符号,如:-d / -f / -e / -x。
文件校验符如下:
-f | 检验文件是否存在,可以取反:!-f |
-d | 检验目录是否存在 |
-e | 检验文件/目录/链接文件是否存在 |
-x | 检验文件是否为可执行文件 |
举个例子测试,在 vhosts 目录下创建:if-demo.conf
server {
listen 8085;
server_name localhost;
set $VAR 1;
set $STEP $VAR; location / {
if ($VAR) {
echo "URL 0: VARIABLE TEST 0";
set $STEP $STEP-0;
} if ($VAR = 1) {
echo "URL 1: VARIABLE TEST 1";
set $STEP $STEP-1;
} if ($http_user_agent ~* Mozilla) {
echo "URL 2: BROWSER";
set $STEP $STEP-2;
} if ($http_user_agent ~ curl) {
echo "URL 3: COMMAND";
set $STEP $STEP-3;
} if (-f /tmp/test.txt) {
echo "URL 4: FILE EXIST";
set $STEP $STEP-4;
} if (!-f /tmp/test.txt) {
echo "URL 5: FILE NOT EXIST";
set $STEP $STEP-5;
echo "STEP: $STEP";
}
}
}
我们定义了两个变量,VAR 和 STEP,VAR 用于测试判断,STEP 用于记录执行了哪些 if,重载访问测试:
可以发现:
我们明明执行了 0 1 3 5 这 4 个 if 判断,但是真正执行的 echo 的却只有最后一个 5。
if 常常被我们用来做客户端验证,比如我们一个网站,如果是电脑打开,我们让他跳转到电脑版,手机打开跳转到手机版。
try_files 其实就是 if 语句精简版,但是个人其实更喜欢 if 一点,所有对 try_files 感兴趣的可以详细的了解一下,我们这里举个简单的例子:
location / {
root /data/www/demo;
index index.html index.htm;
try_files $uri $uri/ @rewrites;
} location @rewrites {
rewrite ^(.+)$ /index.html last;
}
比如:用户访问 http://192.168.100.111/hello/world,那么 $uri 就是 /hello/world,那么 try_files 就会去指定的 root 下查找这个文件是否存在,如果存在则直接返回,如果不存在就访问第二个参数,还不存在就继续,直到最后一个参数,我们把它跳转到对应的 location 上面。在 try_files 会自行判断是文件还是目录。
虽然很方便,但是个人还是更喜欢 if 一些!
注意:
指令:return
停止一切处理,返回结果给客户端,如果返回的状态码是 ,则断开 TCP 连接,不发送任何东西。
如果不带状态码直接返回 URL 则被视为 。简单示例:
在 vhosts 下面新建:return-demo.conf
server {
listen 8086;
server_name localhost; location / {
if ($http_user_agent ~ curl) {
return 200 'COMMAND USER\n';
}
if ($http_user_agent ~ Mozilla) {
return 302 http://www.baidu.com?$args;
}
return 404;
}
}
命令行测试:
浏览器访问:http://192.168.100.111:8086/hello?user=world
综合示例
我们这里做一个结合前面的知识点一起完成的一个示例:
server {
listen 8087;
server_name localhost;
default_type text/html; # 默认来源 PC / 移动端
set $MACHINE pc;
if ( $http_user_agent ~* "(mobile|nokia|iphone|ipad|android|samsung|htc|blackberry)" ){
set $MACHINE mobile;
} # 多重判断变量设计
set $FROM pc;
set $TAG ''; location ^~ /pc/ {
# 拼接来源和请求 URI 头
set $FROM pc;
set $TAG $MACHINE-$FROM; # 根据来源和 URI 头采取不同的处理
if ($TAG = pc-pc) {
rewrite '^/pc/([a-z0-9]{2})/([a-z0-9]{2})/(.*)\.(png|jpg|gif)$' /data?file=$3.$4 last;
}
if ($TAG = mobile-pc) {
return 302 http://m.jd.com?$args;
}
return 200 '<h1>PC INDEX WEB!</h1>';
} location ^~ /mobile/ {
# 拼接来源和请求 URI 头
set $FROM mobile;
set $TAG $MACHINE-$FROM; # 根据来源和 URI 头采取不同的处理
if ($TAG = pc-mobile) {
return 302 http://www.baidu.com?$args;
}
if ($TAG = mobile-mobile) {
return 200 '<h1>WELCOME TO MOBILE WEB!</h1>';
}
return 200 '<h1>MOBILE INDEX WEB!</h1>';
} location ^~ /data {
root /data/www/images;
try_files /$arg_file /image404.html;
} location = /image404.html {
return 200 '<h1>IMAGE NOT FOUND!</h1>';
} # 默认处理
location / {
if ($MACHINE = pc) {
return 200 '<h1>PC DEFAULT WEB</h1>';
}
if ($MACHINE = mobile) {
return 200 '<h1>MOBILE DEFAULT WEB</h1>';
}
}
}
说明:
1. 我们将访问区分电脑端和移动端,当电脑端访问默认端口的时候显示:
2. 当 uri 部分以 /pc/ 开头,则返回电脑的默认页面:
3. 当电脑端访问 /mobile/ 开头并带有参数,则保留参数跳转到百度,如访问: http://192.168.100.111:8087/mobile/test?name=hello
4. 我们新建 /data/www/images 目录并上传 login.png 作为测试,此时电脑端访问指定匹配规则的图片返回图片:
5. 访问不存在的图片报错:
6. 移动端一个意思,这里就不做演示。
7. 配置中包含了多重判断,我们使用一个中间变量了存储作为判断区分。
8. 最后值得注意的是,我们使用 return 返回 HTML 文本,需要定义默认类型:default_type text/html; 否则会变成下载文件。
自带变量
nginx 自带了很多变量,可以参考如下表:我们以请求 http://www.baidu.com/hello/world?name=dylan&age=25 为例
$args | 请求中的完整参数。就是示例中:name=dylan&age=25 |
$arg_PARAMETER | 获取指定参数,如:$arg_name,就能获取到 name 参数 |
$binary_remote_addr | 二进制客户端地址,如:\x0A\xE0B\x0E |
$body_bytes_sent | 向客户端发送 HTTP 响应中包体部分的字节数,前面日志中用过 |
$content_length | 客户端请求头部中 Content_Length 字段 |
$content_type | 客户端请求头部中 Content_Type 字段 |
$cookie_COOKIE | 获取指定 cookie 的值 |
$document_root | 当前请求所使用的 root 配置项的值 |
$uri | 当前请求的 uri,不带任何参数 |
$document_uri | 与 uri 含义相同 |
$request_uri | 原始请求 uri,带完整的参数。$uri 和 $document_uri 可能是内部重定向后的。 |
$host | 请求头部 Host 字段,字段不存在,则以实际 server(虚拟主机)名称代替。 |
$hostname | nginx 所在机器的名称。 |
$http_HEADER | 当前 HTTP 请求相应头部的值,全小写 |
$sent_http_HEADER | 返回客户端 HTTP 响应中相应头部的值 |
$is_args | 请求中的 uri 是否带参数,如果带参数,值为 ?,如果不带参数,值为空 |
$limit_rate | 当前连接限速为多少,0 表示不限速 |
$nginx_version | 当前 nginx 的版本号 |
$query_string | 请求 uri 中的参数,与 args 相同,只读的 |
$remote_addr | 客户端地址 |
$remote_port | 客户端连接使用端口 |
$remote_user | 客户端连接使用账户,使用 auth basic module 时定义的用户名 |
$request_filename | 请求中 uri 经过 root 或 alias 转换以后的路径 |
$request_body | HTTP 请求中的包体,该参数只在 proxy_pass 或 fastcgi_pass 中有意义 |
$request_body_file | HTTP 请求中的包体存储的临时文件名 |
$request_completion | 请求全部完成时,值为"ok",若没完成,就返回客户端,值为空 |
$request_method | HTTP 请求的方法名,如get,put,post |
$scheme | scheme,如请求:https://www.baidu.com,值为 https |
$server_addr | 服务器地址 |
$server_name | 服务器名称 |
$server_port | 服务器端口 |
$server_protocol | 服务器向客户端发送响应的协议,如 HTTP/1.1 或者 HTTP/1.0 表示不限速 |
红色部分为常用的变量!
小结
rewrite 是一个很实用的功能,能够解决我们很多问题,但是同时我们又不推荐把这个做的太为复杂,不适合维护。同样,if set 也是我们日常用的比较多的。这其中牵扯到正则表达式。可能需要多花点时间。