开发必备的 Nginx 知识

536 阅读21分钟

Nginx (engine x) 是一款轻量级的 Web 服务器(软件)、反向代理服务器(软件)及电子邮件(IMAP/POP3)代理服务器(软件)。Nginx 最常见的场景是用来配置域名。

     用户通过浏览器访问域名,只是完成了从客户端到服务器的访问。在这之后,Nginx 就登场了,它会捕获到这些来自于公网的请求,并根据相关配置规则,将请求转交给后端某台服务器上。后端服务器处理完请求后,将响应发送给 Nginx 服务器,Nginx 服务器收到响应后,将其发回给用户客户端,这样就完成了一次完整的请求过程,这其实就是「反向代理」的通俗化解释。

Nginx 有很多优点,比如高性能(高并发支持非常给力)、高扩展性(模块化设计、庞大的第三方模块)、跨平台(二进制发布)、高可靠性(宕机概率极低)等等,所以熟悉 Nginx 的相关功能是很有必要的,尤其对于后端开发来说更是如此。

image.png

一、安装 Nginx

1、Linux 上安装 Nginx

鉴于公司独立部署的项目,一般都要求客户提供 CentOS 7.x 版本(可以在服务器上输入 cat /etc/redhat-release 查看版本)的服务器,所以就单独说下 CentOS 7.x 系统如何安装 Nginx。

先决条件

首先,得确保有一个拥有 CentOS 服务器 root 权限的用户(不一定是 root 用户,普通用户也可以设置 root 权限,有 root 权限的用户,sudo 命令才能生效)。

其次,得确保 CentOS 服务器能够访问公网(主要是为了方便通过 yum 安装软件),否则就只能上传二进制源码包进行手动编译,但这种方式大概率会导致编译失败,因为你基本上无法顺利地解决复杂的软件依赖问题。

第一步——添加 Nginx yum 源

$ sudo rpm -ivh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm

安装完 yum 源后,检查一下是否添加成功

$ sudo yum repolist

如果看到有 nginx repo,则表示 Nginx 仓库添加成功。

第二步——安装 Nginx

$ sudo yum -y install nginx

第三步——启动 Nginx

$ sudo systemctl start nginx

这里注意一下服务器的防火墙状态,如果是开启着(active)的,需要设置允许对 HTTP(默认 80 端口)、HTTPS(默认 443 端口)服务进行访问,我们一般不推荐直接关闭服务器自带的防火墙。

查看防火墙状态(CentOS7 使用的防火墙 firewalld 默认是关闭 http 服务的)——

$ sudo systemctl status firewalld

如果看到 running,则表示防火墙是开启的,就需要输入如下命令设置允许访问 HTTP、HTTPS 服务

$ sudo firewall-cmd --permanent --zone=public --add-service=http 
$ sudo firewall-cmd --permanent --zone=public --add-service=https
$ sudo firewall-cmd --reload

输入下面的命令,查看对 HTTP、HTTPS 服务的访问是否真的开启了

$ sudo firewall-cmd --list-services

如果能看到结果列表里有 http、https 就 OK 了。

再说几个 Nginx 相关的命令。

设置 Nginx 开机自启动——

$ sudo systemctl enable nginx

重载 Nginx 服务的配置——

$ sudo systemctl reload nginx

重启 Nginx 服务——

$ sudo systemctl restart nginx

需要注意的是 CentOS 7 上的 SELinux,我们在使用 Nginx 进行反向代理时需要通过它打开网络访问权限(否则 Nginx 将会不起作用,访问域名会有问题) ,不过这个操作比较麻烦,通常我们的做法是直接关闭服务器上的 SELINUX,具体来说是——

$ sudo vi /etc/selinux/config

将此文件里的 SELINUX=enforcing 改成 SELINUX=disabled

这样修改后,服务器需重启,SELINUX 才会变成 disabled 禁用状态。但服务器一般很少重启,所以我们在改完后,再运行命令

$ sudo setenforce 0

