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

來源:互聯網
上載者:User

背景介紹


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

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

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

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


大致思路


這個問題我們有一個很便利的前提:只有註冊使用者才能夠“撿起”幫幫幣。所以,我們不需要通過“封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;                    ///見: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。或者直接點擊註冊。

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.