嗯….本人是從寫爬蟲開始編程的,不事後面作web寫網站去了,好了,最近web要搞反爬蟲了,哈哈哈,總算有機會把之之前作爬蟲時候見識過的反爬一點點給如今的網站用上了~ 作爬蟲的同志,有怪莫怪嘍~還有求別打死 > <html
首先要提一下AJAX,如今普天下網頁幾乎都是往特定的數據接口請求數據了,除了什麼首屏渲染這種服務端渲染好html之外,幾乎沒有什麼靜態網頁了。我看了有一些帖子說AJAX讓爬蟲難作,但是我以爲結合一些工具(好比chrome的開發者工具),找到AJAX所請求的後端數據接口一點也不難,並且如今本身也寫過一段時間的web後端數據接口,發現接口的設計每每都是往簡單易懂的方向作,外加從2000年出現REST風格,更是讓接口設計愈來愈簡明瞭。因此其實若是一個web站點沒有察覺到有爬蟲的存在,或者察覺到了,可是沒有想要作一點數據保護措施,它是不會再AJAX上作文章的,那麼若是單純的AJAX,其實並無任何反爬的做用,因此別再說AJAX反爬什麼的了,況且AJAX生出來就不是爲了反爬的前端
然而在如今的先後端分離的時代,前端反爬仍是有的搞的,基於我不太懂JavaScript,就不展開來講,我只是據說過什麼參數加密啊,數據混淆什麼的,但其實歸納起來都是一種對數據接口的隱藏,這讓一些不太懂js的人,也跟着懵逼了(好比說我 : <),可是你要知道,前端代碼最終仍是要請求一個url的,不管它把這個過程拆開成多散,弄得多複雜都好,只要是須要數據,就必然須要請求一個後端接口(這個接口能夠是SOAP,不過21世紀恐怕更多的是RESTful的),因此對於數據保護而言,更加須要重點關注的是後端數據接口的保護。python
本反爬蟲之旅系列將會一點點從各個方面壘高數據保護牆,可是請記住,由於網站數據的公開性,因此,只是延緩被盜庫的時間而已,想本身在網站上公開的數據徹底不被爬走是不可能的。那麼咱們的目標就是:讓盜庫耗時被延緩到一個比較長的時間裏面,那麼對於爬取數據方而言,這些數據的價值將會隨着時間的增長而下降,數據的價值=利用價值 - (爬取成本+數據貶值速度) * 爬取時間(不用糾結來源了,我說的)linux
這一篇就講最基礎的「給過頻IP彈驗證碼」這種入門級防禦實現,雖然花錢買點代理IP就能夠搞定這種實現,可是至少也讓他們增長了成本,可是咱們相對地並無花費多少成本,並且過頻IP彈驗證碼除了能反爬,也能抵禦一部分的CC***(短期大量的爬蟲請求堪比CC***啊),雖然沒有多大的做用,可是起碼比裸奔強!這也算是功能上的複用吧nginx
反爬蟲之旅預告:git
過頻IP彈驗證碼[應用外]
數據接口的url設計(uuid)和內容橫向範圍限制(參考angel.co)[應用內]
用戶可見(參考微博)以及內容縱向切割(盈利點思考)[應用內]
統覽
這裏寫圖片描述github
高頻訪問IP彈驗證碼架構圖
P.s. csdn默認水印real醜,直接去掉圖片地址的watermark就能夠了web
OpenResty
我不許備在web應用中作ip的統計和查封,應用就應該只作業務功能,這些基礎東西應該由咱們應用的前部——專業的Nginx實現ajax
Nginx自己就有根據ip訪問頻率的設置,好比「服務器訪問頻率限制和IP限制」就有提到。不過Nginx只能強硬地返回個403狀態碼什麼的,可是咱們此次ip封禁時間比較久,那麼若是誤傷到用戶,咱們僅僅強硬地返回個403,用戶將會毫無辦法證實本身是人,而後要等好久,那就傷用戶就傷得很深了,所以咱們須要一種可讓被誤傷的用戶能及時自行解封的策略,驗證碼就是一個不錯的選擇,但是Nginx該怎麼接入驗證碼呢?redis
在說明怎麼Nginx接入驗證碼以前,我想先說說驗證碼自己,其實就基礎防禦來講,(封IP+驗證碼)是性價比比較高的通常性基礎組合了,比較低廉的成本就能給爬蟲製造麻煩,基於這種組合就能篩選掉一部分廉價爬蟲
而雖說至今爲止,不少驗證碼都被破解了,甚至連新型的基於行爲的驗證碼(好比極驗的拖條驗證和谷大哥的reCaptcha),都有人提出了破解方案(我今天谷歌一下,竟然不止是方案,已經有兩三頁的教程了,我得找個時間學習一下了)。可是,這種破解方案卻不是誰均可以完美絲滑地應用到本身的爬蟲上,這是須要必定功力的
那麼換個角度思考,咱們在某種程度上已經贏了,畢竟咱們只是調用別人一個接口而已,甚至就算咱們本身DIY一個漢字的圖片驗證碼也不費多大功夫(漢字字符粘連+帶隨機噪點+干擾線並不特別難,實在不懂能夠參考這篇「Python 隨機生成中文驗證碼」就有現成代碼~大概長這樣這裏寫圖片描述),而爬蟲要搞定驗證碼要麼本身花錢第三方識別,要麼就本身的團隊開發識別驗證碼的工具,總之又提升了他們爬取成本,殺敵一千,自傷只有五百
雖然有現成的免費的圖片驗證碼生成程序,可是咱們在這篇博文裏面仍是來點新潮的」基於行爲」的驗證碼吧,好比說極驗,而關於極驗的部署後面仍是會提到,我的以爲他們的官方文檔後端部署的python那部分講的不清不楚,後面得本身測試跑一次才知道怎麼改…
那麼迴歸Nginx接入驗證碼的問題,咱們須要Lua,Lua是一個高性能的腳本語言,我感受和Python很像,可是靈活性比不上Python,而執行速度卻比Python快。Lua和C/C++是很親和的,是補充C/C++靈活性的存在,由於有Lua,只要咱們在C/C++中向外引入Lua腳本,那麼如若Lua腳本發生了修改,咱們也並不須要所以從新編譯一次C/C++程序。Nginx自己即是由C/C++編寫,因此天然和Lua親和,然後又有OpenResty項目的存在(捆綁了nginx和lua並自帶經常使用lua模塊),讓Lua在擴展Nginx上成爲頭號選擇
P.s.補充一點,其實Lua在Nginx的應用只是Lua應用中很小的一個點而已,它在遊戲中才是被普遍地應用,由於:第一,遊戲在意性能體驗,因此不少Engine都是用C/C++寫的,天然須要Lua作一點粘合性補充; 第二,Lua的性能僅僅次於C/C++,並且還有爲了榨乾lua性能的LuaJIT的存在,讓lua的性能獲得進一步地提高,故Lua是C/C++後的第二選擇
OpenResty自己沒有什麼好講的,它最大的功勞就是把Lua比較舒服地捆綁到了Nginx上,其餘特性都是Lua自己的東西,因此想把Nginx玩的更加溜,除了完全玩轉Nginx自己之外(Nginx自己的配置就有點像一門小語言了),Lua會是你不二的選擇。
下載安裝OpenResty
下載安裝能夠直接參考官網的教程(看安裝和新手上路就能夠了,之後有空想稍微深刻一點的,能夠直接看OpenResty最佳實踐)
P.s由於我目前工做的本本是MBP,因此是用homebrew安裝的,感受會和linux裏面的openresty有點不太同樣,osx裏面是用openresty這條命令啓動纔算是openresty,而linux貌似是openresty下的nginx啓動的纔算是openresty,才能用好比access_by_lua_file或者content_by_lua這種openresty語法
我自定義的目錄結構以下:
-anti_spider
-conf/
-nginx.conf
-lua/
-access.lua
-log/
-error.log
-geetest_web/
-demo/
-sdk/
-geetest.py
-setup.py
-requirements.txt
Nginx配置
在openresty下接入Lua腳本就一句話,下面給出nginx.conf示範:
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
server {
listen 80;
location / {
access_by_lua_file 'lua/access.lua';
content_by_lua 'ngx.say("Welcome PENIS!")';
}
}
}
access.lua
-- package.path = '/usr/local/openresty/nginx/lua/?.lua;/usr/local/openresty/nginx/lua/lib/?.lua;'
-- package.cpath = '/usr/local/openresty/nginx/lua/?.so;/usr/local//openresty/nginx/lua/lib/?.so;'
-- 鏈接redis
local redis = require 'resty.redis'
local cache = redis.new()
local ok ,err = cache.connect(cache,'127.0.0.1','6379')
cache:set_timeout(60000)
-- 若是鏈接失敗,跳轉到label處
if not ok then
goto label
end
-- 白名單
is_white ,err = cache:sismember('white_list', ngx.var.remote_addr)
if is_white == 1 then
goto label
end
-- 黑名單
is_black ,err = cache:sismember('black_list', ngx.var.remote_addr)
if is_black == 1 then
ngx.exit(ngx.HTTP_FORBIDDEN)
goto label
end
-- ip訪問頻率時間段
ip_time_out = 60
-- ip訪問頻率計數最大值
connect_count = 45
-- 60s內達到45次就ban
-- 封禁ip時間(加入突曲線增加算法)
ip_ban_time, err = cache:get('ip_ban_time:' .. ngx.var.remote_addr)
if ip_ban_time == ngx.null then
ip_ban_time = 300
res , err = cache:set('ip_ban_time:' .. ngx.var.remote_addr, ip_ban_time)
res , err = cache:expire('ip_ban_time:' .. ngx.var.remote_addr, 43200) -- 12h重置
end
-- 查詢ip是否在封禁時間段內,若在則跳轉到驗證碼頁面
is_ban , err = cache:get('ban:' .. ngx.var.remote_addr)
if tonumber(is_ban) == 1 then
-- source攜帶了以前用戶請求的地址信息,方便驗證成功後返回原用戶請求地址
local source = ngx.encode_base64(ngx.var.scheme .. '://' ..
ngx.var.host .. ':' .. ngx.var.server_port .. ngx.var.request_uri)
local dest = 'http://127.0.0.1:5000/' .. '?continue=' .. source
ngx.redirect(dest,302)
goto label
end
-- ip記錄時間key
start_time , err = cache:get('time:' .. ngx.var.remote_addr)
-- ip計數key
ip_count , err = cache:get('count:' .. ngx.var.remote_addr)
-- 若是ip記錄時間的key不存在或者當前時間減去ip記錄時間大於指定時間間隔,則重置時間key和計數key
-- 若是當前時間減去ip記錄時間小於指定時間間隔,則ip計數+1,
-- 而且ip計數大於指定ip訪問頻率,則設置ip的封禁key爲1,同時設置封禁key的過時時間爲封禁ip時間
if start_time == ngx.null or os.time() - tonumber(start_time) > ip_time_out then
res , err = cache:set('time:' .. ngx.var.remote_addr , os.time())
res , err = cache:set('count:' .. ngx.var.remote_addr , 1)
else
ip_count = ip_count + 1
res , err = cache:incr('count:' .. ngx.var.remote_addr)
-- 統計當日訪問ip集合
res , err = cache:sadd('statistic_total_ip:' .. os.date('%x'), ngx.var.remote_addr)
if ip_count >= connect_count then
res , err = cache:set('ban:' .. ngx.var.remote_addr , 1)
res , err = cache:expire('ban:' .. ngx.var.remote_addr , ip_ban_time)
res , err = cache:incrby('ip_ban_time:' .. ngx.var.remote_addr, ip_ban_time)
-- 統計當日屏蔽ip總數
res , err = cache:sadd('statistic_ban_ip:' .. os.date('%x'), ngx.var.remote_addr)
end
end
::label::
local ok , err = cache:close()
Reference:
1.nginx和lua
2.nginx+lua+redis實現驗證碼防採集
3.Nginx+Lua+Redis訪問頻率控制
啓動/重啓nginx
啓動:
nginx -p pwd
-c conf/nginx.conf
重載:(修改了lua腳本或者nginx.conf配置每次都要重載生效)
nginx -p pwd
-c conf/nginx.conf -s reload
Redis統計數據持久化
Lua腳本里面有statistic_ban_ip和statistic_total_ip兩個統計數據,分別記錄了天天的被屏蔽過的ip數量和總共訪問的ip數量,那麼根據這些數據,咱們就能夠作分析,好比statistic_ban_ip/statistic_total_ip每日被封禁ip佔總ip量的百分比,還有能夠結合百度地圖的ip地理定位作被封ip的定位,看看哪一個地區被封殺最嚴重~ 甚至還能夠之後積累了幾個個月甚至幾年的redis記錄,而後能夠作一份 [月被封ip量 - 月份|年份] 的笛卡爾座標系(Cartesian coordinate system),而後能夠深刻分析一下時間分佈,根據這種分佈,適當地調整一下策略,或者甚至能夠作成智能型的
固然如今已經有不少網站前置統計數據的服務了,好比友盟+什麼的,可是咱們所記錄的這些數據是實實在在咱們本身一每天」熬」出來的數據,留在本地作數據分析用,或者給其餘的什麼需求提供數據支持,這個…誰說的準呢?不過數據就是數據,留下來是對的,咱們的這些留下來的數據也不是什麼垃圾數據,何況,實際工做量也不大(就redis增長兩個字段而已),佔用的空間也不大(就一些短字符串而已)
不過問題是,若是你內存不夠,而redis是內存型的數據庫,加之也沒有必要終年累月都把統計數據堆在redis裏面,因此咱們得有把這些統計數據,或者能夠直接說冷數據持久化到硬盤的定時操做,而至於redis的持久化,這裏留個坑,回頭再來填
極驗
如今來說講統覽圖裏面的Captcha WebApi的構建,在上面Lua的腳本里面有一句跳轉到驗證碼接口的:
local dest = 'http://127.0.0.1:5000/' .. '?continue=' .. source
ngx.redirect(dest,302)
裏面的這個http://127.0.0.1:5000/就是統覽圖裏面的Captcha WebApi開放的驗證碼驗證地址,咱們在這個地址上部署的是極驗的驗證碼服務(並沒有廣告意思,易盾貌似也不錯~),你能夠上他們的官網下載他們的demo,我這裏的以Flask demo爲例:
1.git拉下來
git clone https://github.com/GeeTeam/gt3-python-sdk.git
2.構建geetest
python3 setup.py install
3.找到啓動demo裏面的基於flask寫的web api
#直接python3 start.py是不行滴!你還須要flask,並且由於還要訪問redis,再來個redis
pip3 install Flask
pip3 install redis
python3 start.py
#注意要和start.py以及templates/同一層啓動start.py,否則等下找不到templates/下面的login.html和gt.js
#吐槽一下極驗的後端部署文檔的不完整,我也是本身調試着才知道怎麼回事...
Refer: 極驗文檔
好的,既然能跑了,那麼咱們得怎麼改?要知道他們給的demo是沒有redis訪問的!
1.打開start.py,簡單說明一下:
pc_geetest_id和pc_geetest_key你本身申請換上去吧,不詳細說明了;
get_pc_captcha()這個就是官方文檔那個"嗨複雜的"完整流程圖的第一次網站主的客戶端對網站主的服務器的請求接口;
pc_ajax_validate()這個是二次驗證的,返回的是json格式的;
pc_validate_captcha()和pc_ajax_validate()這個功能同樣,只不過這個是返回html;
statichandler()這個估計是前端的腳本須要訪問的,不用理;
login()這個就不用解釋了;
(login.html的內容其實咱們此次徹底不是作用戶登陸,因此用不到提交用戶名密碼,因此用戶名密碼那塊代碼html表格均可以刪掉了)
2.新增一個redis的操做函數
def handle_passed_ip(remote_ip):
import redis r = redis.Redis(host='127.0.0.1', port=6379, db=0) r.delete('ban:' + str(remote_ip)) r.set('count:' + str(remote_ip), 1) return remote_ip
3.改login()
def login():
import base64
# 即經過base64編碼過的記錄着訪問者訪問的原url信息,方便驗證經過跳轉 former_url = base64.b64decode(request.args.get('continue')) session["former_url"] = former_url return render_template('login.html')
4.改pc_ajax_validate()
def pc_ajax_validate():
gt = GeetestLib(pc_geetest_id, pc_geetest_key)
challenge = request.form[gt.FN_CHALLENGE]
validate = request.form[gt.FN_VALIDATE]
seccode = request.form[gt.FN_SECCODE]
status = session[gt.GT_STATUS_SESSION_KEY]
user_id = session["user_id"]
if status:
result = gt.success_validate(
challenge, validate, seccode, user_id, data='', userinfo='')
else:
result = gt.failback_validate(challenge, validate, seccode)
result = {"status": "success"} if result else {"status": "fail"}
remote_ip = request.remote_addr # 獲取訪問者ip remote_ip = handle_passed_ip(remote_ip) #調用咱們新增的redis操做函數 result.update({"former_url": session["former_url"].decode('utf-8')}) return json.dumps(result)
以上後端就改好了,再啓動start.py,那麼統覽圖裏面的Captcha WebApi的驗證碼驗證服務就起來了~至於前端代碼要怎麼改?對不起,那得你本身看官方文檔研究去,不過我感受,他們的前端文檔寫的比後端文檔好…….