如何給網站加入優雅的實時反爬蟲策略

你的網站內容頗有價值,但願被google,百度等正規搜索引擎爬蟲收錄,卻不想讓那些無節操的山寨爬蟲把你的數據扒走不勞而獲。本文將探討如何在網站中加入優雅的反爬蟲策略。javascript

【思路】css

反爬蟲策略要考慮如下幾點:html

  1. 能被google、百度等正規搜索引擎爬蟲抓取,不限流量和併發數;java

  2. 阻止山寨爬蟲的抓取;node

  3. 反爬蟲策略應該是實時檢測的,而不是經過一段時間後的訪問統計分析得出;redis

  4. 誤判後的人性化處理(優雅之所在);express


大部分的爬蟲不是以瀏覽器方式來訪問頁面的,爬蟲只下載網頁的html源代碼,不加載包含在頁面中的js/css/圖片,這是區分爬蟲與否的一個關鍵。一個請求被識別出來不是瀏覽器訪問,必定是爬蟲,爲了知足上面所說的第1點和第2點,進一步對http頭agent進行驗證,是否標記爲google、百度的spider,嚴格一點的話應該判別來源IP是否爲google、baidu的爬蟲IP,這些IP在網上均可以找到。校驗出來IP不在白名單就能夠阻止訪問內容。json

固然,有一部分爬蟲是以瀏覽器載入的方式來抓取內容的,因此,即便被識別出來是瀏覽器訪問的來源ip。還要檢測這個個ip在一個時間片內的併發數,超過必定閥值,能夠認爲是爬蟲,阻止訪問內容。瀏覽器

因爲咱們的反爬蟲策略是基於IP的,會出現誤判,尤爲是併發量限制的判別。咱們須要一種友好的方式來阻止訪問。直接返回50x/40x空白或者錯誤頁面是很粗魯的,當真正的用戶被誤判阻止訪問時可以手動解鎖繼續訪問纔是比較優雅的方式,你們不約而同的會想到驗證碼,對!讓用戶輸入圖形中的驗證碼解鎖,但是咱們日常見到的驗證碼都仍是野蠻的,驗證碼技術從一開始的簡單的數字,發展今天有輸入漢字的、輸入數學計算結果的等等五花八門,不只以複雜的驗證碼刁難用戶,還要加上各類干擾字符,美其名曰提升安全性,其實是開發工程師腦殘扎堆鑽牛角尖的產物,用戶是怨聲載道。驗證碼的目的是區分人工和機器,要作到機器沒法自動操做,同時讓人工操做很方便、優雅。在本文的案例中,咱們採用了一種比較有趣的驗證碼,讓人識別物體,在驗證碼系統中預存了大量的事物,包括動物、植物、傢俱等等平常遇到的東西,驗證用戶的過程就是系統從這些事物中隨機選出少許圖形,並要求用戶選中預設答案中的某一個便可解鎖。安全

回到識別爬蟲的步驟,咱們用流程圖理一下:


【實現】

咱們用nodejs(express)和redis來實現反爬蟲系統,redis用來存放一些計數。

一、判別是否爲瀏覽器訪問

返回頁面請求時,在redis中給該IP的頁面訪問計數+1。在每一個頁面中會引入一個js,當請求這個js文件時在redis中給該IP頁面訪問計數-1,這樣,若是不是瀏覽器的請求,redis中的頁面計數會不斷增大,若是是瀏覽器請求,下載頁面源代碼時增1,隨後瀏覽器加載js文件時減1,redis中的頁面計數會歸零。咱們只須要判斷頁面計數是否爲0來區分是否爲瀏覽器訪問,咱們還能夠給頁面下載完了可是js沒有加載這種特殊狀況留點餘地,設定一個閥值,例如:5,頁面計數大於5就判別出該IP內有爬蟲訪問。

二、爬蟲白名單識別

若是上一步被識別爲爬蟲訪問,則進一步檢測用戶http頭的user-agent、ip,判斷是否在預設的白名單內。若是不在則阻止訪問顯示驗證碼。這個步驟很簡單,不用多說。

三、瀏覽器訪問下的併發量限制

一樣在 redis下給每一個IP作計數,和上面不一樣的是利用redis key的過時機制,每次計數累加時將key設定在必定的時間內過時,好比5秒,這個至關於一個時間片,若是5秒內有另一個請求,會計數增長1,過時時間會延長5秒,若是在一個5秒內沒有其餘請求,這個key就會消失。此後一個請求進來計數從1開始。咱們能夠設定一個閥值,好比20,任意5秒內有20個請求進來爲超限,阻止訪問顯示驗證碼。

四、優雅的驗證碼

系統預設了不少圖片,每一個圖片是一個動物、植物、傢俱等,每一個圖片有一個ID值。從這些圖片中任意抽出3個,而且選中其中一個爲標準答案,注意這個過程都是程序後臺進行,將標準答案ID放在session中。前臺頁面顯示了這3幅圖片,並根據預設的答案要求用戶選擇其中一個,用戶只要選中對應的圖片,將表單提交到後臺,系統將提交的ID與session中ID比較判別是否正確。固然,每一個圖片都有一個固定的ID值有被窮舉的漏洞,有不少改進的餘地,此處僅討論原型不作過多探討。

效果如圖:


好了,接下來我會貼出一些實現的代碼,若是你想看看實現後的效果,能夠訪問碰頭吧(http://www.pengtouba.com/)試驗一下,首頁沒有加反爬蟲策略。打開微信廣場http://www.pengtouba.com/weixin/cast-c1p1s1.html 而後用F5強暴刷新你就會看到效果了。


【代碼】

攔截請求(其餘語言相似,例如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函數是檢驗用戶輸入的驗證碼。

相關文章
相關標籤/搜索