服务器防CC攻击实战应用:精准控制每个IP请求:Nginx与OpenResty的攻防实战
2025/08/24 09:47:13
娌℃湁闃茬嚎锛屽杽鎰忔祦閲忎細娣规病浣狅紱鐩茬洰闄愭祦锛岀湡瀹炵敤鎴峰皢绂讳綘鑰屽幓鈥斺€斾簰鑱旂綉鏈嶅姟鐨勭簿瀵嗗钩琛℃湳
姣忓綋閬囧埌鏈嶅姟鍣ㄧ獊鍙戦珮璐熻浇銆佹伓鎰忕埇铏柉鐙傛姄鍙栥€佹垨绔炰簤鑰呯殑CC鏀诲嚮锛岀簿鍑嗘帶鍒舵瘡涓狪P鐨勮姹傚氨鎴愪簡瀹堟姢鏈嶅姟鐨勬渶鍚庨槻绾裤€
鏈€杩戠爺绌朵簡涓€涓媙ginx鍜孫penResty鐨勯檺閫熻兘鍔涳紝鏈枃鎶婃秹鍙婂埌鐨勪竴浜涙妧鏈師鐞嗗拰閰嶇疆瑕佺偣璁板綍涓嬫潵銆
涓€銆丯ginx鍘熺敓闃叉帶锛氱涓€閬撻槻鐏
鏍稿績妯″潡锛
閰嶇疆绀轰緥锛
# 鍏ㄥ眬閰嶇疆 (http鍧)
limit_req_zone$binary_remote_addr zone=ip_req:10m rate=30r/s;
limit_conn_zone$binary_remote_addr zone=ip_conn:5m;
server {
location / {
# 闄愰€燂細姣忕30璇锋眰+10绐佸彂锛坣odelay绔嬪嵆澶勭悊绐佸彂锛
limit_req zone=ip_req burst=10 nodelay;
# 姣忎釜IP鏈€澶20骞跺彂杩炴帴
limit_conn ip_conn 20;
# 灏佺瓒呴檺IP鏃惰繑鍥429鑰岄潪503
error_page429 = @ratelimit;
}
location@ratelimit {
add_header Retry-After 10;
return429'{"error": "Too Many Requests"}';
}
}
鍏抽敭鍙傛暟璁$畻锛
褰撻渶瑕佸樊寮傚寲绛栫暐銆佽涓哄垎鏋愩€佸姩鎬佸皝绂佹椂锛屽師鐢烴ginx鍔涗笉浠庡績銆傚熀浜嶭ua鐨凮penResty鎻愪緵浜嗘棤闄愬彲鑳斤細
1. 瀹樻柟姝﹀櫒搴擄細lua-resty-limit-traffic
access_by_lua_block {
local lim = require"resty.limit.req".new("ip_req_store", 100, 50)
-- 宸紓鍖栭檺閫燂細VIP鐢ㄦ埛鏀惧闄愬埗
local rate = is_vip(ip) and200or100
lim:set_rate(rate)
if lim:incoming(ip) == "rejected"then
ngx.header["Retry-After"] = 5
return ngx.exit(429)
end
}2. 琛屼负鍒嗘瀽寮曟搸锛氳瘑鍒伓鎰忔ā寮
function detect_abnormal(ip)
local dict = ngx.shared.ip_behavior
local key = ip..":pattern"
-- 缁熻10绉掑唴璇锋眰璺緞鐔靛€
local entropy = calculate_path_entropy(ip)
-- 鐔靛€煎紓甯稿崌楂樺垽瀹氫负鎵弿琛屼负
if entropy > THRESHOLD then
dict:set(key, "scanner", 600)
returntrue
end
end3. 鍒嗗竷寮忓皝绂佺郴缁燂紙Redis鐗堬級
local redis = require "resty.redis"
local red = redis:new()
-- 鑷姩鎷夐粦楂橀鏀诲嚮IP
if req_count(ip) > 1000 then
red:sadd("global:blacklist", ip)
red:expire(ip, 3600)
end
-- 鍏ㄥ眬榛戝悕鍗曟牎楠
if red:sismember("global:blacklist", ip) then
ngx.exit(403)
end涓夈€佸灞傜骇闃插尽鏋舵瀯
鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹
鈹 鍓嶇WAF闃叉姢 鈹 (Cloudflare/AWS WAF)
鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹攢鈹€鈹€鈹€鈹€鈹€鈹€鈹
鈹
鈹屸攢鈹€鈹€鈹€鈹€鈹€鈻尖攢鈹€鈹€鈹€鈹€鈹€鈹 鍔ㄦ€佽鍒
鈹 OpenResty 鈹溾攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹
鈹 娣卞害琛屼负鍒嗘瀽 鈹 鈹
鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹攢鈹€鈹€鈹€鈹€鈹€鈹 鈹
鈹 鈻
鈹屸攢鈹€鈹€鈹€鈹€鈹€鈻尖攢鈹€鈹€鈹€鈹€鈹€鈹 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹
鈹 Nginx 鈹 鈹 Redis闆嗙兢 鈹
鈹 鍩虹闄愭祦 鈹傗梹鈹€鈹€鈹€鈹€鈹€鈻 鍒嗗竷寮忕姸鎬佸悓姝 鈹
鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹攢鈹€鈹€鈹€鈹€鈹€鈹 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹
鈹
鈹屸攢鈹€鈹€鈹€鈹€鈹€鈻尖攢鈹€鈹€鈹€鈹€鈹€鈹
鈹 Fail2ban 鈹 (鏃ュ織鍒嗘瀽鑷姩灏両P)
鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹鍥涖€佸疄鎴樹紭鍖栫瓥鐣
rate = 1000 -- 闈欐€佽祫婧愭斁瀹
elseif uri == "/api/login" then
rate = 2 -- 鐧诲綍鎺ュ彛涓ユ牸闄愬埗
end
local is_cn = geo.lookup(ip).country == "CN"
-- 鍥藉唴IP鏀惧闄愬埗锛堥槻娴峰鏀诲嚮锛
lim:set_rate(is_cn and 200 or 50)
risk_score = model.predict(ip, request_pattern)
lua_rate = 1000 * (1 - risk_score) # 楂橀闄㊣P鑷姩闄嶆潈缁撹锛氱簿搴︿笌鏁堢巼鐨勫钩琛¤壓鏈
娴侀噺鎺у埗濡傚悓璧伴挗涓濓細杩囦弗鍒欎激鍙婄湡瀹炵敤鎴凤紝杩囨澗鍒欐湇鍔″穿婧冦€侽penResty鎻愪緵浜嗗墠鎵€鏈湁鐨勭伒娲绘€э紝浣嗛渶璁颁綇锛
銆愰檮褰曪細閰嶇疆瑕佺偣閫熸煡琛ㄣ€
姣忓綋閬囧埌鏈嶅姟鍣ㄧ獊鍙戦珮璐熻浇銆佹伓鎰忕埇铏柉鐙傛姄鍙栥€佹垨绔炰簤鑰呯殑CC鏀诲嚮锛岀簿鍑嗘帶鍒舵瘡涓狪P鐨勮姹傚氨鎴愪簡瀹堟姢鏈嶅姟鐨勬渶鍚庨槻绾裤€
鏈€杩戠爺绌朵簡涓€涓媙ginx鍜孫penResty鐨勯檺閫熻兘鍔涳紝鏈枃鎶婃秹鍙婂埌鐨勪竴浜涙妧鏈師鐞嗗拰閰嶇疆瑕佺偣璁板綍涓嬫潵銆
涓€銆丯ginx鍘熺敓闃叉帶锛氱涓€閬撻槻鐏
鏍稿績妯″潡锛
鈥 limit_req_zone锛氳姹傞€熺巼闄愬埗锛堟紡妗剁畻娉曪級鈥 limit_conn_zone锛氬苟鍙戣繛鎺ユ帶鍒
閰嶇疆绀轰緥锛
# 鍏ㄥ眬閰嶇疆 (http鍧)
limit_req_zone$binary_remote_addr zone=ip_req:10m rate=30r/s;
limit_conn_zone$binary_remote_addr zone=ip_conn:5m;
server {
location / {
# 闄愰€燂細姣忕30璇锋眰+10绐佸彂锛坣odelay绔嬪嵆澶勭悊绐佸彂锛
limit_req zone=ip_req burst=10 nodelay;
# 姣忎釜IP鏈€澶20骞跺彂杩炴帴
limit_conn ip_conn 20;
# 灏佺瓒呴檺IP鏃惰繑鍥429鑰岄潪503
error_page429 = @ratelimit;
}
location@ratelimit {
add_header Retry-After 10;
return429'{"error": "Too Many Requests"}';
}
}
鍏抽敭鍙傛暟璁$畻锛
鈥 鍏变韩鍐呭瓨澶у皬鍏紡锛1MB 鈮 16,000涓狪P鐘舵€佲€ 绐佸彂鍊煎缓璁細burst = 姝e父宄板€兼祦閲 脳 1.5
褰撻渶瑕佸樊寮傚寲绛栫暐銆佽涓哄垎鏋愩€佸姩鎬佸皝绂佹椂锛屽師鐢烴ginx鍔涗笉浠庡績銆傚熀浜嶭ua鐨凮penResty鎻愪緵浜嗘棤闄愬彲鑳斤細
1. 瀹樻柟姝﹀櫒搴擄細lua-resty-limit-traffic
access_by_lua_block {
local lim = require"resty.limit.req".new("ip_req_store", 100, 50)
-- 宸紓鍖栭檺閫燂細VIP鐢ㄦ埛鏀惧闄愬埗
local rate = is_vip(ip) and200or100
lim:set_rate(rate)
if lim:incoming(ip) == "rejected"then
ngx.header["Retry-After"] = 5
return ngx.exit(429)
end
}2. 琛屼负鍒嗘瀽寮曟搸锛氳瘑鍒伓鎰忔ā寮
function detect_abnormal(ip)
local dict = ngx.shared.ip_behavior
local key = ip..":pattern"
-- 缁熻10绉掑唴璇锋眰璺緞鐔靛€
local entropy = calculate_path_entropy(ip)
-- 鐔靛€煎紓甯稿崌楂樺垽瀹氫负鎵弿琛屼负
if entropy > THRESHOLD then
dict:set(key, "scanner", 600)
returntrue
end
end3. 鍒嗗竷寮忓皝绂佺郴缁燂紙Redis鐗堬級
local redis = require "resty.redis"
local red = redis:new()
-- 鑷姩鎷夐粦楂橀鏀诲嚮IP
if req_count(ip) > 1000 then
red:sadd("global:blacklist", ip)
red:expire(ip, 3600)
end
-- 鍏ㄥ眬榛戝悕鍗曟牎楠
if red:sismember("global:blacklist", ip) then
ngx.exit(403)
end涓夈€佸灞傜骇闃插尽鏋舵瀯
鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹
鈹 鍓嶇WAF闃叉姢 鈹 (Cloudflare/AWS WAF)
鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹攢鈹€鈹€鈹€鈹€鈹€鈹€鈹
鈹
鈹屸攢鈹€鈹€鈹€鈹€鈹€鈻尖攢鈹€鈹€鈹€鈹€鈹€鈹 鍔ㄦ€佽鍒
鈹 OpenResty 鈹溾攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹
鈹 娣卞害琛屼负鍒嗘瀽 鈹 鈹
鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹攢鈹€鈹€鈹€鈹€鈹€鈹 鈹
鈹 鈻
鈹屸攢鈹€鈹€鈹€鈹€鈹€鈻尖攢鈹€鈹€鈹€鈹€鈹€鈹 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹
鈹 Nginx 鈹 鈹 Redis闆嗙兢 鈹
鈹 鍩虹闄愭祦 鈹傗梹鈹€鈹€鈹€鈹€鈹€鈻 鍒嗗竷寮忕姸鎬佸悓姝 鈹
鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹攢鈹€鈹€鈹€鈹€鈹€鈹 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹
鈹
鈹屸攢鈹€鈹€鈹€鈹€鈹€鈻尖攢鈹€鈹€鈹€鈹€鈹€鈹
鈹 Fail2ban 鈹 (鏃ュ織鍒嗘瀽鑷姩灏両P)
鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹鍥涖€佸疄鎴樹紭鍖栫瓥鐣
1. 鍔ㄩ潤鍒嗙闄愭祦
rate = 1000 -- 闈欐€佽祫婧愭斁瀹
elseif uri == "/api/login" then
rate = 2 -- 鐧诲綍鎺ュ彛涓ユ牸闄愬埗
end
2. 鍖哄煙鎬х瓥鐣ュ畾鍒
local is_cn = geo.lookup(ip).country == "CN"
-- 鍥藉唴IP鏀惧闄愬埗锛堥槻娴峰鏀诲嚮锛
lim:set_rate(is_cn and 200 or 50)
3. AI鍔ㄦ€佹潈閲嶈皟鏁
risk_score = model.predict(ip, request_pattern)
lua_rate = 1000 * (1 - risk_score) # 楂橀闄㊣P鑷姩闄嶆潈缁撹锛氱簿搴︿笌鏁堢巼鐨勫钩琛¤壓鏈
娴侀噺鎺у埗濡傚悓璧伴挗涓濓細杩囦弗鍒欎激鍙婄湡瀹炵敤鎴凤紝杩囨澗鍒欐湇鍔″穿婧冦€侽penResty鎻愪緵浜嗗墠鎵€鏈湁鐨勭伒娲绘€э紝浣嗛渶璁颁綇锛
1. 濮嬬粓鐩戞帶鏁堟灉锛氬彲浠ヤ娇鐢≒rometheus+Granfa瀹炴椂瑙傚療闄愭祦褰卞搷2. 娓愯繘寮忓疄鏂斤細鍏堣瀵熷熀绾挎祦閲忥紝鍐嶈缃槇鍊3. 淇濈暀閫冪敓閫氶亾锛氬姩鎬侀厤缃腑蹇冩敮鎸佺绾у洖婊
銆愰檮褰曪細閰嶇疆瑕佺偣閫熸煡琛ㄣ€
| 鍦烘櫙 | Nginx鍘熺敓鏂规 | OpenResty澧炲己鏂规 |
| 鍩虹閫熺巼闄愬埗 | limit_req_zone | lua-resty-limit-traffic |
| 璇锋眰鐗瑰緛鍒嗘瀽 | 鉂 | Lua瀹炴椂璁$畻璇锋眰鐔靛€ |
| 鍒嗗竷寮忕姸鎬佸悓姝 | 鉂 | Redis鍏变韩闆嗙兢鐘舵€ |
| 鍔ㄦ€佸皝绂 | 鎵嬪姩Fail2ban | 鑷姩瀹炴椂榛戝悕鍗 |
| 鎱㈤€熸敾鍑婚槻鎶 | client_body_timeout | 姣绾ц姹傝秴鏃舵帶鍒 |