高頻訪問IP限制 --Openresty(nginx + lua) [反爬蟲之旅][轉]
轉自[https://www.aliyun.com/jiaocheng/123498.html]
- 摘要:前言嗯….本人是從寫爬蟲開始程式設計的,不過後面做web寫網站去了,好了,最近web要搞反爬蟲了,哈哈哈,總算有機會把之以前做爬蟲時候見識過的反爬一點點給現在的網站用上了~做爬蟲的同志,有怪莫怪嘍~還有求別打死><首先要提一下AJAX,現在普天下網頁幾乎都是往特定的資料介面請求資料了,除了什麼首屏渲染這種服務端渲染好html以外,幾乎沒有什麼靜態網頁了。我看了有一些帖子說AJAX讓爬蟲難做,可是我覺得結合一些工具(比如chrome的開發者工具),找到AJAX所請求的後
前言嗯….本人是從寫爬蟲開始程式設計的,不過後面做web寫網站去了,好了,最近web要搞反爬蟲了,哈哈哈,總算有機會把之以前做爬蟲時候見識過的反爬一點點給現在的網站用上了~ 做爬蟲的同志,有怪莫怪嘍~還有求別打死 > <
首先要提一下AJAX,現在普天下網頁幾乎都是往特定的資料介面請求資料了,除了什麼首屏渲染這種服務端渲染好html以外,幾乎沒有什麼靜態網頁了。我看了有一些帖子說AJAX讓爬蟲難做,可是我覺得結合一些工具(比如chrome的開發者工具),找到AJAX所請求的後端資料介面一點也不難,而且現在自己也寫過一段時間的web後端資料介面,發現介面的設計往往都是往簡單易懂的方向做,外加從2000年出現REST風格,更是讓介面設計越來越簡明瞭。所以其實如果一個web站點沒有察覺到有爬蟲的存在,或者察覺到了,但是沒有想要做一點資料保護措施,它是不會再AJAX上做文章的,那麼如果單純的AJAX,其實並沒有任何反爬的作用,所以別再說AJAX反爬什麼的了,何況AJAX生出來就不是為了反爬的
然而在現在的前後端分離的時代,前端反爬還是有的搞的,基於我不太懂JavaScript,就不展開來說,我只是聽說過什麼引數加密啊,資料混淆什麼的,但其實概括起來都是一種對資料介面的隱藏,這讓一些不太懂js的人,也跟著懵逼了(比如說我 : <),但是你要知道,前端程式碼最終還是要請求一個url的,無論它把這個過程拆開成多散,弄得多複雜都好,只要是需要資料,就必然需要請求一個後端介面(這個介面可以是SOAP,不過21世紀恐怕更多的是RESTful的),所以對於資料保護而言,更加需要重點關注的是後端資料介面的保護。
本反爬蟲之旅系列將會一點點從各個方面壘高資料保護牆,但是請記住,因為網站資料的公開性,所以,只是延緩被盜庫的時間而已,想自己在網站上公開的資料完全不被爬走是不可能的。那麼我們的目標就是:讓盜庫耗時被延緩到一個比較長的時間裡面,那麼對於爬取資料方而言,這些資料的價值將會隨著時間的增加而降低,資料的價值=利用價值 - (爬取成本+資料貶值速度) * 爬取時間(不用糾結來源了,我說的)
這一篇就講最基礎的“給過頻IP彈驗證碼”這種入門級防護實現,雖然花錢買點代理IP就可以搞定這種實現,但是至少也讓他們增加了成本,但是我們相對地並沒有花費多少成本,而且過頻IP彈驗證碼除了能反爬,也能抵禦一部分的CC攻擊(短時間大量的爬蟲請求堪比CC攻擊啊),雖然沒有多大的作用,但是起碼比裸奔強!這也算是功能上的複用吧
反爬蟲之旅預告:
1. 過頻IP彈驗證碼[應用外]
2. 資料介面的url設計(uuid)和內容橫向範圍限制(參考angel.co)[應用內]
3. 使用者可見(參考微博)以及內容縱向切割(盈利點思考)[應用內]
統覽 高頻訪問IP彈驗證碼架構圖
P.s. csdn預設水印real醜,直接去掉圖片地址的watermark就可以了
OpenResty我不準備在web應用中做ip的統計和查封,應用就應該只做業務功能,這些基礎東西應該由我們應用的前部——專業的Nginx實現
Nginx本身就有根據ip訪問頻率的設定,比如“伺服器訪問頻率限制和IP限制”就有提到。不過Nginx只能強硬地返回個403狀態碼什麼的,但是我們這次ip封禁時間比較久,那麼如果誤傷到使用者,我們僅僅強硬地返回個403,使用者將會毫無辦法證明自己是人,然後要等很久,那就傷使用者就傷得很深了,因此我們需要一種可以讓被誤傷的使用者能及時自行解封的策略,驗證碼就是一個不錯的選擇,可是nginx該怎麼接入驗證碼呢?
在說明怎麼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/
Nginx配置在openresty下接入Lua指令碼就一句話,下面給出nginx.conf示範:
worker_processes1;
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):
# 處理驗證通過的ip,注意host,port還有db要和你lua訪問的一致!!!
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
# 拿到之前lua跳轉過來攜帶的continue引數
# 即通過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的驗證碼驗證服務就起來了~至於前端程式碼要怎麼改?對不起,那得你自己看官方文件研究去,不過我感覺,他們的前端文件寫的比後端文件好…….
- 以上是高頻訪問IP限制 --Openresty(nginx + lua) [反爬蟲之旅]的內容,更多 爬蟲 高頻 openresty 限制 之旅 訪問 nginx lua 的內容,請您使用右上方搜尋功能獲取相關資訊。