動態

詳情 返回 返回

一種優雅的方式整合限流、冪等、防盜刷 - 動態 詳情

大家在工作中肯定遇到過接口被人狂刷的經歷,就算沒有經歷過,在接口開發的過程中,我們也需要對那些容易被刷的接口或者和會消耗公司金錢相關的接口增加防盜刷功能。例如,發送短信接口以及發送郵件等接口,我看了國內很多產品的短信登錄接口,基本上都是做了防盜刷,如果不做的話,一夜之間,也許公司都賠完了┭┮﹏┭┮。

假設我們正在開發一個發送短信(僅國內)的接口,過程如下

  1. 接口定義為/sendSms
  2. 請求參數只有phone
  3. 在處理請求時,我們對請求參數phone進行了合法性校驗
  4. 如果手機號合法,那麼調用騰訊雲等服務商的發送短信Api,向目標手機號發送短信
  5. 流程結束

上面便是一個最簡單的向手機號發送短信驗證碼的接口,不考慮其他和業務相關的操作。我們現在來分析一下,該接口存在的問題(刷接口)。

  1. 只對請求參數中的手機號進行合法性校驗(11位手機號),並沒有對手機號是否為空號進行驗證,會導致別人構造大量合法但是是空號的手機號
  2. 沒有增加單個手機號,每天最大發送次數
  3. 沒有控制每個手機號發送間隔,會導致同一時間,向相同手機號發送大量短信

既然我們知道了發送短信驗證碼接口存在的缺陷,那我們將這些問題一一解決了,是不是就可以避免接口被盜刷呢?答案是隻能在一定程度上防止被盜刷,因為這些惡意請求中,手機號都是通過程序無限生成的,都能通過我們的正則校驗,所以對手機號進行發送次數和發送間隔限制,對他們是沒有任何效果的。另外,想要避免向空號手機號發送短信的話,還需要額外的引入第三方的空號檢驗Api,增加了新的資源消耗。

我們現在從發送短信驗證碼的接口轉移到其他的接口來看看,尋找一種能夠應用於所有的接口,並能實現限流,冪等,防盜刷功能的方案。

公眾號: 後端隨筆

個人博客:https://knowledge.xcye.xyz/

解決接口請求參數容易被構造

我們其實不難發現,導致接口被盜刷的根本原因在於請求參數很容易通過算法構造構造出來,這些通過程序生成的參數,在我們的程序看來,都是合法的。

{
"phone": "11位手機號"
}

image.png

通過上面兩個對比,我們不難發現,先對於只有一個參數phone的發送短信接口來説,想要構造出淘寶發送短信的參數,難度直接上升了很多個階梯。

我們從解決接口請求參數容易被構造的角度出發,我目前能想到的只有對請求參數進行加密,使用非對稱加密的方式。具體的思路為,客户端在發送請求之前,使用服務端提供的公鑰對請求參數進行加密,讓請求參數看上去不那麼容易被構造出來。服務端獲取到請求參數後,使用私鑰進行解密,然後再進行後續的一些驗證操作。

那麼這樣可以防止接口被盜刷麼?答案是,只能防君子,不能防小人。特別是對於Web端來説,如果發起盜刷的這個人,同樣是一個開發者,他直接F12就可以從js文件中找到公鑰。對於App來説,獲取源碼的方式會更難一點,但是最終公鑰應該還是能夠被找到的。

如果我們解決公鑰容易被獲取的問題,是不是可以通過這種方式防止接口被盜刷呢?如果能夠解決公鑰容易被獲取的問題,在一定程度上,確實是可以解決接口被盜刷的問題,但是現在又將問題轉移到了獲取公鑰接口上,我們還是需要解決獲取公鑰接口被盜刷的問題。

而且如果獲取到的公鑰不能存在時效性,可以被多次使用,那麼這些通過加密實現防盜刷的接口,在公鑰被泄露的情況下,還是會存在被盜刷的問題。想要解決的話,可以讓公鑰只能使用一次,或者只能在很短時間內使用,再者只能被多少個請求使用。我最終的解決方案也是類似於這個,讓令牌只能使用一次。

而且使用公鑰進行加密,通常是防止在請求過程中發生的中間人攻擊,是為了解決參數被修改以及泄露的問題。

Ticket機制

我最終並不是通過解決參數容易被構造來防止盜刷的,我是通過對請求進行是否是機器人判斷,如果是非法請求,強制必須先通過圖形驗證碼,只有合法的請求,服務端才會進行處理。