将 SELINUX 状态临时改为 permissive 状态,也就是宽容状态(只会打印警告信息),这个状态对我们来说已经够用了。

临时改完后,运行命令

$ sudo getenforce

如果显示 Permissive,说明修改成功了。

结论,对于 SELINUX,我们先改它的配置文件(改为禁用状态),再通过命令临时修改其状态为警告状态,就可以满足 Nginx 的使用了。

这种方法安装的不是最新的版本,如需升级版本请按以下操作待更新 使用如下命令 替换文本里的内容
sudo vim /etc/yum.repos.d/nginx.repo
然后sudo yum update

name=nginx mainline repo
baseurl=http://mirrors.ustc.edu.cn/nginx/mainline/centos/$releasever/$basearch/
gpgcheck=0
enabled=1
module_hotfixes=true

二、配置 Nginx

首先,我们要知道 CentOS 服务器上安装的 Nginx,它的全局配置文件路径是 /etc/nginx/nginx.conf。但这个全局配置文件,默认是用来进行 Nginx 进程运行用户(user)、Nginx 工作进程数(worker_processes)等的配置,对于开发人员来说,大多数情况下不用去修改它。

如果是用 Docker 安装的 Nginx,宿主机服务器上的 Nginx 目录通常是 /home/nginx,所以它的全局配置文件路径就是 /home/nginx/nginx.conf,对应容器里面的 /etc/nginx/nginx.conf 目录。

开发人员,一般只需要关注 /etc/nginx/conf.d/default.conf (或 /home/nginx/conf.d/default.conf)以及 /etc/nginx/conf.d/自定义的.conf (或 /home/nginx/conf.d/自定义的.conf)等文件。我们在 conf.d 目录下自行创建的 *.conf 文件,一旦 Nginx 服务重新加载(nginx -s reloadsudo docker exec -it zj-nginx nginx -s reload),马上就会加载生效。

接下来说说最常用的 Nginx 配置。

1、server-反向代理配置

server 块的配置,可以说是运维及开发人员做的最多的配置了。我们常说的做一个域名配置、虚拟主机配置、服务反向代理配置,就是指新增一个 server 块的配置。

比如我们在 conf.d 目录下创建一个 xxx.conf 的配置文件,命令是 vi /etc/nginx/conf.d/xxx.conf

(1)HTTP 反向代理配置

输入如下命令——

server {
    # 让 Nginx 监听本机的 80 端口
    listen 80;
    # 让 Nginx 捕获 foobar.com 这个域名
    server_name foobar.com;
​
    # HTTP1.1 开始支持长连接
    proxy_http_version 1.1;
    # 清理 Client 请求头里 Connection 值
    proxy_set_header Connection "";
​
    # 让 Nginx 捕获 / 这个根路径的访问
    location / {
        # 让 Nginx 把它转发到本机的 8080 端口上
        proxy_pass http://127.0.0.1:8080/;
    }
​
    # 所有访问 TXT 文本文件的 URL,都在 /etc/nginx/txt 目录下找对应的 TXT 文件(常用于微信小程序业务域名校验文件访问的场景)
    location ~ (.txt$) {
        root /etc/nginx/txt/;
    }
}

完成后,按键盘 ESC 键退出插入模式,再按键 :wq 保存并退出,这样就完成了一个最简单的 HTTP 域名反向代理配置。

