Files
jiu/deploy/nginx-jiu-ali.conf
T
wangjia a49a962161 devops: 边缘层加固——nginx limit_conn/UA 拦截/防盗链/下载限速 + jiu_redis 容器(todo #1/#2/#3)
nginx:店铺列表专属 2r/s zone、公开路径 limit_conn、抓取库/空 UA 403(OG 页
不拦空 UA 防误杀社交爬虫)、/images 防盗链(none 放行微信 webview)、
/downloads 限并发+限速。compose 新增 jiu_redis(64mb noeviction 无持久化,
仅回环);env 模板加 REDIS_ADDR。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 10:42:23 +08:00

184 lines
7.5 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 阿里云独立 nginx 版(宿主机直接终结 TLS,不经 pangolin)。
# 与 nginx-jiu.confEC2 pangolin 容器版,listen 127.0.0.1:8445)等价。
# CI deploy-server.sh 的 ali 分支落到 /etc/nginx/conf.d/jiu.conf。
#
# 2026-07-03 备案通过回切:443 ssljiu.51yanmei.com)为唯一正式入口 + 80→443 跳转。
# 8443 明文过渡口已拆(用户拍板:裸 IP 版 client 从未发过,存量客户端都是 https
# 域名 BASE_URLDNS 回切后自动恢复,8443 无人使用)。
# 证书:/etc/nginx/ssl/jiu.51yanmei.com/2026-07-03 从 EC2 letsencrypt 同步,
# 2026-08-28 到期;续期方案=DNS 已指 ali 后 certbot HTTP-01 webroot,见 baize)。
# 后端上游 = 127.0.0.1:8081(非 8080ali 上 8080 已被 pay 项目 payd 占用;
# 对应 /opt/jiu/config/production.env 的 SERVER_PORT=8081)。
limit_req_zone $binary_remote_addr zone=jiu_pub:10m rate=10r/s;
# 店铺公开商品列表专属(主爬取入口,更严)
limit_req_zone $binary_remote_addr zone=jiu_shoplist:10m rate=2r/s;
# per-IP 并发连接(公开路径 20 / 列表 10 / 下载 3
limit_conn_zone $binary_remote_addr zone=jiu_conn:10m;
limit_conn_zone $binary_remote_addr zone=jiu_dl:10m;
limit_req_status 429;
limit_conn_status 429;
# 抓取库 UA / 空 UA 拦截(2026-07 反爬)。刻意不封 curl/wget(运维自用、改 UA 零成本)、
# 不封泛 "bot"(微信/飞书等社交分享爬虫 UA 不可控)。
# $jiu_bad_uaAPI 公开路径用(含空 UA);$jiu_bad_ua_libOG 分享页用(不拦空 UA,
# 社交平台抓卡片的客户端五花八门,空 UA 误杀不可接受)。
map $http_user_agent $jiu_bad_ua {
default 0;
"" 1;
~*(python-requests|python-urllib|scrapy|go-http-client|apache-httpclient|libwww|aiohttp|okhttp|httpx|java/) 1;
}
map $http_user_agent $jiu_bad_ua_lib {
default 0;
~*(python-requests|python-urllib|scrapy|go-http-client|apache-httpclient|libwww|aiohttp|okhttp|httpx|java/) 1;
}
server {
# ali nginx 1.24 无 `http2 on;`1.25.1+),用 listen 参数旧语法
listen 443 ssl http2 default_server; # 正式入口(备案后回切 2026-07-03
server_name jiu.51yanmei.com _;
# 证书:certbot webroot 自动续期(certbot-renew.timer 每日 + deploy 钩子 reload
# 2026-07-03 签发,authenticator=webrootdry-run 验证通过)
ssl_certificate /etc/letsencrypt/live/jiu.51yanmei.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/jiu.51yanmei.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# 安全响应头(SEC-P02)。注意 nginx add_header 继承规则:location 内有自己的
# add_header 时会整体覆盖 server 级——故 /images、/app/index.html、/downloads
# 三处各自补了 HSTS。
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
client_max_body_size 20m;
# certbot HTTP-01 续期通道(webroot 独立目录,不放 marketing——
# deploy-site 的 rsync --delete 会清掉站点目录里的挑战文件)
location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
}
# 商品图片静态文件(防盗链:none 放行空 Referer——微信 webview/App 直连
# 不带 Referer 必须放行;只挡第三方网页热链。回滚=删 valid_referers+if 三行)
location ^~ /images/ {
valid_referers none blocked server_names 51yanmei.com *.51yanmei.com;
if ($invalid_referer) { return 403; }
alias /opt/jiu/images/;
expires 30d;
add_header Cache-Control "public, immutable";
add_header Strict-Transport-Security "max-age=31536000" always;
}
# 文件导入接口(超时更长)
location ~ ^/api/v1/import/ {
proxy_pass http://127.0.0.1:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 300s;
}
# 店铺公开商品列表(主爬取入口):更严的独立限流。
# 正则 location 按文件顺序首匹配,本块必须在下面 (public|auth) 之前。
location ~ ^/api/v1/public/shops/ {
if ($jiu_bad_ua) { return 403; }
limit_req zone=jiu_shoplist burst=5 nodelay;
limit_conn jiu_conn 10;
proxy_pass http://127.0.0.1:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 30s;
}
# 未鉴权公开/登录接口:加最外层 per-IP 限流
location ~ ^/api/v1/(public|auth)/ {
if ($jiu_bad_ua) { return 403; }
limit_req zone=jiu_pub burst=20 nodelay;
limit_conn jiu_conn 20;
proxy_pass http://127.0.0.1:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 30s;
}
# API 反向代理
location ~ ^/(api|health|version) {
proxy_pass http://127.0.0.1:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 30s;
}
# Flutter 管理端(仅 /app/ 前缀)
location /app/ {
alias /opt/jiu/web/;
index index.html;
try_files $uri $uri/ /app/index.html;
}
location = /app/index.html {
alias /opt/jiu/web/index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Strict-Transport-Security "max-age=31536000" always;
expires 0;
}
# 公开商品详情页(扫码跳转)→ 后端注入 OG 标签(UA 只拦抓取库,不拦空 UA)
location ~ ^/product/ {
if ($jiu_bad_ua_lib) { return 403; }
limit_req zone=jiu_pub burst=20 nodelay;
limit_conn jiu_conn 20;
proxy_pass http://127.0.0.1:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 30s;
}
# Flutter base-href=/app/ 会把 /product/:id 重写为 /app/product/:id
location ~ ^/app/product/ {
if ($jiu_bad_ua_lib) { return 403; }
limit_req zone=jiu_pub burst=20 nodelay;
limit_conn jiu_conn 20;
rewrite ^/app(/product/.+)$ $1 break;
proxy_pass http://127.0.0.1:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 30s;
}
# 桌面客户端安装包下载(限并发+超 10m 后限速,防几个并发拖爆小水管带宽)
location /downloads/ {
limit_conn jiu_dl 3;
limit_rate_after 10m;
limit_rate 4m;
alias /opt/jiu/downloads/;
add_header Content-Disposition "attachment";
add_header Cache-Control "no-cache";
add_header Strict-Transport-Security "max-age=31536000" always;
autoindex off;
}
# 营销站点兜底
location / {
root /opt/jiu/marketing;
try_files $uri $uri.html $uri/index.html /index.html;
}
}
# 80 → 443 跳转(仅 jiu 域名;51yanmei.com 门户 80 在 51yanmei.conf 自管)。
# ACME 挑战必须在跳转前放行(HTTP-01 走 80)。
server {
listen 80;
server_name jiu.51yanmei.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
}
location / {
return 301 https://$host$request_uri;
}
}