我基於Ticket機制,客户端在發送請求之前,必須先向服務端申請一個Ticket。服務端在處理申請Ticket請求時,對請求進行判斷,判斷包含了是否是惡意請求和是否需要進行限流。當這兩步都通過後,服務端會生成一個被加密,存在時效性並且只能使用一次的Ticket,客户端發送真正請求時,需要攜帶這個Ticket。每個Ticket只能被使用一次,而且客户端每次都攜帶Ticket,還可以通過Ticket實現請求的冪等性。

這種方案並不和任何的接口耦合,Ticket是攜帶在請求頭上,不會對請求參數造成污染。

申請Ticket

我最終是使用Ticket完成了限流,防盜刷,冪等性這三個功能,為了讓這個功能更加的通用,不和任何的接口相耦合。在申請Ticket時,客户端需要傳遞兩個參數,分別是serviceType和primaryKey。serviceType用於控制該接口的類型,而primaryKey會被用於限流。最終結合配置中心,做到了能夠輕鬆的對任何類型的請求進行獨立的限流,UserAgent黑名單與白名單,Ip限流等操作。

具體的執行過程為(以發送短信驗證碼為例):

  1. 客户端調用接口申請Ticket,傳遞的參數為{serviceType: sms, primaryKey: 用户手機號}
  2. 服務端對客户端請求進行驗證
  1. UserAgent是否在黑名單中(惡意請求的UserAgent基本上都是同一個),UserAgent還可以有很多的玩法,比如類似於Ip一樣,對UserAgent進行限流(會影響一部分正常用户)
  2. 從請求頭中對用户身份進行初步識別。可以和客户端協商好,在一些請求頭值上做文章,幫助服務端識別請求者身份
  3. 對IP進行識別。很多的惡意請求都來自於不同的Ip,有部分來自同一個網段,我們可以對Ip結合serviceType進行限制。
  1. 如果服務端識別請求是惡意請求,則在響應體中將captchaStatus設置為true,表示需要客户端進行圖形驗證碼驗證
  2. 下一步,服務端通過serviceType,從配置中獲取限流規則。通過serviceType+primaryKey作為key,看是否能通過指定的限流。
  3. 通過限流後,服務端使用對稱加密對{captchaStatus, primaryKey}進行加密,得到Ticket。這一步的目的是為了在最終驗證Ticket時,從解密的數據中獲取captchaStatus,避免captchaStatus是由客户端傳遞,從而解決請求繞過圖形驗證碼驗證問題,客户端根據captchaStatus判斷該Ticket是否需要用户通過圖形驗證碼,才能執行後續操作。
  4. 服務端將Ticket放入Redis,並且設置過期時間,然後將{ticket, captchaStatus}返回給客户端。

    image.png

服務端返回的Ticket是加密後的密文,存在過期時間,保存在Redis中,並且只能被使用一次,無法被客户端構造出來。儘管加密算法被不小心泄露,服務端也無法從Redis中查詢到這個"合法的Ticket",所以這個Ticket是足夠安全的。

圖形驗證碼

調用申請Ticket接口後,響應參數中包含兩個參數:captchaStatus, ticket。captchaStatus表示該Ticket是否需要客户端通過圖形驗證碼。

當captchaStatus為true時,客户端調用另一個接口加載圖形驗證碼,在調用接口時,需要攜帶上一步獲得的Ticket,服務端最終會將本次的圖形驗證碼和Ticket進行綁定,最終實現在下一步中通過Ticket獲取圖形驗證碼的驗證結果,具體步驟為:

  1. 客户端攜帶申請到的Ticket加載圖形驗證碼數據
  2. 服務端從請求頭中獲取Ticket,從Db中查詢該Ticket加載過幾次圖形驗證碼,如果超過最大加載次數,那麼直接通知客户端重新申請新Ticket,並且刪除和舊Ticket相關的數據。
  3. 驗證通過後,生成圖形驗證碼數據,得到該圖形驗證碼的key,然後將key和ticket放入Db中存儲起來,目的是為了保存圖形驗證碼驗證結果
  4. 客户端接收到圖形驗證碼數據並加載

    image.png

在防盜刷功能中,最有效的還得是驗證碼功能

服務端驗證Ticket

當客户端完成上面兩個後,客户端現在才開始調用真正的接口(發送短信)。在調用發送短信驗證碼時,客户端需要攜帶申請到的Ticket和圖形驗證碼Key(如果captchaStatus為true)。