我们分析一下上面的指令。

  1. server:不用说,既然写的是 server,那肯定是对服务做配置,或者说是对虚拟主机做配置
  2. listen:指定 Nginx 监听的本地端口,80 是 HTTP 服务的默认端口,443 是 HTTPS 服务的默认端口,监听 80 端口即代表监听 HTTP 连接请求
  3. server_name:一般是域名,很少会使用 IP 地址,上面写的 「foobar.com」 是域名,而且这个域名解析到的 IP 地址,就是当前这台安装了 Nginx 的服务器的公网 IP 地址,这样我们在请求 foobar.com 域名时,才会被这台服务器上的 Nginx 捕获到
  4. proxy_http_version:默认 Nginx 访问后端都是用的短连接(HTTP/1.0),HTTP 协议中对长连接的支持是从 1.1 版本之后才有的,因此最好通过proxy_http_version 指令设置为 1.1,以确保 Nginx 与后端 Server 保持长连接
  5. proxy_set_header:修改请求头的值,比如上面将 Connection 这个请求头的值改为空字符串(只有引号里的值不是 close,都会是长连接),这样 Nginx 与后端 Server 建立连接时就不受 Client 端设置的 Connection 所影响了
  6. location:配置路由规则,后面可以是具体 URL 路径或者正则表达式,上面写的 / 就是表示匹配域名根路径的请求,也就是匹配 foobar.com/ 这个 URL 的请求
  7. proxy_pass:指定将请求反向代理到哪个服务上,上面的命令就是把 foobar.com/ 的请求,反向代理到本机占用 8080 端口的服务上(对于我们公司的项目而言,基本上都是 tomcat 进程,比如 jar 包、war 包、docker 容器)。注意,如果 Nginx 服务器本身无法访问得通后端的服务地址(换句话说就是如果 Nginx 所在的服务器,telnet 不通 127.0.0.1:8080),那么反向代理就是失效的,还是就是服务地址最末尾的正斜杠不要忘了写

以上命令,还需要再注意一下空格、末尾分号,避免出现不必要的 Nginx 配置语法错误。

(2)HTTPS 反向代理配置

再来看一个最简单的 HTTPS 域名反向代理配置。

server {
    listen 443 ssl;
    server_name foobar.com;
​
    ssl_certificate /etc/nginx/ssl/foobar.com.crt;
    ssl_certificate_key /etc/nginx/ssl/foobar.com.key;
​
    # HTTP1.1 开始支持长连接
    proxy_http_version 1.1;
    # 清理 Client 请求头里 Connection 值
    proxy_set_header Connection "";
​
    # 匹配 https://foobar.com/ 根路径的 URL 请求
    location / {
        proxy_pass http://127.0.0.1:8080/;
    }
​
    # 匹配 https://foobar.com/admin/ 路径的 URL 请求
    location /admin/ {
        proxy_pass http://127.0.0.1:13130/;
    }
​
    # 以 .gif/.jpg/.png 为后缀的图片文件都保存在 /data/images 这个目录下
    # 比如访问 https://foobar.com/aaa.png,只要 aaa.png 这个图片存放在 Nginx 服务器的 /data/images 目录下面,就能访问得到
    location ~ .(gif|jpg|png)$ {
        root /data/images;
    }
​
    # 所有访问 TXT 文本文件的 URL,都在 /etc/nginx/txt 目录下找对应的 TXT 文件(常用于微信小程序业务域名校验文件访问的场景)
    location ~ (.txt$) {
        root /etc/nginx/txt/;
    }
}

相比 HTTP 域名的反向代理配置,HTTPS 域名的反向代理配置多了3个新指令。

  1. ssl:紧跟在 listen 监听的 443 端口后面,表示监听 443 端口,同时开启 ssl 访问
  2. ssl_certificate:指定 ssl 证书文件在服务器上存放的路径,Nginx 类型的 ssl 证书文件,在 Linux 服务器上一般是以 .crt 结尾的文件,证书文件路径可以是相对路径,但是为了减少不必要的错误(比如文件权限问题),请设置成绝对路径,比如像上面那样放到 /etc/nginx/ssl 目录下
  3. ssl_certificate_key:指定 ssl 私钥文件在服务器上存放的路径,Nginx 类型的 ssl 私钥文件,一般是以 .key 结尾的文件,同样推荐使用绝对路径,如上面那样放到 /etc/nginx/ssl 目录下

注意上面有三个 location 的配置,1个是匹配 foobar.com/ 这个 URL 的请求,1个是匹配 foobar.com/admin/ 这个 URL 的请求,Nginx 分别将它们反向代理到不同的服务上。最后1个 location 后面跟的是正则表达式(以 ~ 符号开头),它根据正则进行匹配,上面的例子是访问 foobar.com/1.png 这个 URL 时,Nginx 会把服务器上的 /data/images/1.png 图片返回给请求端。

