在 actix-web 上設置限速器

在開發通用的 api 或 web 服務時,開發人員必須不斷考慮一些可能使 api 容易受到攻擊的安全問題。

拒絕服務攻擊 (簡稱 DOS) 是一種可以損害 api 的攻擊類型。服務器上的每個請求都要使用一定數量的資源進行處理,攻擊者會試圖重複發送大量請求,耗盡大量服務器資源,最終迫使服務器將所有資源集中在一個客戶端上,從而無法處理來自其他客戶端的請求。

限速器通過阻止來自客戶端的不成比例的請求來限制客戶端只發送有限數量的請求,而不會佔用大量服務器資源。

當客戶端發送的請求數量超過服務器在一定時間內能夠處理的數量時,服務器用狀態碼 429 響應,並指示客戶端在提交更多請求之前等待。

每個請求都必須首先經過中間件,因爲限速器通常是位於服務器中基於路由中間件的模塊。

雖然我們可以創建自己的限速器中間件,但實現 actix-web 的中間件特性是一種繁瑣的任務,我們寧願使用一個稱爲 actix-governor 的 crate。

在你的 Cargo.toml 文件中加入:

actix-governor = "0.3.2"

服務器必須使用數據庫存儲來自客戶端的請求數量,以執行其速率限制功能。基於 Ram 的數據庫是實現這類功能的理想選擇,因爲與數據庫的交互必須要快,Redis 是這方面使用最多的數據庫之一。但爲了簡單起見,我們將使用內存數據庫。

請記住,當你的服務器位於負載均衡器後面時,不建議使用內存數據庫,因爲應用程序的每個實例都有自己的內存,數據庫不會在它們之間同步。

GovernorConfigBuilder 是邁向限速器中間件的第一步,它將爲你提供 actix-web 應用程序的中間件。

1let governor_conf = GovernorConfigBuilder::default()
2  .per_second(3)
3  .burst_size(20)
4  .finish()
5  .unwrap();

默認情況下,Actix-Governor 將根據請求端的 IP 地址對請求進行速率限制,稍後我們將對此進行更新。actix-governor 使用令牌桶技術對請求進行速率限制;最初,客戶端有一定數量的請求 (令牌) 可以使用;對於每個請求,一個令牌被耗盡,每隔三秒鐘 (根據上面的代碼) 一個令牌被補充。

根據上面的代碼,如果客戶端試圖在不到 3 秒的時間內發送 20 個請求,那麼客戶端將收到來自服務器的狀態代碼 429,告訴他們等待一段時間。

可以像這樣將限速器中間件封裝到路由中:

 1#[actix_web::main]
 2async fn main() -> std::io::Result<(){
 3
 4   let governor_conf = GovernorConfigBuilder::default()
 5  .per_second(3)
 6  .burst_size(20)
 7  .finish()
 8  .unwrap();
 9
10    HttpServer::new(move || {
11        App::new()
12            .wrap(governor_conf)
13            .route("/", web::get().to(some_async_function))
14    })
15    .bind("127.0.0.1:8080")?
16    .run()
17    .await
18}

現在你創建了自己的限速器,但如果你想使用其他關鍵項而不是 Ip 地址來限制請求呢?我們現在就做,基於 http 請求,我們可以提供我們自己的關鍵項,我們將使用授權頭。

爲此,我們必須創建一個必須實現 actix_governor::KeyExtractor Trait 的類型,有一個重要的函數 extract,它將從 Http 請求中提取密鑰。

1pub trait KeyExtractor: Clone {
2    type Key: Clone + Hash + Eq;
3    type KeyExtractionError: Display;
4
5    fn extract(
6        &self,
7        req: &ServiceRequest
8    ) -> Result<Self::Key, Self::KeyExtractionError>;
9}

Key 類型必須是 Clone, std::hash 和 Eq trait。我們將使用字符串作爲關鍵類型,因爲它實現了三個必需的 trait 且 header 值可以轉換爲字符串;對於 KeyExtractionError 類型,我們將使用字符串,當然我們也可以創建我們自己的錯誤類型。

下面是 KeyExtractor 類型的實現:

 1#[derive(Clone)]
 2 pub struct _KeyExtactor;
 3
 4impl KeyExtractor for _KeyExtactor {
 5  type Key = String;
 6  type KeyExtractionError = String;
 7  fn extract(
 8     &self,
 9     req: &actix_web::dev::ServiceRequest,
10  ) -> Result<Self::Key, Self::KeyExtractionError> {
11
12    let head = req.head();
13    match head.headers().get("Authorization") {
14           Some(data) =return Ok(data.to_str().unwrap().to_string()),
15           None =return Err("can not find any token".to_string()),
16   };
17  }
18 }
19
20impl _KeyExtactor{
21  fn new()->Self{
22      _KeyExtactor
23   }
24}

我們可以把這個 keyExtractor 放到 GovernorConfigBuilder 中:

1let governor_conf = GovernorConfigBuilder::default()
2  .key_extractor(_KeyExtactor::new())
3  .per_second(3)
4  .burst_size(20)
5  .finish()
6  .unwrap();

我們不僅創建了自己的限速器中間件,而且還修改了它,使其與 Http 頭一起工作。

完整代碼類似這樣:

 1#[actix_web::main]
 2async fn main() -> std::io::Result<(){
 3
 4#[derive(Clone)]
 5 pub struct _KeyExtactor;
 6
 7impl KeyExtractor for _KeyExtactor {
 8  type Key = String;
 9  type KeyExtractionError = String;
10  fn extract(
11     &self,
12     req: &actix_web::dev::ServiceRequest,
13  ) -> Result<Self::Key, Self::KeyExtractionError> {
14
15    let head = req.head();
16     match head.headers().get("Authorization") {
17           Some(data) =return Ok(data.to_str().unwrap().to_string()),
18           None =return Err("can not find any token".to_string()),
19   };
20  }
21 }
22
23impl _KeyExtactor{
24  fn new()->Self{
25      _KeyExtactor
26   }
27}
28
29   let governor_conf = GovernorConfigBuilder::default()
30
31.key_extractor(_KeyExtactor::new())
32  .per_second(3)
33  .burst_size(20)
34  .finish()
35  .unwrap();
36
37    HttpServer::new(move || {
38        App::new()
39            .wrap(governor_conf)
40            .route("/", web::get().to(some_async_function))
41    })
42    .bind("127.0.0.1:8080")?
43    .run()
44    .await
45}

現在運行服務器並嘗試超過請求閾值,將得到類似這樣的響應:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/S8jcfqAf3gcDDiJNbgPBLA