在互联网业务快速发展的今天,网站面临着各种流量冲击和恶意爬虫的威胁。作为运维工程师,我们需要在保证正常用户访问的同时,有效防范恶意流量和爬虫攻击。本文将深入探讨基于Nginx的限流与防爬虫解决方案,从原理到实践,为大家提供一套完整的防护体系。
在实际运维工作中,我们经常遇到以下问题:
选择Nginx作为限流和防爬虫的核心组件具有以下优势:
Nginx的ngx_http_limit_req_module
模块基于令牌桶算法实现限流。该算法的核心思想是:
令牌桶示意图:┌─────────────┐│ Token Bucket │ ←── 恒定速率添加令牌│ ○ ○ ○ ○ ○ ││ ○ ○ ○ │└─────────────┘ ↓ 用户请求消耗令牌
漏桶算法是另一种流控机制,特点是输出速率恒定:
首先配置最常用的IP限流功能:
http {
# 定义限流区域,基于客户端IP
limit_req_zone $binary_remote_addr zone=ip_limit:10m rate=10r/s;
# 定义连接数限制区域
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
server {
listen 80;
server_name example.com;
location / {
# 应用IP限流:每秒最多10个请求,突发允许5个
limit_req zone=ip_limit burst=5 nodelay;
# 限制单IP最大连接数为10
limit_conn conn_limit 10;
# 自定义限流响应
limit_req_status 429;
limit_conn_status 429;
proxy_pass http://backend;
}
# 限流错误页面
error_page 429 /429.html;
location = /429.html {
root /var/www/html;
internal;
}
}
}
配置说明:
$binary_remote_addr
:使用二进制格式的客户端IP,节省内存zone=ip_limit:10m
:定义10MB内存用于存储限流状态rate=10r/s
:限制每秒10个请求burst=5
:允许突发5个请求nodelay
:超出限制立即返回错误,不排队等待对不同接口应用不同的限流策略:
http {
# API接口限流
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s;
# 静态资源限流
limit_req_zone $binary_remote_addr zone=static_limit:10m rate=50r/s;
# 登录接口严格限流
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=1r/s;
server {
listen 80;
server_name api.example.com;
# API接口限流
location /api/ {
limit_req zone=api_limit burst=2 nodelay;
proxy_pass http://api_backend;
}
# 静态资源限流
location ~* \.(jpg|jpeg|png|gif|css|js)$ {
limit_req zone=static_limit burst=20;
expires 1d;
add_header Cache-Control "public, immutable";
}
# 登录接口特殊保护
location /api/login {
limit_req zone=login_limit burst=1;
# 记录限流日志
access_log /var/log/nginx/login_limit.log combined;
proxy_pass http://auth_backend;
}
}
}
结合GeoIP2模块实现地理位置限流:
http {
# 加载GeoIP2数据库
geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
auto_reload 5m;
$geoip2_metadata_country_build metadata build_epoch;
$geoip2_data_country_code country iso_code;
$geoip2_data_country_name country names en;
}
# 定义不同地区的限流策略
map $geoip2_data_country_code $country_limit_rate {
default 10r/s;
CN 20r/s; # 中国用户更高限制
US 15r/s; # 美国用户
~^(RU|UA)$ 5r/s; # 俄罗斯、乌克兰严格限制
}
# 基于国家的限流区域
limit_req_zone $binary_remote_addr zone=country_limit:10m rate=$country_limit_rate;
server {
listen 80;
server_name global.example.com;
location / {
# 应用地理位置限流
limit_req zone=country_limit burst=5;
# 添加地理信息到响应头(调试用)
add_header X-Country-Code $geoip2_data_country_code;
add_header X-Country-Name $geoip2_data_country_name;
proxy_pass http://backend;
}
}
}
通过分析User-Agent字段识别爬虫:
http {
# 定义恶意爬虫User-Agent模式
map $http_user_agent $is_crawler {
default 0;
# 常见爬虫标识
~*bot 1;
~*spider 1;
~*crawler 1;
~*scraper 1;
# 具体爬虫工具
~*python-requests 1;
~*curl 1;
~*wget 1;
~*scrapy 1;
~*beautifulsoup 1;
# 可疑的空或简短UA
"" 1;
~^.{0,10}$ 1;
}
# 白名单:允许的爬虫
map $http_user_agent $allowed_crawler {
default 0;
~*googlebot 1;
~*bingbot 1;
~*baiduspider 1;
~*slurp 1; # Yahoo
}
server {
listen 80;
server_name example.com;
location / {
# 阻止恶意爬虫(除非在白名单中)
if ($is_crawler) {
set $block_crawler 1;
}
if ($allowed_crawler) {
set $block_crawler 0;
}
if ($block_crawler) {
return 403;
}
proxy_pass http://backend;
}
# 为搜索引擎爬虫提供特殊处理
location /robots.txt {
root /var/www/html;
add_header Cache-Control "public, max-age=3600";
}
}
}
分析请求模式识别自动化工具:
http {
# 检测请求频率异常
limit_req_zone $binary_remote_addr zone=freq_check:10m rate=30r/s;
# 检测无Referer请求
map $http_referer $suspicious_referer {
default 0;
"" 1; # 无Referer
"-" 1; # 明确设置为-
}
# 检测异常请求头组合
map "$http_accept:$http_accept_language:$http_accept_encoding" $suspicious_headers {
default 0;
":::" 1; # 全部为空
~^[^:]*:[^:]*:$ 1; # Accept-Encoding为空
}
server {
listen 80;
server_name example.com;
location / {
# 记录可疑请求
set $risk_score 0;
if ($suspicious_referer) {
set $risk_score "${risk_score}1";
}
if ($suspicious_headers) {
set $risk_score "${risk_score}1";
}
# 高风险请求特殊处理
if ($risk_score ~ "11") {
access_log /var/log/nginx/suspicious.log combined;
limit_req zone=freq_check burst=1 nodelay;
}
proxy_pass http://backend;
}
}
}
通过JavaScript挑战验证真实用户:
http {
# Lua脚本配置(需要安装lua-resty-template)
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
# 挑战验证状态存储
lua_shared_dict challenge_cache 10m;
server {
listen 80;
server_name secure.example.com;
location /challenge {
content_by_lua_block {
local template = require "resty.template"
-- 生成随机挑战
local challenge = ngx.var.request_time .. ngx.var.remote_addr
local hash = ngx.encode_base64(ngx.hmac_sha1("secret_key", challenge))
-- 挑战页面HTML
local html = [[<!DOCTYPE html><html><head>
<title>Verification Required</title>
<meta name="robots" content="noindex, nofollow"></head><body>
<h1>Verifying your browser...</h1>
<script>
// 简单的计算挑战
var result = Math.pow(2, 3) + 5;
var challenge = "{{challenge}}";
// 自动提交
setTimeout(function() {
var form = document.createElement('form');
form.method = 'POST';
form.action = '/verify';
var challengeInput = document.createElement('input');
challengeInput.type = 'hidden';
challengeInput.name = 'challenge';
challengeInput.value = challenge;
var answerInput = document.createElement('input');
answerInput.type = 'hidden';
answerInput.name = 'answer';
answerInput.value = result;
form.appendChild(challengeInput);
form.appendChild(answerInput);
document.body.appendChild(form);
form.submit();
}, 2000);
</script></body></html>
]]
ngx.say(template.compile(html)({challenge = hash}))
}
}
location /verify {
content_by_lua_block {
if ngx.var.request_method ~= "POST" then
ngx.status = 405
ngx.say("Method not allowed")
return
end
-- 验证挑战答案
ngx.req.read_body()
local args = ngx.req.get_post_args()
if args.answer == "13" then -- 2^3 + 5 = 13
-- 设置验证通过标记
local cache = ngx.shared.challenge_cache
cache:set(ngx.var.remote_addr, "verified", 3600) -- 1小时有效
ngx.redirect("/")
else
ngx.status = 403
ngx.say("Verification failed")
end
}
}
location / {
access_by_lua_block {
local cache = ngx.shared.challenge_cache
local verified = cache:get(ngx.var.remote_addr)
if not verified then
ngx.redirect("/challenge")
end
}
proxy_pass http://backend;
}
}
}
建立完整的监控体系:
http {
# 日志格式定义
log_format security_log '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'$request_time $upstream_response_time '
'$geoip2_data_country_code';
# 实时统计
vhost_traffic_status_zone;
server {
listen 80;
server_name monitor.example.com;
location / {
access_log /var/log/nginx/security.log security_log;
# 统计限流事件
if ($limit_req_status = "503") {
access_log /var/log/nginx/rate_limit.log security_log;
}
proxy_pass http://backend;
}
# 监控面板
location /nginx_status {
vhost_traffic_status_display;
vhost_traffic_status_display_format html;
# 限制访问
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
}
}
}
基于日志分析自动更新黑名单:
#!/bin/bash
# auto_blacklist.sh - 自动黑名单脚本
LOG_FILE="/var/log/nginx/security.log"
BLACKLIST_FILE="/etc/nginx/conf.d/blacklist.conf"
TEMP_FILE="/tmp/nginx_blacklist.tmp"
# 分析日志,提取高频访问IP
awk -v date="$(date '+%d/%b/%Y:%H')" '$0 ~ date {
# 提取IP地址
ip = $1
# 统计各种可疑行为
if ($9 == "429" || $9 == "403") suspicious[ip]++
if ($10 > 10000) large_response[ip]++ # 大响应
if ($11 < 0.001) fast_request[ip]++ # 请求过快
total[ip]++
}
END {
for (ip in suspicious) {
if (suspicious[ip] > 100 || large_response[ip] > 50) {
print "deny " ip ";"
}
}
}' $LOG_FILE > $TEMP_FILE
# 更新黑名单文件
if [ -s $TEMP_FILE ]; then
echo "# Auto-generated blacklist - $(date)" > $BLACKLIST_FILE
cat $TEMP_FILE >> $BLACKLIST_FILE
# 重载Nginx配置
nginx -t && nginx -s reload
echo "Blacklist updated with $(wc -l < $TEMP_FILE) entries"
fi
rm -f $TEMP_FILE
合理配置内存使用:
http {
# 优化限流内存使用
limit_req_zone $binary_remote_addr zone=main_limit:50m rate=10r/s;
# 使用更精确的键值以节省内存
map $request_uri $normalized_uri {
~^/api/v1/([^/]+) /api/v1/$1;
~^/static/ /static;
default $request_uri;
}
limit_req_zone "$binary_remote_addr:$normalized_uri"
zone=uri_limit:30m rate=20r/s;
server {
# 配置缓存以减少重复计算
location / {
# 缓存限流状态
limit_req zone=main_limit burst=10;
limit_req zone=uri_limit burst=5;
proxy_pass http://backend;
# 缓存后端响应
proxy_cache my_cache;
proxy_cache_valid 200 1m;
proxy_cache_key "$scheme$proxy_host$normalized_uri";
}
}
}
将配置拆分为可复用的模块:
# /etc/nginx/conf.d/rate_limits.conf
# 基础限流配置
limit_req_zone $binary_remote_addr zone=global_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=1r/s;
# /etc/nginx/conf.d/security_maps.conf
# 安全检测映射
map $http_user_agent $is_malicious_bot {
include /etc/nginx/maps/malicious_bots.map;
}
map $geoip2_data_country_code $is_blocked_country {
include /etc/nginx/maps/blocked_countries.map;
}
# /etc/nginx/conf.d/security_headers.conf
# 安全