反爬蟲:利用ASP.NET MVC的Filter和緩存(入坑出坑)

背景介紹數據庫


爲了平衡社區成員的貢獻和索取,一塊兒幫引入了幫幫幣。當用戶積分(幫幫點)達到必定數額以後,就會「掉落」必定數量的「幫幫幣」。爲了增長趣味性,幫幫幣「掉落」以後全部用戶均可以「撿取」,誰先撿到歸誰。 編程

但這樣就產生了一個問題,由於這個「幫幫幣」是能夠買賣有價值的,因此不免會有惡意用戶用爬蟲不斷的掃描,致使這樣的狀況出現: 緩存

注:經覈實,喬布斯的同窗 其實沒有用爬蟲,就是手工點,點出來的!還能說什麼呢?只能表示佩服啊佩服…… 服務器

因此咱們須要一種機制,阻止這種爬蟲的行爲。 數據結構


大體思路ide


這個問題咱們有一個很便利的前提:只有註冊用戶纔可以「撿起」幫幫幣。因此,咱們不須要經過「封IP」(需獲取真實IP)這種方式來阻斷爬蟲爬行,而是直接封註冊用戶,很是方便。 性能

那麼如何判斷一個請求是真實用戶,仍是爬蟲呢?咱們決定使用最簡單的方法:記錄訪問頻次。當某一個用戶的訪問頻次高於設定值時(好比:5分鐘10次),就斷定該用戶「有爬蟲嫌疑」。 spa

此外,爲了防止誤判(確實有用戶手快),咱們還應該給用戶一個「解鎖」的功能:經過輸入驗證碼來肯定不是爬蟲。 設計


細節設計調試


一個最核心的問題是:用什麼來記錄用戶的訪問頻次

數據庫?感受不必,這個數據又不須要長期保留,訪問一次就作一次I/O操做在性能上接受不了,因此咱們決定使用內存。

可是,具體須要記錄那些數據,又用什麼樣的數據結構呢?

最後咱們選擇使用緩存,記錄最簡單的「用戶ID -> 訪問次數」鍵值對,來解決這個問題,由於:

  • 利用緩存的自動清除(expire)特性,清除過時數據,保證記錄的訪問次數始終是在必定時間內的。
  • 緩存的讀寫速度很快,性能上沒有壓力

固然,這裏其實仍是有那麼點問題的。好比,假設緩存時間是5分鐘,最多訪問次數是10次。0:10,開始緩存訪問次數,一直累加,到0:14,共記錄訪問次數7次,沒有問題;然而,一過0:15,緩存被清空,0:16的時候,緩存裏只有0:15到0:16這一分鐘的數據,沒有過去5分鐘(從0:11到0:16)的數據。因此用戶能夠控制一直爬蟲,訪問9次,而後就歇着,5分鐘事後,再繼續訪問9次,而後再歇5分鐘……

唉~~真這麼拼,我還真沒什麼辦法?但若是這麼一個頻次他能接受的話,我其實也無所謂,你就慢慢爬唄。或者,咱們後臺作更大的監控,把每一個用戶的每次訪問都記錄下來,進行統計,找出異常。那時候可能就真的須要數據庫了(爲了提升性能能夠內存裏放一個DataTable,定時同步到Database)。但暫時來講,沒有這個必要。


此外,還有一個問題,是否是隻須要記錄用戶訪問頻次?

若是按上述方案,在緩存裏記錄訪問頻次,經過緩存數據來判斷是否容許繼續訪問,會有一個問題:緩存到期失效以後,這個用戶就又能夠自由訪問目標頁面了!至關於到期自動解鎖。

我以爲這仍是不科學,若是認定是爬蟲,只能是人工解鎖(識別碼驗證)。因此在數據庫用戶表裏添加一個「已鎖定」(Locked)字段,若是用戶被鎖定,Update其爲當前時間;未鎖定時(解鎖後)爲NULL。


具體實現


爲了重用,咱們須要利用 Authorize Fitler,在它的OnAuthorization()方法裏面進行檢查和記錄。

代碼自己應該比較簡單,if...else...的邏輯:

            ///1. 先根據數據庫撿查當前用戶是否被鎖定
            ///2. 若是被鎖定,直接攔截。不然:
            ///3. 在緩存中檢查有無當前用戶的訪問次數記錄
            ///     3.1 沒有,新建一條他的緩存。不然:
            ///     3.2 檢查該用戶已訪問次數
            ///         3.2.1 若是已到達訪問次數限制,攔截並在數據庫中鎖定該用戶。不然
            ///         3.2.2 累加用戶的訪問次數

 

