如何给网站加入优雅的实时反爬虫策略

摘要: 允许google,baidu等正规爬虫,拒绝那些无节操扒数据的山寨爬虫。本文将探讨如何给网站加入优雅的实时反爬虫策略。

你的网站内容很有价值,希望被google,百度等正规搜索引擎爬虫收录,却不想让那些无节操的山寨爬虫把你的数据扒走坐享其成。本文将探讨如何在网站中加入优雅的反爬虫策略。

【思路】

反爬虫策略要考虑以下几点:

  • 能被google、百度等正规搜索引擎爬虫抓取,不限流量和并发数;
  • 阻止山寨爬虫的抓取;
  • 反爬虫策略应该是实时检测的,而不是通过一段时间后的访问统计分析得出;
  • 误判后的人性化处理(优雅之所在);

大部分的爬虫不是以浏览器方式来访问页面的,爬虫只下载网页的html源代码,不加载包含在页面中的js/css/图片,这是区分爬虫与否的一个关键。一个请求被识别出来不是浏览器访问,一定是爬虫,为了满足上面所说的第1点和第2点,进一步对http头agent进行验证,是否标记为google、百度的spider,严格一点的话应该判别来源IP是否为google、baidu的爬虫IP,这些IP在网上都可以找到。校验出来IP不在白名单就可以阻止访问内容。 当然,有一部分爬虫是以浏览器载入的方式来抓取内容的,所以,即使被识别出来是浏览器访问的来源ip。还要检测这个个ip在一个时间片内的并发数,超过一定阀值,可以认为是爬虫,阻止访问内容。 由于我们的反爬虫策略是基于IP的,会出现误判,尤其是并发量限制的判别。我们需要一种友好的方式来阻止访问。直接返回50x/40x空白或者错误页面是很粗鲁的,当真正的用户被误判阻止访问时能够手动解锁继续访问才是比较优雅的方式,大家不约而同的会想到验证码,对!让用户输入图形中的验证码解锁,可是我们平常见到的验证码都还是野蛮的,验证码技术从一开始的简单的数字,发展今天有输入汉字的、输入数学计算结果的等等五花八门,不仅以复杂的验证码刁难用户,还要加上各种干扰字符,美其名曰提高安全性,实际上是开发工程师脑残扎堆钻牛角尖的产物,用户是怨声载道。验证码的目的是区分人工和机器,要做到机器无法自动操作,同时让人工操作很方便、优雅。在本文的案例中,我们采用了一种比较有趣的验证码,让人识别物体,在验证码系统中预存了大量的事物,包括动物、植物、家具等等日常遇到的东西,验证用户的过程就是系统从这些事物中随机选出少量图形,并要求用户选中预设答案中的某一个即可解锁。 回到识别爬虫的步骤,我们用流程图理一下:

此处输入图片的描述

【实现】

我们用nodejs(express)和redis来实现反爬虫系统,redis用来存放一些计数。

1、判别是否为浏览器访问 返回页面请求时,在redis中给该IP的页面访问计数+1。在每个页面中会引入一个js,当请求这个js文件时在redis中给该IP页面访问计数-1,这样,如果不是浏览器的请求,redis中的页面计数会不断增大,如果是浏览器请求,下载页面源代码时增1,随后浏览器加载js文件时减1,redis中的页面计数会归零。我们只需要判断页面计数是否为0来区分是否为浏览器访问,我们还可以给页面下载完了但是js没有加载这种特殊情况留点余地,设定一个阀值,例如:5,页面计数大于5就判别出该IP内有爬虫访问。

2、爬虫白名单识别 如果上一步被识别为爬虫访问,则进一步检测用户http头的user-agent、ip,判断是否在预设的白名单内。如果不在则阻止访问显示验证码。这个步骤很简单,不用多说。

3、浏览器访问下的并发量限制 同样在 redis下给每个IP做计数,和上面不同的是利用redis key的过期机制,每次计数累加时将key设定在一定的时间内过期,比如5秒,这个相当于一个时间片,如果5秒内有另外一个请求,会计数增加1,过期时间会延长5秒,如果在一个5秒内没有其他请求,这个key就会消失。此后一个请求进来计数从1开始。我们可以设定一个阀值,比如20,任意5秒内有20个请求进来为超限,阻止访问显示验证码。

4、优雅的验证码 系统预设了很多图片,每个图片是一个动物、植物、家具等,每个图片有一个ID值。从这些图片中任意抽出3个,并且选中其中一个为标准答案,注意这个过程都是程序后台进行,将标准答案ID放在session中。前台页面显示了这3幅图片,并根据预设的答案要求用户选择其中一个,用户只要选中对应的图片,将表单提交到后台,系统将提交的ID与session中ID比较判别是否正确。当然,每个图片都有一个固定的ID值有被穷举的漏洞,有很多改进的余地,此处仅讨论原型不做过多探讨。

效果如图:

此处输入图片的描述

【代码】

拦截请求(其他语言类似,例如java可以用拦截器)

app.get('/weixin/*', antiCrawler.openDoor);//需要保护的目录
app.get('/helper/close-door.js', antiCrawler.closeDoor);//伪js文件

antiCrawler.js

