Yan.G WebServer Dev Engineer

nginx

2019-05-05

nginx 权限控制

deny 禁止

deny IP;
deny subnet;
deny all;

ally 允许

allow IP;
allow subnet;
allow all;

alias 和 root

nginx指定文件路径有两种方式root和alias,指令的使用方法和作用域:

[root]
语法:root path
默认值:root html
配置段:http、server、location、if
[alias]
语法:alias path
配置段:location

root的处理结果是:root路径+location路径
alias的处理结果是:使用alias路径替换location路径
alias是一个目录别名的定义,root则是最上层目录的定义。
  • alias 指定的目录是准确的,即 location 匹配访问的path目录下的文件直接是在alias目录下查找的;
  • alias 只能位于location块中
  • alias 虚拟目录配置中,location 匹配的path目录如果后面不带”/”,那么访问的url地址中这个path目录后面加不加”/”不影响访问,访问时它会自动加上”/”
  • 使用 alias 标签的目录块中不能使用rewrite的break(具体原因不明)
  • root 指定的目录是 location 匹配访问的path目录的上一级目录,这个path目录一定要是真实存在root指定目录下的
  • 如果一定要使用 root 目录配置, 可以用 软连接形式产生一个目录
  • location 匹配的path目录后面加上”/”,那么访问的url地址中这个path目录必须要加上”/”,访问时它不会自动加上”/”。如果不加上”/”,访问就会失败!

所以,一般情况下,在nginx配置中的良好习惯是:

  • 在location /中配置root目录;
  • 在location /path中配置alias虚拟目录。

rewrite

基本语法: rewrite regex replacement [flag];
上下文:server, location, if

在配置文件的server块中写,如:
server {
    rewrite 规则 定向路径 重写类型;
}
定向URI字符中有关于协议的任何东西,比如http://或者https://等,进一步的处理就终止了,直接返回客户端302, 如果返回的是30x,那么浏览器根据这个状态码和Location响应头再发起一次请求,然后才能得到想要的响应结果。但是,如果不是返回30x状态码,那么跳转就是内部的,浏览器不做跳转就能得到相应。

规则:可以是字符串或者正则来表示想匹配的目标url
定向路径:表示匹配到规则后要定向的路径,如果规则里有正则,则可以使用 $index 来表示正则里的捕获分组

server {
    # 访问 /last.html 的时候,页面内容重写到 /index.html 中,地址不变
    rewrite /last.html /index.html last;

    # 访问 /break.html 的时候,页面内容重写到 /index.html 中,并停止后续的匹配,地址不变
    rewrite /break.html /index.html break;

    # 访问 /redirect.html 的时候,页面直接302定向到 /index.html中,地址变为 index.html
    rewrite /redirect.html /index.html redirect;

    # 访问 /permanent.html 的时候,页面直接301定向到 /index.html中, 地址变为 idnex.html
    rewrite /permanent.html /index.html permanent;

    # 把 /html/*.html => /post/*.html ,301定向, 地址变为后面捕获的地址
    rewrite ^/html/(.+?).html$ /post/$1.html permanent;

    # 把 /search/key => /search.html?keyword=key, 地址变为捕获后的地址
    rewrite ^/search\/([^\/]+?)(\/|$) /search.html?keyword=$1 permanent;
}

重写类型:

last :相当于Apache里德(L)标记,表示完成rewrite,浏览器地址栏URL地址不变 如果有last参数,那么停止处理任何rewrite相关的指令,立即用替换后的新URI开始下一轮的location匹配

break;本条规则匹配完成后,终止匹配,不再匹配后面的规则,浏览器地址栏URL地址不变 停止处理任何rewrite的相关指令,就如同break 指令本身一样。

redirect:返回302临时重定向,浏览器地址会显示跳转后的URL地址 replacement 如果不包含协议,仍然是一个新的的URI,那么就用新的URI匹配的location去处理请求,不会返回30x跳转。但是redirect参数可以让这种情况也返回30x(默认302)状态码,就像新的URI包含http://和https://等一样。这样的话,浏览器看到302,就会再发起一次请求,真正返回响应结果的就是这第二个请求。

