低成本CC防护:我的实战配置与效果
喜报:被大规模CC了!
平常的一天,突然受到了来自衣食父母华为云的服务器告警:虚拟机网络连接数突破15400个,CPU占用超过 85% 。就知道肯定是被CC了。果然网站被打是催更技术类博主最好的方式(雾
不过我之前是设置过缓存的,有点好奇怎么绕过缓存,事后我抓取了对应时间的Nginx日志access.log:
43.152.29.37 - - [30/Oct/2025:23:27:16 +0800] "GET /3bb6bfea9/90a2c9c9fc?limit=ef616a8b1b145df&token=176fe0f01f&utm_source=a5d4f247077b HTTP/1.0" 404 29335 "http://www.nekopara.uk/3bb6bfea9/90a2c9c9fc?limit=ef616a8b1b145df&token=176fe0f01f&utm_source=a5d4f247077b" "Mozilla/5.0 (Windows NT 11.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0" "91.209.77.93, 43.152.29.37"
43.152.24.20 - - [30/Oct/2025:23:27:16 +0800] "GET /profile/config/delete?search=9844b7e151&version=504397e6490c&utm_campaign=bd5783ef290&page=4b9e8cc670d HTTP/1.0" 404 29335 "http://www.nekopara.uk/profile/config/delete?search=9844b7e151&version=504397e6490c&utm_campaign=bd5783ef290&page=4b9e8cc670d" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36" "138.2.109.39, 43.152.24.20"
43.152.143.43 - - [30/Oct/2025:23:27:16 +0800] "GET /health/admin?utm_medium=eb37025 HTTP/1.0" 404 29335 "http://www.nekopara.uk/health/admin?utm_medium=eb37025" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" "163.61.245.58, 43.152.143.43"
43.174.55.12 - - [30/Oct/2025:23:27:16 +0800] "GET /5de3c/0727afb/info?type=f06d22885b8 HTTP/1.0" 404 29335 "http://www.nekopara.uk/5de3c/0727afb/info?type=f06d22885b8" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15" "103.184.54.67, 43.174.55.12"
43.152.14.36 - - [30/Oct/2025:23:27:16 +0800] "GET /edit/a97b/cba81414/content.htm?offset=09cd68b44dd60&key=030f41284 HTTP/1.0" 404 29335 "http://www.nekopara.uk/edit/a97b/cba81414/content.htm?offset=09cd68b44dd60&key=030f41284" "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36" "49.145.113.40, 43.152.14.36"
43.152.14.27 - - [30/Oct/2025:23:27:16 +0800] "GET /status/356037c/list/app.json?id=8ccefcb24&key=257c67462c3ef80&token=45b973f&lang=373143c9654e06f HTTP/1.0" 404 29335 "http://www.nekopara.uk/status/356037c/list/app.json?id=8ccefcb24&key=257c67462c3ef80&token=45b973f&lang=373143c9654e06f" "Mozilla/5.0 (Windows NT 11.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0" "138.2.109.39, 43.152.14.27"
43.152.24.20 - - [30/Oct/2025:23:27:16 +0800] "GET /upload/view/detail?utm_source=0a511c HTTP/1.0" 404 29335 "http://www.nekopara.uk/upload/view/detail?utm_source=0a511c" "Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Mobile Safari/537.36" "119.93.135.193, 43.152.24.20"
101.33.20.48 - - [30/Oct/2025:23:27:16 +0800] "GET /05ad3/list/view/data.txt?timestamp=7cf23ee09d6dc HTTP/1.0" 404 29335 "http://www.nekopara.uk/05ad3/list/view/data.txt?timestamp=7cf23ee09d6dc" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15" "187.49.87.255, 101.33.20.48"
43.175.30.25 - - [30/Oct/2025:23:27:16 +0800] "GET /profile/4e95d5?limit=b15210f1d9b08&token=63e194b&type=bfea84c37a29f8&nonce=b222a4b1b38a HTTP/1.0" 404 29335 "http://www.nekopara.uk/profile/4e95d5?limit=b15210f1d9b08&token=63e194b&type=bfea84c37a29f8&nonce=b222a4b1b38a" "Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36" "206.84.201.101, 43.175.30.25"
43.152.29.37 - - [30/Oct/2025:23:27:16 +0800] "GET /8ac4af716/info.php?ref=b09658abd0&page=3d48aaa2614300&page=5b84ad8c3&token=717bad8cac HTTP/1.0" 404 16 "http://www.nekopara.uk/8ac4af716/info.php?ref=b09658abd0&page=3d48aaa2614300&page=5b84ad8c3&token=717bad8cac" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36" "41.139.226.11, 43.152.29.37"
43.152.26.48 - - [30/Oct/2025:23:27:16 +0800] "GET /info?offset=34297ebb3908dec×tamp=91d8330f0f6330&sort=8d7f7fe923&category=9f7158e6486f01f HTTP/1.0" 404 29335 "http://www.nekopara.uk/info?offset=34297ebb3908dec×tamp=91d8330f0f6330&sort=8d7f7fe923&category=9f7158e6486f01f" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0" "2.139.62.85, 43.152.26.48"
43.152.143.41 - - [30/Oct/2025:23:27:16 +0800] "GET /6cc1/download?category=248162dd8&token=fc785d9170235a&id=e6c8ef9&limit=b852d3ab3d28 HTTP/1.0" 404 29335 "http://www.nekopara.uk/6cc1/download?category=248162dd8&token=fc785d9170235a&id=e6c8ef9&limit=b852d3ab3d28" "Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Mobile/15E148 Safari/604.1" "203.76.222.119, 43.152.143.41"
好家伙,随机请求URL,每一次都不一样,难怪能MISS,CPU直接被干爆了!
而且,我还惊奇的发现,记录下来的都是EdgeOne的IP,这也暴露了之前设置,传递来源IP没做好。
既然发现了弱点和问题,那我们就来解决它!
修复来源IP传递异常
首先,这个问题出现在同VPC的跳板服务器上,之前只是添加了请求头,但是没有信任上一级也就是来源于CDN的IP的X-Forwarded-For请求头。并且还重复添加了请求头导致最终源服务器上呈现的是两个不一样的X-Forwarded-For,第一个是真实的IP,第二个是CDN的IP。
修复方法很简单,不在跳板服务器添加请求头,并且信任CDN的X-Forwarded-For请求头就好了,直接传递过去。
下面是配置文件:
server {
listen 443 ssl;
server_name www.nekopara.uk;
set_real_ip_from 0.0.0.0/0;
real_ip_header X-Forwarded-For;
ssl_certificate /data/certs/nekopara_uk_cf.pem;
ssl_certificate_key /data/certs/nekopara_uk_cf.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
location / {
client_max_body_size 32m;
proxy_pass https://www.nekopara.uk:443;
proxy_set_header X-Forwarded-Proto $scheme;
}注意!我这里设置信任整个IP段的前提是,我已经在安全组设置了访问白名单,仅CDN的IP可以访问443和80端口,所以这样子设置是安全的。如果你没有设置访问白名单,这样子会有较大的安全风险!你应该在这部分逐个加入CDN的IP进行过滤,并且考虑屏蔽其他IP的请求。
能够正常传递来源IP是后面进行设置的基础,如果没有正常传递来源IP,源站的Nginx配置规则会屏蔽掉CDN的访问导致访问全部被拒绝!一定要注意!
为源站设置请求限流
因为大量这种随机请求的资源消耗是相当可观的,我们需要进行严格的限制。
第一次尝试的解决方案
我第一次考虑的做法是考虑在Nginx的配置文件中添加limit_req_zone $binary_remote_addr zone=dynamic_limit:10m rate=1r/s;和limit_req zone=dynamic_limit burst=2 nodelay;进行限制,普通用户正常访问不以极高的手速快速刷新页面的话基本上不会被阻止。
同时,使用limit_req_zone $binary_remote_addr zone=static_limit:10m rate=50r/s;和limit_req zone=static_limit burst=50 nodelay;对于静态的文件进行独立限流,因为访问网站会产生多个静态资源文件请求,限流规则应该设置得更为宽松,避免误伤正常用户。
还有就是,如果设置了限流,默认Nginx会返回503错误代码并显示:503 Service Temporarily Unavailable。我认为不够友好,于是定义将错误返回为429,也就是Too Many Requests,更好的传达实际情况,并且自定义了429界面。请注意,返回的429界面需要尽可能的小和简单,传达清楚意思即可。如果太过于华丽,文件肯定会比较大,会占用较大的服务器上行带宽。
以下是详细配置文件:
主配置文件nginx.conf:
# /etc/nginx/nginx.conf
user nginx;
# Set number of worker processes automatically based on number of CPU cores.
worker_processes auto;
# Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on;
# Configures default error logger.
error_log /var/log/nginx/error.log warn;
# Includes files with directives to load dynamic modules.
include /etc/nginx/modules/*.conf;
# Include files with config snippets into the root context.
include /etc/nginx/conf.d/*.conf;
events {
# The maximum number of simultaneous connections that can be opened by
# a worker process.
worker_connections 1024;
}
http {
# 动态页面限流配置(严格)
limit_req_zone $binary_remote_addr zone=dynamic_limit:10m rate=1r/s;
# 静态资源限流配置(宽松)
limit_req_zone $binary_remote_addr zone=static_limit:10m rate=50r/s;
# 定义 FastCGI 缓存路径
fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=my_fastcgi_cache:16m inactive=60m;
# 定义缓存键,通常包含请求方法、主机名和 URI
fastcgi_cache_key "$scheme$request_method$host$request_uri";
# Includes mapping of file name extensions to MIME types of responses
# and defines the default type.
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Name servers used to resolve names of upstream servers into addresses.
# It's also needed when using tcpsocket and udpsocket in Lua modules.
#resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001];
# Don't tell nginx version to the clients. Default is 'on'.
server_tokens off;
# Specifies the maximum accepted body size of a client request, as
# indicated by the request header Content-Length. If the stated content
# length is greater than this size, then the client receives the HTTP
# error code 413. Set to 0 to disable. Default is '1m'.
client_max_body_size 1m;
# Sendfile copies data between one FD and other from within the kernel,
# which is more efficient than read() + write(). Default is off.
sendfile on;
# Causes nginx to attempt to send its HTTP response head in one packet,
# instead of using partial frames. Default is 'off'.
tcp_nopush on;
# Enables the specified protocols. Default is TLSv1 TLSv1.1 TLSv1.2.
# TIP: If you're not obligated to support ancient clients, remove TLSv1.1.
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
# Path of the file with Diffie-Hellman parameters for EDH ciphers.
# TIP: Generate with: `openssl dhparam -out /etc/ssl/nginx/dh2048.pem 2048`
#ssl_dhparam /etc/ssl/nginx/dh2048.pem;
# Specifies that our cipher suits should be preferred over client ciphers.
# Default is 'off'.
ssl_prefer_server_ciphers on;
# Enables a shared SSL cache with size that can hold around 8000 sessions.
# Default is 'none'.
ssl_session_cache shared:SSL:2m;
# Specifies a time during which a client may reuse the session parameters.
# Default is '5m'.
ssl_session_timeout 1h;
# Disable TLS session tickets (they are insecure). Default is 'on'.
ssl_session_tickets off;
# Enable gzipping of responses.
#gzip on;
# Set the Vary HTTP header as defined in the RFC 2616. Default is 'off'.
gzip_vary on;
# Helper variable for proxying websockets.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Specifies the main log format.
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# Sets the path, format, and configuration for a buffered log write.
access_log /var/log/nginx/access.log main;
# Includes virtual hosts configs.
include /etc/nginx/http.d/*.conf;
}网站配置文件nekopara.conf:
server {
listen 443 ssl;
server_name nekopara.uk;
ssl_certificate /data/certs/nekopara_uk.pem;
ssl_certificate_key /data/certs/nekopara_uk.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES256-GCM-SHA384';
location / {
return 302 https://www.nekopara.uk$request_uri;
}
}
# Typecho HTTPS服务器块
server {
listen 443 ssl; #proxy_protocol;
server_name www.nekopara.uk;
#set_real_ip_from 127.0.0.1;
set_real_ip_from 192.168.1.255;
real_ip_header X-Forwarded-For;
#real_ip_recursive on;
#set $real_host $http_x_forwarded_host;
#if ($http_x_forwarded_host = '') {
# set $real_host $host;
#}
# 添加这一行,确保生成的重定向不带后端端口
#port_in_redirect off;
# 设置最大上传文件大小为 32M
client_max_body_size 32M;
# SSL证书和密钥路径
ssl_certificate /data/certs/nekopara_uk.pem;
ssl_certificate_key /data/certs/nekopara_uk.key;
# SSL设置(可选但推荐用于安全)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# 网站根目录
root /data/typecho;
index index.html index.php;
# 1. 静态资源限流(宽松,50r/s)
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|eot|svg|mp4|webp|zip|rar|7z|psb)$ {
limit_req zone=static_limit burst=50 nodelay;
try_files $uri $uri/ =404;
add_header Cache-Control "public";
}
# PHP处理,并启用FastCGI缓存
location ~ \.php$ {
limit_req zone=dynamic_limit burst=2 nodelay;
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
try_files $uri =404;
fastcgi_pass unix:/var/run/php-fpm82/php8.2-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# 启用 FastCGI 缓存
fastcgi_cache my_fastcgi_cache;
fastcgi_cache_valid 200 301 302 404 10s; # 设置缓存的有效时间
fastcgi_cache_methods GET HEAD; # 只缓存 GET 和 HEAD 请求
# 不缓存 POST 请求和其他非幂等性请求
set $skip_cache 0;
if ($request_method = POST) {
set $skip_cache 1;
}
if ($query_string != "") {
set $skip_cache 1;
}
# 如果存在特定会话 cookie,则不缓存
if ($http_cookie ~* "PHPSESSID|__typecho_uid|__typecho_authCode") {
set $skip_cache 1;
}
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
# 添加头部信息显示缓存状态
add_header X-FastCGI-Cache $upstream_cache_status;
}
# 3. 默认路由(用于Typecho伪静态)
location / {
try_files $uri $uri/ /index.php?$args;
autoindex off;
}
#自定义503为429 Too Many Requests
error_page 503 =429 /429.html;
location = /429.html {
internal;
root /etc/nginx/error_pages;
#添加响应头告诉客户端或者爬虫请10秒后重试
add_header Retry-After 10 always;
}
# Deny access to hidden files
location ~ /\.ht {
deny all;
}
}自定义429界面429.html:
<meta charset="utf-8">
<title>喵~别急!429 Too Many Requests</title>
<center>
<h1>喵呜~刷新太快啦!</h1>
<p>429 Too Many Requests</p>
<hr>
<p>小喵需要休息10秒~</p>
<p>ฅ^•ﻌ•^ฅ</p>
</center>但是在后续自己模拟单点慢速攻击的时候发现了一个问题,如果是持续不断的请求,即使大部分是429拒绝的相应,仍有夹杂着404的查询相应。虽然单个IP影响不大,但是如果攻击规模较大,仍然会拖垮服务器,并且也会比返回429错误消耗更多的带宽,需要更精准的限制和打击。
这是测试用的Python程序:
import requests
import time
import argparse
import csv
import os
import string
import random
from datetime import datetime
def generate_random_path(length=15):
"""生成指定长度的随机字符串(包含大小写字母和数字)"""
characters = string.ascii_letters + string.digits
return ''.join(random.choices(characters, k=length))
def main():
parser = argparse.ArgumentParser(description='随机URL请求测试脚本(用于验证服务器处理能力)')
parser.add_argument('domain', help='目标域名(例如: example.com)')
parser.add_argument('--interval', type=int, default=1000, help='请求间隔(毫秒,默认1000毫秒=1秒)')
parser.add_argument('--count', type=int, default=100, help='请求总次数(默认100次)')
parser.add_argument('--output', default='random_request_test.csv', help='输出CSV文件名(默认: random_request_test.csv)')
parser.add_argument('--timeout', type=int, default=5, help='请求超时时间(秒,默认5秒)')
parser.add_argument('--user-agent', default='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', help='自定义User-Agent')
args = parser.parse_args()
# 验证域名格式
if not args.domain.startswith('http'):
args.domain = f"https://{args.domain}"
# 创建请求头
headers = {
'User-Agent': args.user_agent,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Connection': 'keep-alive'
}
# 准备结果列表
results = []
start_time = time.time()
print(f"开始随机URL请求测试 (总请求: {args.count}, 间隔: {args.interval}ms, 超时: {args.timeout}s)")
print(f"目标域名: {args.domain}")
print(f"输出文件: {args.output}\n")
# 创建CSV文件
with open(args.output, 'w', newline='', encoding='utf-8') as csvfile:
fieldnames = ['timestamp', 'url', 'status', 'response_time_sec', 'content_length_bytes']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
# 执行请求
for i in range(1, args.count + 1):
# 生成随机路径
random_path = generate_random_path()
url = f"{args.domain}/{random_path}"
# 记录请求开始时间
request_start = time.time()
try:
# 发送请求
response = requests.get(
url,
headers=headers,
timeout=args.timeout
)
response_time = time.time() - request_start
status = response.status_code
content_length = len(response.content)
# 记录结果
result = {
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
'url': url,
'status': status,
'response_time_sec': response_time,
'content_length_bytes': content_length
}
results.append(result)
# 输出进度
print(f"[{i}/{args.count}] {url} | 状态: {status} | 响应时间: {response_time:.3f}s | 大小: {content_length}B")
# 写入CSV
writer.writerow(result)
except Exception as e:
error_msg = str(e)
result = {
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
'url': url,
'status': 'ERROR',
'response_time_sec': 0,
'content_length_bytes': 0
}
results.append(result)
print(f"[{i}/{args.count}] {url} | 错误: {error_msg}")
# 间隔处理(最后一个请求不等待)
if i < args.count:
time.sleep(args.interval / 1000.0)
# 生成报告
total_time = time.time() - start_time
print(f"\n测试完成! 总耗时: {total_time:.2f}秒")
print(f"结果已保存至: {args.output}")
if __name__ == "__main__":
main()这是测试的输出:
[vanilla@NekoWorks ~]$ python3 random_request_test.py www.nekopara.uk --interval 50 --count 500
开始随机URL请求测试 (总请求: 500, 间隔: 50ms, 超时: 5s)
目标域名: https://www.nekopara.uk
输出文件: random_request_test.csv
[1/500] https://www.nekopara.uk/6pKJpS6oAgTGGg9 | 状态: 404 | 响应时间: 0.430s | 大小: 29335B
[2/500] https://www.nekopara.uk/h9utooL2wE8Iu9R | 状态: 404 | 响应时间: 0.373s | 大小: 29335B
[3/500] https://www.nekopara.uk/VQr7eBnBMePGezI | 状态: 404 | 响应时间: 0.389s | 大小: 29335B
[4/500] https://www.nekopara.uk/r87rqw8ftyrOV4E | 状态: 404 | 响应时间: 0.364s | 大小: 29335B
[5/500] https://www.nekopara.uk/V8BBeJqFidADG8a | 状态: 429 | 响应时间: 0.274s | 大小: 266B
[6/500] https://www.nekopara.uk/2FModsb5p1imNZY | 状态: 404 | 响应时间: 0.360s | 大小: 29335B
[7/500] https://www.nekopara.uk/IGr3v06kf9xbw2L | 状态: 429 | 响应时间: 0.217s | 大小: 266B
[8/500] https://www.nekopara.uk/vMUNbNh12WseZ79 | 状态: 429 | 响应时间: 0.265s | 大小: 266B
[9/500] https://www.nekopara.uk/1qkNpklBD60HZlS | 状态: 429 | 响应时间: 0.218s | 大小: 266B
[10/500] https://www.nekopara.uk/cg5pKmaTfI0pJF0 | 状态: 404 | 响应时间: 0.307s | 大小: 29335B
[11/500] https://www.nekopara.uk/xPgKNHa3UTBVzBV | 状态: 429 | 响应时间: 0.217s | 大小: 266B
[12/500] https://www.nekopara.uk/HvwRcAMHNb24egX | 状态: 429 | 响应时间: 0.293s | 大小: 266B
[13/500] https://www.nekopara.uk/A7f0eWXeO35zUg3 | 状态: 404 | 响应时间: 0.287s | 大小: 29335B
[14/500] https://www.nekopara.uk/SdadEt4mQFNUI2P | 状态: 429 | 响应时间: 0.278s | 大小: 266B
[15/500] https://www.nekopara.uk/3ZJncTYxzQovjIA | 状态: 429 | 响应时间: 0.227s | 大小: 266B
[16/500] https://www.nekopara.uk/gljFhsR4cYYk3aP | 状态: 404 | 响应时间: 0.301s | 大小: 29335B
[17/500] https://www.nekopara.uk/CaFhCaIwy98bSRj | 状态: 429 | 响应时间: 0.216s | 大小: 266B
[18/500] https://www.nekopara.uk/os3ZwlQY9d9gikA | 状态: 429 | 响应时间: 0.228s | 大小: 266B
[19/500] https://www.nekopara.uk/Ai1OU2q1ucI5dRV | 状态: 404 | 响应时间: 0.366s | 大小: 29335B
[20/500] https://www.nekopara.uk/HSzYv7RTC1bJBsD | 状态: 429 | 响应时间: 0.214s | 大小: 266B
[21/500] https://www.nekopara.uk/QiX7850u9CB29wj | 状态: 429 | 响应时间: 0.218s | 大小: 266B
[22/500] https://www.nekopara.uk/mczKcopcmVCSgO0 | 状态: 429 | 响应时间: 0.223s | 大小: 266B
[23/500] https://www.nekopara.uk/zbodIK0sYxhY4Yy | 状态: 404 | 响应时间: 0.300s | 大小: 29335B
[24/500] https://www.nekopara.uk/B5KkwqUZLWra8Ec | 状态: 429 | 响应时间: 0.235s | 大小: 266B
[25/500] https://www.nekopara.uk/uy2IuH3f8gD9hfP | 状态: 429 | 响应时间: 0.210s | 大小: 266B
[26/500] https://www.nekopara.uk/lpZdcdLIUEifinU | 状态: 404 | 响应时间: 0.297s | 大小: 29335B可以很明显的发现,每隔1秒就会有一个绕过防护的404请求返回,持续消耗服务器资源。
现在只用了Nginx自带的限流功能,能做到的也就这么多了。
虽然对于单个恶意IP,成功请求一次404消耗的资源极少,服务器压根就没动静;但是,如果是成百上千个恶意IP进行高强度请求,累计负载将变得相当可观!
于是,我想要自己写防御规则,去更加智能的进行限制和防御。
进阶:使用自定义规则进行防护
这部分我们需要安装Nginx的Lua拓展,用Lua来实现防御的逻辑。
apk add nginx-mod-http-lua说点题外话,在尝试进阶方案的时候,我终于深深感受到了国内外大语言模型的技术能力差距。我请教了通义千问,换了几种表述,并且把现有网站配置文件,已经安装的相关软件包都告诉他了,但是它生成的解决方案代码全部报错。试了五次没有一次成功。后面直接找ChatGPT,一次就秒了!看来国内大语言模型还是有待提高啊。
实现思路
AI只能为你写代码,但是具体的程序逻辑这个最好还是要根据自己博客运营的现实情况自己想防护策略,确保精准打击恶意访问,但是不影响正常访客。
我设想的封禁策略是这样的:
- 对于动态响应的请求,这部分一般是博客的页面返回的html,设置限流为单个IP每秒一个请求,对于个人博客来说这已经足以满足正常的访客需求了。并且允许进行两个突发请求,也就是在第一次的一秒内,允许三个请求,来应对第一次页面加载的情况。
- 对于静态文件请求,这部分可以宽松一点,毕竟我实际上看完整加载一次网站静态资源数量会有大约50个,并且静态资源对服务器性能的消耗极少,主要是带宽的消耗。而对于带宽的话在之前的博客已经通过防火墙限制了单个IP能够发起的并发连接数,再加上文件小,基本上不太拉得起来带宽。所以考虑是适当放宽,额外允许50个突发请求,也就是第一秒内允许100个突发请求,足以满足一般个人博客对静态资源的需求。
- 对于随机图片的API,因为后端PHP逻辑简单,代码量少,即使被较高并发轮询也不会消耗太多资源。且一般情况下访问主页,最极端的情况下,也就是文章封面全部是随机封面,且访客不停地往下拉,每秒10个查询请求已经足够,再加上突发再给20个请求,足以应付首页往下拉可以看到20个封面的情况了。剩下的页面需要点加载更多,这就已经需要数秒的时间,此时策略的令牌桶已经补充了10个令牌,足以满足下一步浏览产生的请求。
特别的,为了防止前面提到的即使限制了请求速率,持续不断的恶意请求仍然会每秒产生一个
404页面消耗资源,我还做出了进一步的限制策略:- 从访问开始,就会以10秒为单位作为时间窗口观察请求的数量和结果。
- 如果IP请求在这10秒内请求的结果中
429错误占到5次及以上,则进行为期10秒的临时封禁,期间任何请求均返回429错误。 - 在封禁的10秒内,如果请求数量仍达到3次及以上,则延长10秒封禁时间,并在新的封禁时间内继续计数,如果请求数量仍达到3次及以上,继续延长封禁时间,以此类推。
- 当最新的10秒观察窗口期内,请求数量降低到2个及以下,则解除封禁,可以继续正常访问。
这样的策略,给了正常访客一定的折腾空间,只要封禁前访问请求次数不超限制,基本上就是正常浏览。但是对于恶意请求,一旦触发了封禁机制,就会被一直封禁。因为解除封禁的规则比判定封禁更严格。
实现判定封禁的配置文件
基于这个逻辑,我向ChatGPT详细描述了我的需求和实现思路,并且附上了现有的Nginx配置文件,以及告诉他目前已经安装的相关软件包和正在使用的发行版版本,询问是否需要补充安装软件包。这样详细的提示词给到ChatGPT,它直接轻松秒杀:
修改后的nginx.conf:
# /etc/nginx/nginx.conf
user nginx;
# Set number of worker processes automatically based on number of CPU cores.
worker_processes auto;
# Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on;
# Configures default error logger.
error_log /var/log/nginx/error.log warn;
# Includes files with directives to load dynamic modules.
include /etc/nginx/modules/*.conf;
# Include files with config snippets into the root context.
include /etc/nginx/conf.d/*.conf;
events {
# The maximum number of simultaneous connections that can be opened by
# a worker process.
worker_connections 1024;
}
http {
lua_shared_dict abuse 10m;
lua_shared_dict tb 10m;
# 静态资源限流配置(宽松)
limit_req_zone $binary_remote_addr zone=static_limit:10m rate=50r/s;
# 定义 FastCGI 缓存路径
fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=my_fastcgi_cache:16m inactive=60m;
# 定义缓存键,通常包含请求方法、主机名和 URI
fastcgi_cache_key "$scheme$request_method$host$request_uri";
# Includes mapping of file name extensions to MIME types of responses
# and defines the default type.
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Name servers used to resolve names of upstream servers into addresses.
# It's also needed when using tcpsocket and udpsocket in Lua modules.
#resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001];
# Don't tell nginx version to the clients. Default is 'on'.
server_tokens off;
# Specifies the maximum accepted body size of a client request, as
# indicated by the request header Content-Length. If the stated content
# length is greater than this size, then the client receives the HTTP
# error code 413. Set to 0 to disable. Default is '1m'.
client_max_body_size 1m;
# Sendfile copies data between one FD and other from within the kernel,
# which is more efficient than read() + write(). Default is off.
sendfile on;
# Causes nginx to attempt to send its HTTP response head in one packet,
# instead of using partial frames. Default is 'off'.
tcp_nopush on;
# Enables the specified protocols. Default is TLSv1 TLSv1.1 TLSv1.2.
# TIP: If you're not obligated to support ancient clients, remove TLSv1.1.
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
# Path of the file with Diffie-Hellman parameters for EDH ciphers.
# TIP: Generate with: `openssl dhparam -out /etc/ssl/nginx/dh2048.pem 2048`
#ssl_dhparam /etc/ssl/nginx/dh2048.pem;
# Specifies that our cipher suits should be preferred over client ciphers.
# Default is 'off'.
ssl_prefer_server_ciphers on;
# Enables a shared SSL cache with size that can hold around 8000 sessions.
# Default is 'none'.
ssl_session_cache shared:SSL:2m;
# Specifies a time during which a client may reuse the session parameters.
# Default is '5m'.
ssl_session_timeout 1h;
# Disable TLS session tickets (they are insecure). Default is 'on'.
ssl_session_tickets off;
# Enable gzipping of responses.
#gzip on;
# Set the Vary HTTP header as defined in the RFC 2616. Default is 'off'.
gzip_vary on;
# Helper variable for proxying websockets.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Specifies the main log format.
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# Sets the path, format, and configuration for a buffered log write.
access_log /var/log/nginx/access.log main;
# Includes virtual hosts configs.
include /etc/nginx/http.d/*.conf;
}这里新增了两个共享内存块abuse和tb用于存储相关的请求信息。
修改后的nekopara.conf:
server {
listen 443 ssl;
server_name nekopara.uk;
ssl_certificate /data/certs/nekopara_uk.pem;
ssl_certificate_key /data/certs/nekopara_uk.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES256-GCM-SHA384';
location / {
return 302 https://www.nekopara.uk$request_uri;
}
}
# Typecho HTTPS服务器块
server {
listen 443 ssl; #proxy_protocol;
server_name www.nekopara.uk;
set_real_ip_from 192.168.1.255;
real_ip_header X-Forwarded-For;
# 设置最大上传文件大小为 32M
client_max_body_size 32M;
# SSL证书和密钥路径
ssl_certificate /data/certs/nekopara_uk.pem;
ssl_certificate_key /data/certs/nekopara_uk.key;
# SSL设置(可选但推荐用于安全)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# 网站根目录
root /data/typecho;
index index.html index.php;
# 1. 静态资源限流(宽松,50r/s)
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|eot|svg|mp4|webp|zip|rar|7z|psb)$ {
limit_req zone=static_limit burst=50 nodelay;
try_files $uri $uri/ =404;
add_header Cache-Control "public";
}
# PHP处理,并启用FastCGI缓存
location ~ \.php$ {
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
try_files $uri =404;
fastcgi_pass unix:/var/run/php-fpm82/php8.2-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# 启用 FastCGI 缓存
fastcgi_cache my_fastcgi_cache;
fastcgi_cache_valid 200 301 302 404 10s; # 设置缓存的有效时间
fastcgi_cache_methods GET HEAD; # 只缓存 GET 和 HEAD 请求
# 不缓存 POST 请求和其他非幂等性请求
set $skip_cache 0;
if ($request_method = POST) {
set $skip_cache 1;
}
if ($query_string != "") {
set $skip_cache 1;
}
# 如果存在特定会话 cookie,则不缓存
if ($http_cookie ~* "PHPSESSID|__typecho_uid|__typecho_authCode") {
set $skip_cache 1;
}
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
# 添加头部信息显示缓存状态
add_header X-FastCGI-Cache $upstream_cache_status;
# 动态页面封禁逻辑规则
access_by_lua_block {
local abuse = ngx.shared.abuse -- 计数/封禁存储
local tb = ngx.shared.tb -- token-bucket 存储
local ip = ngx.var.remote_addr or "unknown"
local uri = ngx.var.uri or "/"
-- 参数(可按需调整)
local RATE = 1 -- 平均速率:1 r/s
local BURST = 2 -- burst 值(与 nginx 的 burst 对应)
local CAPACITY = BURST + 1 -- token 桶容量 (burst + 1)
local TRIGGER_WINDOW = 10 -- 触发封禁的观察窗口(秒)
local TRIGGER_COUNT = 5 -- 在观察窗口内达到此次数则封禁
local BAN_TTL = 10 -- 初始封禁时长(秒)
local BAN_EXT_HIT_WINDOW = 10 -- 封禁期间统计窗口(秒)
local BAN_EXT_HIT_COUNT = 3 -- 封禁期间若在最近 BAN_EXT_HIT_WINDOW 秒内请求次数 >= 3 则延长
-- 宽松策略(仅用于特定豁免路径)
local PIC_RATE = 10 -- 更高速率,按需调整
local PIC_BURST = 20 -- 更大突发,按需调整
local PIC_CAPACITY = PIC_BURST + 1
-- 哪些 URI 被豁免为“宽松模式”(只对精确入口豁免)
local function is_pic_exempt(u)
if not u then return false end
if u == "/pic_api" or u == "/pic_api/" or u == "/pic_api/index.php" then
return true
end
return false
end
local pic_exempt = is_pic_exempt(uri)
-- helper keys
local ban_key = "ban:" .. ip
local lim_key = "lim:" .. ip -- 5s 触发计数 key
local banhit_key = "banhit:" .. ip -- 封禁期内计数 key
-- 如果不是 pic_exempt,先检查封禁(pic_exempt 跳过 ban 检查)
if not pic_exempt then
local is_banned = abuse:get(ban_key)
if is_banned then
-- 记录封禁期内的请求计数以便延长封禁
local ok, err = abuse:incr(banhit_key, 1, 0, BAN_EXT_HIT_WINDOW)
if not ok then
abuse:set(banhit_key, 1, BAN_EXT_HIT_WINDOW)
ok = 1
end
if ok >= BAN_EXT_HIT_COUNT then
abuse:set(ban_key, 1, BAN_TTL)
abuse:set(banhit_key, 0, BAN_EXT_HIT_WINDOW)
end
return ngx.exit(503)
end
end
-- token-bucket 通用实现(根据是否豁免使用不同参数)
local now = ngx.now()
local tokens_key = "tokens:" .. ip
local last_key = "last:" .. ip
local tokens = tb:get(tokens_key)
local last = tb:get(last_key)
if tokens == nil then
if pic_exempt then tokens = PIC_CAPACITY else tokens = CAPACITY end
end
if last == nil then
last = now
end
local delta = now - tonumber(last)
if delta < 0 then delta = 0 end
local rate = (pic_exempt and PIC_RATE) or RATE
local capacity = (pic_exempt and PIC_CAPACITY) or CAPACITY
local new_tokens = tonumber(tokens) + delta * rate
if new_tokens > capacity then new_tokens = capacity end
if new_tokens >= 1 then
-- 允许请求,消耗 1 token
new_tokens = new_tokens - 1
tb:set(tokens_key, new_tokens, 60)
tb:set(last_key, now, 60)
return
else
-- 被限流:如果是 pic_exempt,则我们不走封禁逻辑,只返回 503(表现为 429 页面)
if pic_exempt then
-- 可选:可以不计入 abuse 触发计数,避免误封
return ngx.exit(503)
end
-- 严格路径:记录触发计数并在达到阈值时封禁
local cnt, err = abuse:incr(lim_key, 1, 0, TRIGGER_WINDOW)
if not cnt then
abuse:set(lim_key, 1, TRIGGER_WINDOW)
cnt = 1
end
if cnt >= TRIGGER_COUNT then
abuse:set(ban_key, 1, BAN_TTL)
abuse:delete(lim_key)
end
return ngx.exit(503)
end
}
}
# 3. 默认路由(用于Typecho伪静态)
location / {
try_files $uri $uri/ /index.php?$args;
autoindex off;
}
#自定义503为429 Too Many Requests
error_page 503 =429 /429.html;
location = /429.html {
internal;
root /etc/nginx/error_pages;
#添加响应头告诉爬虫重试时间间隔
add_header Retry-After 10 always;
}
# Deny access to hidden files
location ~ /\.ht {
deny all;
}
}主要是新增了access_by_lua_block这个Lua脚本块来实现封禁逻辑,注释已经很清楚了,需要的进行按需修改即可。(虽然我不懂Lua,问LLM就完了)
测试效果
同样的,我们来尝试使用之前的测试脚本进行测试,查看返回的结果:
[vanilla@NekoWorks ~]$ python3 random_request_test.py www.nekopara.uk --interval 50 --count 500
开始随机URL请求测试 (总请求: 500, 间隔: 50ms, 超时: 5s)
目标域名: https://www.nekopara.uk
输出文件: random_request_test.csv
[1/500] https://www.nekopara.uk/tsq5L3DXmTP3pgt | 状态: 404 | 响应时间: 0.430s | 大小: 29758B
[2/500] https://www.nekopara.uk/0imQYTBUKXMaWEe | 状态: 404 | 响应时间: 0.431s | 大小: 29758B
[3/500] https://www.nekopara.uk/mb2uMeZihKa6Qx3 | 状态: 404 | 响应时间: 0.369s | 大小: 29758B
[4/500] https://www.nekopara.uk/ik6TaHfby3dVnO6 | 状态: 404 | 响应时间: 0.446s | 大小: 29758B
[5/500] https://www.nekopara.uk/kOaNyipoAoJp7oz | 状态: 429 | 响应时间: 0.222s | 大小: 266B
[6/500] https://www.nekopara.uk/SQvv9uNbBOukoCM | 状态: 404 | 响应时间: 0.302s | 大小: 29758B
[7/500] https://www.nekopara.uk/3N0aRXMhpnbn1gP | 状态: 429 | 响应时间: 0.310s | 大小: 266B
[8/500] https://www.nekopara.uk/d0G1xWPmmVNqbHw | 状态: 429 | 响应时间: 0.224s | 大小: 266B
[9/500] https://www.nekopara.uk/o2IpKDOdVMCD9r2 | 状态: 404 | 响应时间: 0.435s | 大小: 29758B
[10/500] https://www.nekopara.uk/EdpSSsWhxNtcQ0E | 状态: 429 | 响应时间: 0.291s | 大小: 266B
[11/500] https://www.nekopara.uk/V1GaQNNrt9dz4YP | 状态: 429 | 响应时间: 0.311s | 大小: 266B
[12/500] https://www.nekopara.uk/qQiIxZrIBEAtbeB | 状态: 429 | 响应时间: 0.219s | 大小: 266B
[13/500] https://www.nekopara.uk/c4jCFSFD5bgFwrw | 状态: 429 | 响应时间: 0.241s | 大小: 266B
[14/500] https://www.nekopara.uk/ukHBaBKx9Dn1uIY | 状态: 429 | 响应时间: 0.315s | 大小: 266B
[15/500] https://www.nekopara.uk/KcvHSqSHoamkTb9 | 状态: 429 | 响应时间: 0.277s | 大小: 266B
[16/500] https://www.nekopara.uk/VQPfGcKjPSSx1mn | 状态: 429 | 响应时间: 0.279s | 大小: 266B
[17/500] https://www.nekopara.uk/WCXISBiDEuuEze5 | 状态: 429 | 响应时间: 0.321s | 大小: 266B
[18/500] https://www.nekopara.uk/LxsC9KgFCGojRVQ | 状态: 429 | 响应时间: 0.228s | 大小: 266B
[19/500] https://www.nekopara.uk/aXwLD0nIDKrEw6z | 状态: 429 | 响应时间: 0.220s | 大小: 266B
[20/500] https://www.nekopara.uk/mn5SgwFI1fB3pPL | 状态: 429 | 响应时间: 0.217s | 大小: 266B
[21/500] https://www.nekopara.uk/cDtZOUEUDBeIam6 | 状态: 429 | 响应时间: 0.218s | 大小: 266B
[22/500] https://www.nekopara.uk/xdikUSlbaqhkQBV | 状态: 429 | 响应时间: 0.227s | 大小: 266B
[23/500] https://www.nekopara.uk/fYmwQCdwuUEvb6Q | 状态: 429 | 响应时间: 0.216s | 大小: 266B
[24/500] https://www.nekopara.uk/bI0npEBugVcHenG | 状态: 429 | 响应时间: 0.217s | 大小: 266B
[25/500] https://www.nekopara.uk/ecUcKf1We2GjNZE | 状态: 429 | 响应时间: 0.218s | 大小: 266B可以看到,一旦触发封禁,后面快速请求的结果都是429,非常的成功!
总结与反思
经过一番测试和调试,我可以很自信地说:我的博客已经能够有效抵御常见的中小规模 CC 攻击。对于一个纯粹的个人博客而言,这样的防护能力,我认为已经相当不错了。
但你要问——这套方案能不能完全无视所有 CC 攻击?
我必须很遗憾且负责任地告诉你:不行!
目前的防御策略,对单 IP 高频、多 IP 高频这类“显眼包”式攻击有不错的拦截效果。但如果是海量 IP 发起的低频、慢速、行为模拟正常的 CC 攻击,那我的服务器大概率会当场暴毙。
毕竟,这类请求和真实用户的访问行为太像了,总不能为了防攻击,把正常访客也一并拒之门外吧 😅
所以,如果你真的想用 CC 打垮我的网站,那我也不妨“贴心”地给你“指明方向”,顺便帮你算笔账:
🎯 攻击指南(兼成本核算)
- IP 资源:你需要一个至少 1000+ 独立 IP 的僵尸网络。市面上靠谱的 DDoS 僵尸网络租赁服务,日租成本约 50–100 美元。
- 攻击频率:每个 IP 每秒发起一次请求,或更低频但持续不断,还得随机访问不同 URL,避免触发我的频率封禁规则——别让我在日志里看出破绽。
持续时间:想让我网站瘫痪一整天?那你得连续攻击 24 小时不停歇。这意味着:
- 僵尸网络租赁:$100
- 技术维护与调度(别让 IP 被批量封,算是DLC,可选项):$200+
- 总体成本保守估计:$100–500 美元/天
而我这边呢?
服务器是白嫖的华为云资源,带宽和计算资源基本零成本。你砸几百美元攻击我一天,我关掉网站喝杯咖啡,等你钱烧完,我秒恢复上线。
🤔 所以,请问:
你真的愿意花大几百美元,只为让一个不盈利、没广告、日均访问量不到百人的个人博客下线几天?
这笔钱,够买几十杯星巴克,够报一门编程课,够买台树莓派折腾半年。
相比之下,把它砸在一个“打不赢也赚不到”的小博客上,真的值得吗?
因此,对于那些脚本小子、网络喷子、或者闲着无聊想搞点事情的小学生来说——
这套防护机制已经足够让你“出师未捷身先死”。
而如果你真愿意花大价钱,动用专业资源对我持续攻击……
那我只能说:承蒙厚爱,荣幸之至 😊









































































































































































