服務端接收到請求後,具體的處理步驟如下:

  1. 從請求中獲取Ticket,並且對Ticket進行解密,從Redis中查詢該Ticket是否存在

    儘管我們的防盜刷邏輯被人知曉,他們也不能隨意的構造Ticket

  2. 從解密後的數據中獲取captchaStatus字段的值,如果為true,則表示該Ticket需要執行圖形驗證碼驗證。服務端從DB中查詢和該Ticket最後一次綁定的圖形驗證碼Key的結果,如果沒有進行驗證或者結果為失敗的話,直接結束流程
  3. 對Ticket進行冪等性驗證,主要是通過判斷該Ticket之前是否被使用過,如果上一個請求已經完成,那麼直接從Redis中獲取執行結果,並返回
  4. 當上面都沒有問題後,現在才開始執行最終的業務邏輯,這裏是執行發送短信驗證碼。因為這個功能並不和任何的接口耦合,如果我們需要更細的防盜刷,還可以在具體的接口裏面做文章。
  5. 執行完畢後,需要把Ticket相關的數據都刪除。

image.png

上面便是我實現接口防盜刷的具體過程,現在我們來驗證一下,這個防盜刷是否真的能防(還是以發送短信驗證碼)?

  1. 構造大量合法但空號的手機號

    每次請求時,都需要先申請Ticket,primaryKey為手機號。因為這些惡意請求的UserAgent是相同的,如果我們預先接收到報警並且將UserAgent放入黑名單中,這些請求會直接被攔截。

    就算UserAgent每被攔截,還有Ip等其他的限流措施。如果都通過,我們還可以直接強制要求每一個請求都進行圖形驗證碼驗證,因為圖形驗證碼的破解難度更高,基本上已經勸退很多人了,強制進行圖形驗證碼驗證,對於正常用户來説,也只會降低使用體驗。

    對於手機號為空號來説,如果這個用户確實通過了上面這些措施,那麼基本上可以保證他是一個真實用户,所以手機號是否為空號驗證,我覺得是多此一舉,除非發送短信的資源真的非常寶貴。

  2. Ticket被泄露,被偽造

    在公司沒出內鬼的情況下,Ticket是不可用被偽造出來的,並且就算被偽造出來,這個Ticket也沒有保存至Db。如果該Ticket的captchaStatus為false並且被泄露了,他們也只能在指定時間內使用該Ticket,並且只能使用一次。不可能會存在Ticket無限泄露的情況。

在上面的過程中,服務端驗證請求是否是機器人,還可以在發送真正請求時進行驗證,如果驗證失敗,客户端根據響應體執行對應的操作,然後攜帶Ticket重發請求。

上面的邏輯並沒有對正常用户的驗證結果進行緩存,這會導致,正常用户在調用這些接口時,每調用一次,都需要通過圖形驗證碼。

其他措施

還有其他的措施,也可以增加接口被盜刷的情況。這些措施包括增加防盜刷邏輯被破解難度和防止接口被盜刷。

先説防止接口被盜刷,本質上是防止接口被泄露。對於App來説,某個人想要知道我們接口信息的話,必須對App進行反編譯,我對App反編譯不太瞭解,可以試試那些增大反編譯的措施,就算不進行反編譯,使用Fiddler工具也是可以看到請求信息的。對於Web端來説,用户只需要按F12就可以看到JavaScript代碼,以及每個請求的參數,響應體等。我們可以禁用F12以及右鍵(降低用户體驗),以及在生產環境中,添加當用户按F12後,自動進入無限Debug模式。這兩個操作可以增加我們接口被暴露的風險,從而在一定程度上起到"防盜刷"目的。

對於增加防盜刷邏輯被破解難度來説,市場上有很多的App的限流等規則都被人攻破了,我個人覺得會被攻破,除了接口設計的原因外,還有一個是接口的響應體中提示了很明顯的錯誤信息。比如我們訪問某個增加了防刷功能的接口,該接口提示UserAgent無效,當前Ip已被限流,Ticket無效,未進行圖形驗證碼驗證等很明顯的信息。這些信息其實已經間接提示了讓請求變合法的步驟是什麼,這雖然可以幫助開發人員進行調試,但也間接的幫助了那些發送惡意請求的人。所以為了增大防盜刷邏輯被破解的難度,我們不需要返回這些很明顯的提示信息,可以無論什麼原因,都返回"非法請求",對於公司開發人員來説,他們自己通過code從開發文檔中查詢每個code所代表的意思。

以上便是我對於防止接口被盜刷的一些見解,可能還有更優的方案,但是我目前確實只能想到這一種。另外,也可以使用已有的服務,比如騰訊雲和阿里雲等服務商的驗證碼。我使用的圖形驗證碼是開源的,來自於dromara大佬開源的Java 行文驗證碼,使用起來非常的方便,並且支持滑塊,旋轉,滑動,文字點選,非常感謝大佬。此外,因為每次請求時申請到的Ticket都是加密的,在加密和解密的過程中,性能消耗也是一個可以優化的點,具體得看自己選擇的算法是什麼。

Add a new 評論

Some HTML is okay.