permanent:返回301永久重定向,浏览器地址栏会显示跳转后的URL地址 和redirect参数一样,只不过直接返回301永久重定向

虽说URI有了新的,但是要拼接成完整的URL还需要当前请求的scheme,以及由server_name_in_redirect和port_in_redirect指令决定的HOST和PORT.

last和break的区别

因为301和302不能简单的只返回状态码,还必须有重定向的URL,这就是return指令无法返回301,302的原因了。这里 last 和 break 区别有点难以理解:

last 一般写在server和if中,而break一般使用在 location中 last 不终止重写后的 url 匹配,即新的 url 会再从 server 走一遍匹配流程,而 break 终止重写后的匹配 break 和 last 都能组织继续执行后面的rewrite指令 在 location 里一旦返回break则直接生效并停止后续的匹配location

server {
    location / {
        rewrite /last/ /q.html last;
        rewrite /break/ /q.html break;
    }

    location = /q.html {
        return 400;
    }
}

# 访问/last/时重写到/q.html,然后使用新的uri再匹配,正好匹配到locatoin = /q.html然后返回了400
# 访问/break时重写到/q.html,由于返回了break,则直接停止了

# if判断
# 只是上面的简单重写很多时候满足不了需求,比如需要判断当文件不存在时、当路径包含xx时等条件,则需要用到if

# 语法
if (表达式) {

}

当表达式只是一个变量时,如果值为空或任何以0开头的字符串都会当做false
直接比较变量和内容时,使用=或!=

~正则表达式匹配
~*不区分大小写的匹配
!~区分大小写的不匹配

一些内置的条件判断:
-f和!-f用来判断是否存在文件
-d和!-d用来判断是否存在目录
-e和!-e用来判断是否存在文件或目录
-x和!-x用来判断文件是否可执行

try_files

语法: try_files file ... uri 或 try_files file ... = code

默认值: 无

作用域: server location

按顺序检查文件是否存在,返回第一个找到的文件。结尾的斜线表示为文件夹 -$uri/。如果所有的文件都找不到,会进行一个内部重定向到最后一个参数。

务必确认只有最后一个参数可以引起一个内部重定向,之前的参数只设置内部URI的指向。 最后一个参数是回退URI且必须存在,否则将会出现内部500错误。

命名的location也可以使用在最后一个参数中。与rewrite指令不同,如果回退URI不是命名的location那么$args不会自动保留,如果你想保留$args,必须明确声明。

try_files $uri $uri/ /index.php?q=$uri&$args;
  • 例如:
try_files /app/cache/ $uri @fallback; 

ls 
index index.php index.html;

它将检测 
$document_root/app/cache/index.php,
$document_root/app/cache/index.html 和 $document_root$uri是否存在,如果不存在着内部重定向到 @fallback 。

你也可以使用一个文件或者状态码 (=404)作为最后一个参数,如果是最后一个参数是文件,那么这个文件必须存在。

需要明确的是除最后一个参数外 try_files 本身不会因为任何原因产生内部重定向。


# 例如nginx不解析PHP文件,以文本代码返回

try_files $uri /cache.php @fallback;

因为这个指令设置内部文件指向到 $document_root/cache.php 并返回,但没有发生内部重定向,因而没有进行location段处理而返回文本 。

使用示例

nginx 代理 react 项目

location ^~ /react {
	alias	/Users/nelson/Code/php/admin/build/;
	try_files $uri /platform/index.html;
}

location ^~ /react{
	deny all;
	allow 127.0.0.1;
}

location ~ .*\.(css|js|json)$ {
	expires      10m;
}

location ~ .*\.(jpg|jpeg|png|gif|bmp|txt)$ {
	expires      1h;
}

location ~ .*\.(css|js|json|eot|ttf|svg|woff2|ico|map)$ {
	expires      10m;
}

location ~* \.(sql|bak|inc|old|swp|log)$ {
	return 403;
}

location ~* /(.svn|.git|.env)/ {
	deny all;
}

使用分析

  • alias 虚拟目录,区别于 root,可以定位到访问路径不存在的目录
  • try_files 由于所有地址都由 react 解析,所以所有地址都转到 index.html 上(否者用户刷新就会因为找不到地址而出现 404)

