From 4d108267c37d646b98b58feb3b15bc8ee73915bf Mon Sep 17 00:00:00 2001 From: xfy Date: Wed, 8 Apr 2026 10:27:06 +0800 Subject: [PATCH] =?UTF-8?q?docs(nginx):=20=E6=96=B0=E5=A2=9E=E5=81=A5?= =?UTF-8?q?=E5=BA=B7=E6=A3=80=E6=9F=A5=E8=AF=A6=E8=A7=A3=E4=B8=8E7?= =?UTF-8?q?=E4=B8=AA=E9=AB=98=E7=BA=A7=E6=A8=A1=E5=9D=97=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 04-proxy-loadbalancing: 新增第18节主动健康检查详解 - 被动检查vs主动检查对比、NGINX Plus health_check/match指令 - Stream健康检查、gRPC健康检查、开源替代方案 - 新增 MQTT 模块文档 (33): broker负载均衡、Client ID路由 - 新增 OIDC 模块文档 (34): OpenID Connect认证、JWT验证 - 新增 Keyval 模块文档 (35): 动态键值存储、API管理接口 - 新增 流媒体模块文档 (36): HLS/FLV/MP4伪流媒体配置 - 新增 WebDAV 模块文档 (37): 文件共享服务器配置 - 新增 Zone Sync 模块文档 (38): 多节点状态同步 - 新增 HTTP Tunnel 模块文档 (39): HTTP CONNECT代理隧道 - 更新 README.md 目录索引 Co-Authored-By: Claude Opus 4.6 --- docs/04-nginx-proxy-loadbalancing.md | 555 +++++++++- docs/33-nginx-mqtt-module.md | 1087 +++++++++++++++++++ docs/34-nginx-oidc-module.md | 1501 +++++++++++++++++++++++++ docs/35-nginx-keyval-module.md | 1080 ++++++++++++++++++ docs/36-nginx-streaming-media.md | 904 ++++++++++++++++ docs/37-nginx-dav-module.md | 687 ++++++++++++ docs/38-nginx-zone-sync-module.md | 1502 ++++++++++++++++++++++++++ docs/39-nginx-tunnel-module.md | 603 +++++++++++ 8 files changed, 7918 insertions(+), 1 deletion(-) create mode 100644 docs/33-nginx-mqtt-module.md create mode 100644 docs/34-nginx-oidc-module.md create mode 100644 docs/35-nginx-keyval-module.md create mode 100644 docs/36-nginx-streaming-media.md create mode 100644 docs/37-nginx-dav-module.md create mode 100644 docs/38-nginx-zone-sync-module.md create mode 100644 docs/39-nginx-tunnel-module.md diff --git a/docs/04-nginx-proxy-loadbalancing.md b/docs/04-nginx-proxy-loadbalancing.md index 9bef752..49fea19 100644 --- a/docs/04-nginx-proxy-loadbalancing.md +++ b/docs/04-nginx-proxy-loadbalancing.md @@ -930,4 +930,557 @@ http { } } } -``` \ No newline at end of file +``` + +--- + +## 18. 主动健康检查详解 + +### 18.1 被动检查 vs 主动检查 + +| 特性 | 被动健康检查 (Passive) | 主动健康检查 (Active) | +|------|----------------------|---------------------| +| **实现方式** | 基于真实客户端请求响应判断 | 独立的探测请求周期性检测 | +| **触发时机** | 实际请求失败时 | 按配置间隔主动发起 | +| **资源占用** | 无额外开销 | 需要额外的连接和请求 | +| **发现速度** | 慢(依赖真实流量) | 快(独立探测) | +| **可用性** | 开源 NGINX 内置 | NGINX Plus 商业版 / 第三方模块 | +| **配置位置** | `server` 指令参数 | `upstream` 块或 `location` 指令 | +| **典型参数** | `max_fails`, `fail_timeout` | `interval`, `fails`, `passes`, `match` | + +**被动检查机制**: +```nginx +upstream backend { + # 在 fail_timeout(30s) 内连续失败 max_fails(3) 次,标记为不可用 + server srv1.example.com max_fails=3 fail_timeout=30s; +} +``` + +**主动检查优势**: +- 不依赖真实客户端流量即可检测后端状态 +- 可以检测特定的健康检查端点(如 `/health`) +- 支持自定义匹配规则验证响应内容 +- 支持 gRPC、TCP、UDP 等多种协议 + +### 18.2 HTTP 健康检查指令详解 (NGINX Plus) + +**注意**:HTTP 主动健康检查模块 (`ngx_http_upstream_hc_module`) 是 NGINX Plus 商业订阅的一部分。 + +#### health_check 指令 + +**语法**:`health_check [parameters];` +**上下文**:`location` +**功能**:启用 upstream 服务器组的定期健康检查 + +**参数说明**: + +| 参数 | 语法 | 默认值 | 说明 | +|------|------|--------|------| +| `interval` | `interval=time` | `5s` | 检查间隔时间 | +| `jitter` | `jitter=time` | — | 随机延迟时间,避免多个服务器同时检查 | +| `fails` | `fails=number` | `1` | 连续失败次数判定为不健康 | +| `passes` | `passes=number` | `1` | 连续成功次数判定为健康 | +| `uri` | `uri=uri` | `/` | 健康检查请求的 URI | +| `port` | `port=number` | 服务器端口 | 健康检查使用的端口 | +| `match` | `match=name` | — | 引用 `match` 块进行响应验证 | +| `mandatory` | `mandatory [persistent]` | — | 初始状态为 "checking";`persistent` 在 reload 后保持状态 | +| `keepalive_time` | `keepalive_time=time` | — | 启用健康检查连接的 keepalive | +| `type=grpc` | `type=grpc [grpc_service=name] [grpc_status=code]` | — | 启用 gRPC 健康检查 | + +#### match 指令 + +**语法**:`match name { ... }` +**上下文**:`http` +**功能**:定义响应验证测试集 + +**测试项**: + +| 测试项 | 语法 | 说明 | +|--------|------|------| +| `status` | `status [!] code [code...]` | 状态码匹配,支持范围如 `200-399` | +| `header` | `header header [operator] value` | 响应头匹配,`=` 精确匹配,`~` 正则匹配 | +| `body` | `body ~ "regex"` | 响应体正则匹配(只检查前 256KB) | +| `require` | `require $variable` | 变量非空且不为 "0" | + +**header 操作符**: +- `=` 或 `==`:精确相等 +- `!=`:不相等 +- `~`:正则匹配(区分大小写) +- `~*`:正则匹配(不区分大小写) + +#### 配置示例 + +**基础健康检查**: +```nginx +upstream dynamic { + zone upstream_dynamic 64k; # 共享内存区必须 + + server backend1.example.com weight=5; + server backend2.example.com:8080 fail_timeout=5s slow_start=30s; +} + +server { + location / { + proxy_pass http://dynamic; + health_check; # 使用默认配置 + } +} +``` + +**高级配置**: +```nginx +server { + location / { + proxy_pass http://backend; + health_check interval=10s jitter=2s fails=3 passes=2 + uri=/health port=8080 match=server_ok + keepalive_time=60s; + } +} + +match server_ok { + status 200; # 状态码必须是 200 + header Content-Type = application/json; # Content-Type 精确匹配 + header X-Health-Status ~ ^ok$; # 正则匹配头值 + body ~ "\"status\":\\s*\"healthy\""; # 响应体包含状态标记 +} +``` + +**gRPC 健康检查**(不兼容 `uri` 和 `match`): +```nginx +upstream grpc_backend { + zone grpc_zone 64k; + server grpc1.example.com:50051; + server grpc2.example.com:50051; +} + +server { + location / { + grpc_pass grpc://grpc_backend; + health_check mandatory type=grpc grpc_service=myapp.HealthCheck grpc_status=12; + } +} +``` + +### 18.3 Stream 健康检查指令详解 (NGINX Plus) + +**注意**:Stream 主动健康检查模块 (`ngx_stream_upstream_hc_module`) 是 NGINX Plus 商业订阅的一部分。 + +#### 指令概览 + +| 指令 | 上下文 | 默认值 | 说明 | +|------|--------|--------|------| +| `health_check` | `server` | — | 启用健康检查 | +| `health_check_timeout` | `stream`, `server` | `5s` | 健康检查超时 | +| `match` | `stream` | — | 定义响应验证规则 | + +#### health_check 参数(Stream) + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `interval` | `5s` | 检查间隔 | +| `jitter` | — | 随机延迟 | +| `fails` | `1` | 失败次数阈值 | +| `passes` | `1` | 成功次数阈值 | +| `match` | — | 引用 match 块 | +| `port` | 服务器端口 | 检查端口 | +| `udp` | — | 使用 UDP 协议 | +| `mandatory` | — | 初始状态为 "checking" | +| `persistent` | — | reload 后保持状态 | + +#### match 块(Stream) + +| 测试项 | 语法 | 说明 | +|--------|------|------| +| `send` | `send "string"` | 发送给服务器的字符串(支持 `\x` 十六进制) | +| `expect` | `expect "string"` / `expect ~ "regex"` | 期望的响应 | + +**注意**:只检查服务器返回数据的前 `proxy_buffer_size` 字节。 + +#### 配置示例 + +**TCP 基础检查**: +```nginx +upstream tcp_backend { + zone tcp_zone 64k; + server backend1.example.com:12345 weight=5; + server backend2.example.com:12345; +} + +server { + listen 12346; + proxy_pass tcp_backend; + health_check interval=5s; +} +``` + +**UDP 健康检查**: +```nginx +upstream dns_upstream { + zone dns_zone 64k; + server dns1.example.com:53; +} + +server { + listen 53 udp; + proxy_pass dns_upstream; + health_check udp interval=3s; # 发送探测并期望无 ICMP 不可达回复 +} +``` + +**自定义匹配规则(MySQL 检查)**: +```nginx +upstream mysql_backend { + zone mysql_zone 10m; + server db1.example.com:3306; + server db2.example.com:3306; +} + +match mysql_handshake { + # 发送 MySQL 握手包(十六进制) + send "\x3a\x00\x00\x01\x0a\x35\x2e\x35\x2e\x32\x2d\x6d\x32\x00\x01..."; + # 期望收到包含版本信息的响应 + expect ~ "\x4a\x00\x00\x00\x0a"; +} + +server { + listen 3307; + proxy_pass mysql_backend; + health_check match=mysql_handshake interval=5s; + health_check_timeout 10s; +} +``` + +**HTTP 风格的 TCP 检查**: +```nginx +match http_check { + send "GET /health HTTP/1.0\r\nHost: localhost\r\n\r\n"; + expect ~ "200 OK"; +} + +server { + listen 80; + proxy_pass backend; + health_check match=http_check interval=5s fails=3 passes=2; +} +``` + +### 18.4 自定义健康检查配置示例 + +#### 场景一:API 网关健康检查 + +```nginx +http { + upstream api_backend { + zone api_zone 64k; + + server api1.example.com:8080; + server api2.example.com:8080; + server api3.example.com:8080; + } + + # 健康检查匹配规则 + match api_healthy { + status 200; + header Content-Type = application/json; + body ~ "\"status\":\\s*\"up\""; + } + + server { + listen 80; + server_name api.example.com; + + location / { + proxy_pass http://api_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + + # 健康检查配置 + health_check interval=5s jitter=1s fails=3 passes=2 + uri=/api/health + match=api_healthy; + } + + # 健康检查状态页(NGINX Plus) + location /upstream_status { + upstream_status; + access_log off; + allow 10.0.0.0/8; + deny all; + } + } +} +``` + +#### 场景二:多协议混合检查 + +```nginx +# TCP 服务健康检查 +stream { + upstream redis_backend { + zone redis_zone 64k; + server redis1.example.com:6379; + server redis2.example.com:6379; + } + + match redis_ping { + send "PING\r\n"; + expect ~ "\+PONG"; + } + + server { + listen 6379; + proxy_pass redis_backend; + health_check match=redis_ping interval=10s fails=2 passes=2; + health_check_timeout 3s; + } +} + +# HTTP 服务健康检查 +http { + upstream web_backend { + zone web_zone 64k; + server web1.example.com:80; + server web2.example.com:80; + } + + server { + location / { + proxy_pass http://web_backend; + health_check interval=5s uri=/nginx_health; + } + } +} +``` + +#### 场景三:微服务 gRPC 健康检查 + +```nginx +upstream grpc_services { + zone grpc_zone 64k; + server service1.example.com:50051; + server service2.example.com:50051; +} + +server { + listen 50051 http2; + + location / { + grpc_pass grpc://grpc_services; + # gRPC 健康检查:使用标准 gRPC Health Checking Protocol + # grpc_status=12 (UNIMPLEMENTED) 表示服务未实现健康检查接口 + # grpc_status=0 (OK) 表示服务健康 + health_check mandatory type=grpc grpc_service=grpc.health.v1.Health + interval=5s fails=3 passes=2; + } +} +``` + +### 18.5 健康检查与负载均衡配合 + +#### 状态流转机制 + +``` + 初始状态 + | + v + ┌───────────┐ ┌──────────────┐ + │ checking │────▶│ unhealthy │ + └─────┬─────┘ └──────────────┘ + │ │ + │ passes 次成功 │ fails 次失败 + v v + ┌───────────┐ ┌──────────────┐ + │ healthy │◀────│ │ + └───────────┘ └──────────────┘ +``` + +**关键行为**: +- `checking` 状态:初始或 reload 后,不接收客户端请求 +- `mandatory` 参数:强制等待首次健康检查完成才标记为健康 +- `persistent` 参数:reload 后如之前是健康状态则保持 healthy + +#### 与负载均衡算法结合 + +```nginx +upstream backend { + zone backend 64k; + least_conn; # 最少连接算法 + + server srv1.example.com weight=5; + server srv2.example.com; + server srv3.example.com; + + # 被动检查参数与主动检查并存 + # 被动检查作为兜底,主动检查提供快速发现 +} + +server { + location / { + proxy_pass http://backend; + health_check interval=5s fails=3 passes=2; + + # 故障转移配置 + proxy_next_upstream error timeout http_502 http_503 http_504; + proxy_next_upstream_timeout 5s; + proxy_next_upstream_tries 2; + } +} +``` + +**监控指标集成**: + +```nginx +log_format health_log '$remote_addr - $remote_user [$time_local] ' + '"$request" $status ' + 'upstream=$upstream_addr ' + 'upstream_status=$upstream_status ' + 'health_check=$upstream_health_check_status'; + +server { + location / { + proxy_pass http://backend; + health_check interval=5s; + access_log /var/log/nginx/health.log health_log; + } +} +``` + +### 18.6 开源 NGINX 的替代方案 + +由于主动健康检查是 NGINX Plus 商业特性,开源版本需要使用第三方模块。 + +#### nginx_upstream_check_module (Tengine) + +由阿里巴巴 Tengine 团队开发的第三方模块,支持主动健康检查。 + +**源码地址**:https://github.com/yaoweibin/nginx_upstream_check_module + +**安装方法**: +```bash +# 下载模块源码 +git clone https://github.com/yaoweibin/nginx_upstream_check_module.git + +# 下载 NGINX 源码并解压 +cd /usr/local/src +tar -xzvf nginx-1.24.0.tar.gz +cd nginx-1.24.0 + +# 应用补丁(根据版本选择) +patch -p1 < /path/to/nginx_upstream_check_module/check.patch +# 或 patch -p1 < /path/to/nginx_upstream_check_module/check_1.20.1+.patch + +# 编译安装 +./configure \ + --prefix=/etc/nginx \ + --add-module=/path/to/nginx_upstream_check_module \ + --with-http_ssl_module \ + --with-http_v2_module + +make && make install +``` + +**指令说明**: + +| 指令 | 语法 | 默认值 | 说明 | +|------|------|--------|------| +| `check` | `check interval=ms [fall=N] [rise=N] [timeout=ms] [default_down=true\|false] [type=tcp\|http\|ssl_hello\|mysql\|ajp\|fastcgi]` | 见右侧 | 启用健康检查
`interval`: 检查间隔(ms)
`fall`: 失败次数
`rise`: 成功次数
`timeout`: 超时(ms)
`default_down`: 默认下线状态
`type`: 检查协议 | +| `check_keepalive_requests` | `check_keepalive_requests num` | `1` | 长连接检查次数 | +| `check_http_send` | `check_http_send "packet"` | `GET / HTTP/1.0\r\n\r\n` | HTTP 检查请求包 | +| `check_http_expect_alive` | `check_http_expect_alive [http_2xx] [http_3xx] [http_4xx] [http_5xx]` | `http_2xx` `http_3xx` | 视为健康的 HTTP 状态码 | +| `check_fastcgi_param` | `check_fastcgi_param parameter value` | — | FastCGI 检查参数 | +| `check_status` | `check_status [html\|csv\|json]` | `html` | 状态查看页面格式 | + +**配置示例**: +```nginx +upstream backend { + server 192.168.0.1:80; + server 192.168.0.2:80; + + # 每 5 秒检查一次,失败 3 次下线,成功 2 次上线,超时 4 秒 + check interval=5000 rise=2 fall=3 timeout=4000 type=http; + + # HTTP 健康检查配置 + check_http_send "GET /health HTTP/1.0\r\n\r\n"; + check_http_expect_alive http_2xx http_3xx; + + # 启用长连接检查(可选) + check_keepalive_requests 100; +} + +server { + listen 80; + + location / { + proxy_pass http://backend; + } + + # 健康检查状态页 + location /status { + check_status json; # 可选 html, csv, json + access_log off; + allow 10.0.0.0/8; + deny all; + } +} +``` + +**支持的健康检查类型**: +- `tcp`:仅建立 TCP 连接 +- `http`:发送 HTTP 请求并验证响应 +- `ssl_hello`:发送 SSL Client Hello +- `mysql`:发送 MySQL ping 包 +- `ajp`:发送 AJP ping 包 +- `fastcgi`:发送 FastCGI 请求 + +#### 其他替代方案对比 + +| 方案 | 类型 | 活跃度 | 特点 | +|------|------|--------|------| +| **nginx_upstream_check_module** | 第三方模块 | 中等 | 功能完整,Tengine 使用 | +| **nginx-upsync-module** | 第三方模块 | 低 | 结合 Consul/etcd 动态发现 | +| **OpenResty + lua-resty-healthcheck** | Lua 扩展 | 高 | 灵活可编程 | +| **Traefik** | 替代代理 | 高 | 原生支持主动健康检查 | +| **Envoy** | 替代代理 | 高 | 云原生,功能强大 | + +#### OpenResty + Lua 实现示例 + +```nginx +lua_shared_dict healthcheck 1m; + +upstream backend { + server 127.0.0.1:8081; + server 127.0.0.1:8082; + server 127.0.0.1:8083; +} + +init_worker_by_lua_block { + local hc = require "resty.healthcheck" + local checker = hc.new({ + name = "my-checker", + shm_name = "healthcheck", + checks = { + active = { + healthy = { + interval = 5, + successes = 2, + }, + unhealthy = { + interval = 5, + http_failures = 3, + }, + }, + }, + }) + + checker:add_target("127.0.0.1", 8081) + checker:add_target("127.0.0.1", 8082) + checker:add_target("127.0.0.1", 8083) +} +``` + +**选型建议**: +- 商业环境且有预算:**NGINX Plus**(完整支持,商业支持) +- 开源替代且功能优先:**nginx_upstream_check_module** +- 需要动态配置:**OpenResty + lua-resty-upstream-healthcheck** +- 新架构选型:**Traefik** 或 **Envoy**(原生支持服务发现) \ No newline at end of file diff --git a/docs/33-nginx-mqtt-module.md b/docs/33-nginx-mqtt-module.md new file mode 100644 index 0000000..caa8097 --- /dev/null +++ b/docs/33-nginx-mqtt-module.md @@ -0,0 +1,1087 @@ +# NGINX MQTT 模块指南 + +## 1. MQTT Preread 模块概述 + +### 1.1 模块介绍 + +`ngx_stream_mqtt_preread_module` 模块用于在 preread 阶段从 MQTT CONNECT 消息中提取客户端信息,而无需终止 MQTT 连接。 + +### 1.2 版本要求 + +- 自 **1.23.4** 版本起可用 +- 属于 **NGINX Plus 商业订阅**功能 +- 支持 MQTT 协议版本 **3.1.1** 和 **5.0** + +### 1.3 核心用途 + +MQTT Preread 模块主要用于以下场景: + +1. **基于 Client ID 的路由** - 根据客户端 ID 将连接路由到不同的 MQTT Broker +2. **基于用户名的路由** - 根据用户名进行负载均衡或访问控制 +3. **透明代理** - 在不解密 MQTT 连接的情况下获取连接元数据 +4. **连接分析** - 记录和分析 MQTT 客户端连接信息 + +### 1.4 工作原理 + +``` +客户端 MQTT 连接 + | + v +[NGINX Stream 模块] + | + v +[Preread 阶段] <--- mqtt_preread 在此阶段读取 CONNECT 消息 + | + v +[变量提取] <--- $mqtt_preread_clientid, $mqtt_preread_username + | + v +[路由决策] <--- 基于变量进行 upstream 选择 + | + v +[代理到后端 MQTT Broker] +``` + +在 preread 阶段,NGINX 读取 MQTT CONNECT 消息的前几个字节(不超过 16KB),解析其中的 Client ID 和 Username,然后基于这些信息进行路由决策。 + +--- + +## 2. MQTT Filter 模块概述 + +### 2.1 模块介绍 + +`ngx_stream_mqtt_filter_module` 模块提供完整的 MQTT 协议支持,允许修改 CONNECT 消息中的字段。 + +### 2.2 版本要求 + +- 自 **1.23.4** 版本起可用 +- 属于 **NGINX Plus 商业订阅**功能 +- 支持 MQTT 协议版本 **3.1.1** 和 **5.0** + +### 2.3 核心用途 + +MQTT Filter 模块主要用于以下场景: + +1. **修改 Client ID** - 为客户端分配统一的 Client ID 格式 +2. **修改用户名/密码** - 进行身份认证信息的转换或注入 +3. **代理认证** - 在代理层添加统一的认证信息 +4. **会话管理** - 控制客户端会话标识 + +### 2.4 与 Preread 模块的区别 + +| 特性 | Preread 模块 | Filter 模块 | +|------|--------------|-------------| +| 主要功能 | 读取 CONNECT 信息 | 修改 CONNECT 字段 | +| 处理阶段 | Preread 阶段 | 代理阶段 | +| 是否修改数据 | 否(只读) | 是(读写) | +| 使用场景 | 路由、分析 | 认证、转换 | +| 性能开销 | 极低 | 较低 | + +--- + +## 3. 指令详解 + +### 3.1 MQTT Preread 模块指令 + +#### mqtt_preread + +启用或禁用从 MQTT CONNECT 消息中提取信息。 + +**语法**: +```nginx +mqtt_preread on | off; +``` + +**默认值**:`off` + +**上下文**:`stream`, `server` + +**说明**: +- 在 preread 阶段启用 MQTT CONNECT 消息解析 +- 启用后,可以通过变量 `$mqtt_preread_clientid` 和 `$mqtt_preread_username` 访问提取的信息 +- 仅解析 CONNECT 消息,不修改数据流 + +**配置示例**: +```nginx +stream { + server { + listen 1883; + + # 启用 MQTT preread + mqtt_preread on; + + # 基于 clientid 路由 + proxy_pass $mqtt_backend; + } +} +``` + +--- + +### 3.2 MQTT Filter 模块指令 + +#### mqtt + +为给定虚拟服务器启用 MQTT 协议支持。 + +**语法**: +```nginx +mqtt on | off; +``` + +**默认值**:`off` + +**上下文**:`stream`, `server` + +**说明**: +- 启用后可以使用其他 MQTT 相关指令 +- 必须在 `mqtt_set_connect` 之前启用 + +**配置示例**: +```nginx +stream { + server { + listen 1883; + + # 启用 MQTT 支持 + mqtt on; + + proxy_pass backend; + } +} +``` + +--- + +#### mqtt_buffers + +设置单个连接处理 MQTT 消息的缓冲区数量和大小。 + +**语法**: +```nginx +mqtt_buffers number size; +``` + +**默认值**:`100 1k;` + +**上下文**:`stream`, `server` + +**版本要求**:1.25.1+ + +**说明**: +- 控制用于处理 MQTT 消息的缓冲区配置 +- 较大的缓冲区可以处理更大的 MQTT 消息 +- 根据预期的消息大小和并发连接数调整 + +**配置示例**: +```nginx +stream { + server { + listen 1883; + mqtt on; + + # 设置缓冲区:50 个缓冲区,每个 4KB + mqtt_buffers 50 4k; + + proxy_pass backend; + } +} +``` + +--- + +#### mqtt_rewrite_buffer_size + +设置用于写入修改后消息的缓冲区大小。 + +**语法**: +```nginx +mqtt_rewrite_buffer_size size; +``` + +**默认值**:`4k` 或 `8k`(取决于平台内存页大小) + +**上下文**:`server` + +**版本要求**:1.25.1+ + +**废弃状态**:已废弃,建议使用 `mqtt_buffers` + +**说明**: +- 该指令在 1.25.1 版本中已被废弃 +- 请使用 `mqtt_buffers` 替代 + +--- + +#### mqtt_set_connect + +设置 CONNECT 消息的字段为给定值。 + +**语法**: +```nginx +mqtt_set_connect field value; +``` + +**默认值**:无 + +**上下文**:`server` + +**说明**: +- 支持修改的字段:`clientid`, `username`, `password` +- 值可以包含文本、变量及其组合 +- 可以在同一级别指定多个指令 + +**可用字段**: + +| 字段 | 说明 | +|------|------| +| `clientid` | MQTT 客户端标识符 | +| `username` | 连接用户名 | +| `password` | 连接密码 | + +**配置示例**: +```nginx +stream { + server { + listen 18883; + proxy_pass backend; + proxy_buffer_size 16k; + + mqtt on; + + # 设置 Client ID + mqtt_set_connect clientid "$client"; + + # 设置用户名 + mqtt_set_connect username "$name"; + + # 设置密码(从变量获取) + mqtt_set_connect password "$mqtt_password"; + } +} +``` + +--- + +## 4. 嵌入变量 + +### 4.1 MQTT Preread 变量 + +| 变量 | 说明 | +|------|------| +| `$mqtt_preread_clientid` | CONNECT 消息中的 Client ID 值 | +| `$mqtt_preread_username` | CONNECT 消息中的 Username 值 | + +**变量使用示例**: +```nginx +stream { + # 使用 map 基于 clientid 路由 + map $mqtt_preread_clientid $backend_pool { + ~^device-1 backend_1; + ~^device-2 backend_2; + ~^sensor-.* sensors_backend; + default default_backend; + } + + server { + listen 1883; + mqtt_preread on; + + proxy_pass $backend_pool; + } +} +``` + +--- + +## 5. 配置示例 + +### 5.1 基础 MQTT 代理 + +简单的 MQTT 代理配置,将所有连接转发到后端 Broker: + +```nginx +stream { + upstream mqtt_backend { + server 192.168.1.10:1883; + server 192.168.1.11:1883 backup; + } + + server { + listen 1883; + proxy_pass mqtt_backend; + proxy_timeout 30s; + proxy_connect_timeout 5s; + } +} +``` + +### 5.2 基于 Client ID 的路由 + +根据 MQTT Client ID 将连接路由到不同的后端: + +```nginx +stream { + # 设备组 1:工业传感器 + upstream sensors_backend { + server 10.0.1.10:1883 weight=5; + server 10.0.1.11:1883; + } + + # 设备组 2:智能家居设备 + upstream home_backend { + server 10.0.2.10:1883; + server 10.0.2.11:1883; + } + + # 设备组 3:车联网 + upstream vehicle_backend { + server 10.0.3.10:1883; + server 10.0.3.11:1883; + } + + # 默认后端 + upstream default_backend { + server 10.0.0.10:1883; + } + + # 基于 Client ID 的路由映射 + map $mqtt_preread_clientid $target_backend { + # 传感器设备(匹配 sensor- 开头的 Client ID) + ~^sensor- sensors_backend; + + # 智能家居设备(匹配 home- 开头的 Client ID) + ~^home- home_backend; + + # 车载设备(匹配 vehicle- 或 car- 开头的 Client ID) + ~^vehicle- vehicle_backend; + ~^car- vehicle_backend; + + # 默认后端 + default default_backend; + } + + server { + listen 1883; + + # 启用 MQTT preread + mqtt_preread on; + + # 基于 Client ID 路由 + proxy_pass $target_backend; + + # 连接超时配置 + proxy_timeout 300s; + proxy_connect_timeout 10s; + + # 启用 TCP keepalive + proxy_socket_keepalive on; + } +} +``` + +### 5.3 基于用户名的负载均衡 + +根据用户名进行路由,适用于多租户场景: + +```nginx +stream { + # 租户 A 集群 + upstream tenant_a_backend { + server 10.0.10.10:1883; + server 10.0.10.11:1883; + } + + # 租户 B 集群 + upstream tenant_b_backend { + server 10.0.20.10:1883; + server 10.0.20.11:1883; + } + + # 管理员集群 + upstream admin_backend { + server 10.0.0.10:1883; + } + + # 基于用户名路由 + map $mqtt_preread_username $tenant_backend { + "tenant-a-user" tenant_a_backend; + "tenant-a-admin" tenant_a_backend; + "tenant-b-user" tenant_b_backend; + "tenant-b-admin" tenant_b_backend; + ~^admin-.* admin_backend; + default tenant_a_backend; + } + + server { + listen 1883; + mqtt_preread on; + + proxy_pass $tenant_backend; + proxy_timeout 60s; + } +} +``` + +### 5.4 修改 CONNECT 消息(Filter 模块) + +在代理层修改 MQTT CONNECT 消息中的字段: + +```nginx +stream { + upstream mqtt_backend { + server 192.168.1.10:1883; + } + + server { + listen 1883; + proxy_pass mqtt_backend; + proxy_buffer_size 16k; + + # 启用 MQTT Filter + mqtt on; + + # 设置固定的 Client ID 前缀(追加原始 ID) + mqtt_set_connect clientid "ng-$mqtt_preread_clientid"; + + # 注入代理认证用户名 + mqtt_set_connect username "nginx-proxy"; + + # 设置代理密码(从文件或环境变量获取) + mqtt_set_connect password "$proxy_mqtt_password"; + } +} +``` + +### 5.5 综合配置示例 + +结合 Preread 和 Filter 模块的完整配置: + +```nginx +stream { + # 日志格式 + log_format mqtt_log '$remote_addr [$time_local] ' + 'clientid:$mqtt_preread_clientid ' + 'username:$mqtt_preread_username ' + 'upstream:$upstream_addr ' + 'status:$status'; + + # 设备专用集群 + upstream device_cluster_a { + zone devices 64k; + server 10.0.1.10:1883 weight=5; + server 10.0.1.11:1883; + server 10.0.1.12:1883 backup; + } + + upstream device_cluster_b { + zone devices 64k; + server 10.0.2.10:1883; + server 10.0.2.11:1883; + } + + # 普通设备集群 + upstream default_devices { + zone devices 64k; + least_conn; + server 10.0.3.10:1883; + server 10.0.3.11:1883; + server 10.0.3.12:1883; + } + + # Client ID 到后端映射 + map $mqtt_preread_clientid $backend_pool { + ~^dev-a- device_cluster_a; + ~^device-a- device_cluster_a; + ~^dev-b- device_cluster_b; + ~^device-b- device_cluster_b; + default default_devices; + } + + # 服务器配置 + server { + listen 1883; + access_log /var/log/nginx/mqtt-access.log mqtt_log; + + # 启用 MQTT Preread 获取 Client ID 和用户名 + mqtt_preread on; + + # 启用 MQTT Filter(可选,需要修改 CONNECT 时启用) + mqtt on; + mqtt_buffers 50 4k; + + # 可选:修改 CONNECT 消息 + # mqtt_set_connect clientid "proxy-$mqtt_preread_clientid"; + + # 基于 Client ID 路由 + proxy_pass $backend_pool; + + # 超时配置 + proxy_timeout 300s; + proxy_connect_timeout 10s; + + # 启用 TCP keepalive + proxy_socket_keepalive on; + + # 连接限制 + limit_conn mqtt_conn 100; + } + + # TLS MQTT 端口 + server { + listen 8883 ssl; + + ssl_certificate /etc/nginx/ssl/mqtt.crt; + ssl_certificate_key /etc/nginx/ssl/mqtt.key; + ssl_protocols TLSv1.2 TLSv1.3; + + mqtt_preread on; + proxy_pass $backend_pool; + proxy_timeout 300s; + } +} + +# 连接限制共享内存 +limit_conn_zone $binary_remote_addr zone=mqtt_conn:10m; +``` + +### 5.6 与 SSL/TLS 结合 + +MQTT over TLS 的配置: + +```nginx +stream { + upstream mqtt_ssl_backend { + server 192.168.1.10:8883; + server 192.168.1.11:8883; + } + + # 终止 TLS 并读取 MQTT 信息 + server { + listen 8883 ssl; + + ssl_certificate /etc/nginx/ssl/server.crt; + ssl_certificate_key /etc/nginx/ssl/server.key; + ssl_protocols TLSv1.2 TLSv1.3; + + # 在解密后读取 MQTT 信息 + mqtt_preread on; + mqtt on; + + proxy_pass mqtt_ssl_backend; + + # 上游也使用 SSL + proxy_ssl on; + proxy_ssl_protocols TLSv1.2 TLSv1.3; + } +} +``` + +--- + +## 6. 与 Lolly 项目的关系 + +### 6.1 Lolly 项目简介 + +Lolly 是一个使用 Go 语言编写的高性能 HTTP 服务器与反向代理,基于 fasthttp 构建。它提供了 HTTP/3、WebSocket、TCP/UDP Stream 代理等功能。 + +### 6.2 功能对比 + +| 特性 | NGINX Plus (MQTT) | Lolly | +|------|-------------------|-------| +| MQTT Preread | 支持(商业版) | 未实现 | +| MQTT Filter | 支持(商业版) | 未实现 | +| TCP Stream 代理 | 支持 | 支持 | +| 基于内容路由 | 支持 | 有限支持 | +| SSL/TLS 终端 | 支持 | 支持 | +| 负载均衡 | 丰富算法 | 轮询/加权/最少连接/IP哈希 | + +### 6.3 Lolly 中的 Stream 代理 + +Lolly 目前支持基础的 TCP/UDP Stream 代理: + +```go +// Lolly Stream 代理示例配置(YAML) +stream: + - listen: ":1883" + protocol: "tcp" + upstream: + targets: + - addr: "mqtt1:1883" + weight: 3 + - addr: "mqtt2:1883" + weight: 1 + load_balance: "round_robin" +``` + +当前 Lolly 的 Stream 实现位于 `internal/stream/stream.go`,提供: +- TCP/UDP 代理 +- 负载均衡(轮询、加权轮询、最少连接、IP 哈希) +- 健康检查 +- 会话管理(UDP) + +### 6.4 在 Lolly 中实现 MQTT 支持的方案 + +#### 方案 1:独立 MQTT Preread 中间件 + +在 Lolly 的 Stream 模块中添加 MQTT Preread 功能: + +```go +// internal/stream/mqtt_preread.go +package stream + +import ( + "bufio" + "encoding/binary" + "io" + "net" +) + +// MQTTPrereadConfig MQTT Preread 配置 +type MQTTPrereadConfig struct { + Enabled bool + OnClientID func(clientID string) string // 路由回调 + OnUsername func(username string) string // 认证回调 +} + +// MQTTConnectInfo 解析后的 MQTT CONNECT 信息 +type MQTTConnectInfo struct { + ClientID string + Username string + Password []byte + Protocol byte +} + +// ParseMQTTConnect 从连接读取并解析 MQTT CONNECT 消息 +func ParseMQTTConnect(conn net.Conn) (*MQTTConnectInfo, error) { + // 1. 读取固定头(2-5 字节) + // 2. 读取剩余长度 + // 3. 读取可变头(协议名、协议级别、连接标志) + // 4. 读取 Payload(Client ID、Will Topic、Will Message、Username、Password) + // 5. 返回解析结果 +} +``` + +#### 方案 2:基于配置的路由 + +在 Lolly 配置中添加 MQTT 路由规则: + +```yaml +stream: + - listen: ":1883" + protocol: "tcp" + mqtt_preread: true + routes: + - match: "clientid =~ ^sensor-" + upstream: sensors + - match: "clientid =~ ^home-" + upstream: home_devices + - match: "username == admin" + upstream: admin_cluster + upstreams: + sensors: + targets: + - addr: "mqtt-sensors-1:1883" + - addr: "mqtt-sensors-2:1883" + home_devices: + targets: + - addr: "mqtt-home-1:1883" + admin_cluster: + targets: + - addr: "mqtt-admin-1:1883" +``` + +#### 方案 3:与现有 Stream 模块集成 + +扩展现有的 `internal/stream/stream.go`: + +```go +// Target MQTT 扩展 +type Target struct { + addr string + weight int + healthy atomic.Bool + conns int64 + + // MQTT 特定字段 + mqttMatcher func(*MQTTConnectInfo) bool // 匹配函数 +} + +// Upstream 添加 MQTT 选择支持 +type Upstream struct { + name string + targets []*Target + balancer Balancer + + // MQTT 路由表 + mqttRoutes map[string][]*Target // 标签 -> 目标列表 +} + +// SelectByMQTT 基于 MQTT CONNECT 信息选择目标 +func (u *Upstream) SelectByMQTT(info *MQTTConnectInfo) *Target { + // 1. 检查 MQTT 路由表 + // 2. 回退到默认负载均衡 +} +``` + +### 6.5 实现建议 + +#### 短期建议(PoC 验证) + +1. **实现基础 MQTT CONNECT 解析器** + - 支持 MQTT 3.1.1 和 5.0 + - 提取 Client ID、Username、Password + - 位置:`internal/stream/mqtt.go` + +2. **添加基于 Client ID 的路由** + - 简单的正则匹配 + - 配置文件支持 + - 与现有 upstream 集成 + +3. **性能测试** + - 对比有无 preread 的性能差异 + - 内存占用分析 + +#### 中期建议(功能完善) + +1. **完整 MQTT Filter 支持** + - 支持修改 CONNECT 字段 + - 支持消息重写 + - 配置热重载 + +2. **监控与日志** + - MQTT 特定指标(连接数、消息数) + - 结构化日志输出 + +3. **安全增强** + - 基于 Client ID 的访问控制 + - 速率限制 + +#### 长期建议(企业级功能) + +1. **MQTT 5.0 完整支持** + - 用户属性处理 + - 共享订阅支持 + - 消息过期处理 + +2. **高级路由** + - 基于 Topic 的路由(需要解析 PUBLISH/SUBSCRIBE) + - 动态后端发现 + +3. **与 HTTP 层的集成** + - 统一的配置管理 + - 共享的健康检查 + +### 6.6 代码示例 + +以下是在 Lolly 中实现 MQTT Preread 的参考代码: + +```go +// internal/stream/mqtt.go +package stream + +import ( + "bufio" + "encoding/binary" + "fmt" + "io" + "net" +) + +const ( + // MQTT 控制包类型 + mqttCONNECT = 1 + + // MQTT 协议名 + mqttProtocol311 = "MQTT" // v3.1.1 + mqttProtocol31 = "MQIsdp" // v3.1 +) + +// MQTTPrereadHandler MQTT Preread 处理器 +type MQTTPrereadHandler struct { + config *MQTTPrereadConfig +} + +// NewMQTTPrereadHandler 创建处理器 +func NewMQTTPrereadHandler(config *MQTTPrereadConfig) *MQTTPrereadHandler { + return &MQTTPrereadHandler{config: config} +} + +// Handle 处理连接,解析 MQTT CONNECT 并返回信息 +func (h *MQTTPrereadHandler) Handle(conn net.Conn) (*MQTTConnectInfo, net.Conn, error) { + // 使用 bufio 预读数据 + reader := bufio.NewReaderSize(conn, 4096) + + // 读取固定头第一个字节 + firstByte, err := reader.Peek(1) + if err != nil { + return nil, nil, err + } + + // 检查是否为 CONNECT 包 + packetType := (firstByte[0] >> 4) & 0x0F + if packetType != mqttCONNECT { + return nil, nil, fmt.Errorf("expected CONNECT packet, got %d", packetType) + } + + // 读取剩余长度(可变长度编码) + remainingLen, headerLen, err := decodeRemainingLength(reader) + if err != nil { + return nil, nil, err + } + + // 读取完整的 CONNECT 包 + totalLen := 1 + headerLen + remainingLen + packet, err := reader.Peek(totalLen) + if err != nil { + return nil, nil, err + } + + // 解析 CONNECT 包 + info, err := parseConnectPacket(packet[1+headerLen:]) + if err != nil { + return nil, nil, err + } + + // 创建包装连接,将预读的数据返回给后续处理 + wrappedConn := &mqttConn{ + Conn: conn, + reader: reader, + peeked: totalLen, + } + + return info, wrappedConn, nil +} + +// decodeRemainingLength 解码 MQTT 剩余长度字段 +func decodeRemainingLength(r *bufio.Reader) (int, int, error) { + var value int + var multiplier int = 1 + var headerLen int + + for { + b, err := r.ReadByte() + if err != nil { + return 0, 0, err + } + headerLen++ + + value += int(b&0x7F) * multiplier + multiplier *= 128 + + if (b & 0x80) == 0 { + break + } + + if multiplier > 128*128*128 { + return 0, 0, fmt.Errorf("malformed remaining length") + } + } + + return value, headerLen, nil +} + +// parseConnectPacket 解析 CONNECT 包体 +func parseConnectPacket(data []byte) (*MQTTConnectInfo, error) { + info := &MQTTConnectInfo{} + offset := 0 + + // 读取协议名长度 + protoLen := binary.BigEndian.Uint16(data[offset:]) + offset += 2 + + // 读取协议名 + protoName := string(data[offset : offset+int(protoLen)]) + offset += int(protoLen) + + // 判断协议版本 + if protoName == mqttProtocol311 { + info.Protocol = 4 // MQTT 3.1.1 + } else if protoName == mqttProtocol31 { + info.Protocol = 3 // MQTT 3.1 + } + + // 读取协议级别 + protocolLevel := data[offset] + offset++ + _ = protocolLevel + + // 读取连接标志 + connectFlags := data[offset] + offset++ + + usernameFlag := (connectFlags >> 7) & 1 + passwordFlag := (connectFlags >> 6) & 1 + // willRetain := (connectFlags >> 5) & 1 + willQoS := (connectFlags >> 3) & 3 + willFlag := (connectFlags >> 2) & 1 + // cleanSession := (connectFlags >> 1) & 1 + + // 读取保持连接时间(跳过) + offset += 2 + + // 读取 Client ID + clientIDLen := binary.BigEndian.Uint16(data[offset:]) + offset += 2 + info.ClientID = string(data[offset : offset+int(clientIDLen)]) + offset += int(clientIDLen) + + // 读取 Will Topic 和 Will Message(如果有) + if willFlag == 1 { + // Will Topic + willTopicLen := binary.BigEndian.Uint16(data[offset:]) + offset += 2 + offset += int(willTopicLen) + + // Will Message + willMsgLen := binary.BigEndian.Uint16(data[offset:]) + offset += 2 + offset += int(willMsgLen) + } + + // 读取 Username(如果有) + if usernameFlag == 1 { + usernameLen := binary.BigEndian.Uint16(data[offset:]) + offset += 2 + info.Username = string(data[offset : offset+int(usernameLen)]) + offset += int(usernameLen) + } + + // 读取 Password(如果有) + if passwordFlag == 1 { + passwordLen := binary.BigEndian.Uint16(data[offset:]) + offset += 2 + info.Password = data[offset : offset+int(passwordLen)] + } + + return info, nil +} + +// mqttConn 包装连接,支持预读数据回退 +type mqttConn struct { + net.Conn + reader *bufio.Reader + peeked int +} + +func (c *mqttConn) Read(p []byte) (n int, err error) { + return c.reader.Read(p) +} +``` + +### 6.7 测试验证 + +添加 MQTT Preread 的单元测试: + +```go +// internal/stream/mqtt_test.go +package stream + +import ( + "bytes" + "testing" +) + +func TestParseConnectPacket(t *testing.T) { + // 构造一个 MQTT 3.1.1 CONNECT 包 + // CONNECT + 剩余长度 + 协议名长度(4) + "MQTT" + 协议级别(4) + 连接标志 + 保持连接 + Client ID + packet := []byte{ + // 可变头开始 + 0x00, 0x04, // 协议名长度 = 4 + 'M', 'Q', 'T', 'T', // 协议名 "MQTT" + 0x04, // 协议级别 4 (3.1.1) + 0xC2, // 连接标志: 用户名(1) + 密码(1) + Clean Session(0) + 0x00, 0x3C, // 保持连接 = 60 秒 + // Payload + 0x00, 0x0A, // Client ID 长度 = 10 + 't', 'e', 's', 't', '-', 'c', 'l', 'i', 'e', 'n', 't', // Client ID + 0x00, 0x05, // 用户名长度 = 5 + 'a', 'd', 'm', 'i', 'n', // 用户名 + 0x00, 0x08, // 密码长度 = 8 + 'p', 'a', 's', 's', 'w', 'o', 'r', 'd', // 密码 + } + + info, err := parseConnectPacket(packet) + if err != nil { + t.Fatalf("parseConnectPacket failed: %v", err) + } + + if info.ClientID != "test-client" { + t.Errorf("ClientID = %q, want %q", info.ClientID, "test-client") + } + + if info.Username != "admin" { + t.Errorf("Username = %q, want %q", info.Username, "admin") + } + + if info.Protocol != 4 { + t.Errorf("Protocol = %d, want %d", info.Protocol, 4) + } +} + +func TestDecodeRemainingLength(t *testing.T) { + tests := []struct { + input []byte + expected int + }{ + {[]byte{0x00}, 0}, + {[]byte{0x7F}, 127}, + {[]byte{0x80, 0x01}, 128}, + {[]byte{0xFF, 0x7F}, 16383}, + {[]byte{0x80, 0x80, 0x01}, 16384}, + } + + for _, tt := range tests { + r := bufio.NewReader(bytes.NewReader(tt.input)) + value, _, err := decodeRemainingLength(r) + if err != nil { + t.Errorf("decodeRemainingLength(%v) error: %v", tt.input, err) + continue + } + if value != tt.expected { + t.Errorf("decodeRemainingLength(%v) = %d, want %d", tt.input, value, tt.expected) + } + } +} +``` + +--- + +## 7. 总结 + +### 7.1 NGINX MQTT 模块要点 + +1. **Preread 模块** (`ngx_stream_mqtt_preread_module`) + - 只读解析 MQTT CONNECT 消息 + - 提取 Client ID 和 Username 用于路由 + - 适用于基于内容的负载均衡 + +2. **Filter 模块** (`ngx_stream_mqtt_filter_module`) + - 支持修改 MQTT CONNECT 字段 + - 可用于代理认证和会话管理 + - 与 Preread 模块可以配合使用 + +3. **商业订阅限制** + - 两个模块都需要 NGINX Plus 商业订阅 + - 自 1.23.4 版本起可用 + +### 7.2 适用场景 + +- **物联网平台** - 海量设备接入和路由 +- **多租户 MQTT** - 基于用户名隔离租户 +- **边缘网关** - 设备接入层代理 +- **MQTT 迁移** - 平滑迁移到新 Broker + +### 7.3 Lolly 实现建议优先级 + +1. **P0** - 基础 MQTT CONNECT 解析器 +2. **P1** - 基于 Client ID 的路由 +3. **P2** - MQTT Filter(修改 CONNECT) +4. **P3** - MQTT 5.0 高级特性 + +### 7.4 参考资料 + +- [NGINX MQTT Preread Module](https://nginx.org/en/docs/stream/ngx_stream_mqtt_preread_module.html) +- [NGINX MQTT Filter Module](https://nginx.org/en/docs/stream/ngx_stream_mqtt_filter_module.html) +- [MQTT 3.1.1 Specification](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html) +- [MQTT 5.0 Specification](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html) diff --git a/docs/34-nginx-oidc-module.md b/docs/34-nginx-oidc-module.md new file mode 100644 index 0000000..50f9abb --- /dev/null +++ b/docs/34-nginx-oidc-module.md @@ -0,0 +1,1501 @@ +# NGINX OpenID Connect 模块指南 + +## 1. OIDC 模块概述 + +### 什么是 OpenID Connect + +OpenID Connect (OIDC) 是基于 OAuth 2.0 协议的身份认证层,允许客户端应用程序验证用户身份并获取用户基本信息。NGINX Plus 提供原生 OIDC 支持,可作为 **Relying Party (RP)** 与 Identity Provider (IdP) 集成,实现单点登录 (SSO) 和集中式身份认证。 + +### 模块用途 + +| 场景 | 说明 | +|------|------| +| **单点登录 (SSO)** | 用户一次登录,访问多个受保护应用 | +| **集中式认证** | 统一认证入口,后端服务无需处理认证逻辑 | +| **API 保护** | 验证 JWT Token,保护 API 端点 | +| **身份代理** | 将身份信息传递给后端应用 | +| **会话管理** | 集中管理用户会话生命周期 | + +### 架构图 + +``` +┌─────────┐ ┌─────────────┐ ┌─────────────────┐ +│ Client │────────▶│ NGINX Plus │────────▶│ IdP Provider │ +│ (浏览器) │ │ (OIDC RP) │ │ (Keycloak/Okta) │ +└─────────┘ └──────┬──────┘ └─────────────────┘ + │ + ▼ + ┌──────────────┐ + │ Backend │ + │ Applications │ + └──────────────┘ +``` + +**工作流程**: +1. 用户访问受保护资源,NGINX 检查会话状态 +2. 无有效会话时,重定向到 IdP 登录页面 +3. 用户在 IdP 完成认证,IdP 返回授权码 +4. NGINX 使用授权码交换 ID Token 和 Access Token +5. 用户携带会话 Cookie 访问资源,NGINX 验证 JWT +6. 后端应用接收带有用户信息的请求 + +--- + +## 2. OIDC 认证流程 + +### 2.1 授权码流程 (Authorization Code Flow) + +NGINX Plus 使用标准的 OAuth 2.0 授权码流程,支持 **PKCE** (Proof Key for Code Exchange) 增强安全性。 + +**完整流程图**: + +``` +用户 NGINX Plus IdP Provider + | │ │ + |── 1. 访问 /app ───────────────▶│ │ + | │── 2. 检查会话 ─────────────▶│ + | │ │ + |◀─ 3. 重定向到 /authorize ─────│ │ + | │ │ + |── 4. 登录认证 ───────────────────────────────▶│ │ + | │ │ + |◀─ 5. 返回授权码 ───────────────│ │ + | │ │ + |── 6. 携带 code 回调 ──────────▶│ │ + | │── 7. Token 请求 ───────────▶│ + | │ │ + | │◀─ 8. ID/Access/Refresh Token─│ + | │ │ + |◀─ 9. 设置 Session Cookie ─────│ │ + | │ │ + |── 10. 携带 Cookie 访问 ───────▶│ │ + | │── 11. 验证 JWT ────────────▶│ + | │ │ + | │── 12. 代理到后端 ──────────▶│ +``` + +### 2.2 支持的 IdP 提供商 + +| 提供商 | Discovery URL 格式 | +|--------|-------------------| +| **Keycloak** | `https://keycloak.example.com/realms/{realm}/.well-known/openid-configuration` | +| **Okta** | `https://{your-domain}.okta.com/.well-known/openid-configuration` | +| **Auth0** | `https://{your-domain}.auth0.com/.well-known/openid-configuration` | +| **Azure AD / Entra ID** | `https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration` | +| **Google** | `https://accounts.google.com/.well-known/openid-configuration` | +| **GitHub** | `https://token.actions.githubusercontent.com/.well-known/openid-configuration` | +| **自建 IdP** | `https://auth.example.com/.well-known/openid-configuration` | + +--- + +## 3. 指令参考 + +### 3.1 核心指令 + +#### `auth_jwt` + +启用 JWT 认证。 + +| 属性 | 说明 | +|------|------| +| **语法** | `auth_jwt "realm" [token=$variable] \| off;` | +| **默认值** | `off` | +| **上下文** | `http`, `server`, `location` | + +```nginx +# 基本用法 +location /protected/ { + auth_jwt "API Access"; + auth_jwt_key_file /etc/nginx/jwks.json; +} + +# 从 Cookie 读取 Token +location /api/ { + auth_jwt "API Access" token=$cookie_auth_token; + auth_jwt_key_request /_jwks_uri; +} + +# 从 Header 读取 Token +location /api/ { + auth_jwt "API Access" token=$http_authorization; + auth_jwt_key_request /_jwks_uri; +} +``` + +#### `auth_jwt_key_file` + +指定 JWKS (JSON Web Key Set) 文件路径,用于验证 JWT 签名。 + +| 属性 | 说明 | +|------|------| +| **语法** | `auth_jwt_key_file file;` | +| **默认值** | — | +| **上下文** | `http`, `server`, `location` | + +```nginx +# 本地 JWKS 文件 +auth_jwt_key_file /etc/nginx/oidc/jwks.json; + +# 使用变量(动态配置) +auth_jwt_key_file $oidc_jwt_keyfile; +``` + +#### `auth_jwt_key_request` + +从指定 location 动态获取 JWKS。 + +| 属性 | 说明 | +|------|------| +| **语法** | `auth_jwt_key_request uri;` | +| **默认值** | — | +| **上下文** | `http`, `server`, `location` | + +```nginx +server { + location /protected/ { + auth_jwt "API Access" token=$cookie_auth_token; + auth_jwt_key_request /_jwks_uri; + } + + # 内部 JWKS 端点 + location = /_jwks_uri { + internal; # 仅限内部子请求访问 + proxy_pass https://idp.example.com/.well-known/jwks.json; + proxy_cache jwks_cache; + proxy_cache_valid 200 1h; + } +} +``` + +#### `auth_jwt_require` + +配置 JWT 验证的额外要求。 + +| 属性 | 说明 | +|------|------| +| **语法** | `auth_jwt_require $claim [value] ...;` | +| **默认值** | — | +| **上下文** | `http`, `server`, `location` | + +```nginx +# 要求特定的 issuer +location /protected/ { + auth_jwt "API Access" token=$cookie_auth_token; + auth_jwt_key_request /_jwks_uri; + auth_jwt_require $jwt_claim_iss "https://idp.example.com"; +} + +# 要求特定的 audience +location /api/admin/ { + auth_jwt "Admin Access" token=$cookie_auth_token; + auth_jwt_key_request /_jwks_uri; + auth_jwt_require $jwt_claim_aud "my-api-client"; +} + +# 组合条件 +location /api/sensitive/ { + auth_jwt "Sensitive Access" token=$cookie_auth_token; + auth_jwt_key_request /_jwks_uri; + auth_jwt_require $jwt_claim_iss "https://idp.example.com" $jwt_claim_aud "sensitive-api"; +} +``` + +### 3.2 会话管理指令 + +#### `keyval_zone` + +定义用于存储 OIDC 会话数据的共享内存区域。 + +| 属性 | 说明 | +|------|------| +| **语法** | `keyval_zone zone=name:size [state=file] [timeout=time];` | +| **默认值** | — | +| **上下文** | `http` | + +```nginx +# 基本配置 +keyval_zone zone=oidc_id_tokens:1M timeout=1h; +keyval_zone zone=oidc_access_tokens:1M timeout=1h; +keyval_zone zone=oidc_refresh_tokens:1M timeout=8h; + +# 带状态持久化 +keyval_zone zone=oidc_id_tokens:1M state=/var/lib/nginx/state/oidc_id_tokens.json timeout=1h; +keyval_zone zone=oidc_access_tokens:1M state=/var/lib/nginx/state/oidc_access_tokens.json timeout=1h; +``` + +**参数说明**: +- `zone=name:size` - 共享内存区域名称和大小 +- `state=file` - 状态持久化文件路径 +- `timeout=time` - 数据过期时间 + +#### `keyval` + +定义键值对存储。 + +| 属性 | 说明 | +|------|------| +| **语法** | `keyval $variable $variable zone=name;` | +| **默认值** | — | +| **上下文** | `http` | + +```nginx +# 定义存储变量 +keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens; +keyval $cookie_auth_token $access_token zone=oidc_access_tokens; +keyval $cookie_auth_token $refresh_token zone=oidc_refresh_tokens; +``` + +### 3.3 JavaScript 模块指令 + +#### `js_import` + +导入 NGINX JavaScript (njs) 模块。 + +| 属性 | 说明 | +|------|------| +| **语法** | `js_import module [as namespace];` | +| **默认值** | — | +| **上下文** | `http` | + +```nginx +# 导入 OIDC 处理脚本 +js_import /etc/nginx/conf.d/openid_connect.js; + +# 使用命名空间 +js_import /etc/nginx/conf.d/openid_connect.js as oidc; +``` + +#### `js_set` + +设置 JavaScript 变量。 + +| 属性 | 说明 | +|------|------| +| **语法** | `js_set $variable function;` | +| **默认值** | — | +| **上下文** | `http` | + +```nginx +# 设置 OIDC 认证头 +js_set $oidc_auth_header oidc.authHeader; + +# 设置 ID Token +js_set $id_token oidc.idToken; +``` + +#### `js_content` + +使用 JavaScript 生成响应内容。 + +| 属性 | 说明 | +|------|------| +| **语法** | `js_content function;` | +| **默认值** | — | +| **上下文** | `location` | + +```nginx +location /login { + js_content oidc.login; +} + +location /logout { + js_content oidc.logout; +} + +location = /redirect_uri { + js_content oidc.redirect; +} +``` + +### 3.4 代理配置指令 + +#### `auth_jwt_set` + +将 JWT Claims 提取到变量。 + +| 属性 | 说明 | +|------|------| +| **语法** | `auth_jwt_set $variable claim;` | +| **默认值** | — | +| **上下文** | `http`, `server`, `location` | + +```nginx +server { + location /api/ { + auth_jwt "API Access" token=$cookie_auth_token; + auth_jwt_key_request /_jwks_uri; + + # 提取常用 Claims + auth_jwt_set $jwt_sub sub; + auth_jwt_set $jwt_email email; + auth_jwt_set $jwt_name name; + + proxy_pass http://backend; + proxy_set_header X-User-ID $jwt_sub; + proxy_set_header X-User-Email $jwt_email; + proxy_set_header X-User-Name $jwt_name; + } +} +``` + +--- + +## 4. 变量参考 + +### 4.1 JWT Claims 变量 + +| 变量 | 说明 | +|------|------| +| `$jwt_claim_sub` | 用户唯一标识 (Subject) | +| `$jwt_claim_iss` | Token 签发者 (Issuer) | +| `$jwt_claim_aud` | Token 受众 (Audience) | +| `$jwt_claim_exp` | Token 过期时间 (Expiration) | +| `$jwt_claim_iat` | Token 签发时间 (Issued At) | +| `$jwt_claim_nbf` | Token 生效时间 (Not Before) | +| `$jwt_claim_jti` | Token 唯一标识 (JWT ID) | +| `$jwt_claim_email` | 用户邮箱 | +| `$jwt_claim_name` | 用户名称 | +| `$jwt_claim_preferred_username` | 首选用户名 | +| `$jwt_claim_groups` | 用户组/角色 | +| `$jwt_claim_scope` | 授权范围 | + +### 4.2 会话变量 + +| 变量 | 说明 | +|------|------| +| `$cookie_auth_token` | 会话 Cookie 值 | +| `$session_jwt` | 存储的 ID Token | +| `$access_token` | 存储的 Access Token | +| `$refresh_token` | 存储的 Refresh Token | + +--- + +## 5. IdP 集成配置示例 + +### 5.1 Keycloak 集成 + +**Keycloak 配置要点**: +- 创建 Client,启用 `Standard Flow` (Authorization Code) +- 配置 Valid Redirect URIs: `https://app.example.com/redirect_uri` +- 启用 `Client authentication`,记录 Client Secret +- 配置 Web Origins: `https://app.example.com` + +**NGINX 配置**: + +```nginx +http { + # 加载 JavaScript 模块 + load_module modules/ngx_http_js_module.so; + + # 会话存储 + keyval_zone zone=oidc_id_tokens:1M state=/var/lib/nginx/state/oidc_id_tokens.json timeout=1h; + keyval_zone zone=oidc_access_tokens:1M state=/var/lib/nginx/state/oidc_access_tokens.json timeout=1h; + keyval_zone zone=oidc_refresh_tokens:1M state=/var/lib/nginx/state/oidc_refresh_tokens.json timeout=8h; + + keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens; + keyval $cookie_auth_token $access_token zone=oidc_access_tokens; + keyval $cookie_auth_token $refresh_token zone=oidc_refresh_tokens; + + # IdP 端点配置 + map $host $oidc_authz_endpoint { + default "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth"; + } + + map $host $oidc_token_endpoint { + default "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token"; + } + + map $host $oidc_jwks_uri { + default "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/certs"; + } + + map $host $oidc_userinfo_endpoint { + default "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/userinfo"; + } + + map $host $oidc_end_session_endpoint { + default "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/logout"; + } + + # Client 配置 + map $host $oidc_client { + default "my-nginx-client"; + } + + map $host $oidc_client_secret { + default "your-client-secret-here"; + } + + map $host $oidc_scopes { + default "openid+profile+email"; + } + + map $host $oidc_pkce_enable { + default "1"; + } + + # 导入 OIDC 脚本 + js_import /etc/nginx/conf.d/openid_connect.js; + + # 缓存配置 + proxy_cache_path /var/cache/nginx/jwks levels=1:2 keys_zone=jwks_cache:1m max_size=10m inactive=60m use_temp_path=off; + + server { + listen 443 ssl; + server_name app.example.com; + + ssl_certificate /etc/nginx/ssl/app.example.com.crt; + ssl_certificate_key /etc/nginx/ssl/app.example.com.key; + + # 会话 Cookie 配置 + set $session_cookie "auth_token"; + set $session_cookie_flags "Path=/; Secure; HttpOnly; SameSite=Strict"; + + location / { + # JWT 认证 + auth_jwt "Keycloak SSO" token=$cookie_auth_token; + auth_jwt_key_request /_jwks_uri; + + # 提取用户信息 + auth_jwt_set $jwt_sub sub; + auth_jwt_set $jwt_email email; + + proxy_pass http://backend; + proxy_set_header Host $host; + proxy_set_header X-User-ID $jwt_sub; + proxy_set_header X-User-Email $jwt_email; + proxy_set_header Authorization "Bearer $access_token"; + } + + # OIDC 端点 + location /login { + js_content openid_connect.login; + } + + location /logout { + js_content openid_connect.logout; + } + + location = /redirect_uri { + js_content openid_connect.redirect; + } + + # 内部 JWKS 端点 + location = /_jwks_uri { + internal; + proxy_pass $oidc_jwks_uri; + proxy_cache jwks_cache; + proxy_cache_valid 200 1h; + proxy_cache_use_stale error timeout updating; + proxy_ssl_server_name on; + } + } +} +``` + +### 5.2 Okta 集成 + +**Okta 配置要点**: +- 创建 App Integration,选择 `OIDC - OpenID Connect` +- Application type: `Web Application` +- Sign-in redirect URIs: `https://app.example.com/redirect_uri` +- Sign-out redirect URIs: `https://app.example.com/` +- Grant type: `Authorization Code` + `Refresh Token` + +**NGINX 配置**: + +```nginx +http { + load_module modules/ngx_http_js_module.so; + + # 会话存储 + keyval_zone zone=oidc_id_tokens:1M state=/var/lib/nginx/state/oidc_id_tokens.json timeout=1h; + keyval_zone zone=oidc_access_tokens:1M state=/var/lib/nginx/state/oidc_access_tokens.json timeout=1h; + keyval_zone zone=oidc_refresh_tokens:1M state=/var/lib/nginx/state/oidc_refresh_tokens.json timeout=8h; + + keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens; + keyval $cookie_auth_token $access_token zone=oidc_access_tokens; + keyval $cookie_auth_token $refresh_token zone=oidc_refresh_tokens; + + # Okta 端点配置 + map $host $oidc_authz_endpoint { + default "https://myorg.okta.com/oauth2/default/v1/authorize"; + # 或使用自定义授权服务器 + # default "https://myorg.okta.com/oauth2/ausxxxxxx/v1/authorize"; + } + + map $host $oidc_token_endpoint { + default "https://myorg.okta.com/oauth2/default/v1/token"; + } + + map $host $oidc_jwks_uri { + default "https://myorg.okta.com/oauth2/default/v1/keys"; + } + + map $host $oidc_userinfo_endpoint { + default "https://myorg.okta.com/oauth2/default/v1/userinfo"; + } + + map $host $oidc_end_session_endpoint { + default "https://myorg.okta.com/oauth2/default/v1/logout"; + } + + map $host $oidc_client { + default "0oaxxxxxxxxxxx"; + } + + map $host $oidc_client_secret { + default "your-client-secret"; + } + + map $host $oidc_scopes { + default "openid+profile+email+groups"; + } + + map $host $oidc_pkce_enable { + default "1"; + } + + js_import /etc/nginx/conf.d/openid_connect.js; + + proxy_cache_path /var/cache/nginx/jwks levels=1:2 keys_zone=jwks_cache:1m max_size=10m inactive=60m; + + server { + listen 443 ssl; + server_name app.example.com; + + ssl_certificate /etc/nginx/ssl/app.example.com.crt; + ssl_certificate_key /etc/nginx/ssl/app.example.com.key; + + location / { + auth_jwt "Okta SSO" token=$cookie_auth_token; + auth_jwt_key_request /_jwks_uri; + + auth_jwt_set $jwt_sub sub; + auth_jwt_set $jwt_email email; + auth_jwt_set $jwt_groups groups; + + proxy_pass http://backend; + proxy_set_header X-User-ID $jwt_sub; + proxy_set_header X-User-Email $jwt_email; + proxy_set_header X-User-Groups $jwt_groups; + proxy_set_header Authorization "Bearer $access_token"; + } + + location /login { + js_content openid_connect.login; + } + + location /logout { + js_content openid_connect.logout; + } + + location = /redirect_uri { + js_content openid_connect.redirect; + } + + location = /_jwks_uri { + internal; + proxy_pass $oidc_jwks_uri; + proxy_cache jwks_cache; + proxy_cache_valid 200 1h; + proxy_ssl_server_name on; + } + } +} +``` + +### 5.3 Auth0 集成 + +**Auth0 配置要点**: +- 创建 Application,类型选择 `Regular Web Application` +- Allowed Callback URLs: `https://app.example.com/redirect_uri` +- Allowed Logout URLs: `https://app.example.com/` +- Allowed Web Origins: `https://app.example.com` +- 启用 `Refresh Token` 轮换(可选) + +**NGINX 配置**: + +```nginx +http { + load_module modules/ngx_http_js_module.so; + + # 会话存储 + keyval_zone zone=oidc_id_tokens:1M state=/var/lib/nginx/state/oidc_id_tokens.json timeout=1h; + keyval_zone zone=oidc_access_tokens:1M state=/var/lib/nginx/state/oidc_access_tokens.json timeout=1h; + keyval_zone zone=oidc_refresh_tokens:1M state=/var/lib/nginx/state/oidc_refresh_tokens.json timeout=8h; + + keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens; + keyval $cookie_auth_token $access_token zone=oidc_access_tokens; + keyval $cookie_auth_token $refresh_token zone=oidc_refresh_tokens; + + # Auth0 端点配置 + map $host $oidc_authz_endpoint { + default "https://myapp.auth0.com/authorize"; + } + + map $host $oidc_token_endpoint { + default "https://myapp.auth0.com/oauth/token"; + } + + map $host $oidc_jwks_uri { + default "https://myapp.auth0.com/.well-known/jwks.json"; + } + + map $host $oidc_userinfo_endpoint { + default "https://myapp.auth0.com/userinfo"; + } + + map $host $oidc_end_session_endpoint { + default "https://myapp.auth0.com/v2/logout"; + } + + map $host $oidc_client { + default "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; + } + + map $host $oidc_client_secret { + default "your-client-secret"; + } + + map $host $oidc_scopes { + default "openid+profile+email"; + } + + map $host $oidc_pkce_enable { + default "1"; + } + + # Auth0 特定:返回首页参数 + map $host $oidc_logout_redirect { + default "https://app.example.com/"; + } + + js_import /etc/nginx/conf.d/openid_connect.js; + + proxy_cache_path /var/cache/nginx/jwks levels=1:2 keys_zone=jwks_cache:1m max_size=10m inactive=60m; + + server { + listen 443 ssl; + server_name app.example.com; + + ssl_certificate /etc/nginx/ssl/app.example.com.crt; + ssl_certificate_key /etc/nginx/ssl/app.example.com.key; + + location / { + auth_jwt "Auth0 SSO" token=$cookie_auth_token; + auth_jwt_key_request /_jwks_uri; + + auth_jwt_set $jwt_sub sub; + auth_jwt_set $jwt_email email; + auth_jwt_set $jwt_name name; + auth_jwt_set $jwt_nickname nickname; + + proxy_pass http://backend; + proxy_set_header X-User-ID $jwt_sub; + proxy_set_header X-User-Email $jwt_email; + proxy_set_header X-User-Name $jwt_name; + proxy_set_header Authorization "Bearer $access_token"; + } + + location /login { + js_content openid_connect.login; + } + + location /logout { + js_content openid_connect.logout; + } + + location = /redirect_uri { + js_content openid_connect.redirect; + } + + location = /_jwks_uri { + internal; + proxy_pass $oidc_jwks_uri; + proxy_cache jwks_cache; + proxy_cache_valid 200 1h; + proxy_ssl_server_name on; + } + } +} +``` + +### 5.4 Azure AD / Entra ID 集成 + +**Azure AD 配置要点**: +- 在 Azure Portal 注册应用 +- 添加平台配置:Web +- 重定向 URI: `https://app.example.com/redirect_uri` +- 启用 `ID tokens` (用于隐式和混合流) +- 创建 Client Secret + +**NGINX 配置**: + +```nginx +http { + load_module modules/ngx_http_js_module.so; + + # 会话存储 + keyval_zone zone=oidc_id_tokens:1M state=/var/lib/nginx/state/oidc_id_tokens.json timeout=1h; + keyval_zone zone=oidc_access_tokens:1M state=/var/lib/nginx/state/oidc_access_tokens.json timeout=1h; + keyval_zone zone=oidc_refresh_tokens:1M state=/var/lib/nginx/state/oidc_refresh_tokens.json timeout=8h; + + keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens; + keyval $cookie_auth_token $access_token zone=oidc_access_tokens; + keyval $cookie_auth_token $refresh_token zone=oidc_refresh_tokens; + + # Azure AD 端点配置 + map $host $oidc_authz_endpoint { + default "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize"; + } + + map $host $oidc_token_endpoint { + default "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token"; + } + + map $host $oidc_jwks_uri { + default "https://login.microsoftonline.com/{tenant-id}/discovery/v2.0/keys"; + } + + map $host $oidc_userinfo_endpoint { + default "https://graph.microsoft.com/oidc/userinfo"; + } + + map $host $oidc_end_session_endpoint { + default "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/logout"; + } + + map $host $oidc_client { + default "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; + } + + map $host $oidc_client_secret { + default "your-client-secret"; + } + + # Azure AD 使用空格分隔 scopes + map $host $oidc_scopes { + default "openid+profile+email"; + } + + map $host $oidc_pkce_enable { + default "1"; + } + + js_import /etc/nginx/conf.d/openid_connect.js; + + proxy_cache_path /var/cache/nginx/jwks levels=1:2 keys_zone=jwks_cache:1m max_size=10m inactive=60m; + + server { + listen 443 ssl; + server_name app.example.com; + + ssl_certificate /etc/nginx/ssl/app.example.com.crt; + ssl_certificate_key /etc/nginx/ssl/app.example.com.key; + + location / { + auth_jwt "Azure AD SSO" token=$cookie_auth_token; + auth_jwt_key_request /_jwks_uri; + + auth_jwt_set $jwt_sub sub; + auth_jwt_set $jwt_email email; + auth_jwt_set $jwt_name name; + auth_jwt_set $jwt_oid oid; # Azure AD Object ID + + proxy_pass http://backend; + proxy_set_header X-User-ID $jwt_sub; + proxy_set_header X-User-Email $jwt_email; + proxy_set_header X-User-Name $jwt_name; + proxy_set_header X-Azure-Object-ID $jwt_oid; + proxy_set_header Authorization "Bearer $access_token"; + } + + location /login { + js_content openid_connect.login; + } + + location /logout { + js_content openid_connect.logout; + } + + location = /redirect_uri { + js_content openid_connect.redirect; + } + + location = /_jwks_uri { + internal; + proxy_pass $oidc_jwks_uri; + proxy_cache jwks_cache; + proxy_cache_valid 200 1h; + proxy_ssl_server_name on; + } + } +} +``` + +--- + +## 6. JWT 验证与 Token 刷新 + +### 6.1 JWT 验证机制 + +**验证流程**: + +``` +1. 提取 Token ──▶ 2. 解析 Header ──▶ 3. 获取公钥 ──▶ 4. 验证签名 + │ + ▼ +5. 验证 Claims ◀── 6. 检查过期 ◀── 4. 验证签名 +``` + +**关键验证点**: +- **签名验证**:使用 IdP 的公钥验证 JWT 签名 +- **Issuer 验证**:确认 Token 来自正确的 IdP +- **Audience 验证**:确认 Token 为此应用签发 +- **过期验证**:检查 `exp` Claim +- **生效时间**:检查 `nbf` (Not Before) Claim + +### 6.2 JWKS 缓存配置 + +```nginx +# JWKS 缓存 +proxy_cache_path /var/cache/nginx/jwks levels=1:2 keys_zone=jwks_cache:1m + max_size=10m inactive=60m use_temp_path=off; + +server { + location = /_jwks_uri { + internal; + proxy_pass $oidc_jwks_uri; + proxy_cache jwks_cache; + proxy_cache_valid 200 1h; + proxy_cache_use_stale error timeout updating; + proxy_ssl_server_name on; + + # 连接超时设置 + proxy_connect_timeout 5s; + proxy_send_timeout 5s; + proxy_read_timeout 5s; + } +} +``` + +### 6.3 Token 刷新机制 + +**自动刷新流程**: + +```javascript +// openid_connect.js 中的刷新逻辑 +function refreshToken(r) { + // 1. 检查 Token 是否即将过期 + var token = r.variables.session_jwt; + var payload = JSON.parse(jwtPayload(token)); + var exp = payload.exp; + var now = Math.floor(Date.now() / 1000); + + // 2. 提前 60 秒刷新 + if (exp - now < 60) { + // 3. 使用 Refresh Token 获取新 Token + var refreshToken = r.variables.refresh_token; + return exchangeRefreshToken(r, refreshToken); + } + + return token; +} +``` + +**配置示例**: + +```nginx +server { + location / { + # 使用 JavaScript 处理 Token 刷新 + js_set $validated_token oidc.validateAndRefresh; + + auth_jwt "API" token=$validated_token; + auth_jwt_key_request /_jwks_uri; + + proxy_pass http://backend; + } +} +``` + +### 6.4 多 IdP 支持 + +```nginx +http { + # 根据域名选择 IdP + map $host $oidc_config { + default "keycloak"; + "app1.example.com" "keycloak"; + "app2.example.com" "okta"; + "app3.example.com" "auth0"; + } + + # Keycloak 配置 + map $oidc_config $keycloak_authz_endpoint { + "keycloak" "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth"; + default ""; + } + + map $oidc_config $keycloak_token_endpoint { + "keycloak" "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token"; + default ""; + } + + # Okta 配置 + map $oidc_config $okta_authz_endpoint { + "okta" "https://myorg.okta.com/oauth2/default/v1/authorize"; + default ""; + } + + map $oidc_config $okta_token_endpoint { + "okta" "https://myorg.okta.com/oauth2/default/v1/token"; + default ""; + } + + # 统一端点变量 + map $oidc_config $oidc_authz_endpoint { + "keycloak" $keycloak_authz_endpoint; + "okta" $okta_authz_endpoint; + } + + map $oidc_config $oidc_token_endpoint { + "keycloak" $keycloak_token_endpoint; + "okta" $okta_token_endpoint; + } +} +``` + +--- + +## 7. JavaScript 模块 (njs) 实现 + +### 7.1 基础 openid_connect.js + +```javascript +// openid_connect.js +var qs = require('querystring'); +var jwt = require('jwt'); + +// 生成随机状态码 +function generateState() { + return Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15); +} + +// 生成 PKCE code_verifier +function generateCodeVerifier() { + var bytes = []; + for (var i = 0; i < 32; i++) { + bytes.push(Math.floor(Math.random() * 256)); + } + return bytesToBase64Url(bytes); +} + +// Base64 URL 编码 +function bytesToBase64Url(bytes) { + var base64 = ''; + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + for (var i = 0; i < bytes.length; i += 3) { + var b1 = bytes[i]; + var b2 = bytes[i + 1] || 0; + var b3 = bytes[i + 2] || 0; + var bitmap = (b1 << 16) | (b2 << 8) | b3; + base64 += chars[(bitmap >> 18) & 63]; + base64 += chars[(bitmap >> 12) & 63]; + base64 += (i + 1 < bytes.length) ? chars[(bitmap >> 6) & 63] : '='; + base64 += (i + 2 < bytes.length) ? chars[bitmap & 63] : '='; + } + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +} + +// 登录处理 +function login(r) { + var state = generateState(); + var redirectUri = 'https://' + r.headersIn['Host'] + '/redirect_uri'; + + var params = { + response_type: 'code', + client_id: r.variables.oidc_client, + redirect_uri: redirectUri, + scope: r.variables.oidc_scopes.replace(/\+/g, ' '), + state: state + }; + + // 启用 PKCE + if (r.variables.oidc_pkce_enable === '1') { + var codeVerifier = generateCodeVerifier(); + params.code_challenge = codeVerifier; + params.code_challenge_method = 'S256'; + // 存储 code_verifier 用于后续 token 交换 + r.variables.code_verifier = codeVerifier; + } + + var authUrl = r.variables.oidc_authz_endpoint + '?' + qs.stringify(params); + r.return(302, authUrl); +} + +// Token 交换处理 +function redirect(r) { + var args = r.args; + + // 检查错误 + if (args.error) { + r.return(500, 'Authentication error: ' + args.error_description); + return; + } + + var code = args.code; + var redirectUri = 'https://' + r.headersIn['Host'] + '/redirect_uri'; + + // Token 请求 + var tokenReq = { + method: 'POST', + body: qs.stringify({ + grant_type: 'authorization_code', + code: code, + redirect_uri: redirectUri, + client_id: r.variables.oidc_client, + client_secret: r.variables.oidc_client_secret + }), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + // 发送 Token 请求 + r.subrequest('/_token', tokenReq, function(res) { + if (res.status !== 200) { + r.return(500, 'Token exchange failed'); + return; + } + + var tokenData = JSON.parse(res.responseBody); + var sessionId = generateState(); + + // 存储 Token + r.variables.session_jwt = tokenData.id_token; + r.variables.access_token = tokenData.access_token; + r.variables.refresh_token = tokenData.refresh_token || ''; + + // 设置 Cookie 并重定向 + r.headersOut['Set-Cookie'] = 'auth_token=' + sessionId + '; ' + + r.variables.session_cookie_flags; + r.return(302, '/'); + }); +} + +// 登出处理 +function logout(r) { + var sessionId = r.variables.cookie_auth_token; + + // 清除存储的 Token + if (sessionId) { + delete r.variables.session_jwt; + delete r.variables.access_token; + delete r.variables.refresh_token; + } + + // 清除 Cookie + r.headersOut['Set-Cookie'] = 'auth_token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT'; + + // 如果配置了 end_session_endpoint,重定向到 IdP 登出 + if (r.variables.oidc_end_session_endpoint) { + var logoutUrl = r.variables.oidc_end_session_endpoint + + '?post_logout_redirect_uri=' + + encodeURIComponent('https://' + r.headersIn['Host'] + '/'); + r.return(302, logoutUrl); + } else { + r.return(302, '/'); + } +} + +// Token 验证和刷新 +function validateAndRefresh(r) { + var token = r.variables.session_jwt; + if (!token) { + return ''; + } + + try { + var payload = jwt.decode(token).payload; + var exp = payload.exp; + var now = Math.floor(Date.now() / 1000); + + // Token 即将过期,尝试刷新 + if (exp - now < 60 && r.variables.refresh_token) { + // 异步刷新 Token + refreshAccessToken(r); + } + + return token; + } catch (e) { + return ''; + } +} + +// 导出函数 +export default { login, logout, redirect, validateAndRefresh }; +``` + +### 7.2 高级功能扩展 + +```javascript +// 前端频道登出处理 +function frontChannelLogout(r) { + var sid = r.args.sid; + if (sid) { + // 根据 sid 查找并清除会话 + clearSessionBySid(r, sid); + } + r.return(200, 'OK'); +} + +// 用户信息服务 +function userInfo(r) { + var token = r.variables.access_token; + if (!token) { + r.return(401, 'Unauthorized'); + return; + } + + r.subrequest('/_userinfo', { + method: 'GET', + headers: { + 'Authorization': 'Bearer ' + token + } + }, function(res) { + r.headersOut['Content-Type'] = 'application/json'; + r.return(res.status, res.responseBody); + }); +} + +// 会话检查 +function checkSession(r) { + var token = r.variables.session_jwt; + if (!token) { + r.return(401, JSON.stringify({ active: false })); + return; + } + + try { + var payload = jwt.decode(token).payload; + r.return(200, JSON.stringify({ + active: true, + sub: payload.sub, + exp: payload.exp + })); + } catch (e) { + r.return(401, JSON.stringify({ active: false })); + } +} +``` + +--- + +## 8. 与 Lolly 项目的关系和建议 + +### 8.1 Lolly 项目概述 + +Lolly 是使用 Go 语言编写的高性能 HTTP 服务器与反向代理,与 NGINX 有相似的功能定位: + +| 特性 | NGINX Plus | Lolly | +|------|-----------|-------| +| **语言** | C | Go | +| **OIDC 支持** | 内置 (auth_jwt, njs) | 待实现 | +| **配置** | nginx.conf | YAML | +| **扩展** | njs / C 模块 | Go 中间件 | +| **性能** | 极高 (C + 事件驱动) | 高 (Go + fasthttp) | +| **HTTP/3** | 实验性 | 原生支持 | +| **热重载** | 支持 | 支持 | + +### 8.2 Lolly OIDC 实现建议 + +基于 NGINX OIDC 模块的设计,建议 Lolly 实现以下功能: + +#### 配置结构示例 + +```yaml +# Lolly OIDC 配置 +oidc: + enabled: true + providers: + - name: keycloak + issuer: "https://keycloak.example.com/realms/myrealm" + client_id: "lolly-client" + client_secret: "${KEYCLOAK_SECRET}" + scopes: ["openid", "profile", "email"] + pkce_enabled: true + redirect_uri: "https://app.example.com/auth/callback" + + # JWKS 配置 + jwks: + url: "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/certs" + cache_ttl: "1h" + + # 会话配置 + session: + cookie_name: "auth_token" + cookie_secure: true + cookie_http_only: true + cookie_same_site: "Strict" + ttl: "1h" + + # Token 刷新 + refresh: + enabled: true + before_expiry: "5m" + + # 用户信息转发 + claims_forward: + - claim: "sub" + header: "X-User-ID" + - claim: "email" + header: "X-User-Email" + - claim: "name" + header: "X-User-Name" + +server: + listen: ":443" + + # 全局 OIDC 保护 + oidc: + provider: keycloak + + routes: + # 公开路径(无需认证) + - path: "/health" + public: true + static: + response: '{"status":"ok"}' + + # 受保护 API + - path: "/api/*" + oidc: + provider: keycloak + require_claims: + - claim: "groups" + values: ["api-users"] + proxy: + target: "http://backend:8080" + + # 管理后台(更严格) + - path: "/admin/*" + oidc: + provider: keycloak + require_claims: + - claim: "groups" + values: ["admin"] + proxy: + target: "http://admin:8080" +``` + +#### 建议的实现架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Lolly Server │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ +│ │ OIDC Middleware │ │ Session Store │ │ Token Validator │ │ +│ │ │ │ - In-Memory │ │ - JWKS Fetch │ │ +│ │ - Login Handler │ │ - Redis │ │ - JWT Verify │ │ +│ │ - Callback │ │ - Cookie │ │ - Claims Extract│ │ +│ │ - Logout │ │ │ │ │ │ +│ │ - Refresh │ │ │ │ │ │ +│ └──────────────┘ └──────────────┘ └──────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### 推荐依赖库 + +| 功能 | 推荐库 | 说明 | +|------|--------|------| +| **OIDC Client** | `coreos/go-oidc` | 官方推荐,功能完整 | +| **JWT 解析** | `golang-jwt/jwt` | 最流行的 Go JWT 库 | +| **OAuth2** | `golang.org/x/oauth2` | 标准 OAuth2 实现 | +| **Session** | `gorilla/sessions` | 成熟的 Session 管理 | + +#### 实现优先级建议 + +1. **Phase 1 - 基础功能** + - [ ] JWT 验证中间件 + - [ ] JWKS 获取和缓存 + - [ ] 基本 Claims 提取 + +2. **Phase 2 - 完整认证** + - [ ] 授权码流程 + - [ ] Session Cookie 管理 + - [ ] 登录/登出端点 + +3. **Phase 3 - 高级功能** + - [ ] Token 自动刷新 + - [ ] PKCE 支持 + - [ ] 多 IdP 支持 + +4. **Phase 4 - 企业特性** + - [ ] 前端频道登出 + - [ ] 用户信息服务 + - [ ] 细粒度权限控制 + +### 8.3 迁移建议 + +从 NGINX Plus OIDC 迁移到 Lolly: + +1. **配置转换**:使用工具将 nginx.conf 的 map 转换为 YAML +2. **IdP 配置复用**:Issuer、Client ID、Secret 保持不变 +3. **后端适配**:验证 Header 名称一致性 +4. **Session 迁移**:逐步迁移,支持双写 +5. **灰度切换**:基于域名或路径逐步切换 + +--- + +## 9. 故障排查 + +### 9.1 常见问题 + +#### Token 验证失败 + +```nginx +# 启用详细日志 +error_log /var/log/nginx/error.log debug; + +# 检查 JWKS 是否获取成功 +location = /_jwks_uri { + internal; + proxy_pass $oidc_jwks_uri; + + # 记录响应 + add_header X-JWKS-Status $upstream_status always; + add_header X-JWKS-Cache $upstream_cache_status always; +} +``` + +#### Cookie 问题 + +```nginx +# 确保 Cookie 配置正确 +set $session_cookie "auth_token"; +set $session_cookie_flags "Path=/; Secure; HttpOnly; SameSite=Lax"; + +# 检查 Cookie 是否设置 +add_header X-Debug-Cookie $cookie_auth_token always; +``` + +### 9.2 调试端点 + +```nginx +server { + # 健康检查端点 + location /auth/health { + auth_jwt off; + default_type application/json; + return 200 '{"status":"ok","provider":"$oidc_config"}'; + } + + # Token 信息端点(仅调试) + location /auth/debug { + auth_jwt "Debug" token=$cookie_auth_token; + auth_jwt_key_request /_jwks_uri; + + auth_jwt_set $jwt_sub sub; + auth_jwt_set $jwt_exp exp; + + default_type application/json; + return 200 '{ + "sub": "$jwt_sub", + "exp": "$jwt_exp", + "provider": "$oidc_config" + }'; + } +} +``` + +### 9.3 日志分析 + +```bash +# 查看认证相关日志 +grep "auth_jwt\|oidc\|openid" /var/log/nginx/error.log | tail -100 + +# 监控 Token 刷新 +awk '/token.*refresh/ {print $0}' /var/log/nginx/access.log +``` + +--- + +## 10. 最佳实践 + +### 10.1 安全建议 + +1. **始终启用 HTTPS** + ```nginx + # 拒绝 HTTP 访问 + server { + listen 80; + return 301 https://$host$request_uri; + } + ``` + +2. **使用 Secure Cookie** + ```nginx + set $session_cookie_flags "Path=/; Secure; HttpOnly; SameSite=Strict"; + ``` + +3. **启用 PKCE** + ```nginx + map $host $oidc_pkce_enable { + default "1"; + } + ``` + +4. **定期轮换 Client Secret** + +### 10.2 性能优化 + +1. **JWKS 缓存** + ```nginx + proxy_cache_valid 200 1h; + proxy_cache_use_stale error timeout updating; + ``` + +2. **会话存储优化** + ```nginx + # 共享内存区域大小根据并发用户数调整 + keyval_zone zone=oidc_id_tokens:10M timeout=1h; + ``` + +3. **连接池** + ```nginx + upstream idp_backend { + server keycloak.example.com:443; + keepalive 32; + } + ``` + +### 10.3 高可用配置 + +```nginx +# JWKS 多源备份 +upstream jwks_upstream { + server keycloak-primary.example.com:443; + server keycloak-backup.example.com:443 backup; +} + +location = /_jwks_uri { + internal; + proxy_pass https://jwks_upstream/realms/myrealm/protocol/openid-connect/certs; + proxy_cache jwks_cache; + proxy_cache_valid 200 1h; +} +``` + +--- + +## 参考资料 + +- [NGINX Plus OIDC Reference Implementation](https://github.com/nginxinc/nginx-openid-connect) +- [OpenID Connect Core 1.0 Specification](https://openid.net/specs/openid-connect-core-1_0.html) +- [OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749) +- [JSON Web Token (JWT) Specification](https://tools.ietf.org/html/rfc7519) +- [Proof Key for Code Exchange (PKCE)](https://tools.ietf.org/html/rfc7636) +- [Lolly 项目文档](./README.md) diff --git a/docs/35-nginx-keyval-module.md b/docs/35-nginx-keyval-module.md new file mode 100644 index 0000000..2af1dc8 --- /dev/null +++ b/docs/35-nginx-keyval-module.md @@ -0,0 +1,1080 @@ +# NGINX Keyval 模块详解 + +本文档详细介绍 NGINX Keyval 模块(动态键值存储)的配置与使用方法,包括 HTTP 和 Stream 两个子模块。 + +--- + +## 1. Keyval 模块概述 + +### 什么是 Keyval 模块 + +Keyval 模块是 NGINX 提供的**动态键值存储系统**,允许在运行时通过 API 动态管理键值对数据,无需重启 NGINX 即可更新配置逻辑。 + +### 模块组成 + +| 模块名称 | 上下文 | 商业版本 | 可用版本 | +|---------|--------|---------|---------| +| `ngx_http_keyval_module` | http | 需要 | 1.13.3+ | +| `ngx_stream_keyval_module` | stream | 需要 | 1.13.7+ | + +### 核心特性 + +1. **动态更新**:通过 API 实时增删改查键值对 +2. **内存存储**:使用共享内存,高性能访问 +3. **持久化支持**:支持状态文件持久化,重启后数据不丢失 +4. **过期机制**:支持键值对自动过期(TTL) +5. **集群同步**:支持多节点数据同步 +6. **灵活匹配**:支持精确匹配、IP 子网匹配、前缀匹配 + +--- + +## 2. HTTP 与 Stream Keyval 模块对比 + +| 特性 | HTTP Keyval | Stream Keyval | +|------|-------------|---------------| +| **上下文** | `http` | `stream` | +| **模块名** | `ngx_http_keyval_module` | `ngx_stream_keyval_module` | +| **可用版本** | 1.13.3+ | 1.13.7+ | +| **键变量** | HTTP 变量(`$arg_*`, `$host`, `$uri` 等) | Stream 变量(`$ssl_server_name`, `$remote_addr` 等) | +| **使用场景** | HTTP 请求路由、限流白名单、动态配置 | TCP/UDP 四层代理路由、SSL 分流 | +| **API 管理** | `/api/{version}/http/keyvals/` | `/api/{version}/stream/keyvals/` | + +### HTTP Keyval 典型应用 + +```nginx +http { + # 根据请求参数动态返回值 + keyval $arg_user $user_data zone=user_db; + + server { + location / { + # $user_data 的值由 API 动态管理 + proxy_pass http://backend_$user_data; + } + } +} +``` + +### Stream Keyval 典型应用 + +```nginx +stream { + # 根据 SSL SNI 名称路由到不同后端 + keyval $ssl_server_name $backend zone=ssl_routes; + + server { + listen 443 ssl; + proxy_pass $backend; + ssl_certificate ...; + ssl_certificate_key ...; + } +} +``` + +--- + +## 3. HTTP Keyval 模块 + +### 3.1 指令详解 + +#### keyval_zone + +定义存储键值对数据库的共享内存区域。 + +| 属性 | 说明 | +|------|------| +| **语法** | `keyval_zone zone=name:size [state=file] [timeout=time] [type=string\|ip\|prefix] [sync];` | +| **默认值** | — | +| **上下文** | http | + +**参数说明**: + +| 参数 | 说明 | +|------|------| +| `zone=name:size` | 共享内存区域名称和大小(如 `one:32k`) | +| `state=file` | 状态文件路径,JSON 格式持久化 | +| `timeout=time` | 键值对过期时间(1.15.0+) | +| `type` | 匹配类型:`string`(精确,默认)、`ip`(IP/CIDR)、`prefix`(前缀,1.17.5+) | +| `sync` | 启用集群同步(1.15.0+,需配合 `timeout`) | + +**配置示例**: + +```nginx +http { + # 基本配置 + keyval_zone zone=one:32k; + + # 带持久化 + keyval_zone zone=two:64k state=/var/lib/nginx/state/two.keyval; + + # 带过期时间和同步 + keyval_zone zone=three:1m state=/var/lib/nginx/state/three.keyval timeout=1h sync; + + # IP 类型(用于 IP 黑白名单) + keyval_zone zone=ip_list:256k type=ip; + + # 前缀类型(用于路由前缀匹配) + keyval_zone zone=prefix_routes:128k type=prefix; +} +``` + +#### keyval + +创建变量,其值通过键值数据库查找获得。 + +| 属性 | 说明 | +|------|------| +| **语法** | `keyval key $variable zone=name;` | +| **默认值** | — | +| **上下文** | http | + +**参数说明**: + +| 参数 | 说明 | +|------|------| +| `key` | 查找键,可使用 NGINX 变量(如 `$arg_user`, `$uri`) | +| `$variable` | 创建的变量名,存储查找到的值 | +| `zone=name` | 指定查询的共享内存区域 | + +**配置示例**: + +```nginx +http { + keyval_zone zone=users:64k state=/var/lib/nginx/state/users.keyval; + + # 根据 URL 参数查找 + keyval $arg_user_id $user_role zone=users; + + # 根据 Host 查找 + keyval $host $backend_pool zone=backend_map; + + # 根据 URI 查找 + keyval $uri $cache_ttl zone=ttl_config; + + server { + listen 80; + + location / { + # 使用查找到的值 + proxy_pass http://$user_role; + } + } +} +``` + +### 3.2 匹配类型详解 + +#### string(精确匹配,默认) + +```nginx +http { + keyval_zone zone=exact:32k type=string; + keyval $arg_key $value zone=exact; +} +``` + +- 查找键必须与存储键完全相同 +- 适用于:用户角色映射、配置项查找 + +#### ip(IP/CIDR 匹配,1.17.1+) + +```nginx +http { + keyval_zone zone=ip_allow:256k type=ip; + keyval $remote_addr $allowed zone=ip_allow; + + server { + listen 80; + + location /api { + if ($allowed = "") { + return 403 "Access denied"; + } + proxy_pass http://api_backend; + } + } +} +``` + +- 存储键可以是 IPv4/IPv6 地址或 CIDR +- 查找时匹配包含该地址的子网 +- 适用于:IP 黑白名单、地理位置限流 + +**IP 类型数据示例**: + +```json +{ + "192.168.1.0/24": "allow", + "10.0.0.0/8": "allow", + "192.168.1.100": "admin" +} +``` + +#### prefix(前缀匹配,1.17.5+) + +```nginx +http { + keyval_zone zone=routes:64k type=prefix; + keyval $uri $handler zone=routes; + + server { + location / { + # /api/v1/users 匹配键 /api/v1 + proxy_pass http://$handler; + } + } +} +``` + +- 存储键必须是查找键的前缀 +- 适用于:路由前缀匹配、版本控制 + +**前缀类型数据示例**: + +```json +{ + "/api/v1": "backend_v1", + "/api/v2": "backend_v2", + "/admin": "admin_backend" +} +``` + +--- + +## 4. Stream Keyval 模块 + +### 4.1 指令详解 + +#### keyval_zone(Stream) + +| 属性 | 说明 | +|------|------| +| **语法** | `keyval_zone zone=name:size [state=file] [timeout=time] [type=string\|ip\|prefix] [sync];` | +| **默认值** | — | +| **上下文** | stream | + +与 HTTP 版本的语法完全相同,仅在 `stream` 上下文中使用。 + +#### keyval(Stream) + +| 属性 | 说明 | +|------|------| +| **语法** | `keyval key $variable zone=name;` | +| **默认值** | — | +| **上下文** | stream | + +### 4.2 配置示例 + +#### SSL SNI 路由 + +```nginx +stream { + # 根据 SSL SNI 路由到不同后端 + keyval_zone zone=ssl_routes:64k state=/var/lib/nginx/state/ssl_routes.keyval; + keyval $ssl_server_name $backend zone=ssl_routes; + + server { + listen 443 ssl; + proxy_pass $backend; + + ssl_certificate /etc/nginx/certs/default.crt; + ssl_certificate_key /etc/nginx/certs/default.key; + } +} +``` + +#### 动态 TCP 代理 + +```nginx +stream { + # 根据目标端口路由 + keyval_zone zone=port_map:32k type=string; + keyval $server_port $upstream zone=port_map; + + server { + listen 10000-20000; + proxy_pass $upstream; + } +} +``` + +#### 客户端 IP 限流 + +```nginx +stream { + # IP 黑名单 + keyval_zone zone=blocked_ips:256k type=ip; + keyval $remote_addr $is_blocked zone=blocked_ips; + + server { + listen 3306; + + # 通过 njs 或 map 实现阻断 + # 注意:stream 需要配合其他模块实现拒绝逻辑 + proxy_pass mysql_backend; + } +} +``` + +--- + +## 5. API 管理接口 + +### 5.1 HTTP Keyvals API + +#### 端点概览 + +| 端点 | 方法 | 功能 | +|------|------|------| +| `/api/{version}/http/keyvals/` | GET | 列出所有 HTTP keyval zones | +| `/api/{version}/http/keyvals/{zone}` | GET | 查询指定 zone 的键值对 | +| `/api/{version}/http/keyvals/{zone}` | POST | 添加键值对 | +| `/api/{version}/http/keyvals/{zone}` | PATCH | 修改或删除单个键 | +| `/api/{version}/http/keyvals/{zone}` | DELETE | 清空整个 zone | + +> **注意**:API 版本当前为 `9` + +#### 查询键值对 + +```bash +# 获取所有键值对 +curl http://localhost:8080/api/9/http/keyvals/one + +# 获取特定键 +curl http://localhost:8080/api/9/http/keyvals/one?key=user1 +``` + +**响应示例**: + +```json +{ + "user1": "backend_a", + "user2": "backend_b", + "user3": { + "value": "backend_c", + "expire": 1699123456789 + } +} +``` + +#### 添加键值对(POST) + +```bash +# 添加单个键值对 +curl -X POST http://localhost:8080/api/9/http/keyvals/one \ + -H "Content-Type: application/json" \ + -d '{"user4": "backend_d"}' + +# 添加带过期时间的键值对(毫秒) +curl -X POST http://localhost:8080/api/9/http/keyvals/one \ + -H "Content-Type: application/json" \ + -d '{ + "user5": { + "value": "backend_e", + "expire": 3600000 + } + }' +``` + +**状态码**: +- `201` - 创建成功 +- `409` - 键已存在 +- `400` - 格式错误 +- `413` - 请求体过大(超过 `client_body_buffer_size`) + +#### 修改/删除键值对(PATCH) + +```bash +# 修改键值 +curl -X PATCH http://localhost:8080/api/9/http/keyvals/one \ + -H "Content-Type: application/json" \ + -d '{"user1": {"value": "new_backend", "expire": 7200000}}' + +# 删除键(设置为 null) +curl -X PATCH http://localhost:8080/api/9/http/keyvals/one \ + -H "Content-Type: application/json" \ + -d '{"user1": null}' +``` + +**注意**:PATCH 一次只能更新一个键 + +#### 清空 Zone(DELETE) + +```bash +# 删除 zone 中所有键值对 +curl -X DELETE http://localhost:8080/api/9/http/keyvals/one +``` + +### 5.2 Stream Keyvals API + +Stream keyval 使用相同的 API 格式,只是端点路径不同: + +| 端点 | 方法 | 功能 | +|------|------|------| +| `/api/{version}/stream/keyvals/` | GET | 列出所有 Stream keyval zones | +| `/api/{version}/stream/keyvals/{zone}` | GET | 查询指定 zone | +| `/api/{version}/stream/keyvals/{zone}` | POST | 添加键值对 | +| `/api/{version}/stream/keyvals/{zone}` | PATCH | 修改或删除键 | +| `/api/{version}/stream/keyvals/{zone}` | DELETE | 清空 zone | + +**使用示例**: + +```bash +# 添加 SSL 路由 +curl -X POST http://localhost:8080/api/9/stream/keyvals/ssl_routes \ + -H "Content-Type: application/json" \ + -d '{"api.example.com": "192.168.1.10:8443"}' + +# 查询 +curl http://localhost:8080/api/9/stream/keyvals/ssl_routes +``` + +### 5.3 完整 API 配置示例 + +```nginx +http { + # 定义 keyval zones + keyval_zone zone=users:64k state=/var/lib/nginx/state/users.keyval timeout=1h; + keyval_zone zone=routes:32k state=/var/lib/nginx/state/routes.keyval; + keyval_zone zone=ip_blacklist:256k type=ip; + + # 使用 keyval + keyval $arg_user $user_role zone=users; + keyval $uri $backend zone=routes; + keyval $remote_addr $blocked zone=ip_blacklist; + + # API 服务配置 + server { + listen 8080; + server_name localhost; + + location /api { + # 启用 API 写权限 + api write=on; + + # 安全限制 + allow 127.0.0.1; + allow 10.0.0.0/8; + deny all; + } + } + + # 业务服务 + server { + listen 80; + server_name app.example.com; + + location / { + # IP 黑名单检查 + if ($blocked = "blocked") { + return 403 "IP blocked"; + } + + # 动态路由 + proxy_pass http://$backend; + } + } +} + +stream { + keyval_zone zone=ssl_routes:64k state=/var/lib/nginx/state/ssl_routes.keyval; + keyval $ssl_server_name $backend zone=ssl_routes; + + server { + listen 443 ssl; + proxy_pass $backend; + + ssl_certificate /etc/nginx/certs/default.crt; + ssl_certificate_key /etc/nginx/certs/default.key; + } +} +``` + +--- + +## 6. 使用场景详解 + +### 6.1 动态 IP 黑名单 + +```nginx +http { + # IP 类型 zone,支持 CIDR + keyval_zone zone=blacklist:1m type=ip state=/var/lib/nginx/state/blacklist.keyval; + keyval $remote_addr $blocked zone=blacklist; + + server { + listen 80; + + location / { + # 检查是否在黑名单 + if ($blocked = "blocked") { + return 403 "Access denied"; + } + + proxy_pass http://backend; + } + } +} +``` + +**API 操作**: + +```bash +# 封禁单个 IP +curl -X POST http://localhost:8080/api/9/http/keyvals/blacklist \ + -d '{"192.168.1.100": "blocked"}' + +# 封禁 IP 段 +curl -X POST http://localhost:8080/api/9/http/keyvals/blacklist \ + -d '{"10.0.0.0/8": "blocked"}' + +# 解封 +curl -X PATCH http://localhost:8080/api/9/http/keyvals/blacklist \ + -d '{"192.168.1.100": null}' +``` + +### 6.2 限流白名单 + +```nginx +http { + # 限流白名单(这些 IP 不限流) + keyval_zone zone=rate_whitelist:256k type=ip; + keyval $remote_addr $rate_whitelisted zone=rate_whitelist; + + # 定义限流区域 + limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; + + server { + listen 80; + + location /api { + # 白名单跳过限流 + if ($rate_whitelisted = "whitelisted") { + proxy_pass http://api_backend; + break; + } + + limit_req zone=api_limit burst=20 nodelay; + proxy_pass http://api_backend; + } + } +} +``` + +### 6.3 动态路由映射 + +```nginx +http { + keyval_zone zone=routes:64k type=prefix state=/var/lib/nginx/state/routes.keyval; + keyval $uri $backend zone=routes; + + upstream backend_a { + server 192.168.1.10:8080; + } + + upstream backend_b { + server 192.168.1.11:8080; + } + + server { + listen 80; + + location / { + # 默认后端 + if ($backend = "") { + set $backend "backend_a"; + } + + proxy_pass http://$backend; + } + } +} +``` + +**API 操作**: + +```bash +# 设置路由规则 +curl -X POST http://localhost:8080/api/9/http/keyvals/routes \ + -d '{ + "/api/v1": "backend_a", + "/api/v2": "backend_b", + "/admin": "backend_a" + }' +``` + +### 6.4 A/B 测试动态分配 + +```nginx +http { + keyval_zone zone=ab_test:32k state=/var/lib/nginx/state/ab_test.keyval; + keyval $cookie_userid $ab_group zone=ab_test; + + upstream version_a { + server 192.168.1.10:8080; + } + + upstream version_b { + server 192.168.1.11:8080; + } + + server { + listen 80; + + location / { + # 未分配或指定为 a 的用户 + if ($ab_group = "a") { + proxy_pass http://version_a; + break; + } + + # 指定为 b 的用户 + if ($ab_group = "b") { + proxy_pass http://version_b; + break; + } + + # 新用户默认走版本 A + proxy_pass http://version_a; + } + } +} +``` + +### 6.5 SSL 证书动态路由(Stream) + +```nginx +stream { + # 域名到后端的映射 + keyval_zone zone=ssl_routes:64k state=/var/lib/nginx/state/ssl_routes.keyval; + keyval $ssl_server_name $backend zone=ssl_routes; + + server { + listen 443 ssl; + + # 默认证书 + ssl_certificate /etc/nginx/certs/default.crt; + ssl_certificate_key /etc/nginx/certs/default.key; + + proxy_pass $backend; + } +} + +http { + # API 管理 Stream keyval + server { + listen 8080; + + location /api { + api write=on; + } + } +} +``` + +--- + +## 7. 与 Lolly 项目的关系和建议 + +### 7.1 项目对比 + +[Lolly](https://github.com/xfy/lolly) 是一个使用 Go 语言编写的高性能 HTTP 服务器与反向代理。与 NGINX Keyval 模块相比: + +| 特性 | NGINX Keyval | Lolly | +|------|--------------|-------| +| **动态配置** | 通过 API 实时更新 | 通过配置热重载(HUP 信号) | +| **键值存储** | 共享内存,API 管理 | 配置文件/YAML | +| **持久化** | state 文件自动持久化 | 配置文件持久化 | +| **匹配类型** | string、ip、prefix | 需自定义实现 | +| **商业限制** | NGINX Plus 专有功能 | 开源免费 | +| **集群同步** | 内置支持 | 需自行实现 | + +### 7.2 Lolly 的动态配置建议 + +虽然 Lolly 当前不支持类似 Keyval 的动态键值存储,但可以通过以下方式实现类似功能: + +#### 方案 1:配置热重载 + +Lolly 已支持 SIGHUP 信号触发配置重载: + +```go +// 在配置中定义映射表 +proxy: + - path: "/api" + dynamic_routes: + - key: "user1" + target: "http://backend1:8080" + - key: "user2" + target: "http://backend2:8080" +``` + +修改配置后执行: + +```bash +kill -HUP +``` + +#### 方案 2:自定义中间件实现 + +可以在 Lolly 中实现类似 Keyval 的动态路由中间件: + +```go +// internal/middleware/keyval/keyval.go +package keyval + +import ( + "sync" + "time" +) + +type KeyvalStore struct { + mu sync.RWMutex + data map[string]Entry + file string +} + +type Entry struct { + Value string + Expires time.Time +} + +func (s *KeyvalStore) Get(key string) (string, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + + entry, ok := s.data[key] + if !ok { + return "", false + } + + if !entry.Expires.IsZero() && time.Now().After(entry.Expires) { + return "", false + } + + return entry.Value, true +} + +func (s *KeyvalStore) Set(key, value string, ttl time.Duration) { + s.mu.Lock() + defer s.mu.Unlock() + + entry := Entry{Value: value} + if ttl > 0 { + entry.Expires = time.Now().Add(ttl) + } + + s.data[key] = entry +} +``` + +#### 方案 3:外部存储集成 + +建议 Lolly 集成外部键值存储: + +- **Redis**:支持过期、持久化、集群 +- **etcd**:支持监听变更、服务发现 +- **Consul**:服务发现与健康检查 + +```go +// Redis 集成示例 +import "github.com/redis/go-redis/v9" + +type RedisKeyval struct { + client *redis.Client +} + +func (r *RedisKeyval) Get(ctx context.Context, key string) (string, error) { + return r.client.Get(ctx, key).Result() +} + +func (r *RedisKeyval) Set(ctx context.Context, key, value string, ttl time.Duration) error { + return r.client.Set(ctx, key, value, ttl).Err() +} +``` + +### 7.3 Lolly 潜在增强功能 + +参考 NGINX Keyval 模块,建议 Lolly 可考虑实现: + +1. **内置动态键值 API** + - RESTful API 管理键值对 + - 支持内存存储与持久化 + - TTL 过期机制 + +2. **多种匹配类型** + - 精确匹配 + - IP/CIDR 匹配(用于黑白名单) + - 前缀匹配(用于路由) + +3. **集群同步** + - 基于 Raft 或 gossip 协议 + - 配置变更广播 + +4. **NGINX Plus 兼容 API** + - 兼容 `/api/{version}/http/keyvals/` 接口 + - 便于迁移现有 NGINX Plus 用户 + +--- + +## 8. 最佳实践 + +### 8.1 内存大小规划 + +```nginx +# 估算每个键值对大小 +# key: 平均 50 字节 +# value: 平均 100 字节 +# overhead: 约 50 字节 +# 总计: 约 200 字节/键值对 + +# 10万键值对约需 20MB +keyval_zone zone=large:20m; + +# 设置合理超时,避免内存无限增长 +keyval_zone zone=with_ttl:32k timeout=1h; +``` + +### 8.2 持久化策略 + +```nginx +# 重要数据启用持久化 +keyval_zone zone=critical:64k state=/var/lib/nginx/state/critical.keyval; + +# 临时数据不持久化 +keyval_zone zone=temp:32k timeout=5m; +``` + +### 8.3 安全建议 + +```nginx +http { + # API 严格访问控制 + server { + listen 8080; + + location /api { + api write=on; + + # 只允许特定 IP + allow 127.0.0.1; + allow 10.0.0.0/8; + deny all; + + # 可选:添加基础认证 + auth_basic "API Access"; + auth_basic_user_file /etc/nginx/api.htpasswd; + } + } +} +``` + +### 8.4 监控与日志 + +```nginx +http { + # 记录 keyval 相关请求 + log_format keyval '$remote_addr - $time_local ' + 'keyval_zone=$keyval_zone key=$key val=$value'; + + server { + location / { + # 添加自定义头便于调试 + add_header X-Keyval-Value $user_role; + + proxy_pass http://backend; + } + } +} +``` + +### 8.5 集群部署注意事项 + +启用 `sync` 时的限制: + +```nginx +http { + # sync 需要 timeout 参数 + keyval_zone zone=shared:64k timeout=1h sync; + + # 注意: + # 1. DELETE 操作只在目标节点立即生效 + # 2. 其他节点需等待 timeout 过期 + # 3. PATCH 删除(设为 null)同样受此限制 +} +``` + +--- + +## 9. 完整配置示例 + +### 综合应用场景 + +```nginx +# nginx.conf + +user nginx; +worker_processes auto; + +events { + worker_connections 4096; +} + +http { + # ========== Keyval Zones ========== + + # 用户角色映射(持久化) + keyval_zone zone=user_roles:64k state=/var/lib/nginx/state/user_roles.keyval; + keyval $arg_user $user_role zone=user_roles; + + # IP 黑名单(IP 类型,支持 CIDR) + keyval_zone zone=ip_blacklist:1m type=ip state=/var/lib/nginx/state/blacklist.keyval; + keyval $remote_addr $blocked zone=ip_blacklist; + + # 限流白名单 + keyval_zone zone=rate_whitelist:512k type=ip; + keyval $remote_addr $whitelisted zone=rate_whitelist; + + # 路由映射(前缀匹配) + keyval_zone zone=routes:128k type=prefix state=/var/lib/nginx/state/routes.keyval; + keyval $uri $backend zone=routes; + + # 临时会话(5分钟过期) + keyval_zone zone=sessions:32k timeout=5m; + keyval $cookie_session $session_data zone=sessions; + + # ========== Rate Limit ========== + limit_req_zone $binary_remote_addr zone=general:10m rate=100r/s; + + # ========== Upstreams ========== + upstream backend_admin { + server 192.168.1.10:8080; + } + + upstream backend_api { + server 192.168.1.11:8080; + server 192.168.1.12:8080; + } + + upstream backend_default { + server 192.168.1.20:8080; + } + + # ========== API Server ========== + server { + listen 127.0.0.1:8080; + + location /api { + api write=on; + allow 127.0.0.1; + allow 10.0.0.0/8; + deny all; + } + } + + # ========== Main Server ========== + server { + listen 80; + server_name app.example.com; + + # 安全头 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + + location / { + # 1. 检查 IP 黑名单 + if ($blocked = "blocked") { + return 403 "Access denied"; + } + + # 2. 限流(白名单除外) + if ($whitelisted = "") { + limit_req zone=general burst=200 nodelay; + } + + # 3. 根据路由映射选择后端 + if ($backend = "") { + set $backend "default"; + } + + # 4. 根据用户角色选择后端 + if ($user_role = "admin") { + set $backend "admin"; + } + + # 5. 代理到对应后端 + proxy_pass http://backend_$backend; + + # 传递 keyval 信息到后端 + proxy_set_header X-User-Role $user_role; + proxy_set_header X-Session-Data $session_data; + } + } +} + +stream { + # SSL 证书动态路由 + keyval_zone zone=ssl_routes:64k state=/var/lib/nginx/state/ssl_routes.keyval; + keyval $ssl_server_name $backend zone=ssl_routes; + + server { + listen 443 ssl; + + ssl_certificate /etc/nginx/certs/default.crt; + ssl_certificate_key /etc/nginx/certs/default.key; + + proxy_pass $backend; + proxy_ssl on; + } +} +``` + +--- + +## 10. 常见问题 + +### Q1: Keyval 数据在重启后会丢失吗? + +**A**: 如果配置了 `state` 参数,数据会持久化到 JSON 文件,重启后自动加载。未配置 `state` 的数据会丢失。 + +### Q2: 如何备份 Keyval 数据? + +**A**: 直接备份 state 文件即可: + +```bash +cp /var/lib/nginx/state/*.keyval /backup/ +``` + +### Q3: 集群同步有什么限制? + +**A**: 启用 `sync` 后: +- 添加操作会同步到所有节点 +- 删除操作只在目标节点立即生效,其他节点需等待 `timeout` 过期 + +### Q4: 可以手动编辑 state 文件吗? + +**A**: 不建议。state 文件由 NGINX 自动管理,手动编辑可能导致数据损坏。 + +### Q5: 与 map 模块有什么区别? + +**A**: +- `map` 是静态配置,重启生效 +- `keyval` 是动态存储,API 实时更新 + +### Q6: Stream 模块可以使用哪些变量作为 key? + +**A**: Stream 上下文的可用变量包括: +- `$remote_addr` - 客户端地址 +- `$remote_port` - 客户端端口 +- `$server_addr` - 服务器地址 +- `$server_port` - 服务器端口 +- `$ssl_server_name` - SSL SNI 名称 +- `$ssl_session_id` - SSL 会话 ID + +--- + +## 参考链接 + +- [NGINX HTTP Keyval 模块官方文档](https://nginx.org/en/docs/http/ngx_http_keyval_module.html) +- [NGINX Stream Keyval 模块官方文档](https://nginx.org/en/docs/stream/ngx_stream_keyval_module.html) +- [NGINX HTTP API 模块官方文档](https://nginx.org/en/docs/http/ngx_http_api_module.html) +- [Lolly 项目 GitHub](https://github.com/xfy/lolly) diff --git a/docs/36-nginx-streaming-media.md b/docs/36-nginx-streaming-media.md new file mode 100644 index 0000000..e375b76 --- /dev/null +++ b/docs/36-nginx-streaming-media.md @@ -0,0 +1,904 @@ +# NGINX 流媒体模块指南 + +## 1. 模块概述 + +NGINX 提供多个模块支持 HTTP 流媒体服务,涵盖直播、点播和伪流媒体场景。 + +### 1.1 模块对比 + +| 模块 | 协议/格式 | 用途 | 可用性 | +|------|-----------|------|--------| +| `ngx_http_hls_module` | HLS (HTTP Live Streaming) | MP4/MOV 文件的 HLS 直播流 | NGINX Plus 商业版 | +| `ngx_http_flv_module` | FLV (Flash Video) | FLV 文件伪流媒体 | 开源版 | +| `ngx_http_mp4_module` | MP4 (H.264/AAC) | MP4 文件伪流媒体 | 开源版 | +| `ngx_http_f4f_module` | F4F/F4M (Adobe HDS) | Adobe HTTP Dynamic Streaming | NGINX Plus 商业版 | + +### 1.2 伪流媒体 vs 直播流 + +**伪流媒体 (Pseudo-Streaming)**: +- 客户端通过 `start` 参数请求特定时间点 +- 服务器从该时间点开始发送视频数据 +- 适用于点播场景,支持随机 seek + +**直播流 (Live Streaming)**: +- 实时生成媒体片段 (TS/F4F) +- 动态更新播放列表 (M3U8/F4M) +- 支持 HLS、HDS 等自适应码率协议 + +--- + +## 2. HLS 模块 (ngx_http_hls_module) + +为 MP4 和 MOV 媒体文件提供 HTTP Live Streaming (HLS) 服务器端支持。 + +### 2.1 编译配置 + +```bash +# 商业订阅版本已包含此模块 +# 无需额外编译参数 +``` + +### 2.2 指令详解 + +#### hls + +**语法**:`hls;` + +**默认值**:无 + +**上下文**:`location` + +**说明**:在 surrounding location 中开启 HLS 流媒体服务。 + +```nginx +location / { + hls; +} +``` + +#### hls_fragment + +**语法**:`hls_fragment time;` + +**默认值**:`hls_fragment 5s;` + +**上下文**:`http`, `server`, `location` + +**说明**:为未带 `len` 参数请求的播放列表 URI 定义默认片段长度。 + +```nginx +hls_fragment 10s; # 每个 TS 片段 10 秒 +``` + +#### hls_buffers + +**语法**:`hls_buffers number size;` + +**默认值**:`hls_buffers 8 2m;` + +**上下文**:`http`, `server`, `location` + +**说明**:设置用于读写数据帧的最大缓冲区数量和大小。 + +```nginx +hls_buffers 10 10m; # 10 个缓冲区,每个 10MB +``` + +#### hls_forward_args + +**语法**:`hls_forward_args on | off;` + +**默认值**:`hls_forward_args off;` + +**上下文**:`http`, `server`, `location` + +**说明**:将播放列表请求中的参数添加到片段 (fragment) 的 URI 中。 + +**用途**: +- 客户端授权 +- 配合 `ngx_http_secure_link_module` 保护 HLS 流 + +```nginx +hls_forward_args on; +``` + +#### hls_mp4_buffer_size + +**语法**:`hls_mp4_buffer_size size;` + +**默认值**:`hls_mp4_buffer_size 512k;` + +**上下文**:`http`, `server`, `location` + +**说明**:设置用于处理 MP4 和 MOV 文件的初始缓冲区大小。 + +```nginx +hls_mp4_buffer_size 1m; +``` + +#### hls_mp4_max_buffer_size + +**语法**:`hls_mp4_max_buffer_size size;` + +**默认值**:`hls_mp4_max_buffer_size 10m;` + +**上下文**:`http`, `server`, `location` + +**说明**:在元数据处理期间,缓冲区最大不能超过此值,否则返回 500 错误。 + +**错误日志**:`"mp4 moov atom is too large"` + +```nginx +hls_mp4_max_buffer_size 5m; +``` + +### 2.3 请求参数 + +HLS 播放列表支持以下 URI 参数: + +| 参数 | 说明 | 示例 | +|------|------|------| +| `start` | 起始时间(秒) | `?start=1.000` | +| `end` | 结束时间(秒) | `?end=2.200` | +| `offset` | 偏移时间(秒) | `?offset=1.000` | +| `len` | 片段长度(秒) | `?len=10` | + +### 2.4 配置示例 + +#### 基本 HLS 配置 + +```nginx +server { + listen 80; + server_name hls.example.com; + + location / { + hls; + hls_fragment 5s; + hls_buffers 10 10m; + hls_mp4_buffer_size 1m; + hls_mp4_max_buffer_size 5m; + root /var/video/; + } +} +``` + +#### 安全链接配置 + +配合 `hls_forward_args on` 使用 secure_link: + +```nginx +http { + # 提取基础 URI(去掉 .m3u8 和 .ts 后缀) + map $uri $hls_uri { + ~^(?.*)\.m3u8$ $base_uri; + ~^(?.*)\.ts$ $base_uri; + default $uri; + } + + server { + listen 80; + server_name secure-hls.example.com; + + location /hls/ { + hls; + hls_forward_args on; + alias /var/videos/; + + # 安全链接验证 + secure_link $arg_md5,$arg_expires; + secure_link_md5 "$secure_link_expires$hls_uri$remote_addr secret"; + + if ($secure_link = "") { return 403; } + if ($secure_link = "0") { return 410; } + } + } +} +``` + +### 2.5 请求 URI 示例 + +对于文件 `/var/video/test.mp4`: + +| 类型 | URI 示例 | +|------|----------| +| 播放列表 | `http://hls.example.com/test.mp4.m3u8?offset=1.000&start=1.000&end=2.200` | +| 片段 | `http://hls.example.com/test.mp4.ts?start=1.000&end=2.200` | + +--- + +## 3. FLV 模块 (ngx_http_flv_module) + +为 Flash Video (FLV) 文件提供伪流媒体服务器端支持。 + +### 3.1 编译配置 + +```bash +# 此模块默认不构建,需要显式启用 +./configure --with-http_flv_module ... +``` + +### 3.2 指令详解 + +#### flv + +**语法**:`flv;` + +**默认值**:无 + +**上下文**:`location` + +**说明**:在 surrounding location 中开启 FLV 模块处理。 + +**行为**: +- 特殊处理包含 `start` 参数的请求 +- 从请求的字节偏移量发送文件内容 +- 自动前置 FLV 头 + +```nginx +location ~ \.flv$ { + flv; +} +``` + +### 3.3 请求参数 + +| 参数 | 说明 | 示例 | +|------|------|------| +| `start` | 起始字节偏移量 | `?start=1000` | + +### 3.4 配置示例 + +```nginx +server { + listen 80; + server_name video.example.com; + + location /videos/ { + root /var/www/; + } + + location ~ \.flv$ { + flv; + root /var/www/videos/; + } +} +``` + +--- + +## 4. MP4 模块 (ngx_http_mp4_module) + +为 MP4 文件提供服务器端伪流媒体支持,允许通过 `start` 和 `end` 参数进行随机 seek。 + +### 4.1 编译配置 + +```bash +# 此模块默认不构建,需要显式启用 +./configure --with-http_mp4_module ... +``` + +### 4.2 指令详解 + +#### mp4 + +**语法**:`mp4;` + +**默认值**:无 + +**上下文**:`location` + +**说明**:在 surrounding location 中开启 MP4 模块处理。 + +```nginx +location /video/ { + mp4; +} +``` + +#### mp4_buffer_size + +**语法**:`mp4_buffer_size size;` + +**默认值**:`mp4_buffer_size 512K;` + +**上下文**:`http`, `server`, `location` + +**说明**:设置用于处理 MP4 文件的初始缓冲区大小。 + +```nginx +mp4_buffer_size 1m; +``` + +#### mp4_max_buffer_size + +**语法**:`mp4_max_buffer_size size;` + +**默认值**:`mp4_max_buffer_size 10M;` + +**上下文**:`http`, `server`, `location` + +**说明**:元数据处理期间缓冲区的最大大小。若 moov atom 过大,返回 500 错误。 + +```nginx +mp4_max_buffer_size 5m; +``` + +#### mp4_limit_rate + +**语法**:`mp4_limit_rate on | off | factor;` + +**默认值**:`mp4_limit_rate off;` + +**上下文**:`http`, `server`, `location` + +**说明**:基于文件平均比特率限制响应传输速率。 + +**参数说明**: +- `on`:限速因子为 1.1 +- `factor`:自定义限速因子 + +**注意**:此指令仅适用于商业订阅版本。 + +```nginx +mp4_limit_rate on; # 因子 1.1 +mp4_limit_rate 1.5; # 自定义因子 1.5 +``` + +#### mp4_limit_rate_after + +**语法**:`mp4_limit_rate_after time;` + +**默认值**:`mp4_limit_rate_after 60s;` + +**上下文**:`http`, `server`, `location` + +**说明**:设置开始限速前的初始媒体数据播放时长。 + +**注意**:此指令仅适用于商业订阅版本。 + +```nginx +mp4_limit_rate_after 30s; +``` + +#### mp4_start_key_frame + +**语法**:`mp4_start_key_frame on | off;` + +**默认值**:`mp4_start_key_frame off;` + +**上下文**:`http`, `server`, `location` + +**说明**:强制输出视频从关键帧开始。 + +**行为**: +- 若 `start` 指定的位置非关键帧,使用 edit list 隐藏初始帧 +- 需要 NGINX 1.21.4+ +- 主流播放器(Chrome、Safari 等)支持 edit list + +```nginx +mp4_start_key_frame on; +``` + +### 4.3 请求参数 + +| 参数 | 说明 | 示例 | +|------|------|------| +| `start` | 起始时间(秒) | `?start=238.88` | +| `end` | 结束时间(秒) | `?end=555.55` | + +**组合示例**:`?start=238.88&end=555.55` + +### 4.4 配置示例 + +#### 基本 MP4 配置 + +```nginx +server { + listen 80; + server_name video.example.com; + + location /video/ { + mp4; + mp4_buffer_size 1m; + mp4_max_buffer_size 5m; + root /var/videos/; + } +} +``` + +#### 高级配置(商业版功能) + +```nginx +server { + listen 80; + server_name video.example.com; + + location /video/ { + mp4; + mp4_buffer_size 1m; + mp4_max_buffer_size 5m; + mp4_limit_rate on; + mp4_limit_rate_after 30s; + mp4_start_key_frame on; + root /var/videos/; + } +} +``` + +### 4.5 性能优化建议 + +**文件结构优化**: +- 若 moov atom(元数据)位于文件末尾,NGINX 需要读取整个文件 +- 建议使用工具(如 `qt-faststart`)将 moov atom 移到文件开头: + +```bash +# 使用 qt-faststart 优化 MP4 文件 +qt-faststart input.mp4 output.mp4 +``` + +--- + +## 5. F4F 模块 (ngx_http_f4f_module) + +提供 Adobe HTTP Dynamic Streaming (HDS) 的服务器端支持。 + +### 5.1 模块说明 + +**功能**: +- 处理 `/videoSeg1-Frag1` 形式的请求 +- 使用 `.f4x` 索引文件从 `.f4f` 文件中提取片段 + +**可用性**:仅作为 NGINX Plus 商业订阅的一部分提供。 + +### 5.2 指令详解 + +#### f4f + +**语法**:`f4f;` + +**默认值**:无 + +**上下文**:`location` + +**说明**:在 surrounding location 中开启 F4F 模块处理。 + +```nginx +location /video/ { + f4f; +} +``` + +#### f4f_buffer_size + +**语法**:`f4f_buffer_size size;` + +**默认值**:`f4f_buffer_size 512k;` + +**上下文**:`http`, `server`, `location` + +**说明**:设置用于读取 `.f4x` 索引文件的缓冲区大小。 + +```nginx +f4f_buffer_size 1m; +``` + +### 5.3 配置示例 + +```nginx +server { + listen 80; + server_name hds.example.com; + + location /video/ { + f4f; + f4f_buffer_size 1m; + root /var/f4f/; + } +} +``` + +--- + +## 6. 完整配置示例 + +### 6.1 综合流媒体服务器 + +```nginx +# 负载均衡后端(用于回源) +upstream media_backend { + server 192.168.1.10:8080 weight=5; + server 192.168.1.11:8080 weight=5; + server 192.168.1.12:8080 backup; +} + +# 限速配置 +limit_rate_after 1m; +limit_rate 1m; + +server { + listen 80; + server_name media.example.com; + + # 日志格式 + log_format media '$remote_addr - $remote_user [$time_local] ' + '"$request" $status $bytes_sent ' + '"$http_referer" "$http_user_agent" ' + 'time=$request_time'; + + access_log /var/log/nginx/media-access.log media; + + # HLS 流媒体(NGINX Plus) + location /hls/ { + hls; + hls_fragment 5s; + hls_buffers 10 10m; + hls_mp4_buffer_size 1m; + hls_mp4_max_buffer_size 5m; + alias /var/videos/hls/; + + # 可选:安全链接 + # hls_forward_args on; + # secure_link ... + } + + # FLV 伪流媒体 + location /flv/ { + location ~ \.flv$ { + flv; + alias /var/videos/flv/; + } + } + + # MP4 伪流媒体 + location /mp4/ { + location ~ \.mp4$ { + mp4; + mp4_buffer_size 1m; + mp4_max_buffer_size 5m; + alias /var/videos/mp4/; + + # NGINX Plus 功能 + # mp4_limit_rate on; + # mp4_limit_rate_after 30s; + # mp4_start_key_frame on; + } + } + + # F4F 流媒体(NGINX Plus) + location /hds/ { + f4f; + f4f_buffer_size 1m; + alias /var/videos/hds/; + } + + # 视频文件通用缓存配置 + location ~* \.(mp4|flv|f4f|ts|m3u8)$ { + expires 1d; + add_header Cache-Control "public, immutable"; + + # 跨域支持 + add_header Access-Control-Allow-Origin "*"; + add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS"; + } + + # 播放列表不缓存(实时更新) + location ~ \.m3u8$ { + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + } + + # 状态监控 + location /nginx_status { + stub_status on; + allow 127.0.0.1; + allow 10.0.0.0/8; + deny all; + } +} + +# HTTPS 配置 +server { + listen 443 ssl http2; + server_name media.example.com; + + ssl_certificate /etc/ssl/certs/media.crt; + ssl_certificate_key /etc/ssl/private/media.key; + ssl_protocols TLSv1.2 TLSv1.3; + + # 复用 HTTP 配置 + include /etc/nginx/conf.d/media-locations.conf; +} +``` + +### 6.2 带转码的流媒体配置 + +```nginx +# 使用 ngx_rtmp_module(第三方模块)做 RTMP 转 HLS +rtmp { + server { + listen 1935; + + application live { + live on; + + # 转 HLS + hls on; + hls_path /var/videos/hls/; + hls_fragment 5s; + hls_playlist_length 60s; + + # 多码率 + hls_variant _low BANDWIDTH=500000; + hls_variant _mid BANDWIDTH=1500000; + hls_variant _high BANDWIDTH=5000000; + } + } +} + +http { + server { + listen 80; + server_name live.example.com; + + # 服务 HLS 流 + location /hls/ { + types { + application/vnd.apple.mpegurl m3u8; + video/mp2t ts; + } + + alias /var/videos/hls/; + add_header Cache-Control "no-cache"; + add_header Access-Control-Allow-Origin "*"; + } + } +} +``` + +--- + +## 7. 与 Lolly 项目的关系和建议 + +### 7.1 当前状态对比 + +| 特性 | NGINX 流媒体模块 | Lolly 当前状态 | +|------|------------------|----------------| +| HLS 支持 | 完整(商业版) | 暂未实现 | +| FLV 支持 | 完整(开源版) | 暂未实现 | +| MP4 点播 | 完整(需编译) | 暂未实现 | +| F4F/HDS | 完整(商业版) | 暂未实现 | +| 静态文件 | 完整 | 支持 | +| 文件缓存 | 完整 | 支持 | + +### 7.2 实现建议 + +对于 Lolly 项目,可以考虑以下实现策略: + +#### 1. 伪流媒体实现(MP4/FLV) + +```go +// handler/streaming.go +package handler + +import ( + "github.com/valyala/fasthttp" +) + +// MP4Handler 处理 MP4 伪流媒体请求 +func MP4Handler(ctx *fasthttp.RequestCtx) { + start := ctx.QueryArgs().GetFloat64("start") + end := ctx.QueryArgs().GetFloat64("end") + + // 解析 MP4 moov atom,计算偏移量 + // 从指定时间点开始传输 + // 处理 end 参数截断 +} + +// FLVHandler 处理 FLV 伪流媒体请求 +func FLVHandler(ctx *fasthttp.RequestCtx) { + start := ctx.QueryArgs().GetInt("start") + + // 发送 FLV 头 + // 从指定字节偏移开始传输 +} +``` + +#### 2. HLS 服务实现 + +```go +// handler/hls.go +package handler + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// HLSPlaylistHandler 生成 M3U8 播放列表 +func HLSPlaylistHandler(ctx *fasthttp.RequestCtx) { + videoPath := getVideoPath(ctx) + + // 解析 MP4,计算片段 + segments := generateSegments(videoPath, fragmentDuration) + + // 生成 M3U8 内容 + playlist := generateM3U8(segments) + + ctx.SetContentType("application/vnd.apple.mpegurl") + ctx.WriteString(playlist) +} + +// HLSSegmentHandler 返回 TS 片段 +func HLSSegmentHandler(ctx *fasthttp.RequestCtx) { + // 从 MP4 提取指定时间范围的 TS 数据 + // 或使用预生成的 TS 文件 +} +``` + +#### 3. 配置扩展示例 + +```yaml +# lolly.yaml 流媒体配置示例 +server: + # 静态文件服务(已支持) + static: + - path: "/" + root: "/var/www/html" + + # 流媒体服务(建议新增) + streaming: + # HLS 配置 + hls: + enabled: true + path: "/hls/" + root: "/var/videos/" + fragment: "5s" + buffers: 10 + buffer_size: "10m" + mp4_buffer_size: "1m" + mp4_max_buffer_size: "5m" + + # MP4 伪流媒体 + mp4: + enabled: true + path: "/mp4/" + root: "/var/videos/" + buffer_size: "1m" + max_buffer_size: "5m" + + # FLV 伪流媒体 + flv: + enabled: true + path: "/flv/" + root: "/var/videos/" + + # 跨域配置 + cors: + enabled: true + origins: ["*"] + methods: ["GET", "HEAD", "OPTIONS"] +``` + +### 7.3 技术实现要点 + +#### MP4 文件处理 + +```go +// 关键:解析 moov atom,计算时间到字节的映射 +type MP4Parser struct { + Moov *MoovBox +} + +type MoovBox struct { + Tracks []*Track +} + +type Track struct { + Timescale uint32 + Samples []*Sample +} + +// SeekToTime 返回指定时间对应的文件偏移和样本索引 +func (p *MP4Parser) SeekToTime(seconds float64) (offset int64, sampleIdx int) { + // 遍历样本表,找到对应时间点的样本 + // 计算文件偏移 +} +``` + +#### HLS 切片生成 + +```go +// SegmentInfo 表示一个 TS 片段 +type SegmentInfo struct { + Sequence int + Duration float64 + StartTime float64 + EndTime float64 +} + +// GenerateSegments 将 MP4 切分为片段信息 +func GenerateSegments(videoPath string, fragmentDuration float64) ([]SegmentInfo, error) { + // 解析 MP4 结构 + // 按关键帧边界分割片段 + // 返回片段信息列表 +} +``` + +### 7.4 依赖库建议 + +| 功能 | 推荐库 | +|------|--------| +| MP4 解析 | `github.com/abema/go-mp4` 或 `github.com/deepch/mp4` | +| HLS 生成 | 自行实现或使用 `github.com/grafov/m3u8` | +| 视频转码 | `github.com/xfrr/goffmpeg` (FFmpeg 绑定) | +| FLV 解析 | `github.com/yapingcat/gomedia` | + +### 7.5 性能优化建议 + +1. **文件缓存**:复用现有文件缓存系统缓存解析后的 MP4 元数据 +2. **预生成切片**:对于点播内容,预先生成 TS 片段文件 +3. **零拷贝传输**:大视频文件使用 sendfile +4. **Goroutine 池**:控制并发转码任务数量 +5. **内存复用**:使用 sync.Pool 复用缓冲区 + +### 7.6 安全建议 + +1. **路径遍历防护**:验证请求路径,防止访问非视频目录 +2. **限速控制**:对视频流进行带宽限制 +3. **防盗链**:使用 secure_link 或 JWT token 验证 +4. **CORS 配置**:按需配置跨域访问 + +--- + +## 8. 常见问题 + +### Q1: HLS 播放列表不更新? + +**A**: 确保播放列表响应头禁用缓存: + +```nginx +location ~ \.m3u8$ { + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; +} +``` + +### Q2: MP4 seek 不准确? + +**A**: 启用 `mp4_start_key_frame on`(NGINX Plus 1.21.4+),或使用 edit list 隐藏非关键帧。 + +### Q3: FLV 无法 seek? + +**A**: FLV 需要播放器支持,确保播放器发送 `start` 参数。 + +### Q4: 大文件处理缓慢? + +**A**: 使用 `qt-faststart` 将 moov atom 移到文件开头: + +```bash +qt-faststart input.mp4 output.mp4 +``` + +### Q5: 跨域播放失败? + +**A**: 添加 CORS 响应头: + +```nginx +add_header Access-Control-Allow-Origin "*"; +add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS"; +``` + +--- + +## 9. 参考资源 + +- [NGINX HLS Module](https://nginx.org/en/docs/http/ngx_http_hls_module.html) +- [NGINX FLV Module](https://nginx.org/en/docs/http/ngx_http_flv_module.html) +- [NGINX MP4 Module](https://nginx.org/en/docs/http/ngx_http_mp4_module.html) +- [NGINX F4F Module](https://nginx.org/en/docs/http/ngx_http_f4f_module.html) +- [Apple HLS Specification](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis) +- [Adobe HDS Specification](https://www.adobe.com/devnet/hds.html) diff --git a/docs/37-nginx-dav-module.md b/docs/37-nginx-dav-module.md new file mode 100644 index 0000000..9b612c6 --- /dev/null +++ b/docs/37-nginx-dav-module.md @@ -0,0 +1,687 @@ +# Nginx WebDAV 模块完整指南 + +## 1. WebDAV 模块概述 + +### 1.1 模块简介 + +`ngx_http_dav_module` 是 Nginx 的文件管理自动化模块,通过 WebDAV (Web Distributed Authoring and Versioning) 协议支持远程文件操作。 + +### 1.2 主要用途 + +- 文件上传与管理 +- 远程文件编辑 +- 目录结构创建 +- 文件复制与移动 +- 简单的文件服务器 + +### 1.3 编译要求 + +该模块**不会默认构建**,需要在编译时显式启用: + +```bash +./configure --with-http_dav_module +``` + +### 1.4 支持的 HTTP 方法 + +| 方法 | 说明 | +|------|------| +| `PUT` | 上传/创建文件 | +| `DELETE` | 删除文件或目录 | +| `MKCOL` | 创建目录 (Make Collection) | +| `COPY` | 复制文件或目录 | +| `MOVE` | 移动文件或目录 | + +> **注意**:Nginx WebDAV 模块仅支持上述 5 种方法。需要其他 WebDAV 方法(如 PROPFIND、PROPPATCH、OPTIONS、LOCK、UNLOCK)的客户端将无法与此模块配合工作。 + +--- + +## 2. 指令详解 + +### 2.1 dav_methods + +启用指定的 HTTP 方法。 + +**语法:** +```nginx +dav_methods off | PUT | DELETE | MKCOL | COPY | MOVE ...; +``` + +**默认值:** `off` + +**上下文:** `http`, `server`, `location` + +**说明:** +- `off` - 禁止此模块的所有方法 +- 可以指定一个或多个方法 +- 未列出的方法将被禁止 + +**示例:** +```nginx +# 仅启用 PUT 和 DELETE +dav_methods PUT DELETE; + +# 启用全部支持的方法 +dav_methods PUT DELETE MKCOL COPY MOVE; + +# 禁用所有 WebDAV 方法 +dav_methods off; +``` + +--- + +### 2.2 create_full_put_path + +允许创建所有必需的中间目录。 + +**语法:** +```nginx +create_full_put_path on | off; +``` + +**默认值:** `off` + +**上下文:** `http`, `server`, `location` + +**说明:** +- WebDAV 规范通常要求目标目录必须已存在 +- 设置为 `on` 时,PUT 请求可以自动创建路径中的所有中间目录 +- 对于深度嵌套的文件上传非常有用 + +**示例:** +```nginx +# 允许 PUT /files/a/b/c/file.txt 自动创建 a/b/c/ 目录 +create_full_put_path on; +``` + +--- + +### 2.3 dav_access + +设置新创建的文件和目录的访问权限。 + +**语法:** +```nginx +dav_access users:permissions ...; +``` + +**默认值:** `user:rw` + +**上下文:** `http`, `server`, `location` + +**权限格式:** +``` +user:permissions # 文件所有者权限 +group:permissions # 组权限 +all:permissions # 所有用户权限 +``` + +**权限值:** +- `r` - 读 +- `w` - 写 +- `x` - 执行(目录为进入) + +**示例:** +```nginx +# 默认:用户可读写 +dav_access user:rw; + +# 用户读写,组读,其他用户只读 +dav_access user:rw group:r all:r; + +# 用户完全权限,组读执行,其他无权限 +dav_access user:rwx group:rx all:; + +# 多组权限 +dav_access group:rw all:r; +``` + +--- + +### 2.4 min_delete_depth + +设置 DELETE 操作允许删除的最小路径深度。 + +**语法:** +```nginx +min_delete_depth number; +``` + +**默认值:** `0` + +**上下文:** `http`, `server`, `location` + +**说明:** +- 用于防止意外删除重要目录 +- 路径深度计算以 `/` 分隔的元素数量 +- 请求 URI 的元素数量必须 >= 设定值才能执行 DELETE + +**示例:** +```nginx +# 至少需要 4 层深度才能删除 +min_delete_depth 4; + +# 允许删除:DELETE /users/00/00/name (4 层) +# 禁止删除:DELETE /users/00/00 (3 层) +``` + +--- + +## 3. 配置示例 + +### 3.1 基础文件共享服务器 + +```nginx +server { + listen 80; + server_name dav.example.com; + + location / { + # 根目录 + root /data/www; + + # 临时文件目录(与根目录同一文件系统以获得最佳性能) + client_body_temp_path /data/client_temp; + + # 启用 WebDAV 方法 + dav_methods PUT DELETE MKCOL COPY MOVE; + + # 允许创建中间目录 + create_full_put_path on; + + # 设置文件权限 + dav_access group:rw all:r; + + # 限制写操作的访问 + limit_except GET { + allow 192.168.1.0/24; + deny all; + } + } +} +``` + +### 3.2 只读文件服务器(带选择性上传) + +```nginx +server { + listen 80; + server_name files.example.com; + + # 公共只读区域 + location /public/ { + root /data/files; + dav_methods off; # 禁止写操作 + autoindex on; # 启用目录列表 + } + + # 受限上传区域 + location /uploads/ { + root /data/files; + dav_methods PUT DELETE; + dav_access user:rw; + + # 仅允许特定 IP 上传 + limit_except GET { + allow 10.0.0.0/8; + deny all; + } + } +} +``` + +### 3.3 支持深度目录的上传服务 + +```nginx +server { + listen 80; + server_name upload.example.com; + + location /storage/ { + root /data/storage; + + dav_methods PUT DELETE MKCOL; + create_full_put_path on; # 自动创建嵌套目录 + dav_access user:rw group:r; + + # 防止删除顶层目录 + min_delete_depth 3; + + # 限制请求体大小 + client_max_body_size 100M; + + # 限制写操作 + limit_except GET PUT { + allow 192.168.0.0/16; + deny all; + } + } +} +``` + +### 3.4 带认证的 WebDAV 服务 + +```nginx +server { + listen 80; + server_name secure-dav.example.com; + + location / { + root /data/secure; + + dav_methods PUT DELETE MKCOL COPY MOVE; + create_full_put_path on; + dav_access user:rw; + + # HTTP 基本认证 + auth_basic "WebDAV Access"; + auth_basic_user_file /etc/nginx/.dav_passwd; + + # 限制客户端大小 + client_max_body_size 500M; + } +} +``` + +生成密码文件: +```bash +htpasswd -c /etc/nginx/.dav_passwd username +``` + +### 3.5 带 SSL 的 WebDAV 服务器 + +```nginx +server { + listen 80; + server_name dav.example.com; + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl; + server_name dav.example.com; + + ssl_certificate /etc/nginx/ssl/dav.crt; + ssl_certificate_key /etc/nginx/ssl/dav.key; + ssl_protocols TLSv1.2 TLSv1.3; + + location / { + root /data/www; + + dav_methods PUT DELETE MKCOL COPY MOVE; + create_full_put_path on; + dav_access user:rw group:r all:r; + + client_max_body_size 1G; + } +} +``` + +--- + +## 4. 客户端使用指南 + +### 4.1 使用 curl + +```bash +# 上传文件(PUT) +curl -T localfile.txt http://dav.example.com/remotefile.txt + +# 创建目录(MKCOL) +curl -X MKCOL http://dav.example.com/newdir/ + +# 删除文件(DELETE) +curl -X DELETE http://dav.example.com/file.txt + +# 复制文件(COPY) +curl -X COPY -H "Destination: http://dav.example.com/copy.txt" \ + http://dav.example.com/original.txt + +# 移动文件(MOVE) +curl -X MOVE -H "Destination: http://dav.example.com/moved.txt" \ + http://dav.example.com/original.txt + +# 带认证上传 +curl -T file.txt -u username:password \ + http://dav.example.com/file.txt + +# 查看文件内容(GET) +curl http://dav.example.com/file.txt + +# 列出目录内容(需要客户端支持 PROPFIND) +curl -X PROPFIND http://dav.example.com/ +``` + +### 4.2 使用 cadaver(命令行 WebDAV 客户端) + +```bash +# 安装 +# Debian/Ubuntu +apt-get install cadaver +# macOS +brew install cadaver + +# 连接 +cadaver http://dav.example.com/ + +# cadaver 常用命令 +dav:> ls # 列出目录 +dav:> cd directory # 进入目录 +dav:> put file.txt # 上传文件 +dav:> get file.txt # 下载文件 +dav:> mkdir newdir # 创建目录 +dav:> rm file.txt # 删除文件 +dav:> mv old new # 移动/重命名 +dav:> quit # 退出 +``` + +### 4.3 使用 GNOME Nautilus(文件管理器) + +1. 打开 Nautilus 文件管理器 +2. 按 `Ctrl+L` 或选择"其他位置" +3. 输入地址:`dav://dav.example.com/` +4. 输入凭据(如需要) +5. 可像本地文件夹一样操作 + +### 4.4 挂载为本地文件系统(Linux) + +使用 davfs2: + +```bash +# 安装 +apt-get install davfs2 + +# 挂载 +mount -t davfs http://dav.example.com/ /mnt/webdav + +# 或使用 /etc/fstab 自动挂载 +http://dav.example.com/ /mnt/webdav davfs user,noauto 0 0 +``` + +### 4.5 Windows 资源管理器 + +1. 打开"此电脑" +2. 右键 -> "添加网络位置" +3. 输入:`http://dav.example.com/` +4. 完成向导 + +或使用 `net use`: +```cmd +net use * http://dav.example.com/ +``` + +### 4.6 macOS Finder + +1. Finder -> "前往" -> "连接服务器" +2. 输入:`http://dav.example.com/` +3. 点击"连接" + +--- + +## 5. 与 Lolly 项目的关系 + +### 5.1 Lolly 项目概述 + +Lolly 是一个用 Go 编写的高性能 HTTP 服务器/代理工具,提供: +- 灵活的路由和重写规则 +- 中间件支持 +- 反向代理功能 +- 性能分析工具(pprof) + +### 5.2 与 Nginx WebDAV 的对比 + +| 特性 | Nginx WebDAV | Lolly | +|------|--------------|-------| +| 协议支持 | WebDAV (有限) | 标准 HTTP | +| 配置方式 | 声明式配置 | Go 代码 | +| 扩展性 | 需重新编译 | 热插拔中间件 | +| 性能分析 | 第三方模块 | 内置 pprof | +| 学习曲线 | 低 | 中(需 Go 基础) | + +### 5.3 建议与集成方案 + +#### 方案一:Nginx + Lolly 组合架构 + +``` +客户端 -> Nginx (WebDAV/静态文件) -> Lolly (动态请求/代理) +``` + +```nginx +server { + listen 80; + server_name example.com; + + # WebDAV 文件存储 + location /dav/ { + root /data/storage; + dav_methods PUT DELETE MKCOL; + create_full_put_path on; + dav_access user:rw group:r; + } + + # 动态请求代理到 Lolly + location /api/ { + proxy_pass http://localhost:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # 静态文件由 Nginx 直接服务 + location /static/ { + root /data/www; + expires 30d; + } +} +``` + +#### 方案二:Lolly 实现类似 WebDAV 功能 + +如需在 Lolly 中实现类似 WebDAV 的文件上传功能,可参考以下思路: + +```go +// 概念示例 - 实际实现请参考 Lolly 代码 +router.PUT("/files/*filepath", func(c *gin.Context) { + filepath := c.Param("filepath") + fullPath := "/data/storage/" + filepath + + // 创建中间目录(类似 create_full_put_path on) + os.MkdirAll(path.Dir(fullPath), 0755) + + // 保存上传的文件 + c.SaveUploadedFile(file, fullPath) + + // 设置权限(类似 dav_access) + os.Chmod(fullPath, 0644) + + c.Status(http.StatusCreated) +}) + +router.DELETE("/files/*filepath", func(c *gin.Context) { + filepath := c.Param("filepath") + fullPath := "/data/storage/" + filepath + + // 检查深度(类似 min_delete_depth) + depth := len(strings.Split(filepath, "/")) + if depth < 4 { + c.JSON(http.StatusForbidden, gin.H{"error": "path too shallow"}) + return + } + + os.Remove(fullPath) + c.Status(http.StatusNoContent) +}) +``` + +#### 方案三:Lolly 作为 WebDAV 后端代理 + +```nginx +location /webdav/ { + # 认证和限流在 Nginx 层处理 + auth_basic "WebDAV"; + auth_basic_user_file /etc/nginx/.passwd; + + # 限流 + limit_req zone=webdav burst=10; + + # 代理到 Lolly + proxy_pass http://localhost:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Destination $http_destination; +} +``` + +### 5.4 选择建议 + +| 场景 | 推荐方案 | +|------|----------| +| 简单文件共享 | Nginx WebDAV | +| 需要复杂业务逻辑 | Lolly | +| 高性能静态文件 | Nginx | +| 需要自定义协议扩展 | Lolly | +| 快速部署 | Nginx WebDAV | +| 深度集成应用 | Lolly | + +--- + +## 6. 性能优化建议 + +### 6.1 文件系统建议 + +**强烈推荐**:临时目录和数据目录使用**同一文件系统**。 + +```nginx +location / { + root /data/www; # 数据目录 + client_body_temp_path /data/client_temp; # 临时目录(同文件系统) + + dav_methods PUT DELETE; +} +``` + +**原因:** +- 同文件系统:文件上传使用 `rename()` 系统调用(原子操作,极快) +- 不同文件系统:文件上传需要 `copy + delete`(慢,占用双倍空间) + +### 6.2 客户端大小限制 + +```nginx +# 限制上传文件大小,防止资源耗尽 +client_max_body_size 100M; + +# 调整缓冲区 +client_body_buffer_size 128k; +``` + +### 6.3 超时设置 + +```nginx +# WebDAV 操作可能需要较长时间 +client_body_timeout 300s; +send_timeout 300s; +``` + +--- + +## 7. 故障排查 + +### 7.1 常见问题 + +| 问题 | 可能原因 | 解决方案 | +|------|----------|----------| +| 405 Method Not Allowed | 方法未启用 | 检查 `dav_methods` 配置 | +| 403 Forbidden | 权限不足或 IP 限制 | 检查 `dav_access` 和 `limit_except` | +| 409 Conflict | 目录不存在 | 设置 `create_full_put_path on` | +| 423 Locked | 文件被锁定 | Nginx 不支持锁,换客户端 | +| 500 Internal Error | 路径过深或循环 | 检查 `min_delete_depth` | + +### 7.2 启用调试日志 + +```nginx +error_log /var/log/nginx/error.log debug; +``` + +查看日志: +```bash +tail -f /var/log/nginx/error.log | grep dav +``` + +### 7.3 验证配置 + +```bash +# 测试配置文件语法 +nginx -t + +# 重新加载配置 +nginx -s reload +``` + +--- + +## 8. 完整配置模板 + +```nginx +# /etc/nginx/conf.d/webdav.conf +server { + listen 80; + server_name dav.example.com; + + # 访问日志 + access_log /var/log/nginx/webdav_access.log; + error_log /var/log/nginx/webdav_error.log; + + # 客户端限制 + client_max_body_size 500M; + client_body_buffer_size 128k; + client_body_timeout 300s; + send_timeout 300s; + + location / { + # 根目录 + root /data/webdav; + + # 临时目录(与根目录同文件系统) + client_body_temp_path /data/webdav_tmp; + + # 启用 WebDAV 方法 + dav_methods PUT DELETE MKCOL COPY MOVE; + + # 允许自动创建中间目录 + create_full_put_path on; + + # 文件权限设置 + dav_access user:rw group:r all:r; + + # 防止误删顶层目录 + min_delete_depth 2; + + # HTTP 基本认证 + auth_basic "WebDAV Repository"; + auth_basic_user_file /etc/nginx/.webdav_passwd; + + # 访问控制 + limit_except GET { + # 允许内网 + allow 192.168.0.0/16; + allow 10.0.0.0/8; + allow 127.0.0.1; + # 拒绝其他 + deny all; + } + + # 目录列表(可选) + autoindex on; + autoindex_format html; + autoindex_localtime on; + } +} +``` + +--- + +## 9. 参考资源 + +- [Nginx 官方文档 - ngx_http_dav_module](https://nginx.org/en/docs/http/ngx_http_dav_module.html) +- [WebDAV RFC 4918](https://tools.ietf.org/html/rfc4918) +- [cadaver 客户端](http://www.webdav.org/cadaver/) +- [davfs2 - Linux WebDAV 挂载](http://savannah.nongnu.org/projects/davfs2/) diff --git a/docs/38-nginx-zone-sync-module.md b/docs/38-nginx-zone-sync-module.md new file mode 100644 index 0000000..c73bcd2 --- /dev/null +++ b/docs/38-nginx-zone-sync-module.md @@ -0,0 +1,1502 @@ +# NGINX Zone Sync 模块详解 + +本文档详细介绍 NGINX Zone Sync 模块(集群区域同步)的配置与使用方法,包括工作原理、指令语法、配置示例和最佳实践。 + +--- + +## 1. Zone Sync 模块概述 + +### 什么是 Zone Sync 模块 + +Zone Sync 模块(`ngx_stream_zone_sync_module`)是 NGINX Plus 提供的**集群状态同步机制**,用于在多个 NGINX 节点之间同步共享内存区域的内容。 + +### 核心功能 + +| 功能 | 说明 | +|------|------| +| **多节点共享内存同步** | 在集群节点间自动同步 keyval、sticky sessions、limit_req 等共享内存数据 | +| **集群状态共享** | 实现分布式 NGINX 实例的状态一致性 | +| **动态节点发现** | 支持 DNS 动态发现或静态配置节点 | +| **SSL 加密传输** | 支持节点间通信加密 | +| **高性能** | 基于 TCP 流的高效同步协议 | + +### 模块信息 + +| 属性 | 说明 | +|------|------| +| **模块名称** | `ngx_stream_zone_sync_module` | +| **首次版本** | 1.13.8 | +| **可用性** | NGINX Plus 商业订阅 | +| **上下文** | `stream`, `server` | + +### 支持的同步内容 + +Zone Sync 模块可以同步以下类型的共享内存区域: + +1. **HTTP Sticky Sessions**:`sticky learn` 创建的会话粘性数据 +2. **HTTP Limit Request**:`limit_req` 的超额请求计数 +3. **Keyval 键值对**:HTTP 和 Stream 的 `keyval_zone` 数据 + +--- + +## 2. Zone Sync 工作原理 + +### 2.1 同步架构 + +``` +┌─────────────────┐ ┌─────────────────┐ +│ NGINX Node A │◄───────►│ NGINX Node B │ +│ 192.168.1.10 │ TCP │ 192.168.1.11 │ +│ :12345 │ Sync │ :12345 │ +└─────────────────┘ └─────────────────┘ + ▲ ▲ + │ │ + └───────────┬───────────────┘ + │ + ▼ + ┌─────────────────┐ + │ NGINX Node C │ + │ 192.168.1.12 │ + │ :12345 │ + └─────────────────┘ +``` + +### 2.2 同步协议机制 + +| 机制 | 说明 | +|------|------| +| **传输层** | 基于 TCP 流连接,在 `stream` 块中配置 | +| **轮询机制** | 按 `zone_sync_interval` 间隔轮询共享内存区域更新 | +| **推送机制** | 使用缓冲区推送区域内容到对等节点 | +| **双向同步** | 节点间建立双向连接,互相发送和接收更新 | + +### 2.3 节点发现方式 + +#### 静态配置发现 + +```nginx +stream { + server { + zone_sync; + listen 127.0.0.1:12345; + + # 静态指定集群节点 + zone_sync_server 192.168.1.10:12345; + zone_sync_server 192.168.1.11:12345; + zone_sync_server 192.168.1.12:12345; + } +} +``` + +- 需要手动维护节点列表 +- 添加/删除节点需要重新加载配置 + +#### 动态 DNS 发现 + +```nginx +stream { + resolver 192.168.1.1 valid=10s; + + server { + zone_sync; + listen 127.0.0.1:12345; + + # 通过 DNS 动态解析节点 + zone_sync_server cluster.example.com:12345 resolve; + } +} +``` + +- 使用 `resolve` 参数启用 DNS 监控 +- 自动检测 DNS 记录变化 +- 无需重新加载配置即可扩缩容 + +### 2.4 状态传递流程 + +1. **变更检测**:每个节点按 `zone_sync_interval` 轮询本地共享内存 +2. **变更缓冲**:将检测到的变更写入同步缓冲区(`zone_sync_buffers`) +3. **网络传输**:通过 TCP 连接将变更发送给其他节点 +4. **接收解析**:接收节点解析同步消息并更新本地共享内存 +5. **一致性保证**:所有节点最终达到状态一致 + +### 2.5 节点生命周期管理 + +| 操作 | 步骤 | +|------|------| +| **添加节点** | 1. 更新 DNS(动态)或配置(静态)
2. 启动新 NGINX 实例
3. 自动发现并开始同步 | +| **移除节点** | 1. 更新 DNS 或配置
2. 发送 `QUIT` 信号优雅关闭
3. 其他节点检测到连接关闭 | +| **更换节点 IP** | 1. 更新 DNS 记录
2. 其他节点自动检测到变化
3. 重新建立连接 | + +--- + +## 3. 指令详解 + +### 3.1 zone_sync + +**启用共享内存区域同步**。 + +| 属性 | 说明 | +|------|------| +| **语法** | `zone_sync;` | +| **默认值** | — | +| **上下文** | `server` | +| **版本** | 1.13.8+ | + +**配置示例**: + +```nginx +stream { + server { + # 启用 zone 同步 + zone_sync; + + listen 127.0.0.1:12345; + + zone_sync_server 192.168.1.10:12345; + zone_sync_server 192.168.1.11:12345; + } +} +``` + +**注意事项**: +- 必须在 `stream` 块的 `server` 上下文中使用 +- 需要配合 `zone_sync_server` 指定集群节点 +- 每个节点必须有唯一的监听地址 + +--- + +### 3.2 zone_sync_server + +**定义集群节点地址**。 + +| 属性 | 说明 | +|------|------| +| **语法** | `zone_sync_server address [resolve];` | +| **默认值** | — | +| **上下文** | `server` | +| **版本** | 1.13.8+ | + +**参数说明**: + +| 参数 | 说明 | +|------|------| +| `address` | 节点地址,支持以下格式:
- `IP:port`(如 `192.168.1.10:12345`)
- `域名:port`(如 `nginx-node1.example.com:12345`)
- `unix:/path/to/socket`(Unix 域套接字) | +| `resolve` | 启用 DNS 监控,域名解析变化时自动更新(需要 `resolver` 指令) | + +**配置示例**: + +```nginx +stream { + # 静态配置 + server { + zone_sync; + listen 127.0.0.1:12345; + + zone_sync_server 192.168.1.10:12345; + zone_sync_server 192.168.1.11:12345; + } +} +``` + +```nginx +stream { + resolver 192.168.1.1 valid=30s; + + # 动态 DNS 发现 + server { + zone_sync; + listen 127.0.0.1:12345; + + # 解析到多个 A 记录 + zone_sync_server cluster.example.com:12345 resolve; + } +} +``` + +**注意事项**: +- 每个节点在配置中只能出现一次 +- 集群中所有节点应该使用相同的配置 +- 使用 `resolve` 时必须配置 `resolver` + +--- + +### 3.3 zone_sync_connect_timeout + +**设置与集群节点建立连接的超时时间**。 + +| 属性 | 说明 | +|------|------| +| **语法** | `zone_sync_connect_timeout time;` | +| **默认值** | `5s` | +| **上下文** | `stream`, `server` | +| **版本** | 1.13.8+ | + +**参数说明**: + +| 参数 | 说明 | +|------|------| +| `time` | 超时时间,支持单位:`ms`, `s`, `m`, `h` | + +**配置示例**: + +```nginx +stream { + server { + zone_sync; + listen 127.0.0.1:12345; + + # 连接超时设置为 10 秒 + zone_sync_connect_timeout 10s; + + zone_sync_server 192.168.1.10:12345; + } +} +``` + +**适用场景**: +- 网络延迟较高时需要增加超时 +- 快速失败场景可减少超时 + +--- + +### 3.4 zone_sync_timeout + +**设置连续读写操作之间的超时时间**。 + +| 属性 | 说明 | +|------|------| +| **语法** | `zone_sync_timeout time;` | +| **默认值** | `5s` | +| **上下文** | `stream`, `server` | +| **版本** | 1.13.8+ | + +**说明**: + +该指令替代了早期版本中可能的 `zone_sync_recv_timeout` 和 `zone_sync_send_timeout`,统一控制读写超时。 + +| 参数 | 说明 | +|------|------| +| `time` | 超时时间,两次连续读取或写入之间的最大间隔 | + +**配置示例**: + +```nginx +stream { + server { + zone_sync; + listen 127.0.0.1:12345; + + # 读写超时设置为 30 秒 + zone_sync_timeout 30s; + + zone_sync_server 192.168.1.10:12345; + } +} +``` + +--- + +### 3.5 zone_sync_buffers + +**设置用于推送区域内容的缓冲区数量和大小**。 + +| 属性 | 说明 | +|------|------| +| **语法** | `zone_sync_buffers number size;` | +| **默认值** | `8 4k` 或 `8 8k`(取决于平台内存页大小) | +| **上下文** | `stream`, `server` | +| **版本** | 1.13.8+ | + +**参数说明**: + +| 参数 | 说明 | +|------|------| +| `number` | 缓冲区数量 | +| `size` | 每个缓冲区大小 | + +**重要限制**: +- 单个缓冲区必须足够大以容纳任何共享内存区域中的**单个条目** +- 如果条目超过缓冲区大小,同步将失败 + +**配置示例**: + +```nginx +stream { + server { + zone_sync; + listen 127.0.0.1:12345; + + # 16 个 8k 缓冲区 + zone_sync_buffers 16 8k; + + zone_sync_server 192.168.1.10:12345; + } +} +``` + +**调优建议**: +- 如果 keyval 条目较大(如 JSON 配置),增加 `size` +- 如果同步频繁,增加 `number` + +--- + +### 3.6 zone_sync_interval + +**设置轮询共享内存区域更新的间隔时间**。 + +| 属性 | 说明 | +|------|------| +| **语法** | `zone_sync_interval time;` | +| **默认值** | `1s` | +| **上下文** | `stream`, `server` | +| **版本** | 1.13.8+ | + +**参数说明**: + +| 参数 | 说明 | +|------|------| +| `time` | 轮询间隔,较短间隔意味着更快的同步但更高的 CPU 使用 | + +**配置示例**: + +```nginx +stream { + server { + zone_sync; + listen 127.0.0.1:12345; + + # 每 500ms 轮询一次更新 + zone_sync_interval 500ms; + + zone_sync_server 192.168.1.10:12345; + } +} +``` + +**调优建议**: +- 默认 `1s` 适用于大多数场景 +- 对实时性要求高的场景可减少到 `100-500ms` +- 资源受限场景可增加到 `2-5s` + +--- + +### 3.7 zone_sync_recv_buffer_size + +**设置每个连接的接收缓冲区大小,用于解析同步消息**。 + +| 属性 | 说明 | +|------|------| +| **语法** | `zone_sync_recv_buffer_size size;` | +| **默认值** | `4k` 或 `8k`(与 `zone_sync_buffers` 的 `size × number` 相同) | +| **上下文** | `stream`, `server` | +| **版本** | 1.13.8+ | + +**参数说明**: + +| 参数 | 说明 | +|------|------| +| `size` | 接收缓冲区大小,必须大于或等于 `zone_sync_buffers` 的单个缓冲区大小 | + +**配置示例**: + +```nginx +stream { + server { + zone_sync; + listen 127.0.0.1:12345; + + zone_sync_buffers 16 8k; + # 接收缓冲区至少 8k + zone_sync_recv_buffer_size 16k; + + zone_sync_server 192.168.1.10:12345; + } +} +``` + +--- + +### 3.8 zone_sync_connect_retry_interval + +**设置连接失败后重试的间隔时间**。 + +| 属性 | 说明 | +|------|------| +| **语法** | `zone_sync_connect_retry_interval time;` | +| **默认值** | `1s` | +| **上下文** | `stream`, `server` | +| **版本** | 1.13.8+ | + +**参数说明**: + +| 参数 | 说明 | +|------|------| +| `time` | 重试间隔,较短间隔可以更快恢复但可能增加网络负担 | + +**配置示例**: + +```nginx +stream { + server { + zone_sync; + listen 127.0.0.1:12345; + + # 连接失败后每 5 秒重试一次 + zone_sync_connect_retry_interval 5s; + + zone_sync_server 192.168.1.10:12345; + } +} +``` + +--- + +### 3.9 SSL 相关指令 + +Zone Sync 支持节点间 SSL 加密传输。 + +| 指令 | 说明 | +|------|------| +| `zone_sync_ssl on\|off` | 启用 SSL(默认 `off`) | +| `zone_sync_ssl_certificate file` | SSL 证书文件 | +| `zone_sync_ssl_certificate_key file` | SSL 密钥文件 | +| `zone_sync_ssl_protocols protocols` | SSL 协议版本(如 `TLSv1.2 TLSv1.3`) | +| `zone_sync_ssl_ciphers ciphers` | SSL 加密套件 | +| `zone_sync_ssl_server_name on\|off` | 启用 SNI(默认 `off`) | +| `zone_sync_ssl_name name` | 覆盖 SSL 验证的服务器名称 | +| `zone_sync_ssl_verify on\|off` | 启用证书验证(默认 `off`) | +| `zone_sync_ssl_verify_depth number` | 验证深度(默认 `1`) | +| `zone_sync_ssl_trusted_certificate file` | 受信任的 CA 证书 | +| `zone_sync_ssl_crl file` | 证书吊销列表 | + +**SSL 配置示例**: + +```nginx +stream { + resolver 192.168.1.1 valid=30s; + + server { + zone_sync; + listen 127.0.0.1:12345; + + zone_sync_server cluster.example.com:12345 resolve; + + # 启用 SSL + zone_sync_ssl on; + zone_sync_ssl_certificate /etc/nginx/certs/cluster.crt; + zone_sync_ssl_certificate_key /etc/nginx/certs/cluster.key; + zone_sync_ssl_protocols TLSv1.2 TLSv1.3; + zone_sync_ssl_ciphers HIGH:!aNULL:!MD5; + } +} +``` + +--- + +## 4. 集群配置示例 + +### 4.1 最小化配置 + +```nginx +# nginx.conf + +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + # 定义需要同步的 zone + upstream backend { + server backend1.example.com:8080; + sticky learn + create=$upstream_cookie_session + lookup=$cookie_session + zone=session_store:1m sync; + } + + server { + listen 80; + + location / { + proxy_pass http://backend; + } + } +} + +stream { + server { + # 启用 zone 同步 + zone_sync; + listen 127.0.0.1:12345; + + # 集群节点配置 + zone_sync_server node1.example.com:12345; + zone_sync_server node2.example.com:12345; + } +} +``` + +--- + +### 4.2 三节点集群配置 + +#### 节点 1(192.168.1.10) + +```nginx +# /etc/nginx/nginx.conf (Node 1) + +events { + worker_connections 4096; +} + +stream { + server { + zone_sync; + listen 192.168.1.10:12345; + + zone_sync_server 192.168.1.10:12345; + zone_sync_server 192.168.1.11:12345; + zone_sync_server 192.168.1.12:12345; + + zone_sync_connect_timeout 10s; + zone_sync_timeout 30s; + zone_sync_interval 500ms; + } +} +``` + +#### 节点 2(192.168.1.11) + +```nginx +# /etc/nginx/nginx.conf (Node 2) + +events { + worker_connections 4096; +} + +stream { + server { + zone_sync; + listen 192.168.1.11:12345; + + zone_sync_server 192.168.1.10:12345; + zone_sync_server 192.168.1.11:12345; + zone_sync_server 192.168.1.12:12345; + + zone_sync_connect_timeout 10s; + zone_sync_timeout 30s; + zone_sync_interval 500ms; + } +} +``` + +#### 节点 3(192.168.1.12) + +```nginx +# /etc/nginx/nginx.conf (Node 3) + +events { + worker_connections 4096; +} + +stream { + server { + zone_sync; + listen 192.168.1.12:12345; + + zone_sync_server 192.168.1.10:12345; + zone_sync_server 192.168.1.11:12345; + zone_sync_server 192.168.1.12:12345; + + zone_sync_connect_timeout 10s; + zone_sync_timeout 30s; + zone_sync_interval 500ms; + } +} +``` + +--- + +### 4.3 动态 DNS 发现配置 + +```nginx +# 所有节点使用相同配置 + +events { + worker_connections 4096; +} + +stream { + # 配置 DNS 解析器 + resolver 192.168.1.1 valid=10s; + + server { + zone_sync; + listen 0.0.0.0:12345; + + # 通过 SRV 或 A 记录动态发现节点 + zone_sync_server nginx-cluster.internal:12345 resolve; + + zone_sync_connect_timeout 5s; + zone_sync_connect_retry_interval 2s; + } +} +``` + +**DNS 记录配置示例**: + +``` +# DNS A 记录 +nginx-cluster.internal. IN A 192.168.1.10 +nginx-cluster.internal. IN A 192.168.1.11 +nginx-cluster.internal. IN A 192.168.1.12 +``` + +--- + +### 4.4 同步 keyval 状态 + +```nginx +events { + worker_connections 4096; +} + +http { + # 需要同步的 keyval zone(带 timeout 和 sync 参数) + keyval_zone zone=user_sessions:64k state=/var/lib/nginx/state/sessions.keyval timeout=1h sync; + keyval_zone zone=api_keys:32k state=/var/lib/nginx/state/api_keys.keyval sync; + + keyval $cookie_session $session_data zone=user_sessions; + keyval $arg_api_key $api_info zone=api_keys; + + # API 管理接口 + server { + listen 127.0.0.1:8080; + + location /api { + api write=on; + allow 127.0.0.1; + deny all; + } + } + + server { + listen 80; + + location / { + if ($api_info = "") { + return 403 "API key required"; + } + proxy_pass http://backend; + } + } +} + +stream { + server { + zone_sync; + listen 0.0.0.0:12345; + + zone_sync_server node1.example.com:12345; + zone_sync_server node2.example.com:12345; + + zone_sync_buffers 16 8k; + zone_sync_interval 500ms; + } +} +``` + +--- + +### 4.5 同步 limit_conn 状态 + +```nginx +events { + worker_connections 4096; +} + +http { + # 限制连接数 zone(需要同步) + limit_conn_zone $binary_remote_addr zone=addr_limit:10m; + + # 注意:limit_conn_zone 不直接支持 sync 参数 + # 需要通过 keyval 间接实现分布式限流 + + server { + listen 80; + + location /downloads { + limit_conn addr_limit 10; + proxy_pass http://backend; + } + } +} + +stream { + server { + zone_sync; + listen 0.0.0.0:12345; + + zone_sync_server node1.example.com:12345; + zone_sync_server node2.example.com:12345; + } +} +``` + +**注意**:`limit_conn_zone` 和 `limit_req_zone` 的同步需要 NGINX Plus 特定版本支持。 + +--- + +### 4.6 同步 sticky sessions + +```nginx +events { + worker_connections 4096; +} + +http { + upstream api_backend { + server 192.168.1.10:8080; + server 192.168.1.11:8080; + + # sticky learn 会话粘性,带 sync 参数 + sticky learn + create=$upstream_cookie_route + lookup=$cookie_route + zone=sticky_sessions:2m sync; + } + + server { + listen 80; + + location /api { + proxy_pass http://api_backend; + } + } +} + +stream { + server { + zone_sync; + listen 0.0.0.0:12345; + + zone_sync_server node1.example.com:12345; + zone_sync_server node2.example.com:12345; + + zone_sync_interval 200ms; # 更快的会话同步 + } +} +``` + +--- + +## 5. 与 Keyval 模块配合使用 + +### 5.1 Keyval Zone 同步配置 + +```nginx +events { + worker_connections 4096; +} + +http { + # ========== 需要同步的 keyval zones ========== + + # 用户会话(带过期时间) + keyval_zone zone=sessions:2m state=/var/lib/nginx/state/sessions.keyval timeout=30m sync; + keyval $cookie_session $session_info zone=sessions; + + # 动态路由配置 + keyval_zone zone=routes:512k state=/var/lib/nginx/state/routes.keyval sync; + keyval $uri $backend_pool zone=routes; + + # API 限流黑白名单 + keyval_zone zone=rate_whitelist:1m type=ip sync; + keyval $remote_addr $rate_exempt zone=rate_whitelist; + + # ========== 后端定义 ========== + upstream backend_pool_a { + server 192.168.1.10:8080; + server 192.168.1.11:8080; + } + + upstream backend_pool_b { + server 192.168.1.20:8080; + server 192.168.1.21:8080; + } + + # ========== API 管理服务器 ========== + server { + listen 127.0.0.1:8080; + + location /api { + api write=on; + allow 127.0.0.1; + deny all; + } + } + + # ========== 主业务服务器 ========== + server { + listen 80; + server_name app.example.com; + + location / { + # 白名单跳过限流 + if ($rate_exempt = "exempt") { + proxy_pass http://backend_pool_a; + break; + } + + # 动态路由 + if ($backend_pool = "") { + set $backend_pool "backend_pool_a"; + } + + proxy_pass http://$backend_pool; + } + } +} + +stream { + # Zone Sync 配置 + server { + zone_sync; + listen 0.0.0.0:12345; + + # 集群节点 + zone_sync_server 192.168.1.10:12345; + zone_sync_server 192.168.1.11:12345; + + # 同步参数调优 + zone_sync_buffers 32 16k; + zone_sync_interval 500ms; + zone_sync_timeout 30s; + } +} +``` + +--- + +### 5.2 通过 API 管理同步数据 + +```bash +#!/bin/bash + +# API 端点 +API_BASE="http://127.0.0.1:8080/api/9" + +# 添加会话 +curl -X POST "${API_BASE}/http/keyvals/sessions" \ + -H "Content-Type: application/json" \ + -d '{ + "user123": { + "value": "{\"user_id\": 123, \"role\": \"admin\"}", + "expire": 1800000 + } + }' + +# 添加路由规则 +curl -X POST "${API_BASE}/http/keyvals/routes" \ + -H "Content-Type: application/json" \ + -d '{ + "/api/v1": "backend_pool_a", + "/api/v2": "backend_pool_b", + "/admin": "backend_pool_a" + }' + +# 查询会话数据 +curl "${API_BASE}/http/keyvals/sessions?key=user123" + +# 查询所有路由 +curl "${API_BASE}/http/keyvals/routes" + +# 删除键 +curl -X PATCH "${API_BASE}/http/keyvals/sessions" \ + -H "Content-Type: application/json" \ + -d '{"user123": null}' + +# 清空 zone +curl -X DELETE "${API_BASE}/http/keyvals/routes" +``` + +--- + +### 5.3 集群状态监控 + +```bash +# 查询 Zone Sync 状态 +curl http://127.0.0.1:8080/api/9/stream/zone_sync/ + +# 示例响应 +{ + "connections": { + "active": 2, + "idle": 0, + "closed": 5 + }, + "messages": { + "sent": 12345, + "received": 12300 + }, + "bytes": { + "sent": 1048576, + "received": 1024000 + }, + "zones": { + "sessions": { + "entries": 150, + "size": 32768 + }, + "routes": { + "entries": 25, + "size": 8192 + } + } +} +``` + +--- + +## 6. 性能调优与最佳实践 + +### 6.1 缓冲区大小调优 + +```nginx +stream { + server { + zone_sync; + listen 0.0.0.0:12345; + + # 场景 1:小型键值对(<1KB) + zone_sync_buffers 8 4k; + + # 场景 2:中型配置(1-4KB) + zone_sync_buffers 16 8k; + + # 场景 3:大型 JSON 配置(>4KB) + zone_sync_buffers 32 16k; + + zone_sync_server node1.example.com:12345; + } +} +``` + +--- + +### 6.2 同步间隔调优 + +```nginx +# 高实时性场景(会话同步) +zone_sync_interval 100ms; + +# 一般场景(路由配置) +zone_sync_interval 500ms; + +# 低频率场景(配置同步) +zone_sync_interval 2s; +``` + +--- + +### 6.3 网络调优 + +```nginx +stream { + server { + zone_sync; + listen 0.0.0.0:12345; + + # 高延迟网络(跨数据中心) + zone_sync_connect_timeout 15s; + zone_sync_timeout 60s; + zone_sync_connect_retry_interval 5s; + + # 低延迟网络(同数据中心) + zone_sync_connect_timeout 3s; + zone_sync_timeout 10s; + zone_sync_connect_retry_interval 1s; + + zone_sync_server node1.example.com:12345; + } +} +``` + +--- + +### 6.4 内存规划 + +```nginx +http { + # 估算内存需求 + # 每个键值对:key(50B) + value(100B) + overhead(50B) ≈ 200B + # 10,000 键值对 ≈ 2MB + + keyval_zone zone=large_db:4m sync; + + # 建议预留 50% 余量 + keyval_zone zone=with_margin:6m sync; +} +``` + +--- + +### 6.5 安全建议 + +```nginx +stream { + server { + zone_sync; + # 只监听内网地址 + listen 192.168.1.10:12345; + + # 启用 SSL 加密 + zone_sync_ssl on; + zone_sync_ssl_certificate /etc/nginx/certs/cluster.crt; + zone_sync_ssl_certificate_key /etc/nginx/certs/cluster.key; + zone_sync_ssl_protocols TLSv1.3; + zone_sync_ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + + zone_sync_server node1.internal:12345; + } +} +``` + +--- + +## 7. 故障排查 + +### 7.1 常见问题 + +| 问题 | 可能原因 | 解决方案 | +|------|----------|----------| +| 连接失败 | 防火墙/网络不通 | 检查端口连通性,确认防火墙规则 | +| 同步延迟 | 缓冲区不足 | 增加 `zone_sync_buffers` 大小 | +| 节点无法发现 | DNS 解析问题 | 检查 `resolver` 配置,验证 DNS 记录 | +| 数据不一致 | 网络分区 | 检查网络连接,增加超时时间 | +| 内存增长 | 无过期时间 | 配置 `timeout` 参数 | + +--- + +### 7.2 调试命令 + +```bash +# 测试配置 +nginx -t + +# 重载配置 +nginx -s reload + +# 检查进程状态 +ps aux | grep nginx + +# 查看网络连接 +netstat -tlnp | grep nginx +ss -tlnp | grep nginx + +# 监控日志 +tail -f /var/log/nginx/error.log + +# 查看 Zone Sync API 状态 +curl http://localhost:8080/api/9/stream/zone_sync/ +``` + +--- + +## 8. 与 Lolly 项目的关系和建议 + +### 8.1 项目对比 + +[Lolly](https://github.com/xfy/lolly) 是一个使用 Go 语言编写的高性能 HTTP 服务器与反向代理。与 NGINX Zone Sync 相比: + +| 特性 | NGINX Zone Sync | Lolly | +|------|-----------------|-------| +| **集群同步** | 内置支持,商业功能 | 需自行实现 | +| **状态存储** | 共享内存 | 内存/外部存储 | +| **节点发现** | 静态/DNS | 需自行实现 | +| **传输协议** | TCP(专有协议) | 可自定义 | +| **许可证** | 商业订阅 | 开源免费 | + +--- + +### 8.2 Go 实现分布式状态同步建议 + +#### 方案 1:基于 Gossip 协议 + +```go +// internal/cluster/gossip.go +package cluster + +import ( + "sync" + "time" + + "github.com/hashicorp/memberlist" +) + +type StateSync struct { + list *memberlist.Memberlist + store sync.Map + config *Config +} + +type Config struct { + NodeName string + BindAddr string + BindPort int + JoinNodes []string + SyncInterval time.Duration +} + +func NewStateSync(cfg *Config) (*StateSync, error) { + s := &StateSync{config: cfg} + + mlConfig := memberlist.DefaultLANConfig() + mlConfig.Name = cfg.NodeName + mlConfig.BindAddr = cfg.BindAddr + mlConfig.BindPort = cfg.BindPort + mlConfig.Delegate = s + + list, err := memberlist.Create(mlConfig) + if err != nil { + return nil, err + } + + s.list = list + + if len(cfg.JoinNodes) > 0 { + _, err = list.Join(cfg.JoinNodes) + if err != nil { + return nil, err + } + } + + return s, nil +} + +// 实现 memberlist.Delegate 接口 +func (s *StateSync) NodeMeta(limit int) []byte { + return []byte{} +} + +func (s *StateSync) NotifyMsg(buf []byte) { + // 处理接收到的同步消息 +} + +func (s *StateSync) GetBroadcasts(overhead, limit int) [][]byte { + // 返回待广播的变更 + return nil +} + +func (s *StateSync) LocalState(join bool) []byte { + // 返回本地状态 + return nil +} + +func (s *StateSync) MergeRemoteState(buf []byte, join bool) { + // 合并远程状态 +} + +// 状态操作接口 +func (s *StateSync) Set(key, value string) error { + s.store.Store(key, value) + return s.broadcastUpdate(key, value) +} + +func (s *StateSync) Get(key string) (string, bool) { + val, ok := s.store.Load(key) + if !ok { + return "", false + } + return val.(string), true +} + +func (s *StateSync) Delete(key string) error { + s.store.Delete(key) + return s.broadcastDelete(key) +} +``` + +--- + +#### 方案 2:基于 Redis Pub/Sub + +```go +// internal/cluster/redis_sync.go +package cluster + +import ( + "context" + "encoding/json" + "time" + + "github.com/redis/go-redis/v9" +) + +type RedisSync struct { + client *redis.Client + channel string + nodeID string + store sync.Map +} + +type SyncMessage struct { + Type string `json:"type"` // "set", "delete", "full" + Key string `json:"key,omitempty"` + Value interface{} `json:"value,omitempty"` + NodeID string `json:"node_id"` + Timestamp int64 `json:"timestamp"` +} + +func NewRedisSync(addr, channel, nodeID string) (*RedisSync, error) { + client := redis.NewClient(&redis.Options{ + Addr: addr, + }) + + s := &RedisSync{ + client: client, + channel: channel, + nodeID: nodeID, + } + + // 启动订阅协程 + go s.subscribe(context.Background()) + + return s, nil +} + +func (s *RedisSync) subscribe(ctx context.Context) { + pubsub := s.client.Subscribe(ctx, s.channel) + defer pubsub.Close() + + ch := pubsub.Channel() + for msg := range ch { + if err := s.handleMessage(msg.Payload); err != nil { + // 处理错误 + } + } +} + +func (s *RedisSync) handleMessage(payload string) error { + var msg SyncMessage + if err := json.Unmarshal([]byte(payload), &msg); err != nil { + return err + } + + // 忽略自己发送的消息 + if msg.NodeID == s.nodeID { + return nil + } + + switch msg.Type { + case "set": + s.store.Store(msg.Key, msg.Value) + case "delete": + s.store.Delete(msg.Key) + } + + return nil +} + +func (s *RedisSync) Set(ctx context.Context, key string, value interface{}) error { + msg := SyncMessage{ + Type: "set", + Key: key, + Value: value, + NodeID: s.nodeID, + Timestamp: time.Now().UnixNano(), + } + + data, _ := json.Marshal(msg) + s.store.Store(key, value) + + return s.client.Publish(ctx, s.channel, data).Err() +} + +func (s *RedisSync) Delete(ctx context.Context, key string) error { + msg := SyncMessage{ + Type: "delete", + Key: key, + NodeID: s.nodeID, + Timestamp: time.Now().UnixNano(), + } + + data, _ := json.Marshal(msg) + s.store.Delete(key) + + return s.client.Publish(ctx, s.channel, data).Err() +} +``` + +--- + +#### 方案 3:基于 etcd 的强一致性同步 + +```go +// internal/cluster/etcd_sync.go +package cluster + +import ( + "context" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" +) + +type EtcdSync struct { + client *clientv3.Client + prefix string + nodeID string + ctx context.Context +} + +func NewEtcdSync(endpoints []string, prefix, nodeID string) (*EtcdSync, error) { + client, err := clientv3.New(clientv3.Config{ + Endpoints: endpoints, + DialTimeout: 5 * time.Second, + }) + if err != nil { + return nil, err + } + + return &EtcdSync{ + client: client, + prefix: prefix, + nodeID: nodeID, + ctx: context.Background(), + }, nil +} + +func (s *EtcdSync) Set(key string, value string, ttl time.Duration) error { + ctx, cancel := context.WithTimeout(s.ctx, 5*time.Second) + defer cancel() + + if ttl > 0 { + lease, err := s.client.Grant(ctx, int64(ttl.Seconds())) + if err != nil { + return err + } + _, err = s.client.Put(ctx, s.prefix+key, value, + clientv3.WithLease(lease.ID)) + return err + } + + _, err := s.client.Put(ctx, s.prefix+key, value) + return err +} + +func (s *EtcdSync) Get(key string) (string, error) { + ctx, cancel := context.WithTimeout(s.ctx, 5*time.Second) + defer cancel() + + resp, err := s.client.Get(ctx, s.prefix+key) + if err != nil { + return "", err + } + + if len(resp.Kvs) == 0 { + return "", nil + } + + return string(resp.Kvs[0].Value), nil +} + +func (s *EtcdSync) Watch(ctx context.Context, key string) <-chan string { + ch := make(chan string) + + go func() { + defer close(ch) + + rch := s.client.Watch(ctx, s.prefix+key) + for wresp := range rch { + for _, ev := range wresp.Events { + ch <- string(ev.Kv.Value) + } + } + }() + + return ch +} +``` + +--- + +### 8.3 Lolly 实现建议 + +参考 NGINX Zone Sync 模块,建议 Lolly 可以考虑以下实现: + +1. **模块化设计** + - 抽象 `StateSync` 接口 + - 支持多种后端(内存、Redis、etcd、Gossip) + - 插件式注册 + +2. **配置示例** + +```yaml +# config.yaml +cluster: + enabled: true + node_id: "node-1" + bind_addr: "0.0.0.0" + bind_port: 9000 + join_nodes: + - "192.168.1.10:9000" + - "192.168.1.11:9000" + sync_interval: 500ms + backend: "gossip" # 或 "redis", "etcd" + +# Redis 后端配置 +redis: + addr: "localhost:6379" + channel: "lolly:sync" + +# etcd 后端配置 +etcd: + endpoints: + - "localhost:2379" + prefix: "/lolly/state/" +``` + +3. **API 设计** + +```go +// 定义同步状态接口 +type StateSync interface { + Set(key string, value interface{}, ttl time.Duration) error + Get(key string) (interface{}, bool) + Delete(key string) error + Watch(key string) <-chan StateChange + Close() error +} + +// 状态变更事件 +type StateChange struct { + Key string + Value interface{} + Deleted bool + Timestamp time.Time +} +``` + +4. **与 NGINX Plus API 兼容** + +```go +// 提供与 NGINX Plus 兼容的 REST API +// POST /api/lolly/keyvals/{zone} +// GET /api/lolly/keyvals/{zone} +// PATCH /api/lolly/keyvals/{zone} +// DELETE /api/lolly/keyvals/{zone} +``` + +--- + +## 9. 参考链接 + +- [NGINX Zone Sync 模块官方文档](https://nginx.org/en/docs/stream/ngx_stream_zone_sync_module.html) +- [NGINX Keyval 模块官方文档](https://nginx.org/en/docs/stream/ngx_stream_keyval_module.html) +- [NGINX Plus API 文档](https://nginx.org/en/docs/http/ngx_http_api_module.html) +- [Lolly 项目 GitHub](https://github.com/xfy/lolly) +- [memberlist (HashiCorp)](https://github.com/hashicorp/memberlist) +- [etcd 官方文档](https://etcd.io/docs/) diff --git a/docs/39-nginx-tunnel-module.md b/docs/39-nginx-tunnel-module.md new file mode 100644 index 0000000..2fd5b5d --- /dev/null +++ b/docs/39-nginx-tunnel-module.md @@ -0,0 +1,603 @@ +# NGINX HTTP Tunnel 模块文档 + +## 1. 模块概述 + +### 1.1 简介 + +`ngx_http_tunnel_module` 是 NGINX 的商业模块,用于处理 HTTP CONNECT 请求并建立端到端的虚拟连接隧道。 + +**版本要求**: NGINX 1.29.3 及以上 + +**授权**: 仅作为 F5 NGINX 商业订阅的一部分提供,开源版本不包含此模块 + +### 1.2 核心用途 + +- **HTTP 代理隧道**: 处理 RFC 9110 定义的 CONNECT 方法,用于建立 HTTPS 代理隧道 +- **TCP 穿透**: 允许客户端通过 HTTP 代理与后端 TCP 服务建立直接连接 +- **动态路由**: 支持变量实现动态目标地址和绑定地址 + +### 1.3 配置上下文 + +支持以下配置块: +- `http` +- `server` +- `location` + +--- + +## 2. 指令详解 + +### 2.1 核心指令 + +#### tunnel_pass + +```nginx +tunnel_pass [address]; +``` + +| 属性 | 说明 | +|------|------| +| **默认值** | 无(必须显式配置) | +| **上下文** | http, server, location | +| **支持变量** | 是 | + +**说明**: +- 启用 CONNECT 请求处理 +- 默认目标地址为 `$host:$request_port` +- `address` 可以是:域名、IP 地址、端口、UNIX 套接字路径或上游服务器组名称 +- 支持变量实现动态路由 + +**示例**: +```nginx +tunnel_pass $host:$request_port; +tunnel_pass backend_upstream; +tunnel_pass 127.0.0.1:8443; +``` + +--- + +#### tunnel_allow_upstream + +```nginx +tunnel_allow_upstream string ...; +``` + +| 属性 | 说明 | +|------|------| +| **默认值** | 无 | +| **上下文** | http, server, location | +| **支持变量** | 是 | + +**说明**: +- 定义允许访问后端服务器的条件 +- 所有参数必须非空且不等于 "0" 才允许连接 +- 每次建立连接前都会评估 + +**示例**: +```nginx +tunnel_allow_upstream $allow_port $allow_host; +``` + +--- + +#### tunnel_bind + +```nginx +tunnel_bind address | off; +``` + +| 属性 | 说明 | +|------|------| +| **默认值** | 无 | +| **上下文** | http, server, location | +| **支持变量** | 是 | + +**说明**: +- 指定出站连接到后端服务器时使用的本地 IP 地址(可选端口) +- `off` 取消从上级配置继承的效果 + +**示例**: +```nginx +tunnel_bind 192.168.1.100; +tunnel_bind $local_ip:$local_port; +``` + +--- + +#### tunnel_bind_dynamic + +```nginx +tunnel_bind_dynamic on | off; +``` + +| 属性 | 说明 | +|------|------| +| **默认值** | `off` | +| **上下文** | http, server, location | + +**说明**: +- 启用后,每次连接尝试时都会执行 `tunnel_bind` 操作 +- 适用于 `tunnel_bind` 中使用动态变量的场景 + +--- + +#### tunnel_socket_keepalive + +```nginx +tunnel_socket_keepalive on | off; +``` + +| 属性 | 说明 | +|------|------| +| **默认值** | `off` | +| **上下文** | http, server, location | + +**说明**: +- 配置出站连接的 TCP keepalive 行为 +- `on` 开启 `SO_KEEPALIVE` 选项 + +--- + +### 2.2 超时与缓冲指令 + +#### tunnel_connect_timeout + +```nginx +tunnel_connect_timeout time; +``` + +| 属性 | 说明 | +|------|------| +| **默认值** | `60s` | +| **上下文** | http, server, location | + +**说明**: +- 建立后端连接的超时时间 +- 通常不应超过 75 秒 + +--- + +#### tunnel_read_timeout + +```nginx +tunnel_read_timeout time; +``` + +| 属性 | 说明 | +|------|------| +| **默认值** | `60s` | +| **上下文** | http, server, location | + +**说明**: +- 客户端或后端连接上两次连续读写操作之间的超时 +- 无数据传输时关闭连接 + +--- + +#### tunnel_send_timeout + +```nginx +tunnel_send_timeout time; +``` + +| 属性 | 说明 | +|------|------| +| **默认值** | `60s` | +| **上下文** | http, server, location | + +**说明**: +- 向后端服务器传输请求的超时时间 +- 仅针对两次连续写操作之间 + +--- + +#### tunnel_buffer_size + +```nginx +tunnel_buffer_size size; +``` + +| 属性 | 说明 | +|------|------| +| **默认值** | `16k` | +| **上下文** | http, server, location | + +**说明**: +- 设置用于从后端服务器读取数据的缓冲区大小 +- 同时也设置从客户端读取数据的缓冲区大小 + +--- + +#### tunnel_send_lowat + +```nginx +tunnel_send_lowat size; +``` + +| 属性 | 说明 | +|------|------| +| **默认值** | `0` | +| **上下文** | http, server, location | + +**说明**: +- 非零值时尝试最小化发送操作(使用 `NOTE_LOWAT` 或 `SO_SNDLOWAT`) +- **注意**: 在 Linux、Solaris 和 Windows 上被忽略 + +--- + +### 2.3 上游故障转移指令 + +#### tunnel_next_upstream + +```nginx +tunnel_next_upstream error | timeout | denied | off ...; +``` + +| 属性 | 说明 | +|------|------| +| **默认值** | `error timeout` | +| **上下文** | http, server, location | + +**说明**: +- 指定何时将请求传递给下一台服务器 +- `denied`: 被 `tunnel_allow_upstream` 拒绝 +- `off`: 禁用故障转移 + +**重要限制**: 若已向客户端发送数据,则无法传递到下一台服务器 + +--- + +#### tunnel_next_upstream_timeout + +```nginx +tunnel_next_upstream_timeout time; +``` + +| 属性 | 说明 | +|------|------| +| **默认值** | `0`(无限制) | +| **上下文** | http, server, location | + +**说明**: +- 限制请求传递给下一台服务器的总时间 +- `0` 表示关闭限制 + +--- + +#### tunnel_next_upstream_tries + +```nginx +tunnel_next_upstream_tries number; +``` + +| 属性 | 说明 | +|------|------| +| **默认值** | `0`(无限制) | +| **上下文** | http, server, location | + +**说明**: +- 限制传递给下一台服务器的尝试次数 +- `0` 表示关闭限制 + +--- + +## 3. TCP 隧道配置示例 + +### 3.1 基础 HTTP 代理 + +```nginx +http { + # 定义允许的端口 + map $request_port $allow_port { + 443 1; + default 0; + } + + # 定义允许的域名 + map $host $allow_host { + hostnames; + example.org 1; + *.example.org 1; + default 0; + } + + server { + listen 8000; + resolver dns.example.com; + + # 权限检查 + if ($allow_port != 1) { + return 502; + } + + if ($allow_host != 1) { + return 502; + } + + # 启用隧道穿透 + tunnel_pass; + } +} +``` + +**客户端使用**: +```bash +curl -x http://nginx:8000 https://example.org +``` + +--- + +### 3.2 带上游服务器的 TCP 隧道 + +```nginx +http { + upstream backend_pool { + server 10.0.0.1:8443; + server 10.0.0.2:8443; + server 10.0.0.3:8443; + } + + server { + listen 8000; + + location / { + # 所有 CONNECT 请求转发到上游池 + tunnel_pass backend_pool; + + # 故障转移配置 + tunnel_next_upstream error timeout; + tunnel_next_upstream_tries 3; + tunnel_connect_timeout 30s; + + # 本地绑定地址 + tunnel_bind $server_addr; + } + } +} +``` + +--- + +### 3.3 动态目标路由 + +```nginx +http { + map $http_x_target_host $tunnel_target { + default $host:$request_port; + api.internal 10.0.0.100:8443; + db.internal unix:/var/run/db.sock; + } + + server { + listen 8000; + + location / { + tunnel_pass $tunnel_target; + tunnel_connect_timeout 10s; + tunnel_read_timeout 300s; + } + } +} +``` + +--- + +## 4. WebSocket 隧道配置 + +**注意**: HTTP Tunnel 模块主要用于 TCP 隧道。WebSocket 支持取决于具体实现。 + +### 4.1 WebSocket 代理配置 + +```nginx +http { + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + upstream websocket_backend { + server 127.0.0.1:9000; + } + + server { + listen 8000; + + location /ws/ { + # WebSocket 升级 + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + # 隧道穿透(如后端支持 CONNECT) + tunnel_pass websocket_backend; + + # 长连接超时 + tunnel_read_timeout 3600s; + tunnel_send_timeout 3600s; + } + } +} +``` + +--- + +## 5. 与 stream 模块的区别 + +| 特性 | HTTP Tunnel Module | Stream Module | +|------|-------------------|---------------| +| **层级** | HTTP 层 (L7) | 传输层 (L4) | +| **协议** | 处理 HTTP CONNECT 请求 | 原始 TCP/UDP | +| **授权** | 商业订阅 | 开源免费 | +| **配置块** | `http`/`server`/`location` | `stream`/`server` | +| **路由能力** | 基于 HTTP 头、Host 等 L7 信息 | 仅基于 IP/端口 | +| **变量支持** | 丰富的 HTTP 变量 | 有限的 stream 变量 | +| **访问控制** | 可基于域名、端口、自定义条件 | 基于 IP 的 allow/deny | +| **典型用途** | HTTP 代理、HTTPS 穿透 | 数据库代理、TCP 负载均衡 | + +### 5.1 stream 模块示例对比 + +```nginx +# stream 模块 (L4 层) +stream { + server { + listen 8443; + proxy_pass backend_pool; + } +} + +# tunnel 模块 (L7 层,处理 CONNECT) +http { + server { + listen 8000; + location / { + tunnel_pass backend_pool; + } + } +} +``` + +--- + +## 6. 与 Lolly 项目的关系和建议 + +### 6.1 Lolly 项目概述 + +Lolly 是一个 Go 语言实现的高性能网络代理/隧道项目,专注于: +- 轻量级部署 +- 简洁的配置 +- 高性能转发 + +### 6.2 功能对比 + +| 特性 | NGINX Tunnel | Lolly | +|------|-------------|-------| +| **CONNECT 支持** | 原生支持 | 需确认实现 | +| **配置复杂度** | 高(多指令组合) | 低(简洁配置) | +| **动态路由** | 支持变量 | 需确认 | +| **故障转移** | 完善的上游故障转移 | 需确认 | +| **性能** | 高(C 语言优化) | 高(Go 并发模型) | +| **可观测性** | 标准日志 | 可定制 | +| **扩展性** | 模块扩展 | 代码扩展 | + +### 6.3 对 Lolly 的建议 + +#### 6.3.1 参考设计 + +1. **CONNECT 方法处理**: 参考 `tunnel_pass` 的默认行为 `$host:$request_port` +2. **条件访问控制**: 实现类似 `tunnel_allow_upstream` 的灵活条件评估 +3. **超时分层**: 区分连接、读取、发送超时,默认值参考 NGINX + +#### 6.3.2 差异化优势 + +1. **简化配置**: Lolly 可提供更简洁的单行配置实现常见场景 +2. **原生可观测性**: 内置 pprof、指标导出(已支持 pprof 端点) +3. **动态重载**: Go 的热重载比 NGINX 更友好 + +#### 6.3.3 建议新增功能 + +```yaml +# 建议的 Lolly 配置格式 +tunnel: + enable: true + default_target: "$host:$port" # 类似 tunnel_pass + allowed_ports: [443, 8443] # 类似 map $allow_port + allowed_hosts: ["*.example.com"] + timeouts: + connect: 60s + read: 300s + send: 60s + bind_address: "0.0.0.0" # 类似 tunnel_bind + keepalive: true # 类似 tunnel_socket_keepalive +``` + +#### 6.3.4 实现优先级 + +| 优先级 | 功能 | 参考 NGINX 指令 | +|--------|------|---------------| +| P0 | 基础 CONNECT 处理 | `tunnel_pass` | +| P0 | 访问控制(端口/域名) | `tunnel_allow_upstream` | +| P1 | 连接超时 | `tunnel_connect_timeout` | +| P1 | 读写超时 | `tunnel_read_timeout` / `tunnel_send_timeout` | +| P2 | 本地地址绑定 | `tunnel_bind` | +| P2 | TCP Keepalive | `tunnel_socket_keepalive` | +| P3 | 上游故障转移 | `tunnel_next_upstream` | + +--- + +## 附录 + +### A. 完整配置模板 + +```nginx +http { + # 1. 访问控制映射 + map $request_port $tunnel_allow_port { + 443 1; + 8443 1; + default 0; + } + + map $host $tunnel_allow_host { + hostnames; + example.com 1; + *.example.com 1; + default 0; + } + + # 2. 代理服务器 + server { + listen 8000; + server_name proxy.example.com; + + # DNS 解析 + resolver 8.8.8.8 8.8.4.4 valid=30s; + + # 访问控制 + if ($tunnel_allow_port != 1) { + return 403; + } + + if ($tunnel_allow_host != 1) { + return 403; + } + + # 隧道配置 + tunnel_pass $host:$request_port; + + # 超时配置 + tunnel_connect_timeout 30s; + tunnel_read_timeout 300s; + tunnel_send_timeout 60s; + + # 缓冲配置 + tunnel_buffer_size 32k; + + # Keepalive + tunnel_socket_keepalive on; + + # 日志 + access_log /var/log/nginx/tunnel_access.log; + error_log /var/log/nginx/tunnel_error.log; + } +} +``` + +### B. 调试命令 + +```bash +# 测试 CONNECT 请求 +curl -v -x http://nginx:8000 https://example.org + +# 查看连接状态 +nginx -T | grep tunnel + +# 监控日志 +tail -f /var/log/nginx/tunnel_access.log +tail -f /var/log/nginx/tunnel_error.log +``` + +### C. 参考资料 + +- [NGINX 官方文档](https://nginx.org/en/docs/http/ngx_http_tunnel_module.html) +- [RFC 9110 - CONNECT 方法](https://datatracker.ietf.org/doc/html/rfc9110#section-9.3.6) +- [F5 NGINX 商业订阅](https://www.f5.com/products/nginx)