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


背景介紹


為了平衡社區成員的貢獻和索取,一起幫引入了幫幫幣。當用戶積分(幫幫點)達到一定數額之后,就會“掉落”一定數量的“幫幫幣”。為了增加趣味性,幫幫幣“掉落”之后所有用戶都可以“撿取”,誰先撿到歸誰。

但這樣就產生了一個問題,因為這個“幫幫幣”是可以買賣有價值的,所以難免會有惡意用戶用爬蟲不斷的掃描,導致這樣的情況出現:

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

所以我們需要一種機制,阻止這種爬蟲的行為。


大致思路


這個問題我們有一個很便利的前提:只有注冊用戶才能夠“撿起”幫幫幣。所以,我們不需要通過“封IP”(需獲取真實IP)這種方式來阻斷爬蟲爬行,而是直接封注冊用戶,非常方便。

那么如何判斷一個請求是真實用戶,還是爬蟲呢?我們決定使用最簡單的方法:記錄訪問頻次。當某一個用戶的訪問頻次高於設定值時(比如:5分鍾10次),就判定該用戶“有爬蟲嫌疑”。

此外,為了防止誤判(確實有用戶手快),我們還應該給用戶一個“解鎖”的功能:通過輸入驗證碼來確定不是爬蟲。


細節設計


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

數據庫?感覺沒必要,這個數據又不需要長期保留,訪問一次就做一次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

1、為什么要引入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了。代碼就不重復了。


2、在鎖定用戶的同時,清除該用戶的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。或者直接點擊注冊。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM