跳转至

Nginx服务之解决方案

1. 缓解 DDoS 的攻击

分布式拒绝服务**(DDoS)攻击是一种试图使服务(通常是网站服务)无法正常使用,通过多台机器同时对目标产生大量流量从而使其服务器因为资源耗尽而无法正常提供服务。
通常,攻击者试图同目标系统建立大量连接和发送大量连接请求,使其无法在接受新的连接或者响应速度变的非常缓慢严重影响使用。

1.1 攻击特征

应用层(DDos)的攻击特征

应用层(通常为第7层的HTTP服务)的DDoS攻击是由软件程序发动的,并且可以根据具体的系统漏洞发动最佳的攻击。
例如,对于无法处理大量并发连接的系统,只需要建立大量的连接并定期发送少量流量保持连接的存活,这样就可以耗尽系统的资源。
其他的攻击都可以通过发送大量或超大量的请求来完成。因为发动这些攻击不是真实用户而是机器人,所以攻击者可以很轻松的产生大量的连接并非常快速的发送连接请求。

  • 流量通常来源于一组用于攻击的IP地址,其结果就是每一个IP地址都请求建立大量远超正常用户的需求连接。
  • 因为流量是由机器人产生的,且其目标就是让服务器瘫痪,所以产生流量的速度远超正常用户可能产生的速度。
  • 通常头部User-Agent字段通常为非标准值。
  • 通常头部Referer字段有时会设置为与攻击相关的值。

了解DDoS攻击的特征(不意味这是一份完整的特征列表)可以帮助缓解受到危害。但是这里非常注意的是,不要认为这种流量模式就一定代表是DDoS攻击。
使用代理转发也可能产生这种流量,因为代理会为其用户产生大量真实的连接请求。然而,来自代理的连接请求数通常也比来自DDoS攻击的少很多。

1.2 缓解对策

通过调节**传入流量**和控制来自**代理或后端服务器的流量**来处理

NGINXNGINX Plus含有一些功能,对照上述DDoS攻击特征,可以缓解DDoS攻击。这些功能通过调节**传入流量**和控制来自**代理或后端服务器的流量**来处理DDoS攻击。

可以通过NGINXNGINX Plus设置将来自真实用户的请求速率限制在一个正常值内。例如,你可能认为一个真实用户进入登录页面至少会每两秒产生一次请求。 这样你就可以设置NGINXNGINX Plus要求一个客户端的IP每隔两秒才能产生一次登录请求(相当于每分钟30次)。

# 本例中为客户端IP地址为$binary_remote_addr变量
# limit_req_zone指令配置一个共享内存区域称为one且大小为10m,用于存储请求指定项的状态
# limit_req指令指定location为/login.html的页面使用这块共享内存区域

limit_req_zone $binary_remote_addr zone=one:10m rate=30r/m;

server {
    ...
    location /login.html {
        limit_req zone=one;
    ...
    }
}

可以限制来自单独客户端IP的连接数为一个正常用户适用的值。例如,允许每个客户端IP跟网站的/store区域同时最多建立10个连接。

# 本例中为客户端IP地址为$binary_remote_addr变量
# limit_conn_zone指令配置一个共享内存区域称为addr且大小为10m,用于存储请求指定项的状态
# limit_conn指令指定location为/store的页面使用这块共享内存区域,并且限制每个客户端IP的最大连接数为10个
limit_conn_zone $binary_remote_addr zone=addr:10m;

server {
    ...
    location /store/ {
        limit_conn addr 10;
        ...
    }
}

可以关闭那些请求数据非常稀少的连接,这些连接通常试图保持尽可能长的时间以消耗服务器资源。
Slowloris攻击就是这种类型。client_body_timeout指令控制NGINX在两次用户体写之间等待多长时间
client_header_timeout指令控制NGINX在两次用户头部写之间等待多长时间。这两个指令默认值均为60秒。下面为示例配置NGINX在两次用户体或头部写之间最多等待 5 秒。

1
2
3
4
5
server {
    client_body_timeout 5s;
    client_header_timeout 5s;
    ...
}