2、upstream-负载均衡配置

Nginx 的 ngx_http_upstream_module 模块用来做负载均衡配置,对应的指令是 upstream,这个指令的上下文是 http,通常我们把它写在我们自定义的 *.conf 配置文件的最上面。

Nginx 的 upstream 支持 5 种分配方式,其中轮询、权重、IP 哈希这三种为 Nginx 原生支持的分配方式,fair 和 url_hash 为第三方支持的分配方式(要用这俩,需要额外安装第三方模块)。5 种负载均衡的分配方式,大家可以自行查阅资料了解学习。

下面是我们常用的一个 upstream 示例——

upstream IP {
    server IP0:8100;
    server IP1:8100;
    keepalive_requests 1000000;
    keepalive 1000;
}

可以看到 upstream 指令后面跟的是 IP,意思是这个 upstream 的名字是 IP,后续用 proxy_pass 命令做反向代理时,可以用 IP 这个名字引用此 upstream。

两个 server 对应两个后端服务的地址,第一个是 IP0:8100,第二个是 IP1:8100,端口都是 8100,域名则会根据 Nginx 服务器上 /etc/hosts 文件解析记录,对应到不同的 IP 上。两个 server 的这种写法,会让 Nginx 采用轮询方式进行负载均衡。如果只有一个后端 server,也是可以写在 upstream 里面的,但这种情况就不涉及负载均衡了。

keepalive_requests :指令设置一个 keep-alive 连接上可以服务的请求的最大数量,默认是 100,我们设置成 1000000 基本上是达不到的,这个配置主要是用来让 Nginx 与 Client 端保持长连接用的。如果达到这个参数设置的最大值,则 Nginx 会强行关闭这个长连接,逼迫客户端不得不重新建立新的长连接。

keepalive :指令用来让 Nginx 与后端 Server 保持长连接,这个配置非常重要! 默认情况下,Nginx 访问后端都是用的短连接(HTTP1.0),一个请求来了,Nginx 会新开一个端口和后端建立连接,后端执行完毕后主动关闭该连接。这个指令的含义很容易让人混淆,它不是用来开启/关闭长连接的开关,也不是用来设置超时的 timeout,更不是设置长连接池的最大连接数,它指的是长连接池中的最大空闲连接数,简单粗暴可以直接设置为 keepalive 1000,基本上是没有问题的,可避免频繁出现因请求不均匀导致的连接震荡。

上面这个名为 ucd 的 upstream 配置好之后,在下面的 server 反向代理配置中可以进行对应引用,比如——

# B 端用户中心 upstream
upstream IP {
    server IP0:8100;
    server IP1:8100;
    keepalive_requests 1000000;
    keepalive 1000;
}
​
……
​
server {
    listen 443 ssl;
    server_name foobar.com;
​
    ssl_certificate /etc/nginx/ssl/foobar.com.crt;
    ssl_certificate_key /etc/nginx/ssl/foobar.com.key;
​
    # HTTP1.1 开始支持长连接
    proxy_http_version 1.1;
    # 清理 Client 请求头里 Connection 值
    proxy_set_header Connection "";
​
    # 匹配 https://foobar.com/ucd 根路径的 URL 请求
    location /IP {
        # 引用上方定义的 ucd upstream
        proxy_pass http://IP/;
    }
​
    ……
}

如果服务器频繁出现很多(几千甚至上万)的后端 Server TIME_WAIT 连接,请立即看一下 Nginx 里面有没有对后端配置长连接,特别是 upstream 里的 keepalive、是否启用了 HTTP/1.1、是否重新设置了 Connection 请求头的值等要更加注意。

3、性能配置

(1)TCP 优化

Nginx 关于 TCP 的优化基本都是修改系统内核提供的配置项(写在 /etc/sysctl.conf 配置文件中,关于 Linux 服务器系统内核参数优化,请自行查阅,所以跟具体的 Linux 版本和系统配置有关。

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
​
    keepalive_timeout 60;
    ……
}

第一行的 sendfile 配置可以提高 Nginx 静态资源托管效率。sendfile 是一个系统调用,直接在内核空间完成文件发送,不需要先 read 再 write,没有上下文切换开销。

TCP_NOPUSH 是 Linux FreeBSD 发行版的一个 socket 选项,对应 Linux 中的 TCP_CORK 系统参数,Nginx 里统一用 tcp_nopush 来控制它,并且只有在启用了 sendfile 之后才生效。启用它之后,数据包会累计到一定大小之后才会发送,减小了额外开销,提高网络效率。

TCP_NODELAY 也是一个 socket 选项,启用后会禁用 Nagle 算法,尽快发送数据,某些情况下可以节约 200ms(Nagle 算法原理是:在发出去的数据还未被确认之前,新生成的小数据先存起来,凑满一个 MSS 或者等到收到确认后再发送)。Nginx 只会针对处于 keep-alive 状态的 TCP 连接才会启用 tcp_nodelay

可以看到 TCP_NOPUSH 是要等数据包累积到一定大小才发送,TCP_NODELAY 是要尽快发送,二者相互矛盾。实际上,它们确实可以一起用,最终的效果是先填满包,再尽快发送。

配置最后一行用来指定服务端为每个 TCP 连接最多可以保持多长时间。Nginx 的默认值是 75 秒,有些浏览器最多只保持 60 秒,所以我统一设置为 60 秒。

(2)开启 Gzip

我们知道,用 Webpack 打包前端文件时,可以使用 gzip 压缩。其实 Nginx 也支持对静态资源文件进行 gzip 压缩,虽然会消耗一定的服务器性能,但效率更高,通常压缩后的文本大小可以减小到原来的 1/4 - 1/3,这无疑会加快前端请求的响应速度,用户体验会更好。

http {
    # 开启 gzip
    gzip on;
    # 开启 gzip_vary
    gzip_vary on;
​
    # gzip 压缩级别(1-9),越大压缩效率越高但越耗 CPU
    gzip_comp_level 6;
    # gzip 压缩缓冲区的数量和大小设置
    gzip_buffers 16 8k;
​
    # 表示小于 1000 字节的资源不进行压缩
    gzip_min_length 1000;
    # Nginx 做为反向代理的时候启用,any 表示无条件压缩所有结果数据
    gzip_proxied any;
    # 通过正则表达式,表明哪些UA头不使用gzip压缩
    gzip_disable "msie6";
​
    # HTTP/1.0及以上版本使用 gzip 压缩
    gzip_http_version 1.0;
​
    # 设置需要压缩的 MIME 类型,如果不在设置类型范围内的请求不进行压缩(注意不要对图片文件进行压缩,反而会压大)
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript;
    ... ...
}

gzip_vary 用来输出 Vary 响应头,用来解决某些缓存服务的一个问题。

gzip_disable 指令接受一个正则表达式,当请求头中的 UserAgent 字段满足这个正则时,响应不会启用 GZip,这是为了解决在某些浏览器启用 GZip 带来的问题。特别地,指令值 msie6 等价于 MSIE [4-6].,但性能更好一些。另外,Nginx 0.8.11 后,msie6 并不会匹配 UA 包含 SV1 的 IE6(例如 Windows XP SP2 上的 IE6),因为这个版本的 IE6 已经修复了关于 GZip 的若干 Bug。

默认 Nginx 只会针对 HTTP/1.1 及以上的请求才会启用 GZip,因为部分早期的 HTTP/1.0 客户端在处理 GZip 时有 Bug。现在基本上可以忽略这种情况,于是可以指定 gzip_http_version 1.0 来针对 HTTP/1.0 及以上的请求开启 GZip。

(3)开启缓存

# 设置缓存路径为 /etc/nginx/cache
proxy_cache_path /etc/nginx/cache levels=1:2 keys_zone=cache_one:100m inactive=7d max_size=10g use_temp_path=off;
​
……
​
server {
    # 让 Nginx 监听本机的 80 端口
    listen 80;
    # 让 Nginx 捕获 foobar.com 这个域名
    server_name foobar.com;
​
    # HTTP1.1 开始支持长连接
    proxy_http_version 1.1;
    # 清理 Client 请求头里 Connection 值
    proxy_set_header Connection "";
​
    # 让 Nginx 捕获 / 这个根路径的访问
    location / {
        # 让 Nginx 把它转发到本机的 8080 端口上
        proxy_pass http://127.0.0.1:8080/;
    }
​
    # 所有访问 TXT 文本文件的 URL,都在 /etc/nginx/txt 目录下找对应的 TXT 文件(常用于微信小程序业务域名校验文件访问的场景)
    location ~ (.txt$) {
        root /etc/nginx/txt/;
    }
​
    # 将图片缓存在 cache_one 对应的目录下
    location ~* ^.+.(gif|jpg|jpeg|png)$ {
        log_not_found off;
        access_log off;
        expires 7d;
        proxy_pass http://127.0.0.1:8080;
        # 引用上面定义的 cache_one 缓存
        proxy_cache cache_one;
        proxy_cache_valid 200 302 2h;
        proxy_cache_lock on;
        proxy_cache_lock_timeout 5s;
        # proxy_cache_valid 404 10m;
        # proxy_cache_valid any 1h;
        proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
    }
}
​

可以看到在配置最外层定义一个缓存目录,并指定名称(keys_zone)和其他属性,这样在配置 proxy_pass 时,就可以使用这个缓存了。上面对状态值等于 200 和 304 的响应缓存了 2 小时。

我们一般会对 CSS、JS、图片等资源使用强缓存(对应的响应头是 ExpiresCache-Control,指定过期时间),而入口文件(HTML)一般使用协商缓存(也叫弱缓存,对应的响应头是 Last-ModifiedETag,指定最后修改时间和资源实体特征标记)或不缓存,这样可以通过修改入口文件中对强缓存资源的引入 URL 来达到即时更新的目的。我们的 Vue 前端就符合这种处理方式,可以对 index.html 不做缓存(每次都请求),而对 index.html 里引入的 CSS、JS、图片等做强缓存,这样既能避免更新前端后读取到旧入口文件,也能在读取到新入口文件后减少对强缓存资源的重复请求,可谓一举两得。

(4)HTTPS 优化

建立 HTTPS 连接本身就慢(多了获取证书、校验证书、TLS 握手等等步骤),如果没有优化好只能是慢上加慢。

server {
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 60m;
​
    ssl_session_tickets on;
​
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /xxx/full_chain.crt;
​
    resolver 8.8.4.4 8.8.8.8 valid=300s;
    resolver_timeout 10s;
    ……
}

这部分配置就两部分内容:TLS 会话恢复和 OCSP Stapling。

TLS 会话恢复的目的是为了简化 TLS 握手,有两种方案:Session Cache 和 Session Ticket。他们都是将之前握手的 Session 存起来供后续连接使用,所不同是 Cache 存在服务端,占用服务端资源;Ticket 存在客户端,不占用服务端资源。另外目前主流浏览器都支持 Session Cache,而 Session Ticket 的支持度一般。

ssl_stapling 开始的几行用来配置 OCSP Stapling 策略。浏览器可能会在建立 TLS 连接时在线验证证书有效性,从而阻塞 TLS 握手,拖慢整体速度。OCSP Stapling 是一种优化措施,服务端通过它可以在证书链中封装证书颁发机构的 OCSP(Online Certificate Status Protocol)响应,从而让浏览器跳过在线查询。服务端获取 OCSP 一方面更快(因为服务端一般有更好的网络环境),另一方面可以更好地缓存。

在给 Nginx 指定证书时,需要选择合适的证书链。因为浏览器在验证证书信任链时,会从站点证书开始,递归验证父证书,直至信任的根证书。这里涉及到两个问题:1)服务器证书是在握手期间发送的,由于 TCP 初始拥塞窗口的存在,如果证书太长很可能会产生额外的往返开销;2)如果服务端证书没包含中间证书,大部分浏览器可以正常工作,但会暂停验证并根据子证书指定的父证书 URL 自己获取中间证书。这个过程会产生额外的 DNS 解析、建立 TCP 连接等开销。配置服务端证书链的最佳实践是包含站点证书中间证书两部分。有的证书提供商签出来的证书级别比较多,这会导致证书链变长,选择的时候需要特别注意。

4、安全配置

(1)隐藏不必要的信息

由于某些 Nginx 漏洞只存在于特定的版本,隐藏版本号可以提高安全性,只需要在 nginx.conf 配置文件里加上下面这个就可以了——

server_tokens off;

同样,一些 WEB 语言或框架默认输出的 x-powered-byX-AspNet-VersionX-AspNetMvc-Version 也会泄露网站信息,他们一般都提供了修改或移除的方法,可以自行查看手册。如果部署上用到了 Nginx 的反向代理,也可以通过 proxy_hide_header 指令隐藏它——

proxy_hide_header X-Powered-By;
proxy_hide_header X-AspNet-Version;
proxy_hide_header X-AspNetMvc-Version;

(2)禁用非必要的 HTTP 方法

if ($request_method !~ ^(GET|HEAD|POST)$ ) {
    return 444;
}

444 是 Nginx 自定义的响应状态码,会立即断开连接,没有响应正文,节省资源,提高速度。上面这段配置可以加在 server 下面,也可以加在具体的 location 下面。

(3)合理配置响应头

add_header Strict-Transport-Security "max-age=31536000";
add_header X-Frame-Options deny;
add_header X-Content-Type-Options nosniff;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.qq.com https://*.tencent-cloud.com https://*.zwjk.com https://*.alicdn.com https://*.amap.com; img-src 'self' data: https://*.qq.com https://*.zwjk.com https://*.myqcloud.com; style-src 'self' 'unsafe-inline' https://cdn.bootcss.com; frame-src 'self' https://*.zwjk.com data:; font-src 'self' https://*.yzcdn.cn data:; connect-src 'self' https://*.zwjk.com; worker-src 'self' blob:; object-src 'none’";

Strict-Transport-Security(简称为 HSTS)可以告诉浏览器,在指定的 max-age 内,始终通过 HTTPS 访问我们的站点。即使用户自己输入 HTTP 的地址,或者点击了 HTTP 链接,浏览器也会在本地替换为 HTTPS 再发送请求。

X-Frame-Options 用来指定此网页是否允许被 iframe 嵌套,deny 就是不允许任何嵌套发生。

X-Content-Type-Options 用来指定浏览器对未指定或错误指定 Content-Type 资源真正类型的猜测行为,nosniff 表示不允许任何猜测,防止嗅探风险。

Content-Security-Policy(简称为 CSP)用来指定页面可以加载哪些资源,主要目的是减少 XSS 的发生。上面的设置,允许来自本站、https://.qq.com & https://.tencent-cloud.com & https://.zwjk.com & https://alicdn.com & https://amap.com 的外链 JS,还允许内联 JS,以及在 JS 中使用 eval;允许来自本站、https://.qq.com & https://.zwjk.com & https://.myqcloud.com 的图片,以及内联图片(Data URI 形式);允许本站、cdn.bootcss.com 外链 CSS 以及内联 CSS;允许 iframe 加载来自本站、https://*.zwjk.com 的页面。对于其他未指定的资源,都会走默认规则 self,也就是只允许加载本站的。

X-XSS-Protection 这个响应头,也可以用来防范 XSS。不过由于有了 CSP,就可以不配置它了。

需要注意的是,以上这些响应头现代浏览器才支持,所以并不是说加上他们,网站就可以不管 XSS,万事大吉了。但是鉴于低廉的成本,还是都配上。

(4)HTTPS 安全配置

启用 HTTPS 并正确配置了证书,意味着数据传输过程中无法被第三者解密或修改。有了 HTTPS,也得合理配置好 Nginx,才能发挥最大价值。

ssl_certificate /home/nginx/ssl/server.crt;
ssl_certificate_key /home/nginx/ssl/server.key;
ssl_dhparam /home/nginx/ssl/dhparams.pem;
​
# SSL 加密套件
ssl_ciphers ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:!DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
​
# 可以确保在 TLSv1 握手时,使用服务端的配置项,以增强安全性
ssl_prefer_server_ciphers on;
​
# 指定使用 TLSv1.2 版本
ssl_protocols TLSv1.2;

5、WebSocket 配置

WebSocket 协议与 HTTP 协议不同,但 WebSocket 握手与 HTTP 兼容(例如 WebSocket 应用可以使用标准 HTTP 的 80 和 443 端口),需使用 HTTP 升级工具(Upgrade & Connection 这两个请求头)将连接从 HTTP 升级到 WebSocket。

Nginx 从 1.3 版本开始支持 WebSocket,需在 nginx.conf 或自定义的 *.conf 文件里加上下面的内容——

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}
​
upstream backend {
    server http://127.0.0.1:8080/;
}
……
​
server {
    # 让 Nginx 监听本机的 80 端口
    listen 80;
    # 让 Nginx 捕获 foobar.com 这个域名
    server_name foobar.com;
​
    location /chat/ {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

最上面 map 指令的作用是根据客户端请求中 httpupgrade的值,来构造改变http_upgrade 的值,来构造改变 connection_upgrade 的值,即根据变量 httpupgrade的值创建新的变量http_upgrade 的值创建新的变量 connection_upgrade,创建的规则就是 {} 里面的东西,即默认 connectionupgrade的值会一直是upgrade,如果connection_upgrade 的值会一直是 upgrade,如果 http_upgrade 为空字符串的话,$connection_upgrade 的值会是 close。

这样配置后,到 foobar.com/chat/ 的 WebSocket 连接(ws 协议)就建立成功了,可以看到 Upgrade 和 Connection 这两个响应头的值变成了 websocket 和 Upgrade。当然,上面的 server 里也可以配置 https 地址的转发,这样客户端就可以和后端应用建立 wss 协议的 WebSocket 连接了。

三、网络穿透

用 Nginx 来替代 Nuts 做医院的网络穿透,相对来说性能更高、配置更简单,一定程度上也更安全,查找日志也更方便

和 Nuts 对于前置机服务器的网络要求类似,如果医院能够提供一台能同时连通内外网的前置机服务器,那只需要在这台服务器上安装一个 Nginx 服务就行,用来代理内外网的接口。如果医院提供两台前置机服务器,分别放在 DMZ 区和内网区,那就需要在两台服务器上都安装一下 Nginx。DMZ 区的 Nginx 服务器,代理公网请求到本地部署的服务以及内网的 Nginx 服务上。内网的 Nginx 服务器,则负责接收 DMZ 区 Nginx 服务器转发进来的请求,然后去请求对应的内网接口。

以上不管是哪种情况,都需要医院在相应的网络安全设备(如防火墙&网闸)上进行配置,以确保安全。

需要注意的是,因为是网络穿透,所以性能要比网络直通的那种情况要稍差一些。中间层对于数据包的检测、转发,都需要时间,即便内网带宽很大,可能仍然会给人一种比较慢的感觉。

前置机服务器建议提供 Linux 系统的,如 CentOS 7.x 系列,配置不需要很高,专门提供的前置机服务器配置可以是 4 核、8G、300 G(100G 系统盘 + 200G 数据盘)存储这个规格的。当然,提供 Windows 系统的也行,配置一样即可。

目前,上海长征医院采用的是双 Nginx 的代理方式,基本上医院能接受的也只有这一种方式了。