天天看點

nginx在CDN加速或使用SLB代理後,擷取真實IP,做并發通路限制的方法(限流)

本文80%轉載于張戈部落格,後續加入了自己的了解和想法

原文位址:https://zhangge.net/4879.html

站點在運作時,為了防止DDoS 攻擊、或内部接口調用造成的資料迸發,nginx提供了limit限流子產品:

HttpLimitZoneModule 限制同時并發通路的數量

HttpLimitReqModule 限制通路資料,每秒内最多幾個請求

一、普通配置:

什麼叫普通配置?

普通配置就是針對【使用者浏覽器】→【網站伺服器】這種正常模式的 nginx 配置。那麼,如果我要對單 IP 做通路限制,絕大多數教程都是這樣寫的:

## 使用者的 IP 位址 $binary_remote_addr 作為 Key,每個 IP 位址最多有 50 個并發連接配接
## 你想開 幾千個連接配接 刷死我? 超過 50 個連接配接,直接傳回 503 錯誤給你,根本不處理你的請求了
limit_conn_zone $binary_remote_addr zone=TotalConnLimitZone: ;
limit_conn  TotalConnLimitZone  ;
limit_conn_log_level notice;

## 使用者的 IP 位址 $binary_remote_addr 作為 Key,每個 IP 位址每秒處理 10 個請求
## 你想用程式每秒幾百次的刷我,沒戲,再快了就不處理了,直接傳回 503 錯誤給你
limit_req_zone $binary_remote_addr zone=ConnLimitZone:  rate=10r/s;
limit_req_log_level notice;

## 具體伺服器配置
server {
    listen   ;
    location ~ \.php$ {
                ## 最多 5 個排隊, 由于每秒處理 10 個請求 + 5個排隊,你一秒最多發送 15 個請求過來,再多就直接傳回 503 錯誤給你了
        limit_req zone=ConnLimitZone burst= nodelay;
        fastcgi_pass   ;
        fastcgi_index  index.php;
        include fastcgi_params;
    }   

}
           

這樣一個最簡單的伺服器安全限制通路就完成了,這個基本上你 Google 一搜尋能搜尋到 90% 的網站都是這個例子,而且還強調用“$binary_remote_addr”可以節省記憶體之類的雲雲。

二、CDN 或 SLB 代理之後

為了增加安全、性能,許多站點都用到了CDN加速,或者其他的二級代理,例如阿裡的SLB負載均衡等等。

于是,網站的通路模式就變為:

使用者浏覽器 → CDN 節點 / SLB 節點→ 網站源伺服器

甚至是更複雜的模式:

使用者浏覽器 → CDN/SLB 節點(CDN 入口、CC\DDoS 攻擊流量清洗等) → 阿裡雲盾 → 源伺服器

可以看到,我們的網站中間經曆了好幾層的透明加速和安全過濾, 這種情況下,我們就不能用上面的“普通配置”。因為普通配置中基于【源 IP 的限制】的結果就是,我們把【CDN /SLB節點】或者【阿裡雲盾】給限制了,因為這裡“源 IP”位址不再是真實使用者的 IP,而是中間 CDN /SLB節點的 IP 位址。

我們需要限制的是最前面的真實使用者,而不是中間為我們做加速的加速伺服器。

其實,當一個 CDN /SLB或者透明代理伺服器把使用者的請求轉到後面伺服器的時候,這個 CDN /SLB伺服器會在 Http 的頭中加入一個記錄

X-Forwarded-For : 使用者 IP, 代理伺服器 IP

如果中間經曆了不止一個代理伺服器,這個記錄會是這樣

X-Forwarded-For : 使用者 IP, 代理伺服器 1-IP, 代理伺服器 2-IP, 代理伺服器 3-IP, ….

可以看到經過好多層代理之後, 使用者的真實 IP 在第一個位置, 後面會跟一串中間代理伺服器的 IP 位址,從這裡取到使用者真實的 IP 位址,針對這個 IP 位址做限制就可以了。

那麼針對 CDN /SLB模式下的通路限制配置就應該這樣寫:

http{
    ## 這裡取得原始使用者的IP位址,沒走CDN/SLB的,給到$remote_addr
    map $http_x_forwarded_for  $clientRealIp {
        default $remote_addr;
        ~^(?P<firstAddr>[-\.]+),?.*$	$firstAddr;
    }
    #設定IP白名單,對内部的IP不設限
    map $clientRealIp $limit{
        default $clientRealIp;
        xx.xx.xx.xx "";
    }
    #以真實IP為機關,限制請求數,并傳回429狀态;
    limit_req_status ;
    limit_req_zone $limit zone=ConnLimitZone:m rate=r/s;
    limit_req_log_level notice;

    #以真實IP為機關,限制該IP的并發連接配接數,并傳回429狀态;
    limit_conn_status ;
    limit_conn_zone $limit zone=TotalConnLimitZone:m ;
    limit_conn  TotalConnLimitZone ;
    limit_conn_log_level notice;

    #以通路域名為機關,限制總并發連結數;
    limit_conn_zone $server_name zone=SumConnLimitZone:m;
}