如果你可以识别出用于攻击的IP地址,就可以使用deny指令将其加入黑名单中, 会拒绝来着这个IP的任何连接或请求。例如,如果你确定攻击来自地址123.123.123.1~123.123.123.16

1
2
3
4
location / {
    deny 123.123.123.0/28;
    ...
}

又或者你确定攻击来自地址123.123.123.3123.123.123.5123.123.123.7

1
2
3
4
5
6
location / {
    deny 123.123.123.3;
    deny 123.123.123.5;
    deny 123.123.123.7;
    ...
}

如果网站或应用只允许来自指定IP地址的连接,则可以同时使用allowdeny指令来设置只允许指定IP访问网站或应用。 例如,限制只允许来自本地网络的连接。这里的 +deny all指令禁止所有来自allow指定范围之外IP的连接。

1
2
3
4
5
location / {
    allow 192.168.1.0/24;
    deny all;
    ...
}

可以配置NGINXNGINX Plus使其承受更多来自攻击产生的流量峰值,通过启用缓存并配置相关参数使其暂存后续的请求。下面是一些有用的配置:

  • proxy_cache_use_stale 指令的 updating 参数告诉 NGINX 当它需要获取一个过期缓存对象的更新时,它应当发送一个更新请求,并在此期间使用更新前的对象继续服务于请求的用户。 如果攻击就是在不停的重复请求某个文件,这样可以大大减小对后端服务器的请求数量。

  • proxy_cache_key 指令定义的内置变量(默认关键字有三个,$scheme$proxy_host$request_uri)。 如果包含 $query_string 变量,那么攻击者可以发送大量随机的请求字符串从而导致缓存更新过渡频繁。不建议在关键字中包含 $query_string 变量除非你有什么特别的理由。

1
2
3
proxy_cache_use_stale

proxy_cache_key
1
2
3
4
5
# 可以配置NGINX和NGINX Plus阻塞多种请求
1. 极具指向性针对指定URL的请求
2. 请求头部中的User-Agent字段为非正常值
3. 请求头部中的Referer字段为与攻击相关的值
4. 请求头部中其他字段含有与攻击相关的值

例如,如果确定DDoS攻击的目标URL/foo.php,则可以设置阻塞所有对于该页面的请求。或者发现来自DDoS攻击的请求头部User-Agent字段值为foobar,则可以设置阻塞这些请求。

1
2
3
4
5
6
location / {
    if ($http_user_agent ~* foo|bar) {
        return 403;
    }
    ...
}

NGINXNGINX Plus由于负载均衡,所以经常同时处理比后端服务器更大量的连接。对于NGINX Plus可以限制每个后端服务器的连接数。
例如,如果希望NGINX Plus限制每个网站upstream组内后端服务器最多建立200个连接。

1
2
3
4
5
6
7
8
# max_conns参数为每个服务器指定最大连接数
# queue指令设置当upstream组内后端服务器达到了最大连接数时请求队列的长度
# timeout参数设置请求队列中每项的保留时间
upstream website {
    server 192.168.100.1:80 max_conns=200;
    server 192.168.100.2:80 max_conns=200;
    queue 10 timeout=30s;
}

攻击的另一种方法就是通过发送一个头部中Range字段具有非常大的数值,从而导致服务器的溢出。具体关于如何配置
NGINXNGINX Plus缓解这种攻击可以参见一个案例,如何配置NGINXNGINX Plus抵御CVE-2015-1635

DDoS攻击通常都会导致高负荷。关于如何调整NGINXNGINX Plus以及操作系统处理高负荷可以参见调整NGINX性能。

2. 了解浏览器的缓存机制

缓存位置、缓存过程分析、缓存类型、缓存机制、缓存策略 - 深入理解浏览器的缓存机制

浏览器缓存(Browser Caching)是为了节约网络的资源加速浏览,浏览器在用户磁盘上对最近请求过的文档进行存储,当访问者再次请求这个页面时,浏览器就可以从本地磁盘显示文档,这样就可以加速页面的阅览。

