• 反爬虫:利用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。或者直接点击注册。

  • 相关阅读:
    python json 和 pickle的补充 hashlib configparser logging
    go 流程语句 if goto for swich
    go array slice map make new操作
    go 基础
    块级元素 行内元素 空元素
    咽炎就医用药(慢性肥厚性咽炎)
    春季感冒是风寒还是风热(转的文章)
    秋季感冒 咳嗽 怎么选药
    解决IE浏览器“无法显示此网页”的问题
    常用的 css 样式 记录
  • 原文地址:https://www.cnblogs.com/freeflying/p/9588549.html
Copyright © 2020-2023  润新知