nginx 配置跨域请求

add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept";

location / {
	if ($request_method = 'OPTIONS') {
		add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,PATCH,OPTIONS;
		return 200;
	}
}

防盗链

# 主要使用 valid_referers 进行过滤
语法:valid_referers none | blocked | server_names | string ...;
可用于:server,location

none:请求标头中缺少“Referer”字段,也就是说Referer为空,浏览器直接访问的时候Referer一般为空。
blocked: Referer”字段出现在请求标头中,但其值已被防火墙或代理服务器删除; 这些值是不以“http://” 或 “https://” 开头的字符串;
server_names: 服务器名称,也就是域名列表。

## 设置白名单
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico|webp)$ {
    valid_referers none blocked *.nelsonking.cn;
    if ($invalid_referer) {
        return 403;
    }
}
## 用valid_referers指令设置允许的域名,
## none 
## 没有包含在valid_referers列表中的域名,$invalid_referer变量返回的值为1,最终返回403


## 设置黑名单
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico|webp)$ {
    valid_referers *.baidu.com;
    
    if ($invalid_referer != 1) {
        return 403;
    }
}
## 不是 *.baidu.com 的域名 返回 1
## 也就是 *.baidu.com 的请求返回 403    
  • 黑名单在使用的时候请使用 != 1 去做判断, = 0 的判断方式在 Nginx 新版本并不生效

屏蔽网站配置文件

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|\.md|\.ht\.env|.example|\.gitignore|\.sql)
{
    deny  all;
}

屏幕非常见爬虫

if ($http_user_agent ~* (SemrushBot|python|MJ12bot|AhrefsBot|AhrefsBot|hubspot|opensiteexplorer|leiki|webmeup)) {
     return 444;
}

屏蔽某个目录下执行脚本

#uploads|templets|data 这些目录禁止执行PHP
location ~* ^/(uploads|templets|data)/.*.(php|php5)$ {
    return 444;
}

错误记录

  • The ‘Access-Control-Allow-Origin’ header contains multiple values ‘*, *’, but only one is allowed

    项目使用的 react + laravel 组合方式。 nginx 配置好后, JS 端请求,可以发送成功但是没有回调,console 端提示 经过一顿排查发现 laravel 中 也设置了 ccess-Control-Allow-Origin 信息 实际上 nmginx 和 php 端 只要有一端设置就可以了,如果设置多了就会出现 在值上做数据追加像这样(Access-Control-Allow-Origin: *,d.test.com),在 nginx 端去掉掉后,JS 就正常回调了

  • Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.

    这个错误表示当前请求Content-Type的值不被支持。其实是我们发起了”application/json”的类型请求导致的。这里涉及到一个概念:预检请求(preflight request)

  • Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.

    发送”预检请求”时,需要用到方法 OPTIONS ,所以服务器需要允许该方法。

知识点

  • 服务器默认是不被允许跨域的。给Nginx服务器配置Access-Control-Allow-Origin *后,表示服务器可以接受所有的请求源(Origin),即接受所有跨域的请求。

  • CROS,全称是跨域资源共享 (Cross-origin resource sharing),它就是为了解决跨域请求的。标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。 另外,规范要求,对那些可能对服务器数据产生副作用的HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。 在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

  • 其实Content-Type字段的类型为application/json的请求就是上面所说的搭配某些 MIME 类型的 POST 请求,CORS规定,Content-Type不属于以下MIME类型的,都属于预检请求:
    application/x-www-form-urlencoded
    multipart/form-data
    text/plain
    
  • 所以 application/json的请求 会在正式通信之前,增加一次”预检”请求,这次”预检”请求会带上头部信息 Access-Control-Request-Headers: Content-Type
    OPTIONS /test HTTP/1.1
    Origin: http://d.text.com
    Access-Control-Request-Method: POST
    Access-Control-Request-Headers: Content-Type
    、、、
    
  • 服务器回应时,返回的头部信息如果不包含Access-Control-Request-Headers: Content-Type则表示不接受非默认的的Content-Type。即出现以下错误:
    Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.
    

参考资料


Content