## 具體Server:如下在監聽php/go/java部分新增限制規則即可,或直接放在域名下面,限制全部通路
server {
    listen   ;
    location ~ \.php$ {
        #限制總并發連接配接數
        limit_conn SumConnLimitZone ;
        #最多5個排隊, 由于每秒處理 10 個請求 + 5個排隊,你一秒最多發送 15 個請求過來,再多就直接傳回 429 錯誤給你了
        limit_req  zone=ConnLimitZone  burst=  nodelay;

        fastcgi_pass   ..:;
        fastcgi_index  index.php;
        include fastcgi_params;
    }   
}
           

三、如何驗證

根據以上配置,我們知道nginx,每秒最多允許通過10+5個請求,在壓測時,就會有兩種情況:

  1. 在白名單内(到白名單的伺服器測試),壓測該站點,應該全部通過
  2. 不在白名單内,最多隻允許通過10 +5 個請求,餘下部分應該傳回429

前提:壓測總次數超過 10 +5,否則看不出效果。

centos一般都自帶有 siege壓測工具,還比較好用:

yum -y install siege

使用方法:

siege -c 3 -r 10 -b https://xxxx.xx.com/api/xxxx

-c 3 表示3個使用者

-r 10 表示通路10次

以上表示:3個使用者,每個使用者通路10次請求,共計30次

經測試,每增加一台nginx,相同的配置應該是 x 2,例如:nginx1 的配置是 10+5,nginx2的配置也是10+5,域名部署在這兩台nginx上,請求數最大允許為:20+10

以上測試,如果不能通過,應該是配置問題,那麼我們用echo子產品來調試下:

四、echo 子產品

作者原文提到了 nginx 的一個 echo 子產品,特意玩了下感覺挺有意思的,下面貼一下簡單內建步驟。

①、給 nginx 內建 echo 子產品:

cd /usr/local/src
#下載下傳echo子產品并解壓:
wget https://github.com/openresty/echo-nginx-module/archive/v0.tar.gz
tar zxvf v0.tar.gz

#下載下傳nginx并解壓
wget http://nginx.org/download/nginx-.tar.gz
tar -xzvf nginx-.tar.gz
cd nginx-/

#檢視在用nginx的編譯參數(如果是全新安裝則省略)
/usr/local/nginx/sbin/nginx -V
nginx version: nginx/
built by gcc   (Red Hat -) (GCC) #以下這行即為舊的編譯參數:
configure arguments: --user=www --group=www --prefix=/usr/local/nginx --with-http_gzip_static_module
#在舊的編譯參數基礎上新增【--add-module=/echo子產品的解壓路徑】參數,開始編譯
./configure --prefix=/usr/local/nginx/nginx  --add-module=/usr/local/src/echo-nginx-module-

#make編譯
make -j2

#平滑更新nginx (如果是全新安裝請執行:make install)
mv /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old
cp -f objs/nginx /usr/local/nginx/sbin/
make upgrade
           

以上更新、編譯和添加第三方子產品不熟悉的朋友,可以參考我另外一篇部落格:

https://mp.csdn.net/mdeditor/81136273

②、echo 用法舉例:

其實就和 shell 的 echo 差不多,能否輸出自定義資訊。

比如,在 nginx 裡面配置如下:

location /hello {
  echo "hello, world!";
}
           

通路 http://xxx.com/hello 就會在浏覽器裡面輸出 hello, world! 了(如果域名開了 CDN 可能會報 404)。

又比如,測試本文提到的真實使用者的 IP,隻要在本文第二步配置基礎上,加上如下規則并 reload 即可:

server {
    listen   ;
        server_name  yourdomain.com;
        ## 以下是新增規則:
        ## 當使用者通路 /ip 的時候,我們輸出 $clientRealIp 和 $limit變量,看看這個變量
        ## 值是不是真的 使用者源IP 位址
        location /ip{
                echo $clientRealIp;
                echo $limit;
        }
}
           

認真看的朋友,會問 clientRealIp 和 limit 有什麼差別:

clientRealIp 如果走 SLB/CDN,擷取的就是真實IP,反之,擷取的就是remote_addr

limit 是在clientRealIp的基礎上,排除了“IP白名單”,也就是說,當你的源IP,是白名單時,你的limit,應該為“空”,這樣就不受限流了

是以,我們可以以此來判斷,IP白名單是否有效:

curl http://xxx.xxx.cn/ip

nginx在CDN加速或使用SLB代理後,擷取真實IP,做并發通路限制的方法(限流)

本文介紹到這就差不多結束了,也是在神作的基礎上精簡整理并測試的,如果看完還有些許疑問,請前往檢視神作原文,也許還是大神寫的比較好了解(是否是原創我就不深究了,感覺也是轉來轉去,都沒留連結,悲哀的網際網路)!

轉載:https://zhangge.net/4879.html

繼續閱讀