个人博客低成本CC防护：抵御85%+常见CC攻击
低成本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&timestamp=91d8330f0f6330&sort=8d7f7fe923&category=9f7158e6486f01f HTTP/1.0" 404 29335 "http://www.nekopara.uk/info?offset=34297ebb3908dec&timestamp=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： 喵~别急！429 Too Many Requests 喵呜~刷新太快啦！ 429 Too Many Requests 小喵需要休息10秒~ ฅ^•ﻌ•^ฅ 但是在后续自己模拟单点慢速攻击的时候发现了一个问题，如果是持续不断的请求，即使大部分是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 美元/天而我这边呢？ 服务器是白嫖的华为云资源，带宽和计算资源基本零成本。你砸几百美元攻击我一天，我关掉网站喝杯咖啡，等你钱烧完，我秒恢复上线。🤔 所以，请问：你真的愿意花大几百美元，只为让一个不盈利、没广告、日均访问量不到百人的个人博客下线几天？这笔钱，够买几十杯星巴克，够报一门编程课，够买台树莓派折腾半年。 相比之下，把它砸在一个“打不赢也赚不到”的小博客上，真的值得吗？因此，对于那些脚本小子、网络喷子、或者闲着无聊想搞点事情的小学生来说——这套防护机制已经足够让你“出师未捷身先死”。而如果你真愿意花大价钱，动用专业资源对我持续攻击…… 那我只能说：承蒙厚爱，荣幸之至 😊
绕过黑洞！利用VPC内网互通低成本实现DDoS攻击下网站高可用
VPC内网互通：让被黑洞的服务器仍能访问事情起因很简单，前段时间我的华为云服务器又被闲的蛋疼的人刷了DDoS礼物，也是成功地触发了EIP（弹性公网IP）黑洞封堵。喜报如下：尊敬的(Rin):感谢您使用华为云产品，非常抱歉打扰您。在北京时间2025/09/23 10:26，华为云监测到您的弹性公网IP(EIP)实例 发生EIP封堵事件。 详情：节点：亚太-新加坡实例名称：119.8.185.128实例ID：b002926d-60de-4951-a9e9-b337e3a550d8告警描述：{"reason_zh": 尊敬的Rin：华为云DDoS防护服务在2025-09-23 10:26:56检测到您的EIP 119.8.185.128 异常流量已超过DDoS防护峰值触发封堵，封堵时带宽为 79 Gbps, 您服务器的访问将受到限制，24小时后系统将自动为您解封。您可登陆官网购买DDoS高防服务提升防护能力，保障服务器稳定运行。, "reason_en": Dear Rin: HUAWEI CLOUD Anti-DDoS Service detects that your EIP 119.8.185.128 abnormal traffic has exceeded the DDoS protection peak at 2025-09-23 10:26:56 and triggers blocking. Bandwidth when blocking is 79 Gbps, Access to your server will be restricted, and the system will automatically unblock it for you after 24 hours. You can log in to the official website of HUAWEI CLOUD to purchase Advanced Anti-DDoS service to enhance protection capabilities and ensure stable server operation.}原因分析：尊敬的Rin：华为云DDoS防护服务在2025-09-23 10:26:56检测到您的EIP 119.8.185.128 异常流量已超过DDoS防护峰值触发封堵，封堵时带宽为 79 Gbps您服务器的访问将受到限制，24小时后系统将自动为您解封。您可登陆官网购买DDoS高防服务提升防护能力，保障服务器稳定运行。如有疑问，您可以登录华为云官网提交工单咨询，会有相应工程师为您核实查看。IP我就懒得打码了，反正就是这个网站域名平时解析的IP，有什么好打码的，ping一下就能看到了。我在之前的文章，就有说过一套抗DDoS的方案，并且已经付诸实践了（详见文章：《当网站遭遇DDoS：如何优雅地应对云服务器被封堵》）。很显然，并没有太大影响，在网站主服务器被打黑洞的三分钟后，脚本就自动切换服务器到备用服务器并且套上了Cloudflare的代理，无需我干预，唯一的不足就是在被DDoS期间网站访问变慢了。但是，当时我就有说过，那套方案是有缺陷的，也就是因为网站数据同步是单向的，在被攻击期间交互产生的数据，后面会被覆盖掉。好巧不巧，华为云又送钱了，我寻思着我之前开的华为云服务器已经续费十年多了，不如开个新的云服务器，搞VPC内网互通，彻底解决上述的痛点问题。这里说个题外话，我发现新开的华为云 Flexus L 实例相比于之前已经大幅缩水，网络回国优化没了，而且CPU性能只有原来的三分之一了，已经变得跟阿里云的轻量服务器一个水平了，而且价格还更高。除非你和我一样是白嫖的，否则真金白银买我是不推荐，因为差不多一样的东西，阿里云那边要大概便宜个10%-20%，买华为云真不划算了。现在华为云的 Flexus L 实例算是把华为手机的高价低配学过来了，难绷。但是老实例性能和网络那还是相当不错的。资源准备为了实现这个方案，你需要准备两台云服务器，并且两台服务器必须在同一个地域，以便进行VPC内网互通。虽然同一个地域就足够，但将两台服务器放在同一个可用区会获得更好的内网延迟（通常<1ms），而跨可用区的内网延迟可能会稍高（但通常也在1-5ms范围内）。这就是最重要的前提要求，日常访问的可以是两台服务器的任意一台，只需要它的性能可以满足业务要求。对于另一台服务器，那是作为网站流量跳板服务器，最低配即可，方便节约成本，因为只需要在上面运行一个代理流量程序，在主实例EIP被封堵时代理接管流量入口。因为本方案抗DDoS采用的是Cloudflare的CDN，所以跳板实例的网络可以差一点，只需要保证到Cloudflare的回源服务器线路通畅即可，国内访问绕路都不是事。平时流量也不会从这边走，只是作为保障用途应急使用。网络配置一般情况下，云服务器在同一个地域会默认分配到同一个VPC实现内网互通，如果没有互通，修改两个云服务器的VPC为同一个VPC即可，或者使用VPC对等连接（VPC Peering），详情可以查阅各类云的参考文档，或者直接去发工单骚扰客服。当其中一个服务器ping另一个服务器的内网IP地址有反应，那就说明内网互通成功了，可以进行下一步工作。就像这样：主服务器:NyankoHost [~]# ping 192.168.1.255 PING 192.168.1.255 (192.168.1.255): 56 data bytes 64 bytes from 192.168.1.255: seq=0 ttl=64 time=1.152 ms 64 bytes from 192.168.1.255: seq=1 ttl=64 time=0.950 ms 64 bytes from 192.168.1.255: seq=2 ttl=64 time=0.932 ms 64 bytes from 192.168.1.255: seq=3 ttl=64 time=0.879 ms 64 bytes from 192.168.1.255: seq=4 ttl=64 time=0.899 ms 64 bytes from 192.168.1.255: seq=5 ttl=64 time=0.891 ms 64 bytes from 192.168.1.255: seq=6 ttl=64 time=0.885 ms 64 bytes from 192.168.1.255: seq=7 ttl=64 time=0.878 ms ^C --- 192.168.1.255 ping statistics --- 8 packets transmitted, 8 packets received, 0% packet loss round-trip min/avg/max = 0.878/0.933/1.152 ms代理服务器：MewHost:~# ping 192.168.13.229 PING 192.168.13.229 (192.168.13.229): 56 data bytes 64 bytes from 192.168.13.229: seq=0 ttl=64 time=1.051 ms 64 bytes from 192.168.13.229: seq=1 ttl=64 time=1.032 ms 64 bytes from 192.168.13.229: seq=2 ttl=64 time=1.006 ms 64 bytes from 192.168.13.229: seq=3 ttl=64 time=0.978 ms 64 bytes from 192.168.13.229: seq=4 ttl=64 time=0.988 ms 64 bytes from 192.168.13.229: seq=5 ttl=64 time=0.978 ms 64 bytes from 192.168.13.229: seq=6 ttl=64 time=0.980 ms 64 bytes from 192.168.13.229: seq=7 ttl=64 time=0.999 ms ^C --- 192.168.13.229 ping statistics --- 8 packets transmitted, 8 packets received, 0% packet loss round-trip min/avg/max = 0.978/1.001/1.051 ms设置代理服务器代理服务器Nginx配置文件设置建议使用一个轻量化的Linux发行版来节省资源，例如Alpine，本教程就是基于Alpine Linux 3.20部署的，然后安装上Nginx。编辑Nginx的默认站点配置文件/etc/nginx/http.d/default.conf，修改如下：server { listen 80 default_server; #listen [::]:443 ssl default_server; server_name _; # 这是一个通配符，表示任何未匹配的主机名 #return 444; # 关闭连接，不发送任何响应 root /var/www/; # 静态文件的位置 index index.html; # 默认主页 location / { try_files $uri $uri/ =404; # 尝试提供请求的文件或目录，如果不存在则返回404 } # 处理错误页面 error_page 404 /404.html; location = /404.html { internal; } } server { listen 443 ssl default_server; #listen [::]:443 ssl default_server; server_name _; # 这是一个通配符，表示任何未匹配的主机名 ssl_certificate /etc/nginx/ssl/self_cert.pem; ssl_certificate_key /etc/nginx/ssl/self_cert.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'; root /var/www/; # 静态文件的位置 index index.html; # 默认主页 location / { try_files $uri $uri/ =404; # 尝试提供请求的文件或目录，如果不存在则返回404 } # 处理错误页面 error_page 404 /404.html; location = /404.html { internal; } }这里的配置文件是匹配默认站点，对于https则采用自签名证书匹配后返回默认页面，防止被证书扫描暴露备用服务器IP导致一起被打。至于怎么安装Nginx，怎么启动，怎么生成自签名证书，这些我之前的文章说过很多次了，不会的可以翻一下我的博客文章，例如《实战：帮群友迁移Typecho博客并解决数据库问题》，这里不再赘述。接下来配置代理的配置文件/etc/nginx/http.d/proxy_nekopara_uk.conf：server { listen 443 ssl; server_name www.nekopara.uk; 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; # 必要的代理头，保留原始 Host（按需） proxy_set_header Host $host; # 或改成 $proxy_host / example.com proxy_set_header X-Real-IP $remote_addr; # 把客户端 IP 放入 X-Real-IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }采用的证书建议是使用Cloudflare生成的源服务器证书，因为可以设置15年有效期，有效防止证书忘记更新的情况。这里别忘了，要把代理服务器的hosts文件改一下，因为如果在内网直接配置IP回源的话，会被自己匹配到默认站点而不是网站，你也看到了我配置文件回源的配置是proxy_pass https://www.nekopara.uk:443;，所以你需要修改/etc/hosts文件，加上以下内容：192.168.13.229 nekopara.uk www.nekopara.uk其中，192.168.13.229是主服务器的IP地址，后面则是匹配的域名，这样子反代后的流量走到主服务器就不会匹配默认站点了。代理服务器安全组设置（可选）这部分属于安全加固的内容，为可选项，建议配置。为了最大程度保护用于代理的备用服务器不被扫描暴露，我们需要配置安全组白名单，在80和443端口只放行来自Cloudflare IP的流量。Cloudflare的IP地址段可以在IP 地址范围 | Cloudflare查询到，包括以下IP段：173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22在云服务器的安全组里面配置一个 Cloudflare IP 地址组，添加到80和443端口放行规则里面，只放行该地址组的IP，以及必须的端口，如22，最小化攻击面。设置IP地址组，添加Cloudflare IP：创建新的安全组，并设置80和443放行的源地址为 Cloudflare IP 地址组：切换备用服务器的安全组为新创建的安全组：这样，就保证了备用服务器不会被轻易扫描出来，除了Cloudflare的IP，在别的IP地址访问下，这个服务器看起来压根就没运行网站。其实如果可以，连22端口的规则都可以删了，反正可以内网直接连接，也省得22端口每天都在被高强度爆操。主服务器配置修改你主服务器网站的Nginx配置文件，在服务器块的前面部分加上下面的内容：set_real_ip_from 192.168.1.255; real_ip_header X-Forwarded-For;这是信任内网的服务器提供的远端IP地址，避免在网站的访问日志里面看到访问的IP全是代理服务器的内网IP。完整的配置示例文件如下：# Typecho HTTPS服务器块 server { listen 443 ssl; server_name www.nekopara.uk; #信任并设置代理服务器提供的远端IP 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; # Serve static files directly location / { try_files $uri $uri/ /index.php?$args; autoindex off; } # 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; } # Deny access to hidden files location ~ /\.ht { deny all; } }验证代理情况下真实IP传递效果我采用面向LLM编程写了下面的测试代码，用于展示客户端访问IP的情况，保存为ip.php放到网站目录访问即可看到效果：<?php // 获取客户端IP地址 $ip = $_SERVER['REMOTE_ADDR']; $ip2 = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '未通过代理/CDN'; // 显示IP地址（带简单HTML格式） echo " 您的IP地址信息 "; // 显示REMOTE_ADDR（直接连接IP） echo " "; echo "REMOTE_ADDR: " . htmlspecialchars($ip) . " "; echo "通常是直接连接服务器的IP（可能是代理或真实IP）"; echo " "; // 显示HTTP_X_FORWARDED_FOR（原始客户端IP） echo " "; echo "HTTP_X_FORWARDED_FOR: " . htmlspecialchars($ip2) . " "; echo "通常表示原始客户端IP（仅当使用代理/CDN时存在）"; echo " "; // 添加说明 echo " "; echo "说明： "; echo "• 如果您未使用代理或CDN，HTTP_X_FORWARDED_FOR 可能为空或显示'未通过代理/CDN' "; echo "• 如果您使用了CDN（如Cloudflare）、代理或负载均衡，HTTP_X_FORWARDED_FOR 通常包含您真实的IP地址 "; echo "• REMOTE_ADDR 通常是与服务器直接建立连接的设备IP"; echo " "; ?>通过主服务器IP直接访问会在REMOTE_ADDR显示客户端真实的IP地址，HTTP_X_FORWARDED_FOR显示未通过代理/CDN；通过代理服务器访问则REMOTE_ADDR显示客户端真实的IP地址，HTTP_X_FORWARDED_FOR也显示客户端真实的IP地址。这才是配置正确的情况。这里有一个小技巧，如果需要测试代理的可访问性，不需要配置DNS，这样反而会暴露备用服务器的IP地址，可以编辑自己电脑的hosts文件，指定主机名对应的IP为代理服务器IP进行测试即可。而配置错误的情况下，REMOTE_ADDR或HTTP_X_FORWARDED_FOR会显示VPC内另一个服务器的内网IP地址，这就需要重新配置，毕竟会影响网站的访问记录。直接访问的情况：通过代理访问的情况：配置错误的情况：注意⚠️：测试完成后请删除ip.php文件，避免服务器信息泄漏。配置自动切换脚本我们需要在备用服务器上面运行一个监测脚本监测主服务器状态，当主服务器被黑洞时可以更改Cloudflare DNS配置让流量通过Cloudflare代理后回源，避免访问中断且保护备用服务器。这部分就和之前那篇文章是一样的了，基本上没有什么变化：#!/bin/bash # 🐱 Cloudflare API Token CF_API_TOKEN="neko????????????????????????????????para" # 🐱 域名，例如 nekopara.uk CF_ZONE_NAME="nekopara.uk" # 🐱 记录名称列表，例如 "nekopara.uk www.nekopara.uk" CF_RECORD_NAMES=(nekopara.uk www.nekopara.uk) # 🐱 目标 IP TARGET_IP="119.8.185.128" # 🐱 目标端口 TARGET_PORT="80" # 🐱 备用 IP BACKUP_IP="172.16.1.233" # 🐱 获取 Zone ID get_zone_id() { curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=${CF_ZONE_NAME}" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" | jq -r '.result[0].id' } # 🐱 获取 DNS 记录 ID get_record_id() { local zone_id="$1" local record_name="$2" curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records?type=A&name=${record_name}" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" | jq -r '.result[0].id' } # 🐱 更新 DNS 记录 update_dns_record() { local zone_id="$1" local record_id="$2" local record_name="$3" local ip="$4" local proxied="$5" curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${record_id}" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data "{\"type\":\"A\",\"name\":\"${record_name}\",\"content\":\"${ip}\",\"ttl\":1,\"proxied\":${proxied}}" | jq } # 🐱 检测目标 IP 的 HTTP 状态码 check_target() { curl -s --connect-timeout 10 "http://${TARGET_IP}:${TARGET_PORT}" -o /dev/null return $? } # 🐱 初始化 zone_id=$(get_zone_id) declare -A record_ids for record_name in "${CF_RECORD_NAMES[@]}"; do record_ids["$record_name"]=$(get_record_id "$zone_id" "$record_name") done current_ip="$TARGET_IP" proxied=false failure_count=0 success_count=0 # 🐱 监控循环 while true; do if check_target; then echo "🐾 目标 IP ${TARGET_IP} 响应正常" failure_count=0 success_count=$((success_count + 1)) if [ "$current_ip" != "$TARGET_IP" ] && [ "$success_count" -ge 3 ]; then echo "🐾 切换回原始 IP ${TARGET_IP}，关闭代理" for record_name in "${CF_RECORD_NAMES[@]}"; do update_dns_record "$zone_id" "${record_ids[$record_name]}" "$record_name" "$TARGET_IP" false done current_ip="$TARGET_IP" proxied=false success_count=0 fi else echo "🐾 目标 IP ${TARGET_IP} 无响应" failure_count=$((failure_count + 1)) success_count=0 if [ "$current_ip" != "$BACKUP_IP" ] && [ "$failure_count" -ge 3 ]; then echo "🐾 切换到备用 IP ${BACKUP_IP}，开启代理" for record_name in "${CF_RECORD_NAMES[@]}"; do update_dns_record "$zone_id" "${record_ids[$record_name]}" "$record_name" "$BACKUP_IP" true done current_ip="$BACKUP_IP" proxied=true failure_count=0 fi fi sleep 60 done详细的配置方法，请参考《当网站遭遇DDoS：如何优雅地应对云服务器被封堵》，这里不再赘述。小尾巴最后还有一点别忘了，那就是将VPC内网IP加入iptables访问白名单，如果你按照我之前的文章设置过单IP访问连接数限制的话。在规则列表前面添加白名单：iptables -I INPUT 1 -p tcp -s 192.168.0.0/16 --dport 80 -j ACCEPT iptables -I INPUT 2 -p tcp -s 192.168.0.0/16 --dport 443 -j ACCEPT如果不这么做，当流量变大时，VPC内网的代理服务器的请求就会全部被拦截，导致访问失败，跟网站挂掉没什么两样了。（我 防 我 自 己）总结这样以来，我们就成功构建了一个在平时能够快速访问，而在被DDoS时又不会彻底挂掉的网站，用较低的成本平衡了访问速度和抗DDoS能力。在平时直接走服务器的IP提升访问速度，而在被DDoS时，通过Cloudflare代理保护源服务器。其工作的逻辑和过程大概如下：当被DDoS攻击时：主服务器EIP被打黑洞无法访问 > 备用服务器监测到主服务器无法访问 > 切换为Cloudflare代理流量，并更改DNS解析记录到备用服务器的EIP > 网站继续可访问，但是速度变慢当DDoS停止，主服务器EIP解封：备用服务器监测到主服务器正常访问 > 关闭Cloudflare代理流量，并更改DNS解析记录到主服务器的EIP > 网站恢复访问速度后记后续我发现这个方案有一个需要完善的地方，那就是——对于内网的VPC流量，没有必要走HTTPS，这会造成额外的性能损耗，最合适的方式是使用HTTP进行传输。这时，对应代理服务器的反代配置中的proxy_pass https://www.nekopara.uk:443;则需改成proxy_pass http://www.nekopara.uk:8086;，我建议是通过非标准端口进行HTTP回源，避免冲突。另外，记得不要在安全组放行这个端口给外部访问。而对于主服务器的Nginx配置文件，则增加多一个服务器块，配置可以直接复制，删掉ssl相关的内容，更改端口，设置传递参数，让PHP后端认为端口是443：# Typecho HTTP 服务器块 用于内网传输 server { listen 8086; server_name www.nekopara.uk; #信任并设置代理服务器提供的远端IP set_real_ip_from 192.168.1.255; real_ip_header X-Forwarded-For; # 设置最大上传文件大小为 32M client_max_body_size 32M; # 网站根目录 root /data/typecho; index index.html index.php; # Serve static files directly location / { try_files $uri $uri/ /index.php?$args; autoindex off; } # 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; # 【关键修改】欺骗 PHP，让它以为自己在跑 HTTPS 443 # 必须写在 include 之后，以覆盖默认值 fastcgi_param HTTP_HOST $server_name; fastcgi_param HTTPS $http_x_forwarded_proto; fastcgi_param SERVER_PORT 443; fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; # 启用 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; } # Deny access to hidden files location ~ /\.ht { deny all; } }关键是要设置fastcgi_param HTTP_HOST $server_name; 这个参数，设置完成后，VPC内网互通的网站流量就跑在HTTP上了，可以减轻服务器的TLS加密负担。
基于 E-mote 模型的动态猫娘展示与部署教程看门见山，先给一个展示地址看看效果：猫娘乐园角色E-mote图鉴Github Pages部署的纯静态版本：猫娘乐园角色E-mote图鉴这篇文章或许是最符合网站域名的文章之一？项目缘起很久之前，在我切换到Linux系统后，发现我之前最喜欢的猫娘乐园动态壁纸没办法在Linux上使用，然后根据那个第三方壁纸的相关信息，刨根问底发现了FreeMote这个开源项目，并且看到了他的Web段实现了Demo，就萌生了这个项目的想法了。但是碍于那时候技术能力和能分配的时间有限，于是这个想法就一直搁置了。最近不知道哪根经搭错了，突然就想把这个多年前的想法去实现，重新翻阅了相关的开源项目，借助AI，然后发现当时不能成功是疏漏了一些东西，于是打算再次去尝试实现。一开始的那几天天天碰壁，试了很多不同的方法都不行，但是，偶然的一次机会，我观察到提取的模型和解包的模型有相似之处，于是我就尝试从中间进行合成，没想到，真成功了！接下来的工作就顺风顺水了，但是就是很累人，要组织和转换每一个模型，八个猫娘，主要的六个猫娘还有好几套衣服，那个工作量是真的巨大啊！但是，如你所见，经过不懈努力，这个项目做出来了！接下来，请让我为你介绍——「NekoWebShow」开发技术路线简单来说，这个项目最核心的部分就是FreeMote-SDK，没有这个开源的项目，本项目也无法实现。本项目是在弄清楚FreeMote这套框架逻辑后，将从UlyssesWu制作出来的的猫娘乐园角色动态壁纸解包出来的模型转换为可以让FreeMote-SDK的WebGL驱动的pure.psb格式，然后将各个模型和对应的声音组织好，写一个用于切换不同角色的前端框架，这就是我所做的事情。前期的一部分开发工作记录，我已经写了一篇文章：试玩FreeMote，把二次元老婆搬上浏览器，感兴趣的可以去看看，这篇文章关注点是如何使用和部署。因为前端需要实现的功能比较简单，所以我是直接面向LLM编程了，没有使用其他框架。本来想用MDUI的，但是后面想想还是算了。在这里，要特别感谢嘉然小姐对本项目的支持，她帮助我解决了驱动库无法加载超过32MB的pure.psb文件问题。没有她的支持，部分角色衣服的模型就没办法正常展示出来了。项目分支说明项目有两个分支，一个是php版本，另一个是html版本。之前是只打算做php版本的，毕竟可以代码复用，减少文件数量和代码量，但是后面考虑到如果是本地部署起来做动态壁纸，还要再跑一个php-fpm有点太地狱了，于是就搞了html的版本，纯静态实现，本地只需要部署一个web服务器即可。不过按理来说应该是可以直接打开本地文件而不需要服务器的，但是不知道为什么firefox打开就是报错，显示被阻止，所以也只好设置一个服务器了。不过，后面我试了一下，使用KDE桌面的HTML Wallpaper插件，直接指定本地文件就像，不会有报错，也可以正常显示，估计只是firefox的限制（所以，要是这样就非常好了，本地设置成壁纸web服务器都省了。部署方法一：PHP环境部署首先，确保你有一台服务器，部署上web服务器和php环境，我这里选择nginx和php，采用的操作系统为Alpine Linux 3.20。apk add php php-fpm openssl nginx rc-update add php-fpm83 default rc-update add nginx default service php-fpm82 start service nginx start这样一通操作下来，我们的基本环境就有了。接下来就是配置一下默认页面的规则，当然这是可选项，你可以直接一股脑用默认直接http部署，但是我并不推荐，我认为还是设置个域名配合SSL证书比较好。设置默认匹配页面我们先来生成一个默认的SSL自签名证书用于没有匹配到域名时错误提示页面使用。mkdir -p /etc/nginx/ssl/ openssl genpkey -algorithm RSA -out /etc/nginx/ssl/selfsigned.key -pkeyopt rsa_keygen_bits:2048 openssl req -new -x509 -key /etc/nginx/ssl/selfsigned.key -out /etc/nginx/ssl/selfsigned.crt -days 365 -subj "/"这样子就生成了自签名证书，证书和密钥位置分别在/etc/nginx/ssl/selfsigned.crt和/etc/nginx/ssl/selfsigned.key然后，删除/etc/nginx/http.d/default.conf，再重新创建：rm /etc/nginx/http.d/default.conf nano /etc/nginx/http.d/default.conf内容替换为如下内容：server { listen 80 default_server; #listen [::]:443 ssl default_server; server_name _; # 这是一个通配符，表示任何未匹配的主机名 #return 444; # 关闭连接，不发送任何响应 root /var/www/; # 静态文件的位置 index index.html; # 默认主页 location / { try_files $uri $uri/ =404; # 尝试提供请求的文件或目录，如果不存在则返回404 } # 处理错误页面 error_page 404 /404.html; location = /404.html { internal; } } server { listen 443 ssl default_server; #listen [::]:443 ssl default_server; server_name _; # 这是一个通配符，表示任何未匹配的主机名 ssl_certificate /etc/nginx/ssl/selfsigned.crt; ssl_certificate_key /etc/nginx/ssl/selfsigned.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'; root /var/www/; # 静态文件的位置 index index.html; # 默认主页 location / { try_files $uri $uri/ =404; # 尝试提供请求的文件或目录，如果不存在则返回404 } # 处理错误页面 error_page 404 /404.html; location = /404.html { internal; } } server { listen 80; server_name *.nekopara.uk; # 302 Redirect all traffic to the corresponding HTTPS URL location / { return 302 https://$host$request_uri; } }记得在/var/www/创建一个错误提示页面index.html:nano /var/www/index.html内容我（宝塔 x)已经帮你想好了： 没有找到站点 没有找到站点 您的请求在Web服务器中没有找到对应的站点！ 可能原因： 您没有将此域名或IP绑定到对应站点! 配置文件未生效! 如何解决： 检查是否已经绑定到对应站点，若确认已绑定，请尝试重载Web服务； 检查端口是否正确； 若您使用了CDN产品，请尝试清除CDN缓存； 普通网站访客，请联系网站管理员； 默认页面设置好了，接下来就是设置对应的站点了。部署NekoWebShow我个人使用习惯是在根目录创建/data目录用于存放各种东西，先让我们克隆项目：mkdir /data cd /data/ git clone --depth 1 https://github.com/Chocola-X/NekoWebShow然后就是申请证书并且解析域名到你的服务器，这里不再赘述，有acme.sh可以用，当然你也可以用我的网页在线生成证书：猫娘☆免费SSL证书申请申请完成的证书，放到/data/certs/目录下。包括证书和私钥，例如_nekopara_uk.pem和_nekopara_uk.key。然后是配置nginx服务器：nano /etc/nginx/http.d/nekowebshow.conf配置文件内容如下：server { listen 443 ssl; server_name chocola.nekopara.uk vanilla.nekopara.uk azuki.nekopara.uk coconut.nekopara.uk fraise.nekopara.uk maple.nekopara.uk milk.nekopara.uk cinnamon.nekopara.uk show.nekopara.uk; # SSL certificate and key paths ssl_certificate /data/certs/_nekopara_uk.pem; ssl_certificate_key /data/certs/_nekopara_uk.key; # SSL settings (optional but recommended for security) ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; # Root directory of the website root /data/nekowebshow; index index.html index.php; # Logging #access_log /var/log/nginx/nekopara.uk.access.log; #error_log /var/log/nginx/nekopara.uk.error.log; # Serve static files directly location / { try_files $uri $uri/ /index.html /index.php?$args; } # PHP processing location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # Deny access to hidden files location ~ /\.ht { deny all; } }注意将域名替换为你实际的域名，以及你实际的证书路径，证书建议使用通配符证书。因为我的域名nekopara.uk很符合主题，所以我给每一个角色都添加了子域名，例如chocola.nekopara.uk就很有趣了。对于其他域名，请根据需要自行选择。这时候，打开对应的域名，应该是可以正常工作了，但是你会发现打开新的角色图鉴，就会跳转到我的网站。不要急不要哭，打开index.php让我们来改几个参数即可，这就是为什么我选择php构建的原因。打开index.php找到开头这里：$domain_chocola = 'https://chocola.nekopara.uk'; $domain_vanilla = 'https://vanilla.nekopara.uk'; $domain_azuki = 'https://azuki.nekopara.uk'; $domain_coconut = 'https://coconut.nekopara.uk'; $domain_maple = 'https://maple.nekopara.uk'; $domain_cinnamon = 'https://cinnamon.nekopara.uk'; $domain_milk = 'https://milk.nekopara.uk'; $domain_fraise = 'https://fraise.nekopara.uk'; $background_url = './img/bg.png';替换为对应的域名即可，例如第一条替换为：$domain_chocola = 'https://chocola.example.com';然后背景也是可以修改的，替换掉bg.png或者改为别的背景图URL即可，但是我个人还是认为游戏原本的背景比较搭配。。。修改完成后，也就部署好了。你可以之作为一个普通的角色图鉴使用，当然，我认为更好的做法是把这个做成动态壁纸，使用 KDE 的 HTML Wallpaper 设置对应的URL即可。因为本来我开发这个的初衷，就是想要一个好看的动态壁纸。部署好后Be Like:部署方法二：静态HTML部署相比于方法一，这个就不需要设置php环境了，能够减小系统性能开销。缺点就是，如果你想修改一些网页参数，会比较麻烦。更合适的做法是用在本地桌面背景，而不是展示了。web服务器部署当然，你愿意折腾的话，也不是不行，参照方法一配置nginx环境和证书即可，php就不需要安装了，配置方法这里就不再赘述，就是配置文件nekowebshow.conf要稍微改变一下：server { listen 443 ssl; server_name chocola.nekopara.uk vanilla.nekopara.uk azuki.nekopara.uk coconut.nekopara.uk fraise.nekopara.uk maple.nekopara.uk milk.nekopara.uk cinnamon.nekopara.uk show.nekopara.uk; # SSL certificate and key paths ssl_certificate /data/certs/_nekopara_uk.pem; ssl_certificate_key /data/certs/_nekopara_uk.key; # SSL settings (optional but recommended for security) ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; # Root directory of the website root /data/nekowebshow; index index.html index.php; # Logging #access_log /var/log/nginx/nekopara.uk.access.log; #error_log /var/log/nginx/nekopara.uk.error.log; location / { try_files $uri $uri/ =404; # 尝试提供请求的文件或目录，如果不存在则返回404 } # 处理错误页面 error_page 404 /404.html; location = /404.html { internal; } }要注意，克隆的时候，请克隆html_version分支：git clone --depth 1 --branch html_version https://github.com/Chocola-X/NekoWebShow当然，纯html的版本我已经在Github Pages部署了一份了，如果只是想要设置个动态壁纸不想部署的话，直接设置这个URL就好了。但是缺点是断网就寄。直接用Github Pages链接部署成桌面背景Be Like:本地文件部署如果你设置动态壁纸的软件支持直接读取本地文件且不报错的话，推荐采用这种方式。当然，本地打开html文件，firefox已经拦截跨文件夹请求了（找一个你适合存放这个壁纸的文件夹，壁纸体积大概2.9G，然后和上面一样克隆项目：git clone --depth 1 --branch html_version https://github.com/Chocola-X/NekoWebShow然后在HTML Wallpaper设置壁纸路径即可享用。虽然但是有点吃性能就是了（
浅尝开源项目：FreeMote这是一个比较冷门的开源项目，可以看作是E-Mote的开源实现。之前我还是用Windows做主力机使用的Nekopara动态壁纸就是这个项目实现的，而现在我已经切换到Linux作为主力系统使用一坤年了，是时候想想办法把我的巧克力弄到我的桌面上了。Linux想搞动态壁纸，主要就是两种：视频和网页。而FreeMote-SDK就有网页的WebGL实现。很早之前我就尝试过一次，但是那时候技术力不足，而且没学会查看文档，失败了。这次，既然我都能写文章发出来了，那肯定是取得了阶段性的进展了。相比于Live2D，E-Mote的应用就比较少了，主要是几个Galgame在用，其中就包括NEKOPARA。而绝大部分网站的动态二次元看板娘都是Live2D实现的，E-mote几乎看不见。所以文档少，案例少，基本上是摸著石头过河。我主要是研究网页端的FreeMote，下面，就开始探索吧。Demo分析这是项目Project-AZUSA给的网页Demo project-azusa.github.io 基本上可以在网页实现E-mote的功能。不过看了一下FreeMote-SDK，文档约等于没有，只能自己根据网页Demo进行推断了。看描述，说是需要Pure(unencrypted) PSB文件，当然，此PSB不是Photoshop的文件，而是这一套工具的文件格式。为了得到这个文件，则需要FreeMote的这些工具进行转换。接下来就是那个Demo网页用到的文件，我已经爬取下来了：. ├── data │ └── emote_test2.pure.emtbytes ├── driver │ ├── emoteplayer.js │ └── FreeMoteDriver.js ├── img │ └── bg.jpg ├── index.html └── main.js 4 directories, 6 files从文件命名就可以看出，emote_test2.pure.emtbytes是模型文件。但是其格式却不是psb？但是也有可能只是换了一下后缀名。搞清楚了大概的情况，关键点就在于模型文件的获取，以及尝试去替换另一个模型。虽然FreeMote-SDK没有什么文档，但是FreeMote项目的文档还是相对比较完善的，对于生成特定的模型文件具有比较高的参考价值。Demo模型转换测试开始之前，只需要下载FreeMote的工具集即可，在Release里面就有下载了。首先是先要分析现有的文件，按照文档的说法，我决定先尝试解包实例Demo的模型文件：[chocola@chocola-sa5212m4 data]$ wine ../../Ulysses-FreeMoteToolkit-v4.0.3/PsbDecompile.exe ./emote_test2.pure.emtbytes 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Decompiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Decompiling: emote_test2.pure Done. [chocola@chocola-sa5212m4 data]$ ls emote_test2.pure emote_test2.pure.emtbytes emote_test2.pure.json emote_test2.pure.resx.json [chocola@chocola-sa5212m4 data]$ tree . ├── emote_test2.pure │ └── tex-texture.png ├── emote_test2.pure.emtbytes ├── emote_test2.pure.json └── emote_test2.pure.resx.json 2 directories, 4 files [chocola@chocola-sa5212m4 data]$ 可以看到解包后有tex-texture.png纹理图片和emote_test2.pure.json设置文件，其中纹理图片和Live2D的纹理图片看起来很像。然后，再尝试合成回psb文件：[chocola@chocola-sa5212m4 data]$ wine ../../Ulysses-FreeMoteToolkit-v4.0.3/PsBuild.exe ./emote_test2.pure.json 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Compiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Compiling emote_test2.pure ... Compile emote_test2.pure done. Done. [chocola@chocola-sa5212m4 data]$ ls emote_test2.pure emote_test2.pure.emtbytes emote_test2.pure.json emote_test2.pure.pure.psb emote_test2.pure.resx.json可以看到，得到了emote_test2.pure.pure.psb文件，尝试去替换掉原本的emote_test2.pure.emtbytes文件，看看能不能正常工作。这里需要修改main.js文件，找到文件加载路径，将emote_test2.pure.emtbytes替换为emote_test2.pure.pure.psb就可以了： // load data then, register mouse event player.promiseLoadDataFromURL("./data/emote_test2.pure.pure.psb")然后我测试了一下，一切正常，说明整个工具链是可以用的，WebGL端确实是可以使用工具生成的pure.psb文件。尝试替换模型因为项目文档里面说有psb测试文件可以供使用，存放的仓库是FreeMote.Samples，于是我就去下了一个模型：[FlattenArray][PSBv4][win]dx_ふかみ_駅員服.psb，来进行尝试。因为这个模型文件名太长了不方便，我直接重命名为test.psb了，下面就是操作：[chocola@chocola-sa5212m4 Ext_Asset]$ wine ../Ulysses-FreeMoteToolkit-v4.0.3/PsbDecompile.exe test.psb 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Decompiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Decompiling: test Done. [chocola@chocola-sa5212m4 Ext_Asset]$ tree . ├── test │ └── tex-texture.png ├── test.json ├── test.psb └── test.resx.json 2 directories, 4 files [chocola@chocola-sa5212m4 Ext_Asset]$ wine ../Ulysses-FreeMoteToolkit-v4.0.3/PsBuild.exe test.json 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Compiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Compiling test ... Compile test done. Done. [chocola@chocola-sa5212m4 Ext_Asset]$ wine ../Ulysses-FreeMoteToolkit-v4.0.3/EmtConvert.exe test.pure.psb.lz4 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Converter by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Shell type: LZ4 Done. [chocola@chocola-sa5212m4 Ext_Asset]$ wine ../Ulysses-FreeMoteToolkit-v4.0.3/FreeMoteViewer.exe test.pure.psb.decompressed.psb 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote Viewer by Ulysses, wdwxy12345@gmail.com 最后一个命令，是查看生成的模型test.pure.psb.decompressed.psb是否正常，界面大概长这样：确定无误后，我满怀信心的将模型替换到WebGL端，结果。。。居然报错了？不过细看红色框出来的地方：motion file 'template' has not adaptive spec [win] of motion file. export psb again.似乎是因为模型属性不兼容，针对win这个关键字，我检索了两个模型的json文件。好兄弟，看出来区别了吗？正常运行的那个模型spec和Platform对应的值是ems而不能运行的对应的是win，所以。。。改一下？把win全换成ems。然后再重新转换一下：[chocola@chocola-sa5212m4 Ext_Asset]$ wine ../Ulysses-FreeMoteToolkit-v4.0.3/PsBuild.exe test.json 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Compiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Compiling test ... Compile test done. Done. [chocola@chocola-sa5212m4 Ext_Asset]$ wine ../Ulysses-FreeMoteToolkit-v4.0.3/EmtConvert.exe test.pure.psb.lz4 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Converter by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Shell type: LZ4 Done.不过，似乎改完之后颜色变奇怪了？然后发现原来的那个模型文件颜色也是奇怪的，那没事了，直接开始尝试！虽然动作有点奇怪，但是至少成功了！获取猫娘模型我先不打算解包NEKOPARA，怕东西太多了，打算先拿这个项目的作者自己搞得猫娘动态壁纸开刀。对，就是我之前提到的在Windows上面用的动态壁纸。看了一下那个动态壁纸的文件，应该是Unity3D做的，那只需要解包工具就好了，使用AssetStudio就可以完美解包了。资源的文件在NekoWallpaper_Data/data.unity3d。用AssetStudioGUI打开这个文件，在Asset List选项卡中，点一下Container标题，就可以按照存放的目录排序了，这样子比较好找模型。我们就拿牛奶来开刀吧，需要的文件是milk-winter和milk-winter-b_tex000.png，把这两个文件提取出来。其中没有后缀名称这个，个人推断是psb文件。然后对没有后缀名的文件进行转换：[chocola@chocola-sa5212m4 winter]$ wine ../../../Ulysses-FreeMoteToolkit-v4.0.3/PsbDecompile.exe milk-winter 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Decompiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Decompiling: milk-winter Done. [chocola@chocola-sa5212m4 winter]$ tree . ├── milk-winter ├── milk-winter-b_tex000.png ├── milk-winter.json ├── milk-winter-resources │ └── tex-texture.png └── milk-winter.resx.json 2 directories, 5 files可以看到多了一个纹理图片，但是我查看过后发现实际上是空图片，所以，我将提取出来的图片替换掉了解包出来的空图片。然后，合成psb:[chocola@chocola-sa5212m4 winter]$ wine ../../../Ulysses-FreeMoteToolkit-v4.0.3/PsBuild.exe milk-winter.json 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Compiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Compiling milk-winter ... Compile milk-winter done. Done.但是，有一部分的模型，解包出来是没有纹理的：[chocola@chocola-sa5212m4 pajama]$ tree . ├── vanilla-pajama ├── vanilla-pajama.json └── vanilla-pajama.resx.json 1 directory, 3 files这时候就需要自己添加纹理了，先将提取出来的纹理放到同一个目录下：[chocola@chocola-sa5212m4 pajama]$ tree . ├── vanilla-pajama ├── vanilla-pajama-a-ex2-type2_tex000.png ├── vanilla-pajama.json └── vanilla-pajama.resx.json 1 directory, 4 files在生成psb文件后关联纹理：[chocola@chocola-sa5212m4 pajama]$ wine ../../../Ulysses-FreeMoteToolkit-v4.0.3/PsBuild.exe vanilla-pajama.json 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Compiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Compiling vanilla-pajama ... Compile vanilla-pajama done. Done. [chocola@chocola-sa5212m4 pajama]$ wine ../../../Ulysses-FreeMoteToolkit-v4.0.3/PsBuild.exe link vanilla-pajama.pure.psb vanilla-pajama-a-ex2-type2_tex000.png 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Compiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Link vanilla-pajama.pure done. Done. [chocola@chocola-sa5212m4 pajama]$ tree . ├── vanilla-pajama ├── vanilla-pajama-a-ex2-type2_tex000.png ├── vanilla-pajama.json ├── vanilla-pajama.pure.linked.psb ├── vanilla-pajama.pure.psb └── vanilla-pajama.resx.json 1 directory, 6 files其中，vanilla-pajama.pure.linked.psb就是最终文件了。处理完成后，可以看到，效果非常好：接下来，如前所述，改一下属性值，尝试放到网页上。真不错，下一步工作就是继续完善了，这个阶段性成果我已经很满意了！
当网站遭遇DDoS：如何优雅地应对云服务器被封堵
网站被DDoS后制定解决方案很平常的一天，我在网站主题交流群里，发现有群友网站被打掉线了。刚刚给他发“贺电”，没过多久，我也收到了华为云发来的“贺电”：尊敬的(Rin):感谢您使用华为云产品，非常抱歉打扰您。在北京时间2025/05/22 20:33，华为云监测到您的弹性公网IP(EIP)实例 发生EIP封堵事件。 详情：节点：亚太-新加坡实例名称：119.8.?.?实例ID：88????e7-1??7-4??a-8??d-0??????????5告警描述：{"reason_zh": 尊敬的Rin：华为云DDoS防护服务在2025-05-22 20:33:05检测到您的EIP 119.8.?.? 异常流量已超过DDoS防护峰值触发封堵，封堵时带宽为 67 Gbps, 您服务器的访问将受到限制，24小时后系统将自动为您解封。您可登陆官网购买DDoS高防服务提升防护能力，保障服务器稳定运行。, "reason_en": Dear Rin: HUAWEI CLOUD Anti-DDoS Service detects that your EIP 119.8.?.? abnormal traffic has exceeded the DDoS protection peak at 2025-05-22 20:33:05 and triggers blocking. Bandwidth when blocking is 67 Gbps, Access to your server will be restricted, and the system will automatically unblock it for you after 24 hours. You can log in to the official website of HUAWEI CLOUD to purchase Advanced Anti-DDoS service to enhance protection capabilities and ensure stable server operation.}原因分析：尊敬的Rin：华为云DDoS防护服务在2025-05-22 20:33:05检测到您的EIP 119.8.?.? 异常流量已超过DDoS防护峰值触发封堵，封堵时带宽为 67 Gbps您服务器的访问将受到限制，24小时后系统将自动为您解封。您可登陆官网购买DDoS高防服务提升防护能力，保障服务器稳定运行。如有疑问，您可以登录华为云官网提交工单咨询，会有相应工程师为您核实查看。而且连着几个群友的网站都被攻击，甚至有人还被短信轰炸了，初步断定就是群里面有人搞鬼，但是没有证据抓现行，只能自己做好预案和防护了。由于DDoS攻击基本上都会被云服务器提供商黑洞IP，如果只有单台云服务器，这个真的无解。但是，我有不止一台机器，这可就有解决办法了。我的大体解决思路是这样的：配置服务器访问频率限制，防止高频恶意请求（CC攻击）耗尽服务器资源。设置主服务器定期备份内容到备用服务器。备用服务器部署检测，一分钟一次，判定主服务器状态，如果超过三次无响应，判定为被打挂了。判定打挂后，通过Cloudflare的API，设置域名的A记录IP地址为备用服务器IP地址，并开启Cloudflare的代理防护。当主服务器恢复后，自动将DNS记录切换回主服务器，然后将备份的内容还原到主服务器。接下来，让我们开始任务吧。这里说一下环境条件：目标操作系统为 Alpine Linux 3.20 ，LNMP环境都为系统默认设置安装的，备份的目标为Typecho博客。配置服务器访问频率限制虽然我一开始网站就已经做了缓存配置，是能抗高并发的，不至于像某些人的网站itdog网站测速一测就死。但是，如果是高频的请求，还是会耗尽带宽的资源，导致网络阻塞，访问变慢。所以，是时候配合iptables和内核设置来阻挡恶意请求了。首先是拉黑IP，平时注意留意服务器的网络负载，如果遇到有过高负载历史的时候，善于使用Nginx的日志进行IP追溯，拉黑恶意IP。拉黑命令如下：iptables -A INPUT -s 45.140.143.77 -j DROP iptables -A INPUT -s 106.15.224.186 -j DROP其次，就是设置单个IP的最大连接数，对超出的部分数据包进行丢弃处理（如果是拒绝处理，还是会回传数据包导致上行被占）。这里设置为20：iptables -A INPUT -p tcp --dport 80 -m connlimit --connlimit-above 20 --connlimit-mask 32 -j DROP iptables -A INPUT -p tcp --dport 443 -m connlimit --connlimit-above 20 --connlimit-mask 32 -j DROP一并设置内核参数保证连接处理数量足够：sysctl -w net.netfilter.nf_conntrack_max=10240由于直接丢弃数据包会造成半连接状态，仍然会消耗资源，所以要进行一些规则防止资源耗尽。#防御 SYN Flood # 启用 SYN Cookies（防御 SYN Flood） sysctl -w net.ipv4.tcp_syncookies=1 # 限制 SYN 请求速率（每秒最多 20 个） iptables -A INPUT -p tcp --syn --dport 443 -m connlimit --connlimit-above 20 -j DROP iptables -A INPUT -p tcp --syn --dport 80 -m connlimit --connlimit-above 20 -j DROP # 缩短 SYN 重试次数（加速无效连接释放） sysctl -w net.ipv4.tcp_syn_retries=2 sysctl -w net.ipv4.tcp_synack_retries=2至此，这个设置已经可以防御常见的CC攻击手段。不过需要注意的是，以上命令只是临时生效，重启后就会失效，如果需要永久生效，则需要写入配置文件。不过考虑到云服务器很少重启，问题也不大。设置定期备份我打算使用mysqldump进行数据库导入和导出操作，然后使用rsync进行内容同步，最后使用chmod和chown设置网站目录权限。配置ssh密钥登陆首先，如果要自动化，则需要配置ssh密钥登陆。在客户端生成SSH密钥对：ssh-keygen -t rsa -b 4096 -C "your_email@nekopara.uk"执行后一路回车即可。然后，将公钥复制到服务器：ssh-copy-id root@server.nekopara.uk提示输入密码，输入目标服务器密码即可完成操作。这里your_email@nekopara.uk请替换为你实际邮箱，server.nekopara.uk替换为实际服务器的IP或者域名，推荐是IP。设置自动复制规则在主服务器上，将以下内容保存为backup_blog.sh放到root文件夹：/usr/bin/mysqldump -u typecho_user -p'p@sswd' typecho > "/data/database/typecho_backup.sql" /usr/bin/rsync -avz --no-perms --delete -e ssh /data/ root@backupserver.nekopara.uk:/data/ /usr/bin/ssh root@backupserver.nekopara.uk /root/set_website.sh然后赋予执行权限：chmod +x /root/backup_blog.sh相应的，在备份服务器上，也要在root文件夹创建一个set_website.sh文件进行文件权限的设置和数据库导入：/bin/chown -R nginx:nginx /data/typecho/ /bin/chmod -R 755 /data/typecho/ /usr/bin/mysql -u typecho_user -p'p@sswd' typecho < /data/database/typecho_backup.sql设置完成后，自己执行一次backup_blog.sh脚本看看有没有备份成功。要注意，这里的backupserver.nekopara.uk请替换为实际备份服务器的IP或者域名，推荐是IP。typecho_user替换为实际博客数据库的用户名，p@sswd替换为实际数据库的密码，typecho替换为实际数据库。测试成功后，配置crontab自动任务，一个小时执行一次：0 * * * * "/root"/backup_blog.sh >> /var/log/backup.log 2>&1设置状态检测和切换策略首先，这个前提条件是你要去Cloudflare获取区域API令牌：打开目标站点>点击“获取您的 API 令牌”>点击“创建令牌”>使用“编辑区域 DNS”模板>然后选择特定区域，创建令牌记录下这个令牌，后面会用到，而且以后不再可以查看了，请妥善保存。然后，在root文件夹创建一个dns_auto_update.sh，内容如下：#!/bin/bash # 🐱 Cloudflare API Token CF_API_TOKEN="neko????????????????????????????????para" # 🐱 域名，例如 nekopara.uk CF_ZONE_NAME="nekopara.uk" # 🐱 记录名称列表，例如 "nekopara.uk www.nekopara.uk" CF_RECORD_NAMES=(nekopara.uk www.nekopara.uk) # 🐱 目标 IP TARGET_IP="119.8.?.?" # 🐱 目标端口 TARGET_PORT="80" # 🐱 备用 IP BACKUP_IP="172.16.1.233" # 🐱 获取 Zone ID get_zone_id() { curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=${CF_ZONE_NAME}" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" | jq -r '.result[0].id' } # 🐱 获取 DNS 记录 ID get_record_id() { local zone_id="$1" local record_name="$2" curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records?type=A&name=${record_name}" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" | jq -r '.result[0].id' } # 🐱 更新 DNS 记录 update_dns_record() { local zone_id="$1" local record_id="$2" local record_name="$3" local ip="$4" local proxied="$5" curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${record_id}" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data "{\"type\":\"A\",\"name\":\"${record_name}\",\"content\":\"${ip}\",\"ttl\":1,\"proxied\":${proxied}}" | jq } # 🐱 检测目标 IP 的 HTTP 状态码 check_target() { curl -s --connect-timeout 10 "http://${TARGET_IP}:${TARGET_PORT}" -o /dev/null return $? } # 🐱 初始化 zone_id=$(get_zone_id) declare -A record_ids for record_name in "${CF_RECORD_NAMES[@]}"; do record_ids["$record_name"]=$(get_record_id "$zone_id" "$record_name") done current_ip="$TARGET_IP" proxied=false failure_count=0 success_count=0 # 🐱 监控循环 while true; do if check_target; then echo "🐾 目标 IP ${TARGET_IP} 响应正常" failure_count=0 success_count=$((success_count + 1)) if [ "$current_ip" != "$TARGET_IP" ] && [ "$success_count" -ge 3 ]; then echo "🐾 切换回原始 IP ${TARGET_IP}，关闭代理" for record_name in "${CF_RECORD_NAMES[@]}"; do update_dns_record "$zone_id" "${record_ids[$record_name]}" "$record_name" "$TARGET_IP" false done current_ip="$TARGET_IP" proxied=false success_count=0 fi else echo "🐾 目标 IP ${TARGET_IP} 无响应" failure_count=$((failure_count + 1)) success_count=0 if [ "$current_ip" != "$BACKUP_IP" ] && [ "$failure_count" -ge 3 ]; then echo "🐾 切换到备用 IP ${BACKUP_IP}，开启代理" for record_name in "${CF_RECORD_NAMES[@]}"; do update_dns_record "$zone_id" "${record_ids[$record_name]}" "$record_name" "$BACKUP_IP" true done current_ip="$BACKUP_IP" proxied=true failure_count=0 fi fi sleep 60 done运行这个脚本需要确保jq和curl软件包已经安装。这个shell脚本前面是配置部分，请根据注释的提示替换为实际内容。脚本策略是，每60秒（一分钟）检测一次目标站点的http服务，如果目标站点连续三次异常，则切换到备用服务器并开启Cloudflare的代理防护。当目标服务器恢复正常后，则切换回去。确认脚本无误后，在备用服务器上面创建一个tmux终端挂着就行了：tmux new -t dns_auto ./dns_auto_update.sh总结一下其实漏了一步——将备份服务器的内容还原回目标服务器。这部分就跟第一部分是同理的，不加crontab计划任务就行了。也可以结合切换脚本自行实现。由于我网站流量比较小，对内容的及时性没有这么高的要求，在被打期间我也不会写文章。最多就是在被打期间如果有人评论，那这个评论会在主服务器恢复后的数据同步中被清除，无非就是丢一两条评论，而且大部分时间也没人评论，所以无所谓了。现在想想如果真的搞了，万一恢复的时间赶在同步时间的点子上，冲突搞炸数据库就不好玩了。其实就算是主服务器备份到备胎服务器，也不一定需要计划任务，每次更新文章后连上去执行一下脚本就行，顺手的事情。切换期间如果真有评论想要保留的，手动迁移过去也不费什么事，毕竟被打也算是小概率事件，玩网站一年多了才被打这一次。或许有人会好奇，既然被打就是切Cloudflare，为什么不一直套Cloudflare，这样不就不怕打了吗？其实我不怎么做是有原因的，因为我还是希望网站速度能快一些，而切了Cloudflare，那速度自然就快不起来了。而且，一直套Cloudflare降低网站国内访问速度和质量，和在关键时刻因为DDoS临时切换Cloudflare降低速度和质量，哪个更好还是看得出来的。当然，怎么玩是建立在你有两个服务器到的基础上，如果只有一个，打寄了就寄了，一点办法都没有。其实之前就已经做好被打的预案了，但是实际发生了被DDoS的时候，还是有点手忙脚乱，这下能让我的网站更加完善了。最主要还是想保持网站不掉线罢了。
二次开发 Typecho 后台服务器状态查看插件
发现Typecho后台服务器状态插件在翻Typecho插件的时候，我偶然发现了两双筷子写的Typecho后台服务器状态查看插件，于是就尝试了一下下。这个插件在桌面端网页没问题，感觉还挺不错的。但是一旦变成移动端，后台的布局就会出现问题，逼死强迫症。而且，CPU利用率有时候还会变成0.9999999999%这种离谱的数据，我寻思着就不能取两位小数好看一点吗？于是，我决定，自己修改代码，把服务器信息移动到页面底部的位置，并且修改显示数据的逻辑，让信息看着更加舒适一点。（其实也不完全是我写，是面向LLM编程 X）修改代码的过程首先，需要把注册方法的地方改为页面底部Typecho_Plugin::factory('admin/footer.php')->begin = array('LinuxInfo_Plugin', 'render');然后就是修改一下插入的样式：echo <<<STR $info_str STR;因为我觉的系统信息用鼠标悬浮预览不明显，于是修改为直接独立一行把信息全部展示：return <<<SYS_INFO 服务器信息: 持续运行：{$uptime}，Web服务器：{$_SERVER['SERVER_SOFTWARE']}，PHP 版本：{$php['version']}，SAPI 接口：{$php['sapi']}，PHP内存限制：{$php['mem_max']}，上传文件限制：{$php['up_max']} SYS_INFO;以及让LLM帮忙完善了CPU信息获取的逻辑：private static function cpu_info(){ function GetCoreInformation() { $data = file('/proc/stat'); $cores = []; foreach( $data as $line ) { if( preg_match('/^cpu[0-9]/', $line) ) { $info = explode(' ', $line); $cores[] = [ 'user' => $info[1], 'nice' => $info[2], 'sys' => $info[3], 'idle' => $info[4], 'iowait' => $info[5], 'irq' => $info[6], 'softirq' => $info[7] ]; } } return $cores; } function GetCpuPercentages($stat1, $stat2) { if(count($stat1) !== count($stat2)){ return; } $cpus = []; for( $i = 0, $l = count($stat1); $i < $l; $i++) { $dif = []; $dif['user'] = $stat2[$i]['user'] - $stat1[$i]['user']; $dif['nice'] = $stat2[$i]['nice'] - $stat1[$i]['nice']; $dif['sys'] = $stat2[$i]['sys'] - $stat1[$i]['sys']; $dif['idle'] = $stat2[$i]['idle'] - $stat1[$i]['idle']; $dif['iowait'] = $stat2[$i]['iowait'] - $stat1[$i]['iowait']; $dif['irq'] = $stat2[$i]['irq'] - $stat1[$i]['irq']; $dif['softirq'] = $stat2[$i]['softirq'] - $stat1[$i]['softirq']; $total = array_sum($dif); $cpu = []; foreach($dif as $x=>$y) $cpu[$x] = round($y / $total * 100, 2); $cpus['CPU' . $i] = $cpu; } return $cpus; } $stat1 = GetCoreInformation(); sleep(1); $stat2 = GetCoreInformation(); $data = GetCpuPercentages($stat1, $stat2); $overall_cpu_usage = 0; $core_details = []; foreach ($data as $core => $usage) { $core_usage = 100 - $usage['idle']; $overall_cpu_usage += $core_usage; $core_details[] = "{$core}: " . number_format($core_usage, 2) . "%"; } $overall_cpu_usage = round($overall_cpu_usage / count($data), 2); $core_details_str = implode(", ", $core_details); $sys_load = sys_getloadavg(); $sys_load_1min = number_format($sys_load[0], 3); $sys_load_5min = number_format($sys_load[1], 3); $sys_load_15min = number_format($sys_load[2], 3); return <<<CPU_INFO CPU: {$overall_cpu_usage} % CPU_INFO; }原来的逻辑只是判断单个CPU的占有率，当CPU数量为多个的时候就会不准确，这里我优化成了整体利用率，并且悬停时会显示每个CPU的占用率信息。改造效果虽然不是彻底重构，但是我对改造后的插件还是比较满意的。手机的网页端也舒服了。修改后的插件分享修改后的插件已经放到了GitHub：Typecho-LinuxInfo反正源码也不多，我这里也可以直接放出来：Plugin.php:<?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; /** * 查看系统信息，仅限 Linux 系统，基于两双筷子的插件进行二次开发，使用帮助 * * @package LinuxInfo * @author GTX690战术核显卡导弹 * @version 1.1.0 * @link https://www.nekopara.uk */ class LinuxInfo_Plugin implements Typecho_Plugin_Interface { // 插件名称 protected static $plugin_name = 'LinuxInfo'; /** * 激活插件方法,如果激活失败,直接抛出异常 * * @access public * @return void * @throws Typecho_Plugin_Exception */ public static function activate() { // 如果当前系统不是 linux 则抛出错误 if(PHP_OS != 'Linux') { throw new Typecho_Plugin_Exception(_t("此插件仅支持 Linux 系统")); } // 在后端页面底部 注册方法 Typecho_Plugin::factory('admin/footer.php')->begin = array('LinuxInfo_Plugin', 'render'); } /** * 禁用插件方法,如果禁用失败,直接抛出异常 * * @static * @access public * @return void * @throws Typecho_Plugin_Exception */ public static function deactivate(){} /** * 获取插件配置面板 * * @access public * @param Typecho_Widget_Helper_Form $form 配置面板 * @return void */ public static function config(Typecho_Widget_Helper_Form $form) { /** 分类名称 */ $show_item = array( 'cpu_info' => _t('CPU使用情况') . '（ CPU 使用率、系统负载）', 'mem_info' => _t('内存使用情况') . '（内存使用率、SWAP交换分区）', 'disk_info' => _t('磁盘使用情况') . '（磁盘使用率、磁盘已用空间、磁盘总空间）', 'sys_info' => _t('服务器信息') . ' （系统运行时间、服务器软件、PHP 版本、SAPI 接口、PHP 内存限制、上传文件限制）', ); // 设置默认选中 $show_option_value = ['cpu_info','mem_info','disk_info']; $ShowInfo = new Typecho_Widget_Helper_Form_Element_Checkbox('show_items',$show_item, $show_option_value, _t('选择显示系统信息')); $form->addInput($ShowInfo->multiMode()); $separator = new Typecho_Widget_Helper_Form_Element_Text('separator', NULL, ' ★ ', _t('分隔符'), '设置数据项分割字符'); $form->addInput($separator); } /** * 个人用户的配置面板 * * @access public * @param Typecho_Widget_Helper_Form $form * @return void */ public static function personalConfig(Typecho_Widget_Helper_Form $form){} /** * 插件实现方法 * * @access public * @return void */ public static function render() { $show_items = Typecho_Widget::widget('Widget_Options')->plugin(self::$plugin_name)->show_items; $separator = Typecho_Widget::widget('Widget_Options')->plugin(self::$plugin_name)->separator; // 获取 linux 系统消息 $info_str = ''; $show_items_max = count($show_items) - 1; foreach ($show_items as $show_item_k => $show_item) { $method = $show_item; if (method_exists(__CLASS__, $method)) { $info_str .= self::$method(); } if ($show_items_max != $show_item_k) { $info_str .= $separator; } } echo <<<STR $info_str STR; } // 获取CPU信息 private static function cpu_info(){ function GetCoreInformation() { $data = file('/proc/stat'); $cores = []; foreach( $data as $line ) { if( preg_match('/^cpu[0-9]/', $line) ) { $info = explode(' ', $line); $cores[] = [ 'user' => $info[1], 'nice' => $info[2], 'sys' => $info[3], 'idle' => $info[4], 'iowait' => $info[5], 'irq' => $info[6], 'softirq' => $info[7] ]; } } return $cores; } function GetCpuPercentages($stat1, $stat2) { if(count($stat1) !== count($stat2)){ return; } $cpus = []; for( $i = 0, $l = count($stat1); $i < $l; $i++) { $dif = []; $dif['user'] = $stat2[$i]['user'] - $stat1[$i]['user']; $dif['nice'] = $stat2[$i]['nice'] - $stat1[$i]['nice']; $dif['sys'] = $stat2[$i]['sys'] - $stat1[$i]['sys']; $dif['idle'] = $stat2[$i]['idle'] - $stat1[$i]['idle']; $dif['iowait'] = $stat2[$i]['iowait'] - $stat1[$i]['iowait']; $dif['irq'] = $stat2[$i]['irq'] - $stat1[$i]['irq']; $dif['softirq'] = $stat2[$i]['softirq'] - $stat1[$i]['softirq']; $total = array_sum($dif); $cpu = []; foreach($dif as $x=>$y) $cpu[$x] = round($y / $total * 100, 2); $cpus['CPU' . $i] = $cpu; } return $cpus; } $stat1 = GetCoreInformation(); sleep(1); $stat2 = GetCoreInformation(); $data = GetCpuPercentages($stat1, $stat2); $overall_cpu_usage = 0; $core_details = []; foreach ($data as $core => $usage) { $core_usage = 100 - $usage['idle']; $overall_cpu_usage += $core_usage; $core_details[] = "{$core}: " . number_format($core_usage, 2) . "%"; } $overall_cpu_usage = round($overall_cpu_usage / count($data), 2); $core_details_str = implode(", ", $core_details); $sys_load = sys_getloadavg(); $sys_load_1min = number_format($sys_load[0], 3); $sys_load_5min = number_format($sys_load[1], 3); $sys_load_15min = number_format($sys_load[2], 3); return <<<CPU_INFO CPU: {$overall_cpu_usage} % CPU_INFO; } // 获取内存信息 private static function mem_info(){ if (false === ($str = @file("/proc/meminfo"))) return false; $str = implode("", $str); preg_match_all("/MemTotal\s{0,}\:+\s{0,}([\d\.]+).+?MemFree\s{0,}\:+\s{0,}([\d\.]+).+?Cached\s{0,}\:+\s{0,}([\d\.]+).+?SwapTotal\s{0,}\:+\s{0,}([\d\.]+).+?SwapFree\s{0,}\:+\s{0,}([\d\.]+)/s", $str, $buf); preg_match_all("/Buffers\s{0,}\:+\s{0,}([\d\.]+)/s", $str, $buffers); $mem_info['memTotal'] = round($buf[1][0]/1024, 2); $mem_info['memFree'] = round($buf[2][0]/1024, 2); $mem_info['memBuffers'] = round($buffers[1][0]/1024, 2); $mem_info['memCached'] = round($buf[3][0]/1024, 2); $mem_info['memUsed'] = $mem_info['memTotal']-$mem_info['memFree']; $mem_info['memPercent'] = (floatval($mem_info['memTotal'])!=0)?round($mem_info['memUsed']/$mem_info['memTotal']*100,2):0; $mem_info['memRealUsed'] = $mem_info['memTotal'] - $mem_info['memFree'] - $mem_info['memCached'] - $mem_info['memBuffers']; //真实内存使用 $mem_info['memRealFree'] = $mem_info['memTotal'] - $mem_info['memRealUsed']; //真实空闲 $mem_info['memRealPercent'] = (floatval($mem_info['memTotal'])!=0)?round($mem_info['memRealUsed']/$mem_info['memTotal']*100,2):0; //真实内存使用率 $mem_info['memCachedPercent'] = (floatval($mem_info['memCached'])!=0)?round($mem_info['memCached']/$mem_info['memTotal']*100,2):0; //Cached内存使用率 $mem_info['swapTotal'] = round($buf[4][0]/1024, 2); $mem_info['swapFree'] = round($buf[5][0]/1024, 2); $mem_info['swapUsed'] = round($mem_info['swapTotal']-$mem_info['swapFree'], 2); $mem_info['swapPercent'] = (floatval($mem_info['swapTotal'])!=0)?round($mem_info['swapUsed']/$mem_info['swapTotal']*100,2):0; $mem_per = round($mem_info['memRealUsed'] / $mem_info['memTotal'] * 100, 2); return <<<MEM_INFO 内存: {$mem_per} % MEM_INFO; } // 获取磁盘信息 private static function disk_info(){ $dt = round(@disk_total_space(".")/(1024*1024*1024), 2); //总 $df = round(@disk_free_space(".")/(1024*1024*1024), 2); //可用 $du = $dt-$df; //已用 $disk_use = round($du / $dt * 100, 2); return <<<DISK_INFO 硬盘: {$disk_use} % DISK_INFO; } // 获取 系统信息 private static function sys_info() { if (false === ($str = @file("/proc/uptime"))) return false; $str = explode(" ", implode("", $str)); $str = trim($str[0]); $min = $str / 60; $hours = $min / 60; $days = floor($hours / 24); $hours = floor($hours - ($days * 24)); $min = floor($min - ($days * 60 * 24) - ($hours * 60)); if ($days !== 0) $uptime = $days."天"; if ($hours !== 0) $uptime .= $hours."小时"; $uptime .= $min."分钟"; $php['version'] = PHP_VERSION; $php['sapi'] = php_sapi_name(); $php['mem_max'] = get_cfg_var('memory_limit'); $php['up_max'] = get_cfg_var('upload_max_filesize'); return <<<SYS_INFO 服务器信息: 持续运行：{$uptime}，Web服务器：{$_SERVER['SERVER_SOFTWARE']}，PHP 版本：{$php['version']}，SAPI 接口：{$php['sapi']}，PHP内存限制：{$php['mem_max']}，上传文件限制：{$php['up_max']} SYS_INFO; } }优先使用Github上面的吧，万一那天我修复一些问题，可能只会修改Github上面的仓库的内容而忘记修改这篇文章。当然，我希望以后不会出现问题。使用方法将GitHub上面的Plugin.php或者这个页面的Plugin.php内容保存下载，放到Typecho的插件目录usr/plugins/下的LinuxInfo文件夹（需要自己创建），例如：typecho/usr/plugins/LinuxInfo/Plugin.php，进入Typecho后台启用插件即可。默认不显示系统详细信息，需要自己勾选显示才会展示。不过，需要注意的是，这个插件仅可用于Linux系统，Windows系统不适用。谁用Windows做服务器啊？！！不会吧不会吧？最后还是要感谢一下两双筷子写出来这个有意思的插件。
AlamLinux 8.10 部署 LNMP 环境并安装 NextCloud如果说希望自己的手机可以有一个app自动同步照片到NAS，那NextCloud就是一个不错的选择。虽然服务端确实是臃肿了一些，但是如果只是拿来同步个照片问题不大。我前前后后折腾了一天，后面又在虚拟机上部署失败了四次，终于算摸索出来了。写一篇博客记录一下折腾过程，供日后参考。部署目标：目标操作系统：AlmaLinux8.10。使用Nginx作为Web服务器，MariaDB作为数据库，PHP作为后端部署Nextcloud。NextCloud本体放在/opt/nextcloud/目录下，数据文件存放在RAID1的硬盘目录/mnt/3T-RAID1/nextcloud_data/下。前置任务：安装LNMP环境NextCloud的运行依赖于Nginx(或者Apache)，MariaDB(或者MySQL)，PHP，这些软件一般是部署在Linux上，所以缩写为LNMP环境。如果是经常搞网站的，对这个环境应该挺熟悉的吧，不熟悉就是用宝塔用的（配置Nginx安装Nginx：yum install -y nginx创建Nginx配置文件/etc/nginx/conf.d/nextcloud.conf：upstream php-handler { server unix:/run/php-fpm/www.sock; } # Set the `immutable` cache control options only for assets with a cache busting `v` argument map $arg_v $asset_immutable { "" ""; default ", immutable"; } server { listen 80; listen [::]:80; server_name nextcloud.nekopara.uk; # Prevent nginx HTTP Server Detection server_tokens off; # Enforce HTTPS return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; # With NGinx >= 1.25.1 you should use this instead: # listen 443 ssl; # listen [::]:443 ssl; # http2 on; server_name nextcloud.nekopara.uk; # Path to the root of your installation root /opt/nextcloud; # Use Mozilla's guidelines for SSL/TLS settings # https://mozilla.github.io/server-side-tls/ssl-config-generator/ ssl_certificate /opt/certs/nekopara.uk.pem; ssl_certificate_key /opt/certs/nekopara.uk.key; # Prevent nginx HTTP Server Detection server_tokens off; # HSTS settings # WARNING: Only add the preload option once you read about # the consequences in https://hstspreload.org/. This option # will add the domain to a hardcoded list that is shipped # in all major browsers and getting removed from this list # could take several months. #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; # set max upload size and increase upload timeout: client_max_body_size 512M; client_body_timeout 300s; fastcgi_buffers 64 4K; # Enable gzip but do not remove ETag headers gzip on; gzip_vary on; gzip_comp_level 4; gzip_min_length 256; gzip_proxied expired no-cache no-store private no_last_modified no_etag auth; gzip_types application/atom+xml text/javascript application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/wasm application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy; # Pagespeed is not supported by Nextcloud, so if your server is built # with the `ngx_pagespeed` module, uncomment this line to disable it. #pagespeed off; # The settings allows you to optimize the HTTP2 bandwidth. # See https://blog.cloudflare.com/delivering-http-2-upload-speed-improvements/ # for tuning hints client_body_buffer_size 512k; # HTTP response headers borrowed from Nextcloud `.htaccess` add_header Referrer-Policy "no-referrer" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Permitted-Cross-Domain-Policies "none" always; add_header X-Robots-Tag "noindex, nofollow" always; add_header X-XSS-Protection "1; mode=block" always; # Remove X-Powered-By, which is an information leak fastcgi_hide_header X-Powered-By; # Set .mjs and .wasm MIME types # Either include it in the default mime.types list # and include that list explicitly or add the file extension # only for Nextcloud like below: include mime.types; types { text/javascript mjs; application/wasm wasm; } # Specify how to handle directories -- specifying `/index.php$request_uri` # here as the fallback means that Nginx always exhibits the desired behaviour # when a client requests a path that corresponds to a directory that exists # on the server. In particular, if that directory contains an index.php file, # that file is correctly served; if it doesn't, then the request is passed to # the front-end controller. This consistent behaviour means that we don't need # to specify custom rules for certain paths (e.g. images and other assets, # `/updater`, `/ocs-provider`), and thus # `try_files $uri $uri/ /index.php$request_uri` # always provides the desired behaviour. index index.php index.html /index.php$request_uri; # Rule borrowed from `.htaccess` to handle Microsoft DAV clients location = / { if ( $http_user_agent ~ ^DavClnt ) { return 302 /remote.php/webdav/$is_args$args; } } location = /robots.txt { allow all; log_not_found off; access_log off; } # Make a regex exception for `/.well-known` so that clients can still # access it despite the existence of the regex rule # `location ~ /(\.|autotest|...)` which would otherwise handle requests # for `/.well-known`. location ^~ /.well-known { # The rules in this block are an adaptation of the rules # in `.htaccess` that concern `/.well-known`. location = /.well-known/carddav { return 301 /remote.php/dav/; } location = /.well-known/caldav { return 301 /remote.php/dav/; } location /.well-known/acme-challenge { try_files $uri $uri/ =404; } location /.well-known/pki-validation { try_files $uri $uri/ =404; } # Let Nextcloud's API for `/.well-known` URIs handle all other # requests by passing them to the front-end controller. return 301 /index.php$request_uri; } # Rules borrowed from `.htaccess` to hide certain paths from clients location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) { return 404; } location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; } # Ensure this block, which passes PHP files to the PHP process, is above the blocks # which handle static assets (as seen below). If this block is not declared first, # then Nginx will encounter an infinite rewriting loop when it prepends `/index.php` # to the URI, resulting in a HTTP 500 error response. location ~ \.php(?:$|/) { # Required for legacy support rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php$request_uri; fastcgi_split_path_info ^(.+?\.php)(/.*)$; set $path_info $fastcgi_path_info; try_files $fastcgi_script_name =404; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $path_info; fastcgi_param HTTPS on; fastcgi_param modHeadersAvailable true; # Avoid sending the security headers twice fastcgi_param front_controller_active true; # Enable pretty urls fastcgi_pass php-handler; fastcgi_intercept_errors on; fastcgi_request_buffering off; fastcgi_max_temp_file_size 0; } # Serve static files location ~ \.(?:css|js|mjs|svg|gif|ico|jpg|png|webp|wasm|tflite|map|ogg|flac)$ { try_files $uri /index.php$request_uri; # HTTP response headers borrowed from Nextcloud `.htaccess` add_header Cache-Control "public, max-age=15778463$asset_immutable"; add_header Referrer-Policy "no-referrer" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Permitted-Cross-Domain-Policies "none" always; add_header X-Robots-Tag "noindex, nofollow" always; add_header X-XSS-Protection "1; mode=block" always; access_log off; # Optional: Don't log access to assets } location ~ \.(otf|woff2?)$ { try_files $uri /index.php$request_uri; expires 7d; # Cache-Control policy borrowed from `.htaccess` access_log off; # Optional: Don't log access to assets } # Rule borrowed from `.htaccess` location /remote { return 301 /remote.php$request_uri; } location / { try_files $uri $uri/ /index.php$request_uri; } }配置文件中的主机名要改为你实际的域名，证书位置改为你实际证书放置的目录。测试没问题后，就启动Nginx：nginx -t systemctl enable --now nginx配置PHP安装PHP以及NextCloud需要的模块，由于需要使用PHP8.2，所以需要添加Remi仓库后再安装：yum install https://rpms.remirepo.net/enterprise/remi-release-8.rpm -y dnf module reset php -y dnf module enable php:remi-8.2 -y dnf install -y php php-cli php-gd php-mbstring php-intl php-pecl-apcu php-mysqlnd php-opcache php-json php-zip配置/etc/php.ini，查找配置并修改为以下内容：memory_limit = 512M upload_max_filesize = 500M post_max_size = 600M max_execution_time = 300启动PHP：systemctl enable --now php-fpm配置MariaDB安装MariaDB：yum install mariadb -y安装完成后，启动数据库，设置NextCloud的数据库：systemctl enable --now mariadb mysql -u root -p默认没有密码，直接回车就进数据库了，然后开始配置：CREATE DATABASE netxcloud_db; CREATE USER nextcloud_user@localhost IDENTIFIED BY 'nextcloud_passwd'; GRANT ALL PRIVILEGES ON netxcloud_db.* TO nextcloud_user@localhost; FLUSH PRIVILEGES;数据库名称，用户，密码，自己看着改。至此，前置任务已完成，让我们来开始安装NextCloud。安装NextCloud下载NextCloud并解压，我用地是28.0.14版本：cd /opt wget https://download.nextcloud.com/server/releases/nextcloud-28.0.14.zip unzip nextcloud-28.0.14.zip配置SELinux允许NextCloud进行文件读写操作：semanage fcontext -a -t httpd_sys_rw_content_t "/opt/nextcloud(/.*)?" semanage fcontext -a -t httpd_sys_rw_content_t '/opt/nextcloud/data(/.*)?' semanage fcontext -a -t httpd_sys_rw_content_t '/opt/nextcloud/config(/.*)?' semanage fcontext -a -t httpd_sys_rw_content_t '/opt/nextcloud/apps(/.*)?' semanage fcontext -a -t httpd_sys_rw_content_t '/opt/nextcloud/.htaccess' semanage fcontext -a -t httpd_sys_rw_content_t '/opt/nextcloud/.user.ini' semanage fcontext -a -t httpd_sys_rw_content_t '/opt/nextcloud/3rdparty/aws/aws-sdk-php/src/data/logs(/.*)?' restorecon -Rv '/opt/nextcloud/'设置必要的SELinux规则：setsebool -P httpd_can_connect_ldap on setsebool httpd_unified on setsebool -P httpd_can_network_connect_db on setsebool -P httpd_can_network_connect on setsebool -P httpd_can_network_memcache on setsebool -P mysql_connect_http on设置文件权限：chown -R apache:apache /opt/nextcloud chmod -R 775 /opt/nextcloud同理，设置数据目录权限：semanage fcontext -a -t httpd_sys_rw_content_t "/mnt/3T-RAID1/nextcloud_data(/.*)?" restorecon -Rv /mnt/3T-RAID1/nextcloud_data chown -R apache:apache /mnt/3T-RAID1/nextcloud_data chmod -R 775 /mnt/3T-RAID1/nextcloud_data配置NextCloud在浏览器输入你的域名，按理来说应该可以正常打开NextCloud的安装界面，按照提示设置用户名，密码，文件目录，数据库，安装完成后就应该是可以正常使用的了。
浅浅探究一下阿里云国内服务器域名阻断机制
探究阿里云国内服务器域名阻断机制网站落成一周年！这篇文章的发布时间是2025年3月4日，网站搭建日期是2024年3月4日，正好一周年，也当是发一篇相关的文章庆祝一下。关于备案的个人观点我个人感觉，备案简直是对个人隐私的强奸。因为备案后，你的个人姓名可以直接在工信部官网备案查询查出来，一键开盒。（拿了某个群友的网站开刀）就问你刺不刺激？而且。备案手续繁琐，等待时间长，还有可能因为各种原因导致备案失败。本来别人搞个网站玩玩也就图一乐，你这样一搞直接把别人的心情都搞没了。你看看其他主要国家的互联网，也没见要求备案啊，就你搞特殊！而且，就之前华为云白送代金券购买云服务器的事情，绝大部分人都是购买新加坡的服务器，导致一度缺货。从此就可以看出，对于不少人而言，还是更倾向于不备案直接开网站。不备案直接解析域名的效果如果你不备案直接解析域名，效果如图：但是，就目前而言，直接通过IP进行访问是可以的，也可以签IP证书实现Https访问。发现新大陆！偶然的一次机会，因为为想要分析一下某个一直ssh爆破我服务器的恶意IP，我注册了奇安信，然后发现这个平台还有IP反查网站的功能。于是在好奇心的驱使下，我反查了一下我国内阿里云服务器的IP有没有绑定域名，好家伙，一查发现，还真有！不过这时包含了历史解析记录的，经过筛选，只有三个域名还在解析到我的云服务器：分别是：forever.nk001.com cloud.nk001.com 和 erp.gdlzkj.com并且，这些都是备案域名！用别人备案的域名，能不能正常访问？我在想，如果是别人的备案域名，并且已经是解析到你的服务器了，那能不能通过别人的域名进行访问呢？怀着好奇心，我开始了实验。事实证明——可以！不但可以，还可以利用URL验证，顺利签发单域名证书！配合acme.sh，甚至可以批量签发证书，整合到一个证书文件。这就很离谱了，很明显，如果你的备案域名在云服务器到期后没有停掉解析，当这个域名重新分配给下一个用户后，别人就可以冒用你的域名！如果那个冒用你域名的人干了某些不太好的事情，你作为备案域名的拥有者，有可能也会被请去喝茶。小结一下我个人感觉，阻断的机制应该是根据当前域名备案的情况进行阻断，只阻断不进行备案的域名，而对备案的域名，是直接放行的。哪怕是云服务器已经更换了用户，依然放行。说起来也离谱，这个云服务器是搞活动白嫖的，使用时长为一年，都准备到期了，没想到还有上个用户的赛博遗产留到为我这里。这也说明了，及时取消解析不再属于自己的IP，还是很有必要的。
为Cuckoo主题更换评论头像源，并设置默认头像
配置Cuckoo主题更换评论头像源偶然发现Cravatar抽风了这段时间，偶然发现，似乎Cravatar，抽风了，就算填写QQ号+@qq.com的形式邮箱的评论。也没办法正常获取头像了。于是我怀疑应该是这玩意出问题了，就在网上去找替代品。发现WeAvatar在看来那些推荐头像源的博客，发现大部分推荐的都是Cravatar。但是在偶然一篇文章的评论区，有人留言WeAvatar也不错，试试吧。这个服务似乎是一个个人博主做的，这是他博客的相关内容，有兴趣可以去看看：https://hzbk.net/archives/121072.html本教程的参考资料来源就来源于他的博客和文档。开始使用WeAvatar和大部分类似的镜像源一样，都是和Gravatar的用法是一样的。那就是：URL+邮箱哈希值https://weavatar.com/avatar/（目标邮箱哈希值）除此之外，还有一点是我觉得很赞的，那就是这个头像源可以自定义默认头像！你需要的，只是你默认头像的URL。组合形式就是如下这样：https://weavatar.com/avatar/（目标邮箱哈希值）?s=100&d=你默认头像的URL例如：https://weavatar.com/avatar/ab19c18dc265207b197f1080b493241a?s=100&d=https://teachermate.oss-cn-qingdao.aliyuncs.com/gSTpX-1734422434828-defualt_avatar.png像这样，如果匹配不到头像的话，就会自动返回后面自定义链接的头像。了解了基本的使用方法后，我们来把他装进Cuckoo主题。Cuckoo主题设置WeAvatar如果你只是简单的想要替换原来不正常的头像源，在主题的更多设置，Gravatar头像源里面，设置自定义并添上https://weavatar.com/avatar即可。但是，如果你希望自定义默认头像的话，很明显，这样是没办法完成的，我们需要对主题的文件进行修改。需要修改的文件是function.php，也就是主题根目录里面的那个文件。打开文件，定位到获取头像的函数：// 获取评论者头像 function get_comment_avatar($moe = NULL){ $gravatar = Helper::options()->gravatar; if($gravatar == 'geekzu'){ $host = 'https://sdn.geekzu.org/avatar/'; }elseif($gravatar == 'qiniu'){ $host = 'https://dn-qiniu-avatar.qbox.me/avatar'; }elseif($gravatar == 'cravatar'){ $host = 'https://cravatar.cn/avatar'; }elseif($gravatar == 'cdn'){ $host = Helper::options()->gravatarCdn; } $hash = md5(strtolower($moe)); $avatar = $host.'/'.$hash.'?s=100'; echo $avatar; }通过观察博客生成的头像URL链接，很明显，?s=100这个地方，就是我们需要更改的地方。下面就是修改后的代码：// 获取评论者头像 function get_comment_avatar($moe = NULL){ $gravatar = Helper::options()->gravatar; if($gravatar == 'geekzu'){ $host = 'https://sdn.geekzu.org/avatar/'; }elseif($gravatar == 'qiniu'){ $host = 'https://dn-qiniu-avatar.qbox.me/avatar'; }elseif($gravatar == 'cravatar'){ $host = 'https://cravatar.cn/avatar'; }elseif($gravatar == 'cdn'){ $host = Helper::options()->gravatarCdn; } $hash = md5(strtolower($moe)); $avatar = $host.'/'.$hash.'?s=100&d=https://teachermate.oss-cn-qingdao.aliyuncs.com/gSTpX-1734422434828-defualt_avatar.png'; echo $avatar; }我在后面加上了&d=https://teachermate.oss-cn-qingdao.aliyuncs.com/gSTpX-1734422434828-defualt_avatar.png，而https://teachermate.oss-cn-qingdao.aliyuncs.com/gSTpX-1734422434828-defualt_avatar.png就是头像的URL，不信你可以打开看看。更换完成后，刷新一下博客，嗯，一切正常了。默认头像被换成了凯露，毕竟接头霸王嘛，头安在谁身上都合适，那就给没有头像的人安上吧 XD为Typecho更换头像源另外，我还发现，Typecho后台也是没办法显示头像的，虽然访客看不到，但是我自己用还是有点不爽。按照那个博客的说法，添加define('__TYPECHO_GRAVATAR_PREFIX__', 'https://weavatar.com/avatar/');到站点根目录的config.inc.php中，轻松秒杀。
优化网站访问并发高占用问题也是成功让CPU占用从100%降低到了10%。前言之前尝试过高并发压力测试自己的网站，发现打到128并发基本上就趴窝了。后面与友链的一个站长交流，得知可以通过缓存的方式解决这种高并发处理问题。因为如果不设置缓存的话，即使是同样的请求，也会让php进行反复处理，消耗大量资源。所以，我想尝试一下能不能改善一下这个情况。实验环境准备为了保证我网站能在我进行实验期间正常运行，我决定对我网站数据进行克隆，迁移到我云服务器里面性能最孱弱的那个进行测试。那就是，白嫖的华为云：母鸡CPU: Intel(R) Xeon(R) Gold 6348 (2) @ 2.60 GHz双核CPU512MB内存20G硬盘然后是软件环境，操作系统为Alpine Linux 3.20然后我按照前面我那片迁移博客的方法，把博客迁移了过去。原始表现迁移过程就进行省略了，和前面的博客方法大体相同。我测试下来，32并发就是这玩意的极限了，CPU彻底吃满。再往上加线程已经没有意义了，因为已经寄了。可以看出来，云服务器相比于实体鸡性能差别还是很大的。优化改用Unix Socket进行PHP对接首先是php对接的问题，默认Alpine使用的是TCP网络协议进行本地回环，开销较大。而使用Unix Socket则可以解决这个问题，他具有低延迟和资源占用少的优点。特征TCP 端口Unix Socket通信范围支持远程通信仅限于同一主机内的进程间通信性能较高开销，适合长距离通信更高效，尤其在本地通信中表现出色安全性需要额外的安全措施，如防火墙规则依赖于操作系统权限控制，天然更安全配置复杂度需要设置IP地址和端口号只需指定文件路径为了实现这个目标，我们需要对PHP的配置文件和Nginx的配置文件进行更改。1. 修改 PHP-FPM 配置文件首先，我们需要编辑 PHP-FPM 的池配置文件 /etc/php82/php-fpm.d/www.conf，将其 listen 设置更改为一个 Unix socket，并设置适当的权限。[www] ; 更改 listen 为 Unix socket listen = /var/run/php-fpm82/php8.2-fpm.sock ; 设置 socket 文件的所有者和组 listen.owner = nginx listen.group = nginx listen.mode = 0660确保 /var/run/php-fpm82/ 目录存在并且有正确的权限：sudo mkdir -p /var/run/php-fpm82/ sudo chown root:nginx /var/run/php-fpm82/ sudo chmod 755 /var/run/php-fpm82/2. 更新 Nginx 配置文件然后，编辑你的 Nginx 站点配置文件，例如 /etc/nginx/http.d/nekopara.conf，将 fastcgi_pass 指令更新为指向新的 Unix socket：location ~ \.php$ { 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; }3. 重启服务保存所有更改后，重启 PHP-FPM 和 Nginx 服务以使新配置生效。# 重启 PHP-FPM rc-service php-fpm82 restart # 重新加载 Nginx rc-service nginx reload修改完成后，再进行测试，似乎没有太大改善，还是CPU爆炸。那就尝试一下设置缓存了。为Nginx配置缓存规则1. 定义 FastCGI 缓存区首先，在 Nginx 的主配置文件 /etc/nginx/nginx.conf 添加一个 fastcgi_cache_path 指令来定义缓存存储的位置和参数。http { # 其他配置... # 定义 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"; }在主配置文件加上以上这段即可。确保 /var/cache/nginx/ 目录存在并且有适当的权限：sudo mkdir -p /var/cache/nginx/fastcgi sudo chown nginx:nginx /var/cache/nginx/fastcgi sudo chmod 755 /var/cache/nginx/fastcgi2. 修改站点配置以启用 FastCGI 缓存接下来，在你的站点配置文件，例如 /etc/nginx/http.d/nekopara.conf 中修改处理 PHP 请求的 location ~ \.php$ 块，加入 FastCGI 缓存相关指令。以下是我的示例配置文件：server { listen 4443 ssl proxy_protocol; server_name www.nekopara.uk; #确保后端正确跳转而不带上端口号 set_real_ip_from 0.0.0.0; real_ip_header proxy_protocol; set $real_host $http_x_forwarded_host; if ($http_x_forwarded_host = '') { set $real_host $host; } # 添加这一行，确保生成的重定向不带后端端口 port_in_redirect off; # 设置最大上传文件大小为 8M client_max_body_size 8M; # 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; # Serve static files directly location / { try_files $uri $uri/ /index.php?$args; autoindex off; } # PHP处理，并启用FastCGI缓存 location ~ \.php$ { 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 1m; # 设置缓存的有效时间 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; } if ($http_cookie ~* "PHPSESSID|_sf|your-session-cookie") { # 如果存在特定会话 cookie，则不缓存 set $skip_cache 1; } fastcgi_cache_bypass $skip_cache; fastcgi_no_cache $skip_cache; # 添加头部信息显示缓存状态 #add_header X-FastCGI-Cache $upstream_cache_status; } # Deny access to hidden files location ~ /\.ht { deny all; } }3. 测试并应用配置保存所有更改后，测试 Nginx 配置文件是否正确：nginx -t如果一切正常，重新加载 Nginx 使新配置生效：rc-service nginx reload此时在进行并发压力测试，感觉就是在挠痒痒：即使上压力到256线程，也就那样：Nginx FastCGI 缓存配置参数解释1. fastcgi_cache_pathfastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=my_fastcgi_cache:16m inactive=60m;/var/cache/nginx/fastcgi：指定缓存文件存储的路径。levels=1:2：定义缓存目录结构为两级子目录，有助于分散文件，避免单个目录下文件过多导致性能问题。keys_zone=my_fastcgi_cache:16m：创建一个名为 my_fastcgi_cache 的共享内存区域，大小为 16MB。这个区域用于存储缓存键（key）和元数据（如过期时间）。16MB 可以存储大约 80,000 个缓存项。inactive=60m：设置缓存条目在多长时间内没有被访问就会被认为是不活跃的并从缓存中移除。这里设置为 60 分钟。2. fastcgi_cache_keyfastcgi_cache_key "$scheme$request_method$host$request_uri";这个指令定义了用来生成缓存键的变量组合。每个唯一的 URL 请求（包括协议、请求方法、主机名和 URI）都会生成一个唯一的缓存键，确保不同的请求不会互相干扰。3. fastcgi_cache_validfastcgi_cache_valid 200 301 302 404 1m;指定哪些 HTTP 状态码应该被缓存以及它们的有效期。例如，这里表示状态码 200 (OK)、301 (永久重定向) 和 302 (临时重定向) 404 (未找到页面) 的响应会被缓存 1 分钟。4. fastcgi_cache_methodsfastcgi_cache_methods GET HEAD;指定只缓存特定的 HTTP 请求方法，这里是 GET 和 HEAD 请求。POST 请求通常包含表单提交等操作，不应该被缓存。5. $skip_cache 和相关条件set $skip_cache 0; if ($request_method = POST) { set $skip_cache 1; } if ($query_string != "") { set $skip_cache 1; } if ($http_cookie ~* "PHPSESSID|_sf|your-session-cookie") { set $skip_cache 1; } fastcgi_cache_bypass $skip_cache; fastcgi_no_cache $skip_cache;$skip_cache：这是一个自定义变量，用于标记是否跳过缓存。初始值设为 0，意味着默认情况下启用缓存。条件判断：通过一系列 if 语句检查请求是否是 POST 请求、是否有查询字符串或者是否存在特定的会话 cookie。如果任意条件满足，则将 $skip_cache 设置为 1，即禁用缓存。fastcgi_cache_bypass 和 fastcgi_no_cache：这两个指令分别控制是否绕过缓存（即直接发送请求给后端）和是否不让缓存当前请求的结果。当 $skip_cache 为 1 时，两者都生效，确保动态或个性化内容不被缓存。6. add_header X-FastCGI-Cacheadd_header X-FastCGI-Cache $upstream_cache_status;向 HTTP 响应头添加 X-FastCGI-Cache 字段，显示缓存的状态（如 HIT, MISS 等），便于调试和监控。这部分是可以不要的。理解了这些缓存参数的意义，你就可以根据需要自己调节了。博客状态我发布这篇文章的时候，已经完成了这些优化并且把博客迁移上云了。毕竟白送的服务器，不用白不用。虽然云服务器隐私安全一直是一个问题，云服务器提供商能不能靠得住这点存疑。但是，如果只是博客这种，反正都是要公开的内容，上云又何尝不可？当然，家里云还是会保留的，毕竟它的主要任务是NAS，让我随时随地可以访问我的文件，博客之前只是想在上面顺带弄着玩的玩具。随着博客访问量逐渐大了起来，迁移上云给家里云减负何尝不是一个更好的选择？这个云服务器已经白嫖续费到2029年了，所以这个博客至少也能在上面运行差不多五年时间，已经比我购买的域名时间还长了。迁移吧！定期把内容备份到家里云就是了。PS:不知道你有没有注意到网站底部的服务器运行时间，从200多天变成了80多天？