/**
* anti crawler
* Created by Cherokee on 14-7-13.
*/var settings = require("../settings.json");var redis = require("redis");var cache = require("../lib/cache.js");var vcode = require('../lib/vcode.js');var ac_redis_cli = redis.createClient(settings['anti_crawler_redis_db']['port'],settings['anti_crawler_redis_db']['host']);var IP_RECORD_EXPIRE = settings['anti_crawler_redis_db']['ip_record_expire'];var IP_LOCK_EXPIRE = settings['anti_crawler_redis_db']['ip_lock_expire'];var IP_HAIR_EXPIRE = settings['anti_crawler_redis_db']['ip_hair_expire'];var DOOR_THRESHOLD = settings['anti_crawler_redis_db']['door_threshold'];var HAIR_THRESHOLD = settings['anti_crawler_redis_db']['hair_threshold'];


ac_redis_cli.on('ready',function(){
console.log('redis for anti-crawler is ready');
});

ac_redis_cli.on('error',function(err){
console.error('redis for anti-crawler error'+err);
});

ac_redis_cli.on('end',function(){
console.error('redis for anti-crawler closed');
});

ac_redis_cli.select(settings['anti_crawler_redis_db']['db'],function(err){
if(err)throw err;
 else {
cache.set('ac_redis_cli',ac_redis_cli,77760000);
console.log('redis for anti-crawler switch db :'+settings['anti_crawler_redis_db']['db']);
}
});

exports.openDoor = function(req, res, next) {
var ua = req.get('User-Agent');
    var ip = req.ip;
    var url = req.url;

    if(/\/weixin\//.test(url)){
        ac_redis_cli.exists('lock:'+ip,function(err,bol){
            if(bol){
                send421(req,res);
            }else{
                ac_redis_cli.get('door:'+ip,function(err,d_num){
                    if(d_num>DOOR_THRESHOLD){//some one didn't use browser
                        if(isTrustSpider(ua,ip)){//it's trusted spider
                            kickDoor(ip,function(val){
                                leaveHair(ip,function(val){
                                    next();
                                });
                            });
                        }else{
                            blockIt(req,res);
                        }
                    }else{//perhaps using simulated browser to crawl pages
                        ac_redis_cli.get('hair:'+ip,function(err,h_num){
                            if(h_num>HAIR_THRESHOLD){//suspicious
                                blockIt(req,res);
                            }else {
                                kickDoor(ip,function(val){
                                    leaveHair(ip,function(val){
                                        next();
                                    });
                                });
                            }
                        });
                    }
                });
            }
        });
    }
};

exports.closeDoor = function(req,res){
    ac_redis_cli.multi()
        .decr('door:'+req.ip)
        .expire('door:'+req.ip,IP_RECORD_EXPIRE)
        .exec(function(err, replies){
            if(replies&&parseInt(replies[0])<0){
                ac_redis_cli.set('door:'+req.ip,0,function(err){
                    res.set('Content-Type', 'application/x-javascript');
                    res.send(200,'{"zeroize":true}');
                });
            }else{
                res.set('Content-Type', 'application/x-javascript');
                res.send(200,'{"zeroize":false}');
            }
        });
}

exports.verify = function(req,res){
    var vcode = req.body.vcode;
    var origin_url = req.body.origin_url;
    if(req.session.vcode&&vcode==req.session.vcode){
        req.session.vcode = null;
        ac_redis_cli.multi()
            .del('lock:'+req.ip)
            .del('door:'+req.ip)
            .del('hair:'+req.ip)
            .exec(function(err, replies){
                res.redirect(origin_url);
            });
    }else send421(req,res,origin_url);

}
var blockIt = function(req,res){
    ac_redis_cli.multi()
        .set('lock:'+req.ip,1)
        .expire('lock:'+req.ip,IP_LOCK_EXPIRE)
        .exec(function(err, replies){
            send421(req,res);
        });
}
var send421 = function(req,res,origin_url){
    var code_map = {};
    var code_arr = [];

    while(code_arr.length<3){
        var rindex = Math.ceil(Math.random() * vcode.list.length) - 1;
        if(!code_map[rindex]){
            code_map[rindex] = true;
            code_arr.push(rindex);
        }
    }
    var answer = code_arr[Math.ceil(Math.random() * 3) - 1];
    req.session.vcode = answer;
    res.status(421).render('weixin/421',{'code_list':code_arr,'code_label':vcode.list[answer],'origin_url':origin_url||req.url});
}
var isTrustSpider = function(ua,ip){
    var trustBots  = [
        /Baiduspider/ig,
        /Googlebot/ig,
        /Slurp/ig,
        /Yahoo/ig,
        /iaskspider/ig,
        /Sogou/ig,
        /YodaoBot/ig,
        /msnbot/ig,
        /360Spider/ig
    ];
    for(var i=0;i<trustBots.length;i++){
        if(trustBots[i].test(ua))return true;
    }
    return false;
}
var kickDoor = function(ip,callback){
    ac_redis_cli.multi()
        .incr('door:'+ip)
        .expire('door:'+ip,IP_RECORD_EXPIRE)
        .exec(function(err, replies){
            if(callback)callback(replies?replies[0]:null);
        });
}
var leaveHair = function(ip,callback){
ac_redis_cli.multi()
.incr('hair:'+ip)
 .expire('hair:'+ip,IP_HAIR_EXPIRE)
.exec(function(err, replies){
if(callback)callback(replies?replies[0]:null);
});
}

实际应用中不仅要检测User-agent,还要有IP白名单检测,以上代码并没有包含 IP白名单。 send421函数就是显示验证码的步骤,verify函数是检验用户输入的验证码。