高頻訪問IP限制 --Openresty(nginx + lua) [反爬蟲之旅]

嗯….本人是從寫爬蟲開始編程的,不過後面做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***啊),雖然沒有多大的作用,但是起碼比裸奔強!這也算是功能上的複用吧

反爬蟲之旅預告:

過頻IP彈驗證碼[應用外]
數據接口的url設計(uuid)和內容橫向範圍限制(參考angel.co)[應用內]
用戶可見(參考微博)以及內容縱向切割(盈利點思考)[應用內]
統覽
這裏寫圖片描述

高頻訪問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/
-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):

處理驗證通過的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的驗證碼驗證服務就起來了~至於前端代碼要怎麼改?對不起,那得你自己看官方文檔研究去,不過我感覺,他們的前端文檔寫的比後端文檔好…….

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章