diff --git a/hhs/DEV/Nginx/Nginx.md b/hhs/DEV/Nginx/Nginx.md
new file mode 100644
index 0000000..7d6c046
--- /dev/null
+++ b/hhs/DEV/Nginx/Nginx.md
@@ -0,0 +1,1005 @@
+---
+tags: [tech, infrastructure, web-server, proxy]
+create time: 2026-05-18 20:30
+---
+
+# Nginx
+
+## 概述
+
+Nginx(读作 "engine-x")是一个高性能的 **HTTP 服务器**和**反向代理服务器**,同时也支持 IMAP/POP3/SMTP 协议。由 Igor Sysoev 开发,以其高并发、低内存消耗闻名。
+
+> [!QUESTION] 为什么选择 Nginx?
+> - **异步事件驱动架构**:能轻松支撑数万并发连接
+> - **轻量级**:处理静态文件只需 Apache 1/5 的内存
+> - **热部署**:配置文件修改后可无缝 reload,不中断现有请求
+> - **生态成熟**:大量第三方模块和社区方案
+
+核心用途:**反向代理**、**负载均衡**、**静态资源服务**、**SSL/TLS 终止**、**API 网关**。
+
+## 安装
+
+### 主流方式
+
+| 方式 | 适用场景 | 命令示例 |
+|------|---------|---------|
+| 系统包管理器 | 快速部署、稳定优先 | `sudo apt install nginx` / `sudo yum install nginx` |
+| 官方仓库 | 需要最新版本 | 添加 `nginx.org` 源后安装 |
+| Docker | 容器化部署 | `docker run -d --name nginx -p 80:80 nginx:alpine` |
+| 源码编译 | 需要自定义模块 | `./configure --with-http_ssl_module && make && sudo make install` |
+
+> [!TIP] 生产环境推荐
+> 使用官方仓库或 Docker Alpine 镜像,体积小且更新及时。开发环境直接用系统包管理器即可。
+
+### 验证安装
+
+```bash
+nginx -v # 查看版本
+nginx -t # 测试配置语法
+systemctl status nginx # 查看服务状态
+```
+
+## 核心概念
+
+### 架构模型
+
+```mermaid
+graph LR
+ Master["Master Process 管理进程"] --> WorkerA["Worker Process 1 处理请求"]
+ Master --> WorkerB["Worker Process 2 处理请求"]
+ Master --> WorkerC["Worker Process 3 处理请求"]
+ WorkerA --> Conn1["Connection A"]
+ WorkerA --> Conn2["Connection B"]
+ WorkerB --> Conn3["Connection C"]
+ WorkerB --> Conn4["Connection D"]
+ WorkerC --> Conn5["Connection E"]
+ style Master fill:#f9d,stroke:#333
+ style WorkerA fill:#ccf,stroke:#333
+ style WorkerB fill:#ccf,stroke:#333
+ style WorkerC fill:#ccf,stroke:#333
+```
+
+Nginx 采用 **Master-Worker 多进程模型**:
+
+- **Master 进程**:负责读取配置、管理 Worker、平滑重载
+- **Worker 进程**:每个 Worker 独立运行,用**事件驱动**方式处理请求。一个请求不会在其他 Worker 中排队
+
+> [!NOTE] 关键问题
+> 为什么不用多线程?因为线程切换开销大、锁竞争严重。Nginx 的事件驱动模型(epoll/kqueue)在 Linux/BSD 上能做到单个 Worker 处理上万并发连接,这就是著名的 **"C10K 问题"**解决方案之一。
+
+### 配置文件结构
+
+主配置文件通常是 `/etc/nginx/nginx.conf`,采用**嵌套块结构**:
+
+```nginx
+# 全局块
+worker_processes auto; # Worker 数量(auto = CPU 核数)
+error_log /var/log/nginx/error.log warn;
+
+events { # events 块
+ worker_connections 1024; # 每个 Worker 的最大连接数
+}
+
+http { # http 块
+ include mime.types;
+ default_type application/octet-stream;
+ sendfile on;
+
+ # upstream 块 — 定义后端服务器池
+ upstream backend {
+ server 127.0.0.1:8080;
+ }
+
+ server { # server 块 — 虚拟主机
+ listen 80;
+ server_name example.com;
+
+ location / { # location 块 — URL 匹配路由
+ proxy_pass http://backend;
+ }
+ }
+}
+```
+
+配置文件加载顺序:**全局块 → events 块 → http 块 → server 块 → location 块**。外层指令对内层生效(继承关系)。
+
+## 常用配置实战
+
+### 1. 反向代理
+
+最经典的用法——将客户端请求转发到后端服务。
+
+```nginx
+server {
+ listen 80;
+ server_name api.example.com;
+
+ location / {
+ proxy_pass http://127.0.0.1:3000;
+
+ # 必须传递的信息头
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # WebSocket 支持
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ # ── Proxy Buffering(缓冲控制)──
+ proxy_buffering on; # 默认开启,先接收后端完整响应再发给客户端
+ proxy_buffer_size 4k; # 第一块缓冲区大小(通常存响应头)
+ proxy_buffers 8 4k; # 后续缓冲区数量和大小
+ proxy_busy_buffers_size 8k; # 忙缓冲区(一边发客户端一边收后端)
+ # proxy_buffering off; # 关闭缓冲 = 流式转发,适合 SSE/WebSocket
+ }
+}
+```
+
+> [!NOTE] 反向代理 vs 正向代理
+> - **正向代理**:代理**客户端**,帮内网用户访问外网(如 VPN)
+> - **反向代理**:代理**服务端**,对外隐藏后端真实地址(Nginx 的主要角色)
+
+> [!NOTE] Proxy Buffering 工作原理
+> ```mermaid
+> sequenceDiagram
+> participant Client as 客户端
+> participant Nginx as Nginx (Buffer)
+> participant Backend as 后端服务
+> Client->>Nginx: 请求
+> Nginx->>Backend: 转发请求
+> Backend-->>Nginx: 响应数据写入 Buffer
+> Note over Nginx: 整个响应写完后...
+> Nginx-->>Client: 从 Buffer 发送给客户端
+> alt buffering off
+> Nginx-->>Client: 实时转发 (Stream)
+> end
+> ```
+> - **开启 buffering**(默认):后端可以先完成处理再发送,Nginx 承担内存压力,提升吞吐
+> - **关闭 buffering**:实时转发,内存占用低但要求后端尽快返回;适合 SSE、大文件下载
+
+#### Proxy Cache — 反向缓存
+
+Nginx 还可以作为**反向缓存代理**,将后端的响应缓存在本地磁盘。
+
+```nginx
+http {
+ # 定义缓存区域
+ proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m
+ max_size=1g inactive=60m use_temp_path=off;
+
+ server {
+ location /api/ {
+ proxy_pass http://backend;
+
+ # 启用缓存
+ proxy_cache app_cache; # 使用哪个缓存区
+ proxy_cache_key "$scheme$request_method$host$request_uri"; # 缓存 key
+ proxy_cache_valid 200 302 10m; # 200/302 响应缓存 10 分钟
+ proxy_cache_valid 404 1m; # 404 缓存 1 分钟
+ proxy_cache_valid any 5m; # 其他状态码缓存 5 分钟
+
+ # 缓存命中时添加头部(方便调试)
+ add_header X-Cache-Status $upstream_cache_status;
+
+ # 绕过缓存的条件(未登录用户不走缓存)
+ proxy_cache_bypass $http_authorization;
+ proxy_no_cache $http_authorization;
+
+ # 过期后仍向客户端提供旧缓存(stale),后台异步刷新
+ proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
+ proxy_cache_background_update on;
+ proxy_cache_revalidate on;
+ }
+ }
+}
+```
+
+`X-Cache-Status` 返回值说明:
+
+| 值 | 含义 |
+|------|------|
+| `HIT` | 缓存命中,直接返回 |
+| `MISS` | 缓存未命中,回源获取并缓存 |
+| `EXPIRED` | 缓存过期,向后端重新验证 |
+| `UPDATING` | 后台正在更新缓存,返回旧版本 |
+| `BYPASS` | 被配置主动跳过 |
+
+### 2. 负载均衡
+
+当后端有多台服务器时,Nginx 可以将流量分发到不同节点。
+
+```nginx
+upstream frontend {
+ # 三种常用策略:
+
+ # 轮询(默认)— 按顺序依次分配
+ # server 10.0.0.1:8080;
+ # server 10.0.0.2:8080;
+
+ # 权重 — 数值越大分到的请求越多
+ server 10.0.0.1:8080 weight=3;
+ server 10.0.0.2:8080 weight=1;
+
+ # IP Hash — 同一客户端 IP 固定分配到同一台后端
+ # ip_hash;
+
+ # 最少连接 — 分配到当前活跃连接最少的节点
+ # least_conn;
+}
+
+server {
+ listen 80;
+ location / {
+ proxy_pass http://frontend;
+ }
+}
+```
+
+常见策略对比:
+
+| 策略 | 适用场景 | 注意 |
+|------|---------|------|
+| **轮询 (round-robin)** | 通用场景 | 各节点性能相近时效果最好 |
+| **加权轮询 (weight)** | 节点性能差异大 | 可根据硬件能力分配权重 |
+| **IP Hash** | 有 Session 粘性需求 | Cookie 登录可能失效 |
+| **最少连接 (least_conn)** | 请求处理时间不均 | 长连接场景需留意偏差 |
+
+### 3. 健康检查与后端管理
+
+Nginx 社区版提供**被动式**健康检查(通过 fail_timeout 和 max_fails 参数),商业版(Plus)支持主动探测。
+
+```nginx
+upstream backend {
+ server 10.0.0.1:8080 weight=3 max_fails=3 fail_timeout=30s;
+ server 10.0.0.2:8080 weight=1 max_fails=3 fail_timeout=30s;
+
+ # 标记为 down — 不参与任何流量(常用于灰度发布逐步放量)
+ # server 10.0.0.3:8080 down;
+
+ # max_conns — 限制分配到该节点的并发数,超限时返回错误
+ # server 10.0.0.4:8080 max_conns=100;
+
+ # keepalive — 保持与后端的长连接,避免频繁建连开销
+ keepalive 32; # 每个 Worker 最多保持的空闲连接数
+ keepalive_timeout 60s;
+ keepalive_requests 100;
+}
+```
+
+> [!NOTE] passive health check 机制
+> `max_fails=3 fail_timeout=30s` 含义:**在 30 秒内,某个后端失败超过 3 次,则该后端在 30 秒内被剔除**。失败次数计数是从后端恢复后重新累加的。
+
+使用 `keepalive` 时需要在 proxy 层做配套设置:
+
+```nginx
+http {
+ upstream backend {
+ server 127.0.0.1:8080;
+ keepalive 32;
+ }
+
+ server {
+ location / {
+ proxy_pass http://backend;
+
+ # 关键:使用 HTTP/1.1 + Connection 头告诉 Nginx 复用连接
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ }
+ }
+}
+```
+
+> [!QUESTION] 为什么需要 `proxy_set_header Connection ""`?
+>
+> HTTP/1.0 默认关闭 keepalive,如果后端期望长连接,客户端(Nginx)发送的 `Connection: close` 会导致连接立即关闭,使 `keepalive` 配置形同虚设。清空该头部即可让 TCP 连接持续复用。
+
+### 4. 静态文件服务
+
+利用 Nginx 处理静态资源的优势,大幅减轻后端压力。
+
+```nginx
+server {
+ listen 80;
+ server_name cdn.example.com;
+
+ # 根目录指向
+ root /var/www/html;
+ index index.html index.htm;
+
+ # 精确匹配
+ location /assets/ {
+ expires 30d; # 浏览器缓存 30 天
+ add_header Cache-Control "public, immutable";
+ gzip_static on; # 提供 .gz 预压缩文件
+ }
+
+ # 错误页面
+ error_page 404 /404.html;
+ error_page 500 502 503 504 /50x.html;
+}
+```
+
+> [!TIP] 静态文件优化要点
+> - **gzip_static on**:如果预先生成了 `.gz` 文件,直接发送,节省 CPU
+> - **sendfile on**:启用内核零拷贝,静态文件传输效率极高
+> - **expires**:合理设置缓存头部,减少重复请求
+
+### 5. HTTPS / SSL
+
+```nginx
+server {
+ listen 443 ssl http2;
+ server_name example.com;
+
+ ssl_certificate /etc/nginx/ssl/cert.pem;
+ ssl_certificate_key /etc/nginx/ssl/key.pem;
+
+ # 推荐的现代 TLS 配置
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
+ ssl_prefer_server_ciphers off;
+
+ location / {
+ proxy_pass http://127.0.0.1:3000;
+ }
+}
+
+# HTTP → HTTPS 自动跳转
+server {
+ listen 80;
+ server_name example.com;
+ return 301 https://$host$request_uri;
+}
+```
+
+> [!NOTE] Let's Encrypt 免费证书
+> 用 Certbot 可以一键申请和续期免费证书:
+> ```bash
+> sudo certbot --nginx -d example.com
+> ```
+> Certbot 会自动帮你写入 Nginx 配置并完成 SSL 设置。
+
+### 6. 读写分离 & 限流
+
+```nginx
+# 请求速率限制
+limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
+
+# 连接数限制
+limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
+
+server {
+ location /api/ {
+ limit_req zone=api_limit burst=20 nodelay; # 突发 20 个请求
+ limit_conn conn_limit 10; # 每个 IP 最多 10 连接
+
+ proxy_pass http://backend;
+ }
+}
+```
+
+### 7. IP 访问控制
+
+通过 `allow` / `deny` 指令实现基于客户端 IP 的白名单和黑名单。
+
+```nginx
+# ── 场景 1:管理后台仅允许内网访问 ──
+location /admin/ {
+ allow 10.0.0.0/8; # 允许内网段
+ allow 192.168.1.0/24; # 允许办公网
+ deny all; # 拒绝其余所有 IP
+
+ proxy_pass http://admin-backend;
+}
+
+# ── 场景 2:特定接口封禁恶意 IP ──
+map $remote_addr $blocked_ip {
+ default 0;
+ 192.168.1.100 1; # 已知攻击者
+ 10.99.0.0/16 1; # 整个 C 段封禁
+}
+
+server {
+ location / {
+ if ($blocked_ip) {
+ return 403; # 直接返回,不转发到后端
+ }
+
+ proxy_pass http://backend;
+ }
+}
+```
+
+> [!TIP] 推荐做法
+> `allow/deny` 是在 Nginx 层面直接拦截(return 403),比让请求到达后端更安全高效。对于大名单封禁场景,使用 `geo` / `map` 模块配合数组匹配更灵活。
+
+### 8. HTTP Basic Auth 认证
+
+为特定接口添加用户名密码登录。
+
+```bash
+# 生成密码文件(首次)
+sudo htpasswd -c /etc/nginx/.htpasswd admin
+# 后续添加用户去掉 -c 参数即可
+sudo htpasswd /etc/nginx/.htpasswd developer
+```
+
+```nginx
+location /api/internal/ {
+ auth_basic "Restricted Area"; # 弹窗提示标题
+ auth_basic_user_file /etc/nginx/.htpasswd;
+
+ proxy_pass http://backend;
+}
+```
+
+## 安全加固
+
+生产环境中的 Nginx 是第一道防线,需要做好基础安全设置。
+
+```nginx
+http {
+ # ── 隐藏版本信息 ──
+ server_tokens off; # response header 中不显示 Nginx 版本
+ more_clear_headers Server; # 需 headers-more 模块,完全抹除 Server 头
+
+ # ── 安全响应头 ──
+ add_header X-Frame-Options "SAMEORIGIN" always; # 防止点击劫持
+ add_header X-Content-Type-Options "nosniff" always; # 禁止 MIME 嗅探
+ add_header X-XSS-Protection "1; mode=block" always; # XSS 过滤器
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+
+ # ── HSTS(强制 HTTPS)──
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+
+ # ── CORS 跨域支持 ──
+ set $cors_origin "";
+ if ($http_origin ~* "^https://(app\.example\.com|cdn\.example\.com)$") {
+ set $cors_origin $http_origin;
+ }
+
+ add_header Access-Control-Allow-Origin $cors_origin always;
+ add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
+ add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With" always;
+
+ location /api/ {
+ # 处理浏览器预检请求 (OPTIONS)
+ if ($request_method = OPTIONS) {
+ return 204;
+ }
+
+ proxy_pass http://backend;
+ }
+}
+```
+
+### 防盗链
+
+```nginx
+location ~* \.(jpg|jpeg|png|gif|webp|svg|ico)$ {
+ root /var/www/static;
+
+ # valid_referers — 只允许指定域名引用图片
+ valid_referers none blocked server_names
+ *.example.com example.com;
+
+ if ($invalid_referer) {
+ # 非法引用时返回 403 或一张占位图
+ return 403;
+ # rewrite ^/ https://example.com/no-hotlink.png break;
+ }
+}
+```
+
+## 高级特性
+
+### Location 匹配优先级
+
+```mermaid
+graph TD
+ A["URL请求到达"] --> B{"精确匹配\\n^~ ?"}
+ B -->|"是"| E["使用精确匹配"]
+ B -->|"否"| C{"正则匹配\\n按顺序?"}
+ C -->|"第一个匹配"| F["使用正则匹配 \\n立即停止"]
+ C -->|"无匹配"| D{"前缀最长匹配\\n是否有^~ ?"}
+ D -->|"是"| G["使用前缀匹配 \\n跳过正则"]
+ D -->|"否"| H["使用最长前缀匹配"]
+ style E fill:#9f9,stroke:#333
+ style F fill:#f99,stroke:#333
+ style G fill:#9cf,stroke:#333
+ style H fill:#ff9,stroke:#333
+```
+
+Location 匹配遵循严格优先级:
+
+| 优先级 | 类型 | 写法 | 说明 |
+|--------|------|------|------|
+| ① | 精确 | `=` | 完全相等才命中 |
+| ② | 最佳前缀 | `^~` | 最长前缀匹配后立即停止搜索正则 |
+| ③ | 正则 | `~` / `~*` | 按配置文件中的**顺序**匹配,第一个命中就停止 |
+| ④ | 普通前缀 | (无前缀符) | 记录最长匹配,但最终可能被正则覆盖 |
+
+```nginx
+location = /exact-match { # ① 精确匹配
+ return 200 "exact";
+}
+
+location ^~ /static/ { # ② 最佳前缀,跳过正则
+ root /var/www;
+}
+
+location ~* \.(jpg|png|gif)$ { # ③ 正则,忽略大小写
+ expires 7d;
+}
+
+location /images { # ④ 普通前缀
+ alias /var/www/images;
+}
+```
+
+> [!QUESTION] 思考题
+> 请求 `/images/logo.png` 会被哪个 location 处理?
+> - ① `/images` 和普通前缀
+> - ② `~* \.(jpg|png|gif)$` 正则
+>
+> **答案**:②。虽然 `/images` 的前缀更长,但正则匹配的优先级高于普通前缀。只有 `^~` 才能阻止正则匹配。
+
+### Gzip 压缩
+
+```nginx
+http {
+ gzip on;
+ gzip_vary on; # 添加 Vary: Accept-Encoding
+ gzip_proxied any; # 对所有代理请求都压缩
+ gzip_comp_level 6; # 压缩级别 1-9,6 是性价比最优
+ gzip_min_length 1k; # 小于 1k 的不压缩
+
+ gzip_types text/plain text/css application/json application/javascript
+ text/xml application/xml application/xml+rss text/javascript
+ image/svg+xml; # 需要压缩的内容类型
+}
+```
+
+### 日志配置
+
+```nginx
+http {
+ # 自定义日志格式
+ log_format main '$remote_addr - $remote_user [$time_local] '
+ '"$request" $status $body_bytes_sent '
+ '"$http_referer" "$http_user_agent" '
+ '$request_time $upstream_response_time';
+
+ access_log /var/log/nginx/access.log main;
+ error_log /var/log/nginx/error.log warn;
+
+ # 按小时分割日志
+ map $time_iso8601 $logdate {
+ ~^(\d{4}-\d{2}-\d{2}) %Y-%m-%d;
+ }
+}
+```
+
+> [!NOTE] 常用变量速查
+> | 变量 | 含义 |
+> |------|------|
+> | `$remote_addr` | 客户端真实 IP |
+> | `$request_uri` | 原始请求 URI(含参数) |
+> | `$status` | 响应状态码 |
+> | `$body_bytes_sent` | 发送给客户端的字节数 |
+> | `$request_time` | 请求处理总耗时(秒) |
+> | `$upstream_response_time` | 后端响应耗时 |
+> | `$http_host` | Host 请求头 |
+> | `$https` | 是否 HTTPS(on/off) |
+
+### Stream 模块 — L4 四层代理
+
+Nginx 不仅可以代理 HTTP(L7),通过 `stream` 块还可以做 **TCP/UDP 四层负载均衡**。
+
+```nginx
+# stream 块在 http 块同级
+stream {
+ # SSH 连接到内网跳板机
+ upstream ssh_backend {
+ server 10.0.0.5:22;
+ server 10.0.0.6:22;
+ }
+
+ # Redis / MySQL 等数据库代理
+ upstream redis_backend {
+ least_conn;
+ server 10.0.1.10:6379;
+ server 10.0.1.11:6379;
+ }
+
+ server {
+ listen 8022; # 对外暴露 SSH 端口
+ proxy_pass ssh_backend;
+ proxy_timeout 30s;
+ }
+
+ server {
+ listen 6379; # 对外暴露 Redis 端口
+ proxy_pass redis_backend;
+ }
+}
+
+http {
+ # ... HTTP 配置照常
+}
+```
+
+> [!NOTE] Stream vs HTTP
+> | 维度 | `http {}` 块 | `stream {}` 块 |
+> |------|-------------|----------------|
+> | 工作层级 | L7 应用层 | L4 传输层 |
+> | 协议支持 | HTTP、HTTPS | TCP、UDP |
+> | 可解析内容 | URL、Header、Cookie | 无法解析应用数据 |
+> | 典型场景 | Web 代理、静态服务 | SSH/DB 代理、游戏服务器 |
+
+### SSI — 服务端包含
+
+Nginx 内置 SSI 支持,可以在 HTML 页面中嵌入其他页面的片段。
+
+```nginx
+location / {
+ ssi on; # 启用 SSI
+ ssi_silent_errors on; # 忽略子请求错误,不影响主响应
+ root /var/www;
+}
+```
+
+HTML 中使用 `` 指令:
+
+```html
+
+
主内容区域
+
+```
+
+> [!CAUTION] 性能注意
+> SSI 会为每个 include 发起内部子请求,频繁使用会增加响应延迟。现代架构中更推荐前端组件化方案(React/Vue)。仅在遗留系统维护时考虑 SSI。
+
+---
+
+## 性能调优
+
+### 核心参数参考值
+
+```nginx
+worker_processes auto; # 等于 CPU 核心数
+
+events {
+ worker_connections 4096; # 根据预期并发调整
+ use epoll; # Linux 默认就是 epoll,可显式指定
+}
+
+http {
+ sendfile on; # 零拷贝传输静态文件
+ tcp_nopush on; # 与 sendfile 配合,减少数据包数量
+ tcp_nodelay on; # 禁用 Nagle 算法,降低延迟
+
+ keepalive_timeout 65; # 保持连接超时
+ client_max_body_size 20m; # 上传文件大小限制
+
+ open_file_cache max=10000 inactive=20s; # 文件描述符缓存
+ open_file_cache_valid 30s;
+ open_file_cache_min_uses 2;
+}
+```
+
+### 调优原则
+
+| 方向 | 方法 | 效果 |
+|------|------|------|
+| **并发提升** | `worker_connections` + `worker_processes` | 最大并发 ≈ workers × connections |
+| **I/O 优化** | `sendfile` + `tcp_nopush` | 静态文件传输减少 30%~50% |
+| **连接复用** | `keepalive_timeout` + `proxy_keepalive` | 减少 TCP 握手开销 |
+| **磁盘 I/O** | `open_file_cache` | 避免频繁打开文件描述符 |
+
+## 故障排查
+
+### 常用命令
+
+```bash
+# 查看 Nginx 进程信息
+ps aux | grep nginx
+
+# 重新加载配置(零停机)
+sudo nginx -s reload
+
+# 测试配置文件语法
+sudo nginx -t
+
+# 查看监听端口
+sudo ss -tlnp | grep nginx
+
+# 实时查看错误日志
+sudo tail -f /var/log/nginx/error.log
+
+# 检查配置中包含的所有 server_name
+grep -r server_name /etc/nginx/
+```
+
+### 常见问题速查
+
+> [!ERROR] 502 Bad Gateway
+> **原因**:后端服务未启动或无法连接。
+> **排查**:确认后端服务正常运行;检查 `proxy_pass` 的地址和端口;查看后端日志。
+
+> [!ERROR] 504 Gateway Timeout
+> **原因**:后端响应超时(默认 60 秒)。
+> **解决**:增加 `proxy_read_timeout`,或优化后端查询性能。
+> ```nginx
+> proxy_connect_timeout 5s;
+> proxy_send_timeout 10s;
+> proxy_read_timeout 60s;
+> ```
+
+> [!ERROR] 413 Request Entity Too Large
+> **原因**:上传文件超过 `client_max_body_size`。
+> **解决**:在对应 location 中增大限制。
+> ```nginx
+> client_max_body_size 100m;
+> ```
+
+> [!WARNING] 403 Forbidden(静态文件)
+> **常见原因**:权限不足或 `index` 文件不存在。
+> **解决**:确认 Nginx 用户(`www-data`/`nginx`)对目录有读权限;确认文件存在。
+
+## 配置管理与部署
+
+### 配置文件拆分
+
+生产环境中,不建议把所有配置写在一个大文件中。利用 `include` 实现模块化拆分。
+
+```
+/etc/nginx/
+├── nginx.conf # 主配置(全局 + events + http)
+├── conf.d/
+│ ├── api.conf # API 代理配置
+│ ├── web.conf # Web 前端配置
+│ └── static.conf # 静态资源服务配置
+├── sites-available/ # 可用站点配置(软链到 enabled)
+│ ├── example.com
+│ └── staging.example.com
+├── sites-enabled/ # 已启用站点
+├── upstream/
+│ ├── backend-pool.conf # 后端服务器池定义
+│ └── cache.conf
+├── snippets/ # 可复用片段
+│ ├── ssl-params.conf # SSL 安全参数
+│ ├── rate-limit.conf # 限流规则
+│ └── security-headers.conf
+└── mime.types
+```
+
+```nginx
+# nginx.conf — 主入口保持精简
+worker_processes auto;
+pid /run/nginx.pid;
+error_log /var/log/nginx/error.log warn;
+
+events { worker_connections 1024; }
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ # 引入所有 upstream 定义
+ include /etc/nginx/upstream/*.conf;
+
+ # 引入所有 server 配置
+ include /etc/nginx/conf.d/*.conf;
+ include /etc/nginx/sites-enabled/*;
+}
+```
+
+> [!TIP] 为什么这样设计?
+> - `conf.d/` 下的 `.conf` 文件按功能划分,每个文件一个主题
+> - `sites-available` / `sites-enabled` 模式源自 Apache:修改后通过 `ln -s` 启用,方便回滚
+> - `snippets/` 存放通用模板,避免多处复制粘贴同一份 SSL 或安全配置
+
+### Docker Compose 完整示例
+
+用 Docker Compose 一键拉起 Nginx + 后端服务的开发环境。
+
+```yaml
+# docker-compose.yml
+services:
+ nginx:
+ image: nginx:alpine
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - ./nginx/conf.d:/etc/nginx/conf.d:ro
+ - ./nginx/certs:/etc/nginx/certs:ro
+ - ./frontend/dist:/usr/share/nginx/html:ro
+ - nginx-cache:/var/cache/nginx
+ - nginx-run:/var/run
+ depends_on:
+ - api
+ - webapp
+ restart: unless-stopped
+
+ api:
+ build: ./api
+ environment:
+ - NODE_ENV=production
+ - DATABASE_URL=${DATABASE_URL}
+ expose:
+ - "3000"
+ restart: unless-stopped
+
+ webapp:
+ build: ./webapp
+ expose:
+ - "8080"
+ restart: unless-staged
+
+volumes:
+ nginx-cache:
+ nginx-run:
+```
+
+```nginx
+# nginx/conf.d/default.conf
+upstream api_backend {
+ server api:3000;
+}
+
+server {
+ listen 80;
+ server_name _;
+
+ location /api/ {
+ proxy_pass http://api_backend;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+
+ location / {
+ root /usr/share/nginx/html;
+ try_files $uri $uri/ /index.html; # SPA 路由兜底
+ }
+}
+```
+
+> [!NOTE] Docker Nginx 注意事项
+> - 挂载配置时使用 `:ro` 只读标志,防止容器内被篡改
+> - `/var/run` 和 `/var/cache/nginx` 需要用 volume,否则重载时会因缺少 pid 文件而报错
+> - SPA 应用需要 `try_files` 兜底到 `index.html`,否则直接刷新子路由会 404
+
+### 灰度发布(Canary Release)
+
+Nginx 可以实现基于权重或客户端特征的渐进式流量切分。
+
+```nginx
+http {
+ # 基于请求头实现灰度(前端在特定版本时携带 x-canary: true)
+ map $http_x_canary $canary_upstream {
+ default "";
+ "true" canary_group;
+ }
+
+ # 基于用户 IP 的哈希分流(同一 IP 始终访问同一版本)
+ geo $hash_upstream {
+ default production_pool;
+ 10.0.0.0/8 canary_group; # 仅内部 IP 走新版
+ }
+
+ upstream production_pool {
+ server 10.1.0.1:8080;
+ server 10.1.0.2:8080;
+ }
+
+ upstream canary_group {
+ server 10.2.0.1:8080; # 新版本仅一台机器测试
+ }
+
+ server {
+ location / {
+ # 优先使用 header 分流,其次 IP hash,最后默认生产
+ set $target $canary_upstream;
+ if ($target = "") {
+ set $target $hash_upstream;
+ }
+ if ($target = "") {
+ set $target production_pool;
+ }
+
+ proxy_pass http://$target;
+ }
+ }
+}
+```
+
+> [!TIP] 灰度发布常见策略
+> | 阶段 | 流量分配 | 适用场景 |
+> |------|---------|---------|
+> | **内部测试** | 100% 内部 IP → 新版 | 开发团队自测 |
+> | **小流量灰度** | 1% ~ 5% 随机 → 新版 | 验证核心指标 |
+> | **逐步放量** | 逐步提升 10% → 50% → 100% | A/B 结果良好 |
+> | **全量上线** | 100% → 新版 | 灰度结束 |
+
+### NJS — Nginx JavaScript 脚本
+
+Nginx 支持嵌入 JavaScript 代码,实现动态配置逻辑(无需 reload)。
+
+```bash
+# 安装 njs 模块
+apt install nginx-module-njs # Debian/Ubuntu
+```
+
+```nginx
+# 加载 JS 脚本
+load_module /usr/lib/nginx/modules/ngx_http_js_module.so;
+
+http {
+ js_import filter.js;
+
+ server {
+ location / {
+ js_content filter.add_custom_header;
+ proxy_pass http://backend;
+ }
+ }
+}
+```
+
+```javascript
+// filter.js
+export function addCustomHeader(r) {
+ r.headersOut["X-Custom-Version"] = "v2.1";
+ r.headersOut["X-Served-By"] = r.serverName;
+ r.sendHeader();
+ r.finish();
+}
+
+export default { addCustomHeader };
+```
+
+> [!NOTE] NJS vs OpenResty
+> - **NJS**:轻量级,适合简单逻辑(改 Header、条件路由),语法受限但性能极高
+> - **OpenResty**(Nginx + Lua):完整的编程平台,可以操作数据库、调用外部 API,适合复杂网关场景
+
+---
+
+## 与 CDN 的配合
+
+典型的生产架构中,Nginx 常作为 **边缘节点** 或 **回源层** 与 CDN 配合工作:
+
+```mermaid
+sequenceDiagram
+ participant Client as 客户端
+ participant CDN as CDN 边缘节点
+ participant Nginx as Nginx 边缘/回源
+ participant App as 应用后端
+
+ Client->>CDN: 请求静态资源
+ CDN->>CDN: 缓存命中?
+ alt 命中
+ CDN-->>Client: 返回缓存内容
+ else 未命中
+ CDN->>Nginx: 回源请求
+ Nginx->>App: 转发请求
+ App-->>Nginx: 响应数据
+ Nginx-->>CDN: 返回内容并缓存
+ CDN-->>Client: 返回内容
+ end
+```
+
+> [!TIP] Nginx + CDN 的最佳实践
+> - 静态资源走 CDN,动态请求回源到 Nginx + 后端
+> - Nginx 可以作为 CDN 的边缘服务器,做一层统一入口
+> - 通过 `X-Forwarded-For` 追踪真实的用户 IP(即使经过了 CDN)
+
+## 关联笔记
+
+- [[hhs/DEV/Docker/Docker.md]]
+- [[hhs/DEV/Linux/Linux.md]]
diff --git a/hhs/DEV/OSS/OSS.md b/hhs/DEV/OSS/OSS.md
new file mode 100644
index 0000000..a7d4cfd
--- /dev/null
+++ b/hhs/DEV/OSS/OSS.md
@@ -0,0 +1,667 @@
+---
+tags: [后端, Go, 对象存储, S3, 文件上传]
+create time: 2026-05-07 19:12
+---
+
+# OSS 对象存储
+
+## 概述
+
+OSS(Object Storage Service)即**对象存储**,是一种将非结构化数据作为对象存储在云端的方案。阿里云的 OSS、AWS 的 S3 以及兼容 S3 协议的 MinIO 本质上是同一种技术——通过 HTTP API 对海量二进制数据进行增删改查。理解其核心概念和工作模式,是构建现代 Web 应用基础设施的关键一环。
+
+思考题:和传统的关系型数据库相比,为什么图片、视频、日志文件这类数据不适合存在 MySQL 里?
+
+## 正文
+
+### 1. 核心概念
+
+对象存储有三个基本概念:
+
+| 概念 | 说明 | 类比关系型数据库 |
+|------|------|------------------|
+| **Bucket(存储桶)** | 对象的容器,类似"数据库" | Database / Table |
+| **Object(对象)** | 实际存储的数据,包含 Key + Body + Metadata,类似"行记录" | Row |
+| **Key(键)** | 对象的全局唯一标识,类似文件系统路径 | File Path |
+
+> [!key] 关键点:扁平结构
+> 对象存储是一个**扁平结构**——没有真正的目录层级,所谓的 `folder/` 只是 Key 中的一部分字符(如 `photos/2026/vacation.jpg`)。管理面板按 `/` 做可视化分组,但这仅是展示层面的伪层级。
+
+```mermaid
+flowchart LR
+ subgraph OSS_BUCKET["Bucket: my-app-data"]
+ direction TB
+ O1["Key: uploads/avatar/001.png
Body: (PNG binary data)"]
+ O2["Key: docs/report.pdf
Body: (PDF binary data)"]
+ O3["Key: backups/db-dump.sql.gz
Body: (SQL compressed)"]
+ end
+
+ classDef bucketStyle fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
+ class OSS_BUCKET bucketStyle
+```
+
+**Region 和 Endpoint:**
+
+每个 Bucket 必须属于一个地域(Region),Region 决定了数据的物理存储位置。访问时需要使用对应的 Endpoint:
+
+| 地域 | 内网 Endpoint | 外网 Endpoint |
+|------|---------------|---------------|
+| 杭州 (oss-cn-hangzhou) | `oss-cn-hangzhou-internal.aliyuncs.com` | `oss-cn-hangzhou.aliyuncs.com` |
+| 上海 (oss-cn-shanghai) | `oss-cn-shanghai-internal.aliyuncs.com` | `oss-cn-shanghai.aliyuncs.com` |
+| 北京 (oss-cn-beijing) | `oss-cn-beijing-internal.aliyuncs.com` | `oss-cn-beijing.aliyuncs.com` |
+
+> [!tip] 省钱提速第一步
+> 当你的 Go 服务和 OSS Bucket **在同一 Region** 时,务必使用**内网 Endpoint**。内网流量免费且速度比外网快 5~10 倍。这是最容易被忽略的基础优化。
+
+### 2. Go SDK 选型
+
+目前 Go 生态中有三套主流 OSS/S3 SDK,选择哪一套取决于你的目标服务:
+
+```mermaid
+graph TD
+ A["Go 对象存储 SDK"] --> B["Aliyun OSS SDK
alibabacloud-oss-go-sdk-v2"]
+ A --> C["Minio Client SDK
minio/go-minio"]
+ A --> D["AWS SDK for Go v2
aws/aws-sdk-go-v2/s3"]
+
+ B --> B1["适用于阿里云 OSS"]
+ B --> B2["API 完全覆盖 OSS 功能"]
+ B --> B3["绑定阿里云鉴权体系"]
+
+ C --> C1["兼容 S3/OSS/COS/GCS 等"]
+ C --> C2["API 接近标准库 io.Reader"]
+ C --> C3["推荐用于多云/混合云场景"]
+
+ D --> D1["AWS 官方 SDK"]
+ D --> D2["功能最全面"]
+ D --> D3["体积大, 编译慢"]
+
+ style C1 fill:#e8f5e9,stroke:#2e7d32
+ style B1 fill:#fff3e0,stroke:#f57c00
+ style D1 fill:#e3f2fd,stroke:#1565c0
+```
+
+| 对比项 | Aliyun OSS SDK | Minio Client | AWS S3 SDK v2 |
+|--------|---------------|-------------|---------------|
+| 适配服务 | 仅阿里云 OSS | S3/OSS/COS/GCS等 | AWS S3 / Cloudflare R2 |
+| 安装大小 | ~5MB | ~2MB | ~150MB |
+| API 风格 | RESTful 包装 | `io.Reader` 流式 | 深度异步链式 |
+| 预签名 URL | ✅ 原生支持 | ✅ `PresignedGetObject` | ✅ `PresignObjectAPI` |
+| 推荐度 | 只用阿里云 | **多云通用首选** | AWS 重度用户 |
+
+> [!summary] 选型结论
+> 如果你只做阿里云,用官方 SDK;如果需要同时对接 S3、MinIO 或其他兼容 S3 的服务,选择 Minio Client SDK。
+
+### 3. 初始化客户端
+
+**方式一:阿里云官方 SDK(v2)**
+
+```go
+import (
+ oss "github.com/alibabacloud-go/oss-20190517/v2/client"
+)
+
+// 从环境变量读取凭证,避免硬编码
+client := oss.NewClient(&oss.Config{
+ Endpoint: oss.String("oss-cn-hangzhou.aliyuncs.com"),
+ AccessKeyId: oss.String(os.Getenv("OSS_ACCESS_KEY_ID")),
+ AccessKeySecret: oss.String(os.Getenv("OSS_ACCESS_KEY_SECRET")),
+})
+```
+
+**方式二:Minio Client(更简洁,跨云兼容)**
+
+```go
+import "github.com/minio/minio-go/v7"
+
+client, err := minio.New("oss-cn-hangzhou.aliyuncs.com", &minio.Options{
+ Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
+ Secure: true, // HTTPS
+})
+if err != nil {
+ log.Fatal(err)
+}
+```
+
+> [!danger] 安全红线
+> 永远不要将 AccessKey 写在代码里。生产环境应使用环境变量、密钥管理服务(KMS / Vault / 云厂商 IAM Role)注入。AK/SK 泄露 = 别人可以读写你的数据甚至产生账单。
+
+### 4. Bucket 操作
+
+#### 创建与查询
+
+```go
+// 创建 Bucket(Minio 写法)
+bucketName := "my-app-uploads"
+location := "cn-hangzhou"
+
+err := client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{
+ Region: location,
+})
+// 如果已存在则 ErrBucketExists 忽略即可
+
+// 检查 Bucket 是否存在
+exists, _ := client.BucketExists(ctx, bucketName)
+```
+
+#### 权限模型
+
+| ACL | 说明 | 适用场景 |
+|-----|------|---------|
+| `Private`(默认) | 只有 Owner 可读写 | 用户上传的文件、备份 |
+| `PublicRead` | Owner 读写 + 所有人可读 | CDN 加速的图片资源 |
+| `PublicReadWrite` | 所有人可读写 | **极不推荐** — 任何人都可以上传和删除文件,可能被恶意刷费 |
+
+### 5. 文件上传:三种架构对比
+
+前端要上传图片/文件到 OSS,主要有以下三种架构方案。选择合适的方案直接影响系统的**可扩展性**、**安全性**和**开发成本**。
+
+#### 方案 A:服务端中转上传(Server Relay)
+
+```mermaid
+sequenceDiagram
+ participant FE as 浏览器 / 客户端
+ participant GO as Gin Server
+ participant OSS as OSS 服务
+
+ FE->>GO: POST 文件 (multipart/form-data)
+ Note over GO: 解析请求体 (io.Reader 流式)
+ GO->>OSS: PUT 上传 (Stream 转发)
+ OSS-->>GO: 200 OK (ETag, Location)
+ GO-->>FE: 200 OK (返回 OSS 访问地址)
+```
+
+**工作流程:**
+1. 客户端将文件 POST 到 Go 服务端(multipart/form-data)
+2. Go 服务端接收后,通过 SDK 将文件写入 OSS
+3. 返回文件的 OSS 访问地址给客户端
+
+```go
+func uploadWithRelay(c *gin.Context) {
+ // 限制文件大小(防内存溢出 + 防恶意大文件)
+ c.Request.ParseMultipartForm(32 << 20) // 最大 32MB
+
+ file, header, err := c.Request.FormFile("file")
+ if err != nil {
+ c.JSON(400, gin.H{"error": "invalid file"})
+ return
+ }
+ defer file.Close()
+
+ // 校验文件类型
+ buf := make([]byte, 512)
+ file.Read(buf)
+ contentType := http.DetectContentType(buf)
+ allowed := map[string]bool{
+ "image/jpeg": true, "image/png": true, "image/webp": true,
+ }
+ if !allowed[contentType] {
+ c.JSON(400, gin.H{"error": "unsupported file type"})
+ return
+ }
+ // rewind for reading again
+ file.Seek(0, io.SeekStart)
+
+ // 重命名防止冲突(UUID_原文件名)
+ fileName := uuid.New().String() + "_" + header.Filename
+
+ _, err = client.PutObject(c.Request.Context(), bucketName, fileName, file, -1, minio.PutObjectOptions{
+ ContentType: contentType,
+ })
+ if err != nil {
+ c.JSON(500, gin.H{"error": err.Error()})
+ return
+ }
+
+ // 记录元信息到数据库
+ db.Create(&FileRecord{Key: fileName, Size: header.Size, Type: contentType})
+
+ c.JSON(200, gin.H{
+ "url": fmt.Sprintf("https://%s.%s/%s", bucketName, endpoint, fileName),
+ "size": header.Size,
+ })
+}
+```
+
+这段代码演示了服务端中转上传的完整流程:首先通过 `ParseMultipartForm` 限制文件体积防止内存溢出;接着用 `http.DetectContentType` 读取文件头 512 字节进行类型校验,避免恶意文件伪装上传;然后使用 UUID 重命名解决命名冲突和路径遍历风险;最后将对象写入 OSS 并将元信息持久化到数据库。需要注意的是,客户端文件先被读入 Go 进程的内存缓冲区(`file.Seek(0, io.SeekStart)` rewind),再通过 SDK Stream 转发到 OSS——这就是为什么该方案在并发量大时会成为瓶颈。
+
+#### 方案 B:预签名 URL 直传(Pre-Signed URL)
+
+```mermaid
+sequenceDiagram
+ participant FE as 浏览器 / 客户端
+ participant GO as Gin Server
+ participant OSS as OSS 服务
+
+ FE->>GO: GET /upload/url?filename=photo.jpg
+ Note over GO: 验证登录态和权限
+ GO->>OSS: PresignedPutObject(photo.jpg, 15min)
+ OSS-->>GO: (预签名URL)含签名和过期参数
+ GO-->>FE: (返回URL和文件元信息)
+
+ FE->>OSS: PUT 直接上传
Content-Type: image/jpeg
文件二进制数据
+ OSS-->>FE: 200 OK
+
+ opt 通知服务端完成
+ FE->>GO: POST /upload/callback
文件名和元信息
+ Note over GO: 验证对象存在于 OSS
+ GO-->>FE: 200 OK (记录元信息到 DB)
+ end
+```
+
+**工作流程:**
+1. 客户端请求服务端获取预签名 URL
+2. 服务端调用 OSS 的 `PresignedPutObject` 生成有时效性的上传链接
+3. 客户端拿到 URL 后**直接 PUT 到 OSS**,不经服务端
+4. 上传完成后回调服务端,记录元信息到数据库
+
+> [!info] 详细说明
+> 预签名 URL 的安全要点、完整代码实现及回调验证逻辑详见 [第 7 节](#7-预签名-url-免服务端中转)。
+
+#### 方案 C:STS 临时凭证直传(STS Credentials)
+
+```mermaid
+sequenceDiagram
+ participant FE as 浏览器 / 客户端
+ participant GO as Gin Server
+ participant STS as STS 服务
+ participant OSS as OSS 服务
+
+ FE->>GO: 请求上传权限
+ Note over GO: 验证用户身份
+ GO->>STS: AssumeRole (角色Arn + SessionName)
+ STS-->>GO: AccessKeyId + AccessKeySecret + SecurityToken
有效期15分钟
+ GO-->>FE: (返回临时凭证)
+
+ FE->>OSS: 用临时凭证直接上传
PostObject API
+ OSS-->>FE: 200 OK
+```
+
+**工作流程:**
+1. 客户端请求服务端获取上传权限
+2. 服务端调用 STS(Security Token Service)AssumeRole 获取临时凭据
+3. 将临时凭据(AccessKeyId + Secret + Token)发给客户端
+4. 客户端直接使用这些凭据通过 OSS PostObject API 上传
+
+```go
+import "github.com/aliyun/aliyun-sts-go-sdk"
+
+func getSTSCredentials(c *gin.Context) {
+ stsClient := stssdk.NewClientWithAccessKey("cn-hangzhou", masterAK, masterSK)
+ request := stssdk.CreateAssumeRoleRequest()
+ request.RoleArn = "acs:ram:::role/oss-upload-role"
+ request.RoleSessionName = "web-session-" + userID
+ request.DurationSeconds = 900 // 15 分钟
+ request.Policy = `{
+ "Statement": [{
+ "Action": ["oss:PutObject", "oss:PostObject"],
+ "Effect": "Allow",
+ "Resource": ["acs:oss:*:*:my-bucket/uploads/*"]
+ }]
+ }`
+
+ response, err := stsClient.AssumeRole(request)
+ if err != nil {
+ c.JSON(500, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(200, gin.H{
+ "accessKeyId": response.Credentials.AccessKeyId,
+ "accessKeySecret": response.Credentials.AccessKeySecret,
+ "securityToken": response.Credentials.SecurityToken,
+ "expiration": response.Credentials.Expiration,
+ })
+}
+```
+
+> [!tip] 关键区别
+> 预签名 URL 是**一次性**的、针对特定文件的上传链接;STS 临时凭证是一组**通用**的上传凭据,可以在有效期内用于上传任意文件(受 Policy 约束)。
+
+#### 三种方案对比
+
+| 维度 | 方案 A:服务端中转 | 方案 B:预签名 URL 直传 | 方案 C:STS 临时凭证 |
+|------|-------------------|------------------------|--------------------|
+| **流量走向** | 客户端 → 服务端 → OSS | 客户端 → OSS | 客户端 → OSS |
+| **服务端带宽消耗** | N × 文件大小 ⚠️ | 零带宽 ✅ | 零带宽 ✅ |
+| **水平扩展性** | 差(服务端成为瓶颈) | 优秀 | 优秀 |
+| **延迟** | 往返两次(双 RTT) | 一次直传(单 RTT) | 一次直传(单 RTT) |
+| **控制粒度** | 粗(整个文件过服务端) | 中(URL 级控制) | 细(Policy 级可控) |
+| **实现复杂度** | 低 ✅ | 中 ⚡ | 高 ❌ |
+| **安全性** | 高(服务端全拦截) | 中高(签名+过期) | 中(需配置好 Policy) |
+| **适合场景** | 小文件 (<1MB)、需要服务端二次处理 | 普通上传(头像、截图等) | 大文件、批量上传、企业级应用 |
+
+#### 如何选型?
+
+```mermaid
+flowchart TD
+ A["用户上传文件"] --> B{"文件大小?"}
+ B -- "<= 1MB" --> C{"是否需要服务端
内容分析/转码?"}
+ B -- "> 1MB 或不确定" --> D{"预期并发量?"}
+
+ C -- "否" --> E["✅ 方案 B: 预签名 URL
最简单+零带宽开销"]
+ C -- "是" --> F["✅ 方案 A: 服务端中转
可控但限并发"]
+
+ D -- "QPS < 100" --> G["✅ 方案 B: 预签名 URL"]
+ D -- "QPS >= 100 或
大批量/大文件" --> H["✅ 方案 C: STS 临时凭证
灵活且可扩展"]
+```
+
+> [!success] 经验法则
+> 大多数 Web 应用选**方案 B(预签名 URL)**就足够了。只有在需要对文件内容进行服务端分析(如病毒扫描、图像压缩、OCR),或者并发量极大时才考虑方案 A 或 C。
+
+---
+
+*本节各方案代码已包含完整实现要点;更多细节可参考:[对象基本操作](#6-对象基本操作)、[预签名 URL 详解](#7-预签名-url-免服务端中转)、[并发与安全注意事项](#9-并发与安全注意事项)。*
+
+### 6. 对象基本操作
+
+#### 上传(服务端直传示例)
+
+```go
+// 小文件:< 100MB 时直接用 FPutObject
+err := client.FPutObject(ctx, bucketName, "uploads/photo.jpg", "./photo.jpg", minio.PutObjectOptions{
+ ContentType: "image/jpeg",
+})
+
+// 大文件:自动分片上传(Minio SDK 内部处理)
+file, _ := os.Open("large-video.mp4")
+_, err = client.PutObject(ctx, bucketName, "videos/large.mp4", file, -1, minio.PutObjectOptions{
+ PartSize: 10 << 20, // 每片 10MB
+ ContentType: "video/mp4",
+})
+```
+
+> [!info] 分片阈值
+> Minio SDK 默认当文件超过 **5MB** 时自动启动分片上传,最大支持单文件 **50TB**。无需手动实现。
+
+#### 下载
+
+```go
+// 完整下载 → io.Reader
+obj, err := client.GetObject(ctx, bucketName, "uploads/photo.jpg", minio.GetObjectOptions{})
+defer obj.Close()
+
+data, _ := io.ReadAll(obj)
+_ = data // 写入数据库或直接返回给前端
+
+// 范围下载(只读文件的某个区间)
+opts := minio.GetObjectOptions{}
+opts.Set("Range", "bytes=0-1023") // 只读前 1KB
+```
+
+这里演示了两种下载模式:`GetObject` 返回的是 `io.ReadCloser`,可以配合 `io.ReadAll` 一次性加载(适合小文件),也可以通过 HTTP `Range` 头实现**断点续传/视频拖拽**——OSS 只返回请求的字节区间,大幅节省带宽。
+
+#### 删除
+
+```go
+// 删除单个对象
+client.RemoveObject(ctx, bucketName, "uploads/old-photo.jpg", minio.RemoveObjectOptions{})
+
+// 批量删除
+objsCh := make(chan minio.ObjectInfo, 100)
+for info := range objsCh {
+ client.RemoveObject(ctx, bucketName, info.Key, minio.RemoveObjectOptions{})
+}
+close(objsCh)
+client.RemoveObjects(ctx, bucketName, objsCh, minio.RemoveObjectsOptions{})
+```
+
+`RemoveObject` 是同步调用,适合单文件删除。批量删除使用 `RemoveObjects` API——它接收一个 object channel,SDK 内部**并发执行删除请求**(默认并发数 10),比逐个循环调 `RemoveObject` 快数个量级。注意 channel 必须先 `close()` 再调用 `RemoveObjects`,否则会阻塞。
+
+#### 列出对象
+
+```go
+// 列出以 "uploads/" 开头的对象(模拟目录效果)
+objects := client.ListObjects(ctx, bucketName, minio.ListObjectsOptions{
+ Prefix: "uploads/", // 前缀过滤
+ Recursive: true, // false 则只在虚拟"目录"层面
+})
+
+for obj := range objects {
+ fmt.Printf("%-40s %8d bytes %s\n", obj.Key, obj.Size, obj.LastModified)
+}
+```
+
+`ListObjects` 返回的是一个 channel,可以**边取边处理**而无需一次性加载全部结果——对于百万级文件的 Bucket,这避免了将全量元数据拉入内存。当 `Recursive=false` 时配合 `Delimiter("/")` 可模拟"目录树"分页效果,类似 AWS S3 的"共同前缀"语义。
+
+### 7. 预签名 URL — 免服务端中转
+
+**这是对象存储最常用的高级模式之一。**
+
+> **核心问题**:用户上传图片时,如果不走对象存储直传,而是先 POST 到 Go 服务端再由服务端转发到 OSS——当 1000 个用户同时上传 5MB 图片时,Go 进程需要消耗多少带宽?
+
+答案很简单:**5MB × 1000 = 5GB 内存 + 带宽压力**。这就是为什么应该让客户端直接跟 OSS 对话。
+
+**预签名 URL 流程:**
+
+```mermaid
+sequenceDiagram
+ participant FE as 浏览器 / 客户端
+ participant GO as Gin Server
+ participant OSS as OSS 服务
+
+ FE->>GO: GET /upload/url
?filename=photo.jpg
+ Note over GO: 验证用户登录态和权限
+ GO->>OSS: PresignedGetObject(photo.jpg, 15min)
+ OSS-->>GO: (预签名URL)
含签名和过期参数
+ GO-->>FE: (返回URL和类型信息)
+
+ FE->>OSS: PUT直接上传
Content-Type: image/jpeg
文件二进制数据
+ OSS-->>FE: 200 OK
+
+ opt 通知服务端完成
+ FE->>GO: POST /upload/callback
文件名和元信息
+ GO-->>FE: 200 OK
+ end
+```
+
+> [!note] 安全要点
+> 预签名 URL 有时间限制(通常 5~15 分钟),过期后无效。即使 URL 泄露也无法长期滥用。
+
+**代码示例:**
+
+```go
+func getPresignedUploadURL(c *gin.Context) {
+ filename := c.Query("filename")
+ contentType := c.DefaultQuery("contentType", "application/octet-stream")
+
+ // 生成一个有效期 15 分钟的 PUT 预签名 URL
+ // 注意:PresignedPutObject 不允许设置 Content-Type 约束
+ // 如需严格校验,应在 Callback 中二次确认
+ url, err := client.PresignedPutObject(
+ context.Background(),
+ bucketName,
+ filepath.Join("uploads", filename),
+ 15*time.Minute,
+ )
+ if err != nil {
+ c.JSON(500, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(200, gin.H{
+ "uploadURL": url.String(),
+ "fileName": filepath.Join("uploads", filename),
+ "contentType": contentType,
+ "expiresIn": 900, // 秒
+ })
+}
+```
+
+**POST 端回调验证(防越权上传):**
+
+```go
+func uploadCallback(c *gin.Context) {
+ var req struct {
+ FileName string `json:"fileName" binding:"required"`
+ FileSize int64 `json:"fileSize" binding:"required"`
+ }
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(400, gin.H{"error": "invalid params"})
+ return
+ }
+
+ key := filepath.Join("uploads", req.FileName)
+
+ // 检查对象是否真的存在于 OSS 中
+ _, err := client.StatObject(context.Background(), bucketName, key, minio.StatObjectOptions{})
+ if err != nil {
+ c.JSON(404, gin.H{"error": "file not found in OSS"})
+ return
+ }
+
+ // 保存元信息到数据库
+ db.Create(&FileRecord{Key: key, Size: req.FileSize})
+ c.JSON(200, gin.H{"status": "recorded"})
+}
+```
+
+### 8. 生命周期管理(Lifecycle)
+
+频繁删除和上传会产生大量碎片化的旧版本,合理配置 Lifecycle 可以节省成本并避免磁盘爆炸:
+
+```go
+import "github.com/minio/minio-go/v7/pkg/lifecycle"
+
+err := client.SetBucketLifecycle(ctx, bucketName, &lifecycle.Configuration{
+ Rules: []lifecycle.Rule{
+ {
+ ID: "expire-old-uploads",
+ Status: lifecycle.RuleEnabled,
+ Prefix: "uploads/",
+ Expiration: lifecycle.Expiration{Days: 90}, // 90天后自动删除
+ },
+ {
+ ID: "transition-rare-access",
+ Status: lifecycle.RuleEnabled,
+ Prefix: "backups/",
+ Transition: lifecycle.Transition{
+ Days: 30,
+ StorageClass: "GLACIER", // 转归档存储(S3 兼容写法)
+ },
+ },
+ },
+})
+```
+
+这里定义了两条规则:**第一条**清理 `uploads/` 目录下超过 90 天的旧文件,防止用户缓存无限膨胀;**第二条**将 `backups/` 下超过 30 天的备份数据从标准存储自动迁移到更便宜的 Glacier/归档存储。两种动作都按 Key 前缀(Prefix)作用——相当于对虚拟目录设置独立策略。注意阿里云 SDK 对应的是 `SetBucketLifecycleRule` API,参数名略有不同但语义一致。
+
+**存储类型与成本对比(阿里云为例):**
+
+| 存储类型 | 单价 (元/GB/月) | 取回费用 | 最低存储时长 | 适用场景 |
+|---------|---------------|---------|------------|---------|
+| 标准存储(Standard) | ~0.12 | 无 | 无 | 活跃数据、频繁访问 |
+| 低频访问(IA) | ~0.084 | 有 | 30天 | 很少读取但需即时访问 |
+| 归档存储 | ~0.042 | 有 + 恢复时间 | 60天 | 合规存档、历史备份 |
+| 冷归档 | ~0.021 | 有 + 数小时恢复 | 180天 | 极低频、合规留存 |
+
+> [!tip] 策略建议
+> 新创建的 Bucket 使用标准存储即可。在业务逻辑中标记哪些文件属于"冷数据",然后通过 Lifecycle 规则自动降级。不要人为在每个上传接口中判断冷热。
+
+### 9. 并发与安全注意事项
+
+**常见陷阱和解决方案:**
+
+```mermaid
+flowchart TD
+ subgraph TRAPS["常见陷阱"]
+ T1["AccessKey泄露
导致账单爆炸"]
+ T2["未限流
OSS请求被拒绝"]
+ T3["命名冲突
同名文件覆盖"]
+ T4["内存溢出
全量读入内存"]
+ T5["内网跨地域
用了公网Endpoint"]
+ end
+
+ subgraph FIXES["解决方案"]
+ F1["使用RAM子账号+STS
临时凭证(15min过期)"]
+ F2["客户端侧限速+重试退避
指数退避算法"]
+ F3["使用UUID或NanoID重命名
fileUUID_扩展名"]
+ F4["始终用io.Reader流式处理
避免ReadAll读大文件"]
+ F5["确保ECS与Bucket同Region
使用内网internal endpoint"]
+ end
+
+ T1 --> F1
+ T2 --> F2
+ T3 --> F3
+ T4 --> F4
+ T5 --> F5
+
+ style T1 fill:#ffebee,stroke:#c62828
+ style T2 fill:#ffebee,stroke:#c62828
+ style T3 fill:#fff3e0,stroke:#f57c00
+ style T4 fill:#ffebee,stroke:#c62828
+ style T5 fill:#fff3e0,stroke:#f57c00
+
+ style F1 fill:#e8f5e9,stroke:#2e7d32
+ style F2 fill:#e8f5e9,stroke:#2e7d32
+ style F3 fill:#e8f5e9,stroke:#2e7d32
+ style F4 fill:#e8f5e9,stroke:#2e7d32
+ style F5 fill:#e8f5e9,stroke:#2e7d32
+```
+
+**正确使用流式下载避免 OOM:**
+
+```go
+// ❌ 错误做法:大文件会把整个内容加载到内存
+obj, _ := client.GetObject(ctx, bucketName, "large-backup.sql.gz", minio.GetObjectOptions{})
+data, _ := io.ReadAll(obj) // 10GB 文件 → 10GB 内存占用 = 必然 OOM
+
+// ✅ 正确做法:边读边写,零拷贝
+obj, _ := client.GetObject(ctx, bucketName, "large-backup.sql.gz", minio.GetObjectOptions{})
+defer obj.Close()
+
+dst, _ := os.Create("./backup-latest.sql.gz")
+defer dst.Close()
+
+_, err := io.Copy(dst, obj) // 内存占用恒定(~64KB 内部缓冲区)
+```
+
+这段代码对比了两种下载方式的核心区别。`io.ReadAll` 会将数据一次性全部读入切片,对于大文件会导致内存爆炸;而 `io.Copy` 使用固定大小的缓冲区(默认 32KB ~ 64KB),无论源文件大小如何,内存增长始终为常量级别——这就是**流式处理**的精髓。
+
+**STS 调用方式对比:**
+
+| 使用场景 | 所在章节 | 返回形式 | 典型消费者 |
+|---------|---------|---------|-----------|
+| HTTP 端点直传 | [§5.C](#方案-csts-临时凭证直传) | `gin.H` JSON | 浏览器前端 |
+| 内部服务间传递 | 下方 `GetTempCreds()` | `*sts.Credentials` 结构体 | 其他 Go 微服务 |
+
+两种方式的 **AssumeRole 调用链路完全相同**(见下),区别仅在封装层级:HTTP handler 多了一层入参解析和响应序列化。生产环境中通常将其抽象为一个内部 `GetTempCreds(roleArn string, userID string)` 函数,由 HTTP handler 或其他服务统一调用,避免重复实现。
+
+```go
+import "github.com/aliyun/aliyun-sts-go-sdk"
+
+// GetTempCreds 内部服务调用的核心函数(§5.C 的 HTTP handler 底层即调用此函数)
+func GetTempCreds(roleArn, sessionName string) (*sts.Credentials, error) {
+ client := stssdk.NewClientWithAccessKey("cn-hangzhou", ak, sk)
+ request := stssdk.CreateAssumeRoleRequest()
+ request.RoleArn = roleArn
+ request.RoleSessionName = sessionName
+ request.DurationSeconds = 900 // 15 分钟
+
+ response, err := client.AssumeRole(request)
+ if err != nil {
+ return nil, err
+ }
+ return &response.Credentials, nil
+}
+```
+
+> [!abstract] 教学提示:STS 的核心思路是"最小权限原则"——前端只获得一个有时效性的短期令牌,即使被拦截也只能在 15 分钟内使用,且只能用于指定的 Bucket 和路径。
+
+### 10. 部署检查清单
+
+新建 OSS 集成项目时,逐项确认:
+
+- [ ] 创建独立 RAM 子账号(不要直接用主账号 AK)
+- [ ] 最小权限授权:只允许目标 Bucket 的操作
+- [ ] 服务端和内网都使用 `-internal` Endpoint
+- [ ] 文件大小限制前置到中间件层(参考 `MaxMultipartMemory`)
+- [ ] 文件名使用 UUID 重命名,防止冲突和路径遍历
+- [ ] 生成预签名 URL 时设置合理的过期时间
+- [ ] 上传成功后通过 callback 或事件通知更新数据库元信息
+- [ ] 配置 Lifecycle 自动清理过期文件
+- [ ] 大文件传输加进度条和断点续传支持(客户端侧)
+
+## 关联笔记
+
+- [[GIN/8-file-upload]] — 文件上传后如何存入 OSS,以及 MaxMultipartMemory 的内存优化
+- [[GIN/11-static-files]] — 静态文件服务架构中 OSS/S3 + CDN 的集成模式
+- [[部署与运维基础]] — 容器环境中磁盘、网络和密钥管理的注意事项
diff --git a/hhs/DEV/跨域问题/跨域问题.md b/hhs/DEV/跨域问题/跨域问题.md
new file mode 100644
index 0000000..7ec7909
--- /dev/null
+++ b/hhs/DEV/跨域问题/跨域问题.md
@@ -0,0 +1,388 @@
+---
+tags: [CORS, 跨域, 安全, HTTP, Web开发, 前后端分离]
+create time: 2026-05-18 14:30
+---
+
+# 企业项目跨域问题解决方案
+
+## 概述
+
+跨域(Cross-Origin Resource Sharing, CORS)是前后端分离架构中最常见的网络限制问题。本文从浏览器同源策略出发,系统讲解跨域产生的原理、常见场景及完整解决方案,涵盖服务端配置、Nginx 反向代理和客户端降级方案,并附带 Go / Spring Boot / Node.js / Nginx 的实际代码示例。
+
+> [!question] 思考:为什么浏览器要阻止跨域请求?
+>
+> 根源在于浏览器的「同源策略」(Same-Origin Policy)——它是一种安全机制,防止恶意网站读取你当前页面的 Cookie、LocalStorage 和 API 响应。但这一保护也带来了开发的困扰:**我们需要在保持安全的前提下,让不同域的服务之间正常通信。**
+
+## 什么是跨域
+
+### 同源的判定
+
+两个 URL 只要 **协议(protocol)**、**域名(host)**、**端口(port)** 三者完全一致,就称为同源。任意一项不同,即为跨域。
+
+| 当前 URL | 目标 URL | 是否跨域 | 原因 |
+|----------|----------|----------|------|
+| `https://api.example.com` | `https://app.example.com` | ✅ 跨域 | 域名不同 |
+| `http://localhost:3000` | `http://localhost:8080` | ✅ 跨域 | 端口不同 |
+| `https://example.com` | `http://example.com` | ✅ 跨域 | 协议不同 |
+| `https://example.com/api` | `https://example.com/data` | ❌ 同域 | 仅路径不同(不算跨域) |
+
+### 跨域的本质
+
+跨域不是请求发不出去,而是 **浏览器拒绝接收响应**。实际过程如下:
+
+```mermaid
+flowchart TD
+ A["前端发起 AJAX / Fetch 请求"] --> B{"是否为跨域请求?"}
+ B -- '否' --> C["正常发送, 浏览器接收响应"]
+ B -- '是' --> D{"是否预检请求?"}
+ D -- '简单请求' --> E["直接发送请求"]
+ D -- '复杂请求' --> F["先发 OPTIONS 预检"]
+ F --> G{"服务端返回 200 + 正确 CORS 头?"}
+ G -- '是' --> E
+ G -- '否' --> H["浏览器拦截预检, 不发送真实请求"]
+ E --> I{"服务端是否携带 CORS 响应头?"}
+ I -- '是' --> C
+ I -- '否' --> J["浏览器拒绝响应, 控制台报错"]
+```
+
+关键结论:**服务器必须设置正确的 CORS 响应头,浏览器才会放行响应给 JavaScript。**
+
+## 常见跨域场景
+
+### 开发环境(本地 vs 后端)
+
+```mermaid
+flowchart LR
+ DevBrowser["浏览器
localhost:5173"] --> Backend["Go API :8080
(请求 /api/users)"]
+```
+
+这是开发阶段最常见的跨域场景——前端 dev server 跑在 `localhost:5173`,后端服务在 `:8080`,端口不同即触发跨域。
+
+### 生产环境(多子域名)
+
+```mermaid
+flowchart LR
+ App["app.company.com"] --> Api["api.company.com
(需配 CORS)"]
+```
+
+主站和 API 属于不同子域名,同样需要处理跨域。
+
+### 嵌入场景(iframe / CDN)
+
+- 页面加载第三方 CDN 上的静态资源(字体、JS、CSS)→ 通常没问题(GET 请求天然不受限)
+- 页面内 iframe 向自身域名发 XHR 请求 → 受跨域限制
+- 调用第三方 API(如支付回调、地图 SDK)→ 取决于对方是否支持 CORS
+
+## 解决方案
+
+### 方案一:服务端配置 CORS(推荐首选)
+
+**适用场景**:自有服务端,可直接修改响应头。
+
+#### Go (Gin 框架)
+
+使用官方 `gin-contrib/cors` 中间件:
+
+```go
+import "github.com/gin-contrib/cors"
+
+router := gin.Default()
+
+config := cors.DefaultConfig()
+config.AllowOrigins = []string{"https://app.example.com"} // 明确允许的源
+config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE"}
+config.AllowHeaders = []string{"Origin", "Content-Type", "Authorization"}
+config.AllowCredentials = true // 携带 Cookie 时必须设为 true
+config.MaxAge = 12 * time.Hour // 预检请求缓存时长
+
+router.Use(cors.New(config))
+```
+
+`AllowOrigins` 白名单是安全的第一道防线——它告诉浏览器"只有这些域名可以读取我的接口数据"。`AllowCredentials` 用于允许携带 Cookie(如登录态),但配合白名单使用,绝不可与通配符 `*` 同时出现。`MaxAge` 设置预检请求在浏览器的缓存时长,减少不必要的 OPTIONS 请求。
+
+> [!tip] 生产环境安全要点
+>
+> - **不要用 `AllowOrigins = ["*"]` 配合 `AllowCredentials = true`**,这会引发 panic 或无效配置。
+> - 应显式列出允许的前端域名白名单,避免恶意站点劫持接口。
+> - 如需支持多个前端域名,可从环境变量或配置中心动态读取。
+
+#### Java (Spring Boot)
+
+Spring Boot 提供了多种 CORS 配置方式:
+
+**全局配置(实现 WebMvcConfigurer):**
+
+```java
+@Configuration
+public class CorsConfig implements WebMvcConfigurer {
+
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ registry.addMapping("/api/**") // 生效的路径
+ .allowedOrigins("https://app.example.com")
+ .allowedMethods("GET", "POST", "PUT", "DELETE")
+ .allowedHeaders("*")
+ .allowCredentials(true)
+ .maxAge(3600); // 预检缓存 1 小时
+ }
+}
+```
+
+**注解级别(细粒度控制单个 Controller):**
+
+```java
+@CrossOrigin(origins = "https://app.example.com", maxAge = 3600)
+@RestController
+@RequestMapping("/api/admin")
+public class AdminController { ... }
+```
+
+#### Node.js (Express)
+
+```js
+const cors = require('cors');
+
+app.use(cors({
+ origin: ['https://app.example.com', 'https://admin.example.com'],
+ credentials: true,
+ methods: ['GET', 'POST', 'PUT', 'DELETE'],
+ allowedHeaders: ['Content-Type', 'Authorization'],
+}));
+```
+
+此配置传入 Express 的中间件链中,会对所有路由生效。如需限定路径范围,可改为 `app.use('/api', cors(config))` 。与 Go / Java 方案相比,Node.js 方案最轻量——无需额外注解或配置类,只需在启动时注册一次即可。
+
+### 方案二:Nginx 反向代理(零侵入)
+
+**适用场景**:无法修改后端代码、多语言混合后端统一治理、隐藏后端地址提升安全。
+
+核心思路:让前端访问 Nginx 的 `/api` 路径,Nginx 把请求转发到后端,同时注入 CORS 响应头。
+
+```nginx
+server {
+ listen 443 ssl;
+ server_name app.example.com;
+
+ # --- 前端静态资源 ---
+ location / {
+ root /usr/share/nginx/html;
+ try_files $uri $uri/ /index.html;
+ }
+
+ # --- API 反代 + CORS ---
+ location /api/ {
+ # 处理预检请求
+ if ($request_method = 'OPTIONS') {
+ add_header 'Access-Control-Allow-Origin' '$http_origin';
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
+ add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
+ add_header 'Access-Control-Max-Age' 1728000;
+ add_header 'Content-Type' 'text/plain; charset=utf-8';
+ add_header 'Content-Length' 0;
+ return 204; # 直接结束预检,不走后端
+ }
+
+ # 真实请求,注入响应头
+ add_header 'Access-Control-Allow-Origin' '$http_origin' always;
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
+ add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
+
+ proxy_pass http://backend_server:8080;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+}
+```
+
+> [!warning] Nginx 注意事项
+>
+> - `if ($request_method = 'OPTIONS')` 中的 `if` 在 Nginx 中有陷阱,但它在这里是安全的用法(只操作 header)。
+> - `always` 参数确保即使错误响应(4xx / 5xx)也带上 CORS 头。
+> - `return 204` 直接返回空体,避免 OPTIONS 请求打到后端增加负载。
+
+### Nginx 方案的架构图
+
+```mermaid
+flowchart TD
+ Browser["浏览器 app.example.com"] --> Nginx["Nginx 443
(GET /api/users)"]
+ Nginx --> Backend["后端 :8080
(proxy_pass)"]
+ Backend --> JSONResponse["JSON 响应"]
+ Nginx --> BrowserReply["返回含 CORS 头的
JSON 响应"]
+ BrowserReply --> Browser
+```
+
+对比之下,不加 Nginx 时的直连模式:
+
+```mermaid
+flowchart TD
+ Browser["浏览器
app.example.com
❌ 跨域 + 无 CORS 头"] --> Backend["后端 api.example.com:8080"]
+```
+
+此时浏览器直接访问后端接口,因域名和端口均不同触发跨域限制,且服务端未配置 CORS 头,浏览器拦截响应。
+
+### 方案三:开发期 Vite/Webpack Proxy(开发专用)
+
+**适用场景**:本地开发时绕过跨域,上线后由 Nginx 处理。
+
+#### Vite 配置 (`vite.config.ts`)
+
+```ts
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8080', // 后端地址
+ changeOrigin: true, // 改写 Host 头
+ secure: false, // 自签证书时可关闭校验
+ },
+ },
+ },
+});
+```
+
+这样前端发 `fetch('/api/users')` 时,Vite dev server 会转发到 `http://localhost:8080/api/users`,因为是同源请求,不存在跨域。
+
+#### Webpack (`vue.config.js` / `webpack.config.js`)
+
+```js
+module.exports = {
+ devServer: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8080',
+ changeOrigin: true,
+ },
+ },
+ },
+};
+```
+
+`changeOrigin: true` 会将被代理请求的 Host 头改为后端地址,这对于某些依赖 Host 做鉴权的后端服务是必要的。Webpack proxy 同样只在本地开发生效,生产环境仍需 Nginx 或 CORS 中间件配合。
+
+> [!info] Vite/Webpack Proxy 与正式方案的区别
+>
+> | | 开发 Proxy | Nginx / 服务端 CORS |
+> |---|---|---|
+> | 用途 | 仅限本地开发 | 生产环境 |
+> | 需要后端改吗 | 不需要 | 不需要(Nginx)/ 需要(服务端) |
+> | HTTPS 支持 | 需额外配置 | 原生支持 |
+> | 部署复杂度 | 无 | 低 |
+
+### 方案四:JSONP / 服务端重定向(历史方案,了解即可)
+
+#### JSONP
+
+利用 `
+
+```
+
+```go
+// Go 后端示例
+func JSONPHandler(w http.ResponseWriter, r *http.Request) {
+ callback := r.URL.Query().Get("callback")
+ data := `{"name": "John", "age": 30}`
+ w.Header().Set("Content-Type", "application/javascript")
+ fmt.Fprintf(w, "%s(%s)", callback, data)
+}
+```
+
+> [!note] JSONP 的局限性
+>
+> - **只支持 GET**——POST/PUT/DELETE 等无法实现
+> - **无错误处理**——脚本加载失败时只能通过 `onerror` 粗略捕获
+> - **安全风险**——任何提供 JSONP 的接口都可能被恶意站点调用,需额外做 Referer 校验
+> - **已被 CORS 全面取代**——现代浏览器全部支持 CORS,新项目不应考虑此方案
+
+#### 服务端重定向
+
+后端自己调后端,前端只请求同源接口。适合微服务间调用,但不解决浏览器侧跨域。
+
+## 进阶话题
+
+### 带凭据的请求(Cookie / Authorization Header)
+
+当 `withCredentials: true`(fetch)或 `xhr.withCredentials = true`(XMLHttpRequest)时:
+
+1. 服务端 `Access-Control-Allow-Origin` **不能**为 `*`,必须是具体域名
+2. 服务端需设置 `Access-Control-Allow-Credentials: true`
+3. 浏览器会在请求中带上 Cookie
+
+```ts
+// 前端 fetch 带 Cookie
+fetch('https://api.example.com/user/profile', {
+ method: 'GET',
+ credentials: 'include', // 关键:带上 Cookie
+ headers: { 'Authorization': 'Bearer xxx' },
+});
+```
+
+`credentials: 'include'` 是让浏览器发送 Cookie 的关键。如果只设 `credentials` 而不配服务端 `Access-Control-Allow-Credentials: true`,或者后端 `Allow-Origin` 仍为 `*`,请求同样会被拦截——这是一个"双方都满足才能通过"的条件。
+
+### 预检请求的优化
+
+复杂请求(自定义头、PUT/DELETE 等)会先发送 `OPTIONS` 预检。可通过以下方式减少开销:
+
+#### Max-Age 缓存的影响
+
+```mermaid
+flowchart LR
+ A["Max-Age = 0
每次请求都发 OPTIONS"] --> B["⚠️ 高延迟,
服务器压力大"]
+ C["Max-Age = 86400
24小时内复用预检结果"] --> D["✅ 首慢后续快,
负载低"]
+```
+
+#### 设置方式
+
+服务端或 Nginx 中设置响应头即可:
+
+- `Access-Control-Max-Age: 86400` — 缓存 24 小时,超过需重新预检
+- 一般建议设为 **1 天到 7 天**,具体取决于接口变更频率
+
+### RESTful 动词与预检的关系
+
+| 方法 | 是否可能预检 | 原因 |
+|------|-------------|------|
+| GET | ❌ 不会 | 简单请求 |
+| POST (application/x-www-form-urlencoded / multipart/form-data / text/plain) | ❌ 可能不会 | Content-Type 在白名单内 |
+| POST (application/json) | ✅ 会预检 | Content-Type 不在简单请求白名单 |
+| PUT / DELETE / PATCH | ✅ 会预检 | 不在简单请求方法的白名单 |
+
+### 常见问题排查清单
+
+> [!check] 跨域问题排查流程
+>
+> 1. **确认跨域类型** — 打开浏览器 DevTools → Network,看请求状态码是 `200` 还是 `cors error`
+> 2. **检查请求头** — Request Headers 里是否有 `Origin`,值是否正确
+> 3. **检查响应头** — Response Headers 中是否有 `Access-Control-Allow-Origin`,值和前端域名匹配吗
+> 4. **区分简单请求和预检** — 有无单独的 `OPTIONS` 请求发出?OPTIONS 返回什么?
+> 5. **credentials 场景** — 是否同时满足了 `具体 Allow-Origin` + `Allow-Credentials: true`
+> 6. **多级代理链路** — 经过网关 / WAF / CDN 时,确认每一层都不删除 CORS 头
+
+## 方案选型速查表
+
+| 场景 | 推荐方案 | 理由 |
+|------|----------|------|
+| 前后端同公司,后端可改 | 服务端 CORS 中间件 | 最直接,语义清晰 |
+| 多语言后端 / 不便改代码 | Nginx 反代注入 CORS 头 | 统一治理,零侵入 |
+| 本地开发 | Vite/Webpack Proxy | 开发体验最佳 |
+| 生产环境隐藏后端 IP | Nginx 反代 | 安全 + 跨域双收益 |
+| 纯静态页调用第三方公开 API | 让第三方配好 CORS | 自身无能为力 |
+
+## 关联笔记
+
+- [[hhs/GIN/3-middleware/cors-registration-scope/cors-preflight]] — Gin 框架下预检请求的详细机制
+- [[hhs/DEV/Nginx/Nginx]] — Nginx 配置速查,含反向代理、负载均衡等
+- [[hhs/DEV/XSS与CSRF攻击/XSS与CSRF攻击]] — 同源策略是防御 XSS/CSRF 的基础
+- [[hhs/KingSoft/docs/Go语言web开发/03-服务端鉴权认证方案]] — Cookie / Session 鉴权与 CORS 凭据的配合
+- [[hhs/MS/02-服务治理/01-API网关]] — 微服务架构中通过 API 网关统一处理跨域