精簡註釋代碼以下:
    public class NeedLogOn : AuthorizeAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            HttpContextBase context = filterContext.HttpContext;

            ///Autofac相關操做,獲取正取的ISharedService實例
            ISharedService service = AutofacConfig.Container.Resolve<ISharedService>();
            _NavigatorModel model = service.Get();  //從數據庫獲取當前User的信息

            ///截斷式編程,減小if...else的{}嵌套
            if (model.Locked.HasValue)
            {
                ///model.Locked 來自數據庫,用戶已經被鎖定,攔截
                visitTooMuch(filterContext);
                return;
            }

            string cacheKey = CacheKey.MAX_VISIT + model.Id;

            ///很是有意思,不能直接使用int值類型,必須使用引用類型的
            VisitCounter amount;
            if (context.Cache[cacheKey] == null)
            {
                amount = new VisitCounter { Value = 1 };
                ///新創建一條Cache
                context.Cache.Add(cacheKey, amount, null,
                    DateTime.Now.AddSeconds(Config.Seconds),
                    Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
            }
            else
            {
                amount = context.Cache[cacheKey] as VisitCounter;
                if (amount.Value >= Config.MaxVisit)
                {
                    ///在數據庫中鎖定該用戶
                    service.LockCurrentUser();
                    BaseService.Commit();

                    ///當即清除Cache
                    context.Cache.Remove(cacheKey);

                    visitTooMuch(filterContext);
                    return;}
                else
                {
                    ///不能使用:currentVisitAmount++;
                    ///context.Cache[cacheKey] = currentVisitAmount;
                    ///見:https://stackoverflow.com/questions/2118067/cached-item-never-expiring
                    amount.Value++;
                }
            }
        }
    }

    public class VisitCounter
    {
        public int Value { get; set; }
    }

 仔細觀察代碼,你會發現兩個問題。這就是飛哥我曾經掉的坑啊!o(╥﹏╥)o

一、爲何要引入VisitCounter類?

緩存裏就存放着這個類的實例,而這個類其實就包裹一個int Value;幹嗎呢,這是?爲何不直接用int呢?直接把int存到Cache裏不行嗎?

不行啊!艹。

存進去,沒問題;取出來,也沒問題;但更新(累加)的時候有問題啊。你怎麼更新?

            //取出緩存
            currentVisitAmount = Convert.ToInt32(context.Cache[cacheKey]);

            //累加
            currentVisitAmount++;
            //再存進去
            context.Cache[cacheKey] = currentVisitAmount;

 

這樣不行的,具體的解釋看這裏:Cached item never expiring

簡單的說,context.Cache[cacheKey] = currentVisitAmount; 這一句,等於從新插入了一條永不過時的緩存。萬萬沒想到啊!這個bug把飛哥都差點搞瘋了,原本cache的調試都很是麻煩,還搞個這種幺蛾子。

因此解決的辦法是什麼呢?在Cache裏存一個引用類型值,而後不改Cache,只改引用類實例裏的值就OK了。代碼就不重複了。


二、在鎖定用戶的同時,清除該用戶的cache

這裏啊,曾經走了點彎路。

我最開始是在解鎖用戶的時候清除該用戶的Cache。

        [NeedLogOn]
        public ActionResult Unlock()
        {
            string userId = getCurrentUserId();
            string cacheKey = CacheKey.MAX_VISIT + userId;
            HttpContext.Cache.Remove(cacheKey);

            return View(new ImageCodeModel());
        }

 

結果不知道咋回事,時靈時不靈。我把本地代碼,鏈接服務器數據庫,開着Debug模式,一步一會的進去看,OK,沒問題;但把本地代碼發佈到服務器,duang,不行了?!無法調試,只有寫log啥的,坑得我不要不要的……

後來忽然發現,這裏有「壞代碼的味道」:重複。你看這個cacheKey的構建,是否是在 NeedLogOn.OnAuthorization()裏構建過一次?重複使用的代碼是否是就應該封裝?因此呢,開始呢,是想弄一個方法出來得到cacheKey,好比striing GetVisitLimitCacheKey()啥的,但這個方法要讓Controller裏的UnLock()和Filter裏的OnAuthorization()都能調用,放在哪裏呢?

忽然靈光一閃:爲何 Cache.Remove 要寫在UnLock()裏面呢?

其實只要用戶被鎖定,他的緩存信息就沒用了。由於咱們已經在數據庫中標明瞭他被Locked,因此NeedLogOn.OnAuthorization()攔截住他,不須要Cache呀!儘早的清除這個Cache,還能提升那麼一點點的性能。

最關鍵的是,這樣代碼更緊湊了:cacheKe在同一個方法裏被使用,cache操做在同一個方法類完成,避免了代碼分散耦合,優雅多了!


++++++++++++++++++++

 

最後的最後,請你們幫個小忙,我作的一個小調查:你願不肯意成爲「好心人」?

忘了給註冊人和邀請碼:葉飛,1786。或者直接點擊註冊。

相關文章
相關標籤/搜索