从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker 的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。

Memory Cache 也就是内存中的缓存,主要包含的是当前页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放(一旦我们关闭 Tab 页面,内存中的缓存也就被释放了)。

Disk Cache 也就是存储在硬盘中的缓存,读取速度虽然慢点,但是什么都能存储到磁盘中,与 Memory Cache 相比,优势是容量和存储时效性。它会根据 HTTP Header 中的字段判断哪些资源缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。

Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂。如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了。

强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存策略。

浏览器与服务器通信的方式为应答模式,浏览器每次发起请求都会先在浏览器缓存中查找该请求的结果以及缓存标识,浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中。

1
2
3
4
5
6
map $sent_http_content_type $cache_control_type {
    default        "private";
    ~text/html     "no-store, no-transform";
}

add_header Cache-Control $cache_control_type;

ExpiresWeb 服务器响应消息头字段,在响应 http 请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。且其是 HTTP/1.0 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

HTTP/1.1 中是最重要的规则,主要用于控制网页缓存,可以在请求头或者响应头中设置,并且可以组合使用多种指令。

3. 配置 TCP/UDP 代理转发

Nginx TCP/UDP Stream -> 主流发行版都已经默认自带了

我们都知道 nginx 是一款优秀的反向代理服务,用过 nginx 的 upstream 一般都是用于 http 代理的,如果想对后端服务的 TCP 进行代理呢? 这时候就需要使用到 stream 了,就可以用于代理 mysqlredis 等服务。

1
2
3
4
5
6
# /etc/nginx/stream.d/test1.conf

server {
    listen     9999;
    proxy_pass 120.120.120.120:5432;
}
# /etc/nginx/stream.d/test2.conf

upstream mysql_servers {
    least_conn;
    server 120.120.120.120:3306 max_fails=3 fail_timeout=5s; # 5s内出现3次错误
    server 120.120.120.121:3306 max_fails=3 fail_timeout=5s;
    server 120.120.120.122:3306 max_fails=3 fail_timeout=5s;
}

server {
    listen                     9999; # 端口
    proxy_connect_timeout        5s; # 与被代理服务器建立连接的超时时间
    proxy_timeout               10s; # 获取被代理服务器的响应最大超时时间
    proxy_next_upstream          on; # 将未返回响应的客户端连接请求传递给upstream中的下一个服务器
    proxy_next_upstream_tries     3; # 转发尝试请求最多3次
    proxy_next_upstream_timeout 10s; # 总尝试超时时间为10s
    proxy_socket_keepalive       on; # 开启SO_KEEPALIVE选项进行心跳检测

    proxy_pass mysql_servers;
}

4. 日常通常会设置的配置

记录日常使用通常会设置和优化的地方

避免记录 favicon.ico 和 robots.txt 的请求到日志里面

location = /favicon.ico {
    log_not_found off;
    access_log off;
}

location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;
}

HTTP 和 HTTPS 协议相互引用

# https server with http location
server {
    listen 443;
    location / {
        rewrite ^ http://$host$request_uri? permanent;
    }
}

# http server redirects to https except one location
server {
    listen 80;
    location / {
        rewrite ^ https://$host$request_uri? permanent;
    }
}

标识移动端设备进行重定向

location / {

    # mobile site handling as per user agent
    set $mobile_rewrite do_not_perform;     // variable to store action. default set to not perform redirection to mobile site.

    if ($http_user_agent ~* "(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino") {
        set $mobile_rewrite perform;
    }

    if ($http_user_agent ~* "^(1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-)") {
        set $mobile_rewrite perform;
    }

    # google bot mobile handling
    if ($http_user_agent ~* "(googlebot-mobile)") {
        set $mobile_rewrite perform;
    }

    if ($mobile_rewrite = perform) {
        proxy_pass http://www.mobile-domain.com:$port;
    }
}

重定向请求到其他服务器上面

1
2
3
4
server {
   server_name example.com;
   return 301 $scheme://example.net$request_uri;
}