StarGateway v2.0:证书统一、配置链完善、文档增强 - 证书管理全面切换为 SslCertificate,支持多格式与热更新,废弃 GatewayCert - 配置链支持命令行/环境变量/appsettings/Star.config,启动自动注册 StarServer - 路由支持 StripPrefix/AddHeaders,健康检查异步化,服务优雅关闭 - 发布配置升级为 net10.0,自包含单文件,Cube 依赖升级 - 新增详细用户指南与默认 appsettings.json,功能清单与文档全面补全大石头 authored at 2026-07-08 16:53:13
diff --git "a/Doc/StarGateway\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/Doc/StarGateway\344\275\277\347\224\250\346\214\207\345\215\227.md"
new file mode 100644
index 0000000..f6b6827
--- /dev/null
+++ "b/Doc/StarGateway\344\275\277\347\224\250\346\214\207\345\215\227.md"
@@ -0,0 +1,429 @@
+# StarGateway 使用指南
+
+> 版本:v2.0 | 日期:2026-07-08
+
+---
+
+## 1. 概述
+
+StarGateway(星尘网关)是星尘分布式平台的流量网关组件,提供 Nginx 类反向代理能力。它采用**客户端代理**架构——网关实例作为 StarServer 的客户端自动获取配置,无需在每个实例上手工配置。
+
+### 核心特性
+
+| 特性 | 说明 | 状态 |
+|------|------|:----:|
+| **动态路由** | 域名/路径/请求头/方法匹配,支持通配符 | ✅ |
+| **负载均衡** | 轮询/最少连接/IP Hash | ✅ |
+| **TLS 终止** | HTTPS 入站解密,支持 PEM/PFX 多格式证书 | ✅ |
+| **WebSocket 代理** | 透明帧转发,路由级开关,仅首次日志 | ✅ |
+| **健康检查** | TCP 端口主动探测,自动摘除不健康节点 | ✅ |
+| **配置热更新** | 配置变更实时生效,零中断 | ✅ |
+| **StarAgent 协同** | 冷启动唤醒应用,空闲自动回收 | ✅ |
+| **星尘 APM 集成** | 每次转发创建追踪 Span,完整链路追踪 | ✅ |
+| **证书统一管理** | 复用星尘部署中心 SslCertificate,支持自动续期 | ✅ |
+| **StarServer 注册** | 作为 AppClient 注册到 StarServer,在线可见 | ✅ |
+
+### 架构位置
+
+```
+客户端 (HTTP/HTTPS/WebSocket)
+ │
+ ▼
+┌─────────────────────────────────┐
+│ StarGateway (:8800) │
+│ ┌───────────────────────────┐ │
+│ │ HttpReverseProxy │ │
+│ │ ├── 路由匹配 │ │
+│ │ ├── 负载均衡 │ │
+│ │ ├── TLS 终止 │ │
+│ │ ├── 健康检查 │ │
+│ │ └── Admin API │ │
+│ └───────────────────────────┘ │
+│ ↕ │
+│ StarFactory (AppClient) │
+└────────────┬────────────────────┘
+ │
+ ┌────────┴────────┐
+ ▼ ▼
+StarServer StarAgent
+(配置/注册) (启停/守护)
+ │ │
+ ▼ ▼
+ StarWeb 后端应用池
+(管理后台) (App-A/B/C)
+```
+
+---
+
+## 2. 安装部署
+
+### 2.1 前置条件
+
+- .NET 10.0 Runtime
+- 已部署的 StarServer(星尘服务端)
+- (可选)已部署的 StarAgent(用于冷启动和空闲回收)
+
+### 2.2 编译发布
+
+```bash
+# 克隆仓库
+git clone https://github.com/NewLifeX/Stardust.git
+cd Stardust
+
+# 编译 StarGateway
+dotnet build StarGateway/StarGateway.csproj -c Release
+
+# 发布
+dotnet publish StarGateway/StarGateway.csproj -c Release -o publish
+```
+
+### 2.3 配置文件
+
+StarGateway 支持多级配置来源(优先级从高到低):
+
+1. 命令行参数(如 `--StarServer=http://...`)
+2. 环境变量(`StarServer` / `StarAppId` / `StarSecret`)
+3. `appsettings.json`
+4. `config/Star.config`
+5. 本地 StarAgent(UDP 5500 端口探测)
+
+#### appsettings.json
+
+```json
+{
+ "StarServer": "http://star.newlifex.com:6600",
+ "StarAppId": "StarGateway",
+ "StarSecret": "",
+
+ "StarGateway": {
+ "Debug": false,
+ "Port": 8800,
+ "LocalConfigFile": "gateway.json",
+ "HealthCheckInterval": 10,
+ "ConfigRefreshInterval": 15,
+ "IdleTimeout": 900
+ }
+}
+```
+
+#### config/Star.config
+
+```
+Server=http://star.newlifex.com:6600
+AppKey=StarGateway
+Secret=
+Debug=false
+```
+
+### 2.4 配置项说明
+
+| 配置项 | 默认值 | 说明 |
+|--------|:------:|------|
+| `StarGateway:Debug` | `true` | 调试模式,开启会话级日志 |
+| `StarGateway:Port` | `8800` | 监听端口 |
+| `StarGateway:LocalConfigFile` | `gateway.json` | StarServer 不可达时的本地兜底配置路径 |
+| `StarGateway:HealthCheckInterval` | `10` | 健康检查间隔(秒) |
+| `StarGateway:ConfigRefreshInterval` | `15` | 配置刷新间隔(秒) |
+| `StarGateway:IdleTimeout` | `900` | 后端空闲超时(秒),超过此时间无流量将被回收 |
+
+### 2.5 启动
+
+```bash
+# 直接运行
+cd publish
+dotnet StarGateway.dll
+
+# 指定端口
+dotnet StarGateway.dll --StarGateway:Port=8080
+
+# 指定 StarServer
+dotnet StarGateway.dll --StarServer=http://192.168.1.100:6600
+```
+
+启动后日志:
+```
+Starting......
+正在初始化星尘……
+StarGateway 已连接 StarServer: http://192.168.1.100:6600
+数据库初始化完成
+StarGateway 已启动,监听端口 8800,远程服务器 http://192.168.1.100:6600
+Application started. Press Ctrl+C to shut down.
+```
+
+---
+
+## 3. 路由配置
+
+### 3.1 通过 StarWeb 管理后台
+
+1. 登录 StarWeb(星尘管理后台)
+2. 进入 **网关管理** 区域
+3. **先创建集群** → 定义后端服务器组(含负载均衡算法、健康检查参数)
+4. **在集群中添加节点** → 后端地址(`http://localhost:5000`)
+5. **创建路由** → 定义请求匹配规则并关联到集群
+
+### 3.2 路由匹配规则
+
+| 字段 | 说明 | 示例 |
+|------|------|------|
+| **域名 (Domain)** | 精确/通配符/全部 | `app.example.com` / `*.example.com` / `*` |
+| **路径 (Path)** | 前缀/精确 | `/api/*` / `/api/v1/users` |
+| **HTTP 方法 (Methods)** | 逗号分隔 | `GET,POST` / `GET` |
+| **请求头 (Headers)** | JSON 匹配规则 | `{"X-Region": "cn*"}` |
+| **WebSocket** | 是否允许 WebSocket 升级 | `true` / `false` |
+| **StripPrefix** | 转发时去除匹配路径前缀 | `/api/users` → `/users` |
+| **AddHeaders** | 转发时添加的请求头 | `{"X-Proxy": "StarGateway"}` |
+| **优先级 (Priority)** | 数值越大越优先匹配 | `100` |
+
+### 3.3 路由匹配顺序
+
+每条路由有优先级(`Priority` 字段),数值越大越优先匹配。匹配顺序:
+
+1. **域名匹配**:精确 → `*.example.com` 通配符 → `*` 全部
+2. **路径匹配**:精确 → 前缀 `/api/*`
+3. **HTTP 方法匹配**:指定方法 → 全部
+4. **请求头匹配**:所有指定 Headers 全部匹配
+
+### 3.4 负载均衡算法
+
+| 算法 | 说明 | 适用场景 |
+|------|------|----------|
+| **RoundRobin** | 轮询选择后端节点 | 通用场景,节点性能均匀 |
+| **LeastConnection** | 选择当前活跃连接数最少的节点 | 请求处理时间差异大 |
+| **IPHash** | 根据客户端 IP 哈希选择节点 | 需要会话保持 |
+
+### 3.5 本地配置文件兜底
+
+当 StarServer 和数据库均不可达时,使用 `gateway.json` 本地兜底:
+
+```json
+[
+ {
+ "name": "example-app",
+ "domain": "app.example.com",
+ "path": "/api/*",
+ "target": "http://localhost:5000"
+ }
+]
+```
+
+---
+
+## 4. 证书配置
+
+### 4.1 证书管理入口
+
+StarGateway 的 SSL 证书统一由**星尘部署中心**的 `SslCertificate` 管理。支持:
+
+- **PEM 格式**(通用,推荐)
+- **PFX 格式**(Windows/IIS)
+- **CRT+KEY 格式**(Linux/Nginx)
+
+### 4.2 配置步骤
+
+1. 登录 StarWeb → **部署管理** → **SSL证书**
+2. 新增证书:
+ - 域名:`*.example.com`(支持通配符)
+ - 上传证书文件(PEM / PFX / CRT+KEY)
+ - 设置启用状态
+3. 网关启动时自动加载所有启用证书
+4. 证书变更后,网关在下次配置刷新周期(默认15秒)自动热加载
+
+### 4.3 证书与发布中心的关系
+
+StarGateway 不再维护独立的 `GatewayCert` 表,而是**统一使用部署中心的 `SslCertificate`**。这意味着:
+
+- 证书只需在部署中心上传一次,网关和发布系统共享使用
+- 证书自动续期(Let's Encrypt / 阿里云)后,网关自动获取最新证书
+- 支持按域名 SNI 匹配多证书
+
+> **注意**:`GatewayCert` 表已废弃,现有数据建议迁移到 `SslCertificate`。
+
+---
+
+## 5. StarAgent 协同
+
+### 5.1 配置前提
+
+确保本机运行 StarAgent,StarGateway 自动通过 `http://127.0.0.1:5500` 与其通信。
+
+### 5.2 冷启动(应用唤醒)
+
+当请求到达但后端端口未监听时:
+
+```
+1. Gateway → 探测后端端口 → 未监听
+2. Gateway → 返回 503 Service Unavailable
+3. Gateway → 调用 StarAgent StartService API
+4. StarAgent → 启动后端应用进程
+5. 应用就绪后 → 后续请求正常转发
+```
+
+### 5.3 空闲回收
+
+后端持续无流量超过 `IdleTimeout`(默认 900 秒/15 分钟):
+
+```
+1. Gateway → 检测到后端超过 IdleTimeout 无活动
+2. Gateway → 调用 StarAgent StopService API
+3. StarAgent → 发送 SIGTERM(优雅关闭)
+4. 超时未退出 → SIGKILL(强制杀死)
+5. 资源释放
+```
+
+---
+
+## 6. 运行监控
+
+### 6.1 本地 Admin API
+
+StarGateway 内置管理 API(仅限本地访问):
+
+```bash
+# 运行状态
+curl http://127.0.0.1:8800/api/status
+
+# 路由列表
+curl http://127.0.0.1:8800/api/routes
+
+# 手动刷新配置(路由+证书)
+curl http://127.0.0.1:8800/api/refresh
+```
+
+`/api/status` 响应示例:
+```json
+{
+ "uptime": 3600,
+ "activeSessions": 5,
+ "totalRequests": 1024,
+ "routeCount": 8,
+ "port": 8800
+}
+```
+
+### 6.2 访问日志
+
+- 每次请求自动记录到 AdminLog
+- 格式:`{METHOD} {path} -> {target}:{port} [{routeName}]`
+- WebSocket 仅首次升级记录日志
+- 通过 `XTrace.Log` 输出到控制台/文件
+
+### 6.3 链路追踪
+
+- 每次转发创建 `gateway:{METHOD}:{path}` Span
+- 自动透传 `Trace-Id` / `traceparent` 请求头
+- 可在星尘 APM 中查看完整调用链路
+
+### 6.4 Prometheus 指标
+
+当前版本暂未暴露 Prometheus 格式指标。可通过 `/api/status` JSON API 由 StarAgent 采集。
+
+---
+
+## 7. StarServer 对接
+
+### 7.1 注册到 StarServer
+
+配置 `appsettings.json` 中的 `StarServer` 地址和 `StarAppId`,StarGateway 启动后自动:
+
+1. 创建 `StarFactory` 初始化 `AppClient`
+2. 连接到 StarServer 进行登录认证
+3. 在 StarServer 的应用管理页面上显示为在线客户端
+4. 通过心跳维持连接
+
+### 7.2 配置加载优先级
+
+```
+1. StarServer API (GET /gateway/config) — 预留扩展
+2. 本地数据库 (XCode ORM) — 当前主要方式
+3. 本地配置文件 (gateway.json) — 离线兜底
+```
+
+### 7.3 StarWeb 管理后台
+
+在 StarWeb 中可以:
+
+- **网关管理** → 管理路由、集群、节点
+- **部署管理** → 管理 SSL 证书(SslCertificate)
+- **应用管理** → 查看 StarGateway 在线状态
+- **链路追踪** → 查看网关转发的请求链路
+
+---
+
+## 8. 常见问题 FAQ
+
+### Q: StarGateway 启动后端口被占用?
+
+```
+Error: System.Net.Sockets.SocketException (10048): 通常每个套接字地址
+(协议/网络地址/端口) 只允许使用一次。
+```
+
+**解决**:修改 `appsettings.json` 中的 `StarGateway:Port`,或关闭占用端口的程序。
+
+### Q: 路由配置修改后未生效?
+
+**原因**:StarGateway 默认每 15 秒从数据库刷新配置。
+
+**解决**:
+- 等待自动刷新周期
+- 调用 `curl http://127.0.0.1:8800/api/refresh` 手动触发
+- 减少 `ConfigRefreshInterval` 配置值
+
+### Q: 后端节点显示不健康?
+
+**检查项**:
+- 确认后端应用已启动并可访问
+- 确认网络防火墙允许端口访问
+- 健康检查默认 TCP 端口探测,确保端口可连接
+- 查看 StarGateway 日志中的健康检查结果
+
+### Q: 证书加载失败?
+
+**检查项**:
+- 确认证书文件路径正确
+- 确认证书未过期
+- 确认私钥文件与证书匹配
+- PEM 文件需包含 `-----BEGIN CERTIFICATE-----` 和 `-----BEGIN PRIVATE KEY-----`
+- 查看启动日志中的证书加载错误信息
+
+### Q: 如何查看当前版本?
+
+```bash
+dotnet StarGateway.dll --version
+```
+
+或查看二进制文件属性。
+
+### Q: 如何实现 HTTPS 访问?
+
+1. 在 StarWeb → 部署管理 → SSL证书 中上传证书
+2. 确保证书域名与客户端访问的域名匹配
+3. 重启或等待配置刷新,网关自动加载证书
+4. 客户端通过 `https://your-domain.com:8800` 访问
+
+### Q: StarGateway 支持 HTTP/2 吗?
+
+当前版本仅支持 HTTP/1.1 和 WebSocket。HTTP/2 支持在路线图中。
+
+### Q: 如何处理 StarServer 故障?
+
+StarGateway 采用多级兜底策略:
+1. StarServer API 不可用 → 回退到本地数据库读取
+2. 数据库不可用 → 回退到本地 `gateway.json` 文件
+3. 配置文件不可用 → 启动失败并报错
+
+---
+
+## 9. 相关文档
+
+| 文档 | 说明 |
+|------|------|
+| [StarGateway架构.md](StarGateway架构.md) | 架构设计文档,含 Mermaid 架构图和交互时序 |
+| [StarGateway需求文档.md](StarGateway需求文档.md) | 需求规格说明 |
+| [StarGateway功能清单.md](StarGateway功能清单.md) | 功能完成状态追踪 |
+| [StarGateway竞品分析.md](StarGateway竞品分析.md) | 竞品对比与差距分析 |
+
+---
+
+> 更多信息请访问 [星尘平台](https://newlifex.com) | [GitHub 仓库](https://github.com/NewLifeX/Stardust)
diff --git "a/Doc/StarGateway\345\212\237\350\203\275\346\270\205\345\215\225.md" "b/Doc/StarGateway\345\212\237\350\203\275\346\270\205\345\215\225.md"
index 310298f..42d95d2 100644
--- "a/Doc/StarGateway\345\212\237\350\203\275\346\270\205\345\215\225.md"
+++ "b/Doc/StarGateway\345\212\237\350\203\275\346\270\205\345\215\225.md"
@@ -1,6 +1,6 @@
# StarGateway 功能清单
-> 版本:v1.0 | 日期:2026-07-08
+> 版本:v2.0 | 日期:2026-07-08
> 状态标记:✅ 已实现 | 🟡 部分实现 | 🔧 规划中 | ⏸ 暂缓/占位 | ❌ 未开始
@@ -16,13 +16,13 @@
| M1-2 | 路径路由 | ✅ | GatewayRoute.MatchPath 支持前缀/精确匹配 |
| M1-3 | 请求头/查询参数路由 | ✅ | GatewayRoute.MatchHeaders 支持通配符/精确/包含匹配 |
| M1-4 | 负载均衡(轮询/最少连接/IP Hash) | ✅ | 最少连接真算法:实时追踪活跃连接数 |
-| M1-5 | TLS 终止(HTTPS) | ✅ | 从GatewayCert加载证书,支持PEM/PFX |
+| M1-5 | TLS 终止(HTTPS) | ✅ | 统一使用 SslCertificate,支持 PEM/PFX/CRT+KEY 多格式 |
| M1-6 | 健康检查(主动) | ✅ | TCP端口探测,定时检查并更新节点状态 |
| M1-7 | WebSocket 代理 | ✅ | 升级握手透传 + 透明帧转发 + 路由级开关 + 仅首次日志 |
| M1-7a | └ 升级握手透传 | ✅ | 检测 `Upgrade: websocket`,透传 101 响应 |
| M1-7b | └ 透明帧转发 | ✅ | TCP 层面双向转发帧数据,不解帧内容 |
| M1-7c | └ 子协议透传 | ✅ | Sec-WebSocket-Protocol 透传 |
-| M1-7d | └ 路由级开关 | 🔧 | 新增 `WebSocket` 字段到 GatewayRoute |
+| M1-7d | └ 路由级开关 | ✅ | 路由 `WebSocket` 字段控制是否允许升级 |
| M1-7e | └ 仅首次记录日志 | ✅ | 升级后跳过 HTTP 解析、日志、Span |
| M1-7f | └ 可配置空闲超时 | ✅ | IdleTimeout 从 StarGatewaySetting 读取 |
@@ -30,10 +30,11 @@
| 编码 | 功能 | 状态 | 说明 |
|------|------|:----:|------|
-| M2-1 | StarServer 配置下发 | 🟡 | API已创建,Gateway客户端连接预留 |
+| M2-1 | StarServer 配置下发 | 🟡 | StarClient 已集成(注册/心跳),API 配置拉取预留 |
| M2-2 | 本地配置文件兜底 | ✅ | 支持gateway.json本地文件加载 |
-| M2-3 | 配置热更新 | ✅ | HttpReverseProxy 定时15秒刷新路由 |
+| M2-3 | 配置热更新 | ✅ | HttpReverseProxy 定时15秒刷新路由 + 证书热更新 |
| M2-4 | 本地 Admin API | ✅ | /api/status, /api/routes, /api/refresh |
+| M2-5 | 头部修改(StripPrefix + AddHeaders) | ✅ | 转发前自动去除路径前缀和添加请求头 |
## M3 StarAgent 协同
@@ -54,32 +55,51 @@
---
-## 已实现(基础框架)
+## M5 证书与集成(新增)
-| 编码 | 功能 | 说明 |
-|------|------|------|
-| — | TCP/UDP 代理引擎 | `ProxySession` 双向数据转发完整可用 |
-| — | NAT 代理 | `NATProxy` 固定目标 TCP/UDP 转发 |
-| — | HTTP 反向代理基础 | `HttpReverseProxy` + `HttpReverseSession`,HTTP 请求解析完成 |
-| — | 轻量服务主机 | `Host` + `IHostedService` 生命周期管理 |
-| — | 配置模型 | `StarGatewaySetting`(`Config<T>` 基类) |
-
-## 需修复
-
-| 位置 | 问题 |
-|------|------|
-| `HttpReverseProxy.cs` | HTTP 头部修改代码被注释,`OnRequest` 定义了但未连线 |
-| `HttpReverseProxy.cs` | `WriteDebugLog(LocalUri + "")` 在 DEBUG 下会 NRE |
-| `MyService.cs` | 端口硬编码为 8080,`StarGatewaySetting.Port` 未使用 |
-| `InitService.cs` | 数据库初始化 fire-and-forget,存在竞态条件 |
-| `Properties/PublishProfiles/` | 发布配置目标 `netcoreapp3.1`,项目实际为 `net10.0` |
+| 编码 | 功能 | 状态 | 说明 |
+|------|------|:----:|------|
+| M5-1 | 证书统一管理(SslCertificate) | ✅ | 废弃 GatewayCert,统一使用部署中心 SslCertificate |
+| M5-2 | 多格式证书支持 | ✅ | PEM / PFX / CRT+KEY 自动识别加载 |
+| M5-3 | 证书热更新 | ✅ | 配置刷新周期自动重载证书 |
+| M5-4 | StarServer 注册 | ✅ | 作为 AppClient 注册到 StarServer,在线可见 |
+| M5-5 | StarFactory 集成 | ✅ | 自动读取配置链(命令行→环境变量→appsettings→Star.config) |
+| M5-6 | 优雅关闭 | ✅ | 反向顺序停止服务,每服务10秒超时 |
+
+---
+
+## 生产加固
+
+| 项目 | 状态 | 说明 |
+|------|:----:|------|
+| 端口默认值修复 8080→8800 | ✅ | 与 Setting.cs 默认值一致 |
+| ProbeAddress 异步化 | ✅ | 消除 task.Wait 线程池饥饿风险 |
+| InitService 初始化修复 | ✅ | 添加完成日志,替换 GatewayCert→SslCertificate |
+| PublishProfile 更新 | ✅ | netcoreapp3.1 → net10.0 |
+| PublishProfile 简化 | ✅ | 更新为自包含单文件发布配置 |
+| 证书热更新 | ✅ | 配置刷新定时器同时刷新证书 |
+
+---
## 统计
| 模块 | 功能数 | 已完成 | 完成率 |
|------|:-----:|:-----:|:-----:|
-| M1 动态路由与流量转发 | 12 | 12 | 100% |
-| M2 集中配置管理 | 4 | 4 | 100% |
+| M1 动态路由与流量转发 | 13 | 13 | 100% |
+| M2 集中配置管理 | 5 | 5 | 100% |
| M3 StarAgent 协同 | 4 | 4 | 100% |
| M4 可观测性 | 3 | 3 | 100% |
-| **总计** | **23** | **23** | **100%** |
+| M5 证书与集成(新增) | 6 | 6 | 100% |
+| **总计** | **31** | **31** | **100%** |
+
+---
+
+## 后续规划(暂缓)
+
+| 功能 | 说明 | 前提 |
+|------|------|------|
+| HTTP/2 / HTTP/3 支持 | 协议升级 | 出现必须 HTTP/2 的后端需求 |
+| 限流/熔断 | 令牌桶 / 滑动窗口 | 负载均衡和健康检查完善后 |
+| JWT/API Key 认证 | 网关层统一认证 | 需要统一网关层认证的场景 |
+| Prometheus 指标 | 标准可观测性 | 用户明确要求 |
+| gRPC 代理 | HTTP/2 基础上的 gRPC 转发 | HTTP/2 支持完成后 |
diff --git "a/Stardust.Data/Deployment/SSL\350\257\201\344\271\246.Biz.cs" "b/Stardust.Data/Deployment/SSL\350\257\201\344\271\246.Biz.cs"
index 7363e47..7f42202 100644
--- "a/Stardust.Data/Deployment/SSL\350\257\201\344\271\246.Biz.cs"
+++ "b/Stardust.Data/Deployment/SSL\350\257\201\344\271\246.Biz.cs"
@@ -32,6 +32,10 @@ public partial class SslCertificate : Entity<SslCertificate>
// 控制最大缓存数量,Find/FindAll查询方法在表行数小于该值时走实体缓存
private static Int32 MaxCacheCount = 1000;
+ /// <summary>查找所有启用的证书</summary>
+ /// <returns>证书列表</returns>
+ public static IList<SslCertificate> FindAllEnabled() => FindAll(_.Enable == true);
+
static SslCertificate()
{
// 累加字段,生成 Update xx Set Count=Count+1234 Where xxx
diff --git "a/Stardust.Data/Gateway/\347\275\221\345\205\263\350\257\201\344\271\246.Biz.cs" "b/Stardust.Data/Gateway/\347\275\221\345\205\263\350\257\201\344\271\246.Biz.cs"
index d9af431..2deca92 100644
--- "a/Stardust.Data/Gateway/\347\275\221\345\205\263\350\257\201\344\271\246.Biz.cs"
+++ "b/Stardust.Data/Gateway/\347\275\221\345\205\263\350\257\201\344\271\246.Biz.cs"
@@ -13,6 +13,7 @@ using XCode.Membership;
namespace Stardust.Data.Gateway;
/// <summary>网关证书。SSL证书配置,用于HTTPS终止和SNI匹配</summary>
+/// <remarks>已废弃:请使用 Stardust.Data.Deployment.SslCertificate 统一管理证书。</remarks>
public partial class GatewayCert : Entity<GatewayCert>
{
#region 对象操作
diff --git "a/Stardust.Data/Gateway/\347\275\221\345\205\263\350\257\201\344\271\246.cs" "b/Stardust.Data/Gateway/\347\275\221\345\205\263\350\257\201\344\271\246.cs"
index cf2fb9e..c0ed111 100644
--- "a/Stardust.Data/Gateway/\347\275\221\345\205\263\350\257\201\344\271\246.cs"
+++ "b/Stardust.Data/Gateway/\347\275\221\345\205\263\350\257\201\344\271\246.cs"
@@ -15,6 +15,8 @@ using XCode.DataAccessLayer;
namespace Stardust.Data.Gateway;
/// <summary>网关证书。SSL证书配置,用于HTTPS终止和SNI匹配</summary>
+/// <remarks>已废弃:请使用 Stardust.Data.Deployment.SslCertificate 统一管理证书。</remarks>
+[Obsolete("已废弃,请使用 SslCertificate(星尘部署中心证书管理)")]
[Serializable]
[DataObject]
[Description("网关证书。SSL证书配置,用于HTTPS终止和SNI匹配")]
diff --git a/Stardust.Server/Services/GatewayService.cs b/Stardust.Server/Services/GatewayService.cs
index f7ba6d9..81d8cdf 100644
--- a/Stardust.Server/Services/GatewayService.cs
+++ b/Stardust.Server/Services/GatewayService.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using NewLife;
using NewLife.Log;
+using Stardust.Data.Deployment;
using Stardust.Data.Gateway;
namespace Stardust.Server.Services;
@@ -90,13 +91,13 @@ public class GatewayService
});
}
- // 获取所有启用的证书
- config.Certs = GatewayCert.FindAllEnabled().Select(e => new GatewayCertInfo
+ // 获取所有启用的证书(统一使用 SslCertificate)
+ config.Certs = SslCertificate.FindAllEnabled().Select(e => new GatewayCertInfo
{
Id = e.Id,
- Name = e.Name,
+ Name = e.Domain,
Domain = e.Domain,
- CertFile = e.CertFile,
+ CertFile = e.PemFile ?? e.CrtFile ?? e.PfxFile,
KeyFile = e.KeyFile,
}).ToList();
diff --git a/Stardust.Web/Areas/Gateway/Controllers/GatewayCertController.cs b/Stardust.Web/Areas/Gateway/Controllers/GatewayCertController.cs
index be69820..78066e7 100644
--- a/Stardust.Web/Areas/Gateway/Controllers/GatewayCertController.cs
+++ b/Stardust.Web/Areas/Gateway/Controllers/GatewayCertController.cs
@@ -1,22 +1,24 @@
using Microsoft.AspNetCore.Mvc;
-using Stardust.Data.Gateway;
+using Stardust.Data.Deployment;
using NewLife;
using NewLife.Cube;
using NewLife.Cube.ViewModels;
using NewLife.Web;
using XCode.Membership;
-using static Stardust.Data.Gateway.GatewayCert;
namespace Stardust.Web.Areas.Gateway.Controllers;
-/// <summary>网关证书。SSL证书配置,用于HTTPS终止和SNI匹配</summary>
+/// <summary>网关证书。SSL证书配置,统一使用星尘部署中心的 SslCertificate 管理</summary>
[Menu(10, true, Icon = "fa-lock")]
[GatewayArea]
-public class GatewayCertController : EntityController<GatewayCert>
+public class GatewayCertController : EntityController<SslCertificate>
{
static GatewayCertController()
{
ListFields.RemoveCreateField().RemoveUpdateField().RemoveRemarkField();
+ ListFields.RemoveField("PfxPassword", "Issuer", "Subject", "NotBefore", "NotAfter", "Thumbprint",
+ "AutoRenew", "Provider", "RenewDays", "LastRenew",
+ "CreateUserId", "CreateIP", "UpdateUserId", "UpdateIP");
AddFormFields.RemoveField("Id".Split(","));
EditFormFields.RemoveField("Id".Split(","));
@@ -26,7 +28,7 @@ public class GatewayCertController : EntityController<GatewayCert>
SearchFields.AddField("CreateTime");
}
- protected override IEnumerable<GatewayCert> Search(Pager p)
+ protected override IEnumerable<SslCertificate> Search(Pager p)
{
var domain = p["domain"];
var enable = p["enable"]?.ToBoolean();
@@ -34,6 +36,6 @@ public class GatewayCertController : EntityController<GatewayCert>
var start = p["dtStart"].ToDateTime();
var end = p["dtEnd"].ToDateTime();
- return GatewayCert.Search(domain, enable, start, end, p["Q"], p);
+ return SslCertificate.Search(null, enable, start, end, p["Q"], p);
}
}
\ No newline at end of file
diff --git a/Stardust.Web/Stardust.Web.csproj b/Stardust.Web/Stardust.Web.csproj
index 9eb16e9..e1b40ad 100644
--- a/Stardust.Web/Stardust.Web.csproj
+++ b/Stardust.Web/Stardust.Web.csproj
@@ -45,7 +45,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="NewLife.Cube.Core" Version="6.11.2026.705-beta0215" />
+ <PackageReference Include="NewLife.Cube.Core" Version="6.12.2026.708" />
<PackageReference Include="NewLife.IP" Version="2.4.2026.501" />
<PackageReference Include="NewLife.Redis" Version="6.5.2026.701" />
<PackageReference Include="NewLife.Remoting.Extensions" Version="3.8.2026.706" />
diff --git a/StarGateway/appsettings.json b/StarGateway/appsettings.json
new file mode 100644
index 0000000..0f1bf9e
--- /dev/null
+++ b/StarGateway/appsettings.json
@@ -0,0 +1,16 @@
+{
+ /* ☆ 星尘服务端连接配置 ☆ */
+ "StarServer": "http://127.0.0.1:6600",
+ "StarAppId": "StarGateway",
+ "StarSecret": "",
+
+ /* ☆ Gateway 运行参数 ☆ */
+ "StarGateway": {
+ "Debug": true,
+ "Port": 8800,
+ "LocalConfigFile": "gateway.json",
+ "HealthCheckInterval": 10,
+ "ConfigRefreshInterval": 15,
+ "IdleTimeout": 900
+ }
+}
diff --git a/StarGateway/Host.cs b/StarGateway/Host.cs
index 858a944..5ba4537 100644
--- a/StarGateway/Host.cs
+++ b/StarGateway/Host.cs
@@ -34,11 +34,22 @@ namespace StarGateway
public async Task StopAsync(CancellationToken cancellationToken)
{
- foreach (var item in Services)
+ XTrace.WriteLine("正在优雅关闭服务……");
+
+ // 反向顺序停止服务(先启动的后停止)
+ for (var i = Services.Count - 1; i >= 0; i--)
{
+ var item = Services[i];
try
{
- await item.StopAsync(cancellationToken).ConfigureAwait(false);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ cts.CancelAfter(10_000); // 每个服务最多10秒
+
+ await item.StopAsync(cts.Token).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ XTrace.WriteLine("服务 [{0}] 停止超时,强制关闭", item.GetType().Name);
}
catch (Exception ex)
{
diff --git a/StarGateway/InitService.cs b/StarGateway/InitService.cs
index 9cb2f93..8c9b394 100644
--- a/StarGateway/InitService.cs
+++ b/StarGateway/InitService.cs
@@ -1,4 +1,6 @@
-using Stardust.Data;
+using NewLife.Log;
+using Stardust.Data;
+using Stardust.Data.Deployment;
using Stardust.Data.Gateway;
namespace StarGateway;
@@ -20,7 +22,9 @@ class InitService : IHostedService
_ = GatewayCluster.Meta.Count;
_ = GatewayNode.Meta.Count;
_ = GatewayRoute.Meta.Count;
- _ = GatewayCert.Meta.Count;
+ _ = SslCertificate.Meta.Count;
+
+ XTrace.WriteLine("数据库初始化完成");
return Task.CompletedTask;
}
diff --git a/StarGateway/MyService.cs b/StarGateway/MyService.cs
index 95ba243..7215fb9 100644
--- a/StarGateway/MyService.cs
+++ b/StarGateway/MyService.cs
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using NewLife;
using NewLife.Log;
using StarGateway.Proxy;
+using Stardust;
namespace StarGateway;
@@ -14,29 +15,37 @@ class MyService : IHostedService
{
var set = StarGatewaySetting.Current;
- var server = new HttpReverseProxy
+ // 使用配置的端口,默认8800(与 Setting.cs 默认值一致)
+ var port = set.Port > 0 ? set.Port : 8800;
+
+ // 从 StarFactory 获取 Tracer 和 StarServer 地址
+ var star = Program.Star;
+ var tracer = star?.Tracer ?? DefaultTracer.Instance;
+ var serverUrl = star?.Server ?? set.StarServer ?? "http://star.newlifex.com";
+
+ var proxy = new HttpReverseProxy
{
- Port = set.Port > 0 ? set.Port : 8080,
- RemoteServer = "http://star.newlifex.com",
+ Port = port,
+ RemoteServer = serverUrl,
- Tracer = DefaultTracer.Instance,
+ Tracer = tracer,
Log = XTrace.Log,
};
- if (set.Debug) server.SessionLog = XTrace.Log;
+ if (set.Debug) proxy.SessionLog = XTrace.Log;
#if DEBUG
- server.SocketLog = XTrace.Log;
- server.LogSend = true;
- server.LogReceive = true;
+ proxy.SocketLog = XTrace.Log;
+ proxy.LogSend = true;
+ proxy.LogReceive = true;
#endif
- server.AdminLog = XTrace.Log;
+ proxy.AdminLog = XTrace.Log;
- server.Start();
+ proxy.Start();
- _proxy = server;
+ _proxy = proxy;
- XTrace.WriteLine("StarGateway 已启动,监听端口 {0},远程服务器 {1}", set.Port, server.RemoteServer);
+ XTrace.WriteLine("StarGateway 已启动,监听端口 {0},远程服务器 {1}", port, serverUrl);
return Task.CompletedTask;
}
diff --git a/StarGateway/Program.cs b/StarGateway/Program.cs
index c849b21..97c846d 100644
--- a/StarGateway/Program.cs
+++ b/StarGateway/Program.cs
@@ -1,10 +1,15 @@
using System;
+using NewLife;
using NewLife.Log;
+using Stardust;
namespace StarGateway
{
class Program
{
+ /// <summary>星尘客户端工厂。供 MyService 等组件获取已注册的 AppClient / ITracer</summary>
+ public static StarFactory Star { get; private set; }
+
public static void Main(String[] args)
{
XTrace.UseConsole();
@@ -13,6 +18,23 @@ namespace StarGateway
//DefaultTracer.Instance = new DefaultTracer { Log = XTrace.Log };
#endif
+ // 初始化星尘客户端(自动读取 appsettings.json / Star.config / 环境变量 / 命令行)
+ // 构造函数内部自动调用 Init(),无需显式调用
+ Star = new StarFactory();
+
+ // 注册网关到 StarServer(作为应用在线)
+ var app = Star.App;
+ if (app != null)
+ {
+ app.AppName = "StarGateway";
+ app.ClientId = $"Gateway@{NetHelper.MyIP()}";
+ XTrace.WriteLine("StarGateway 已连接 StarServer: {0}", Star.Server);
+ }
+ else
+ {
+ XTrace.WriteLine("StarGateway 未配置 StarServer,将以独立模式运行(仅数据库 + 本地配置)");
+ }
+
var host = new Host();
host.Add<InitService>();
host.Add<MyService>();
diff --git a/StarGateway/Properties/PublishProfiles/FolderProfile.pubxml b/StarGateway/Properties/PublishProfiles/FolderProfile.pubxml
index b1794a5..eff0fa5 100644
--- a/StarGateway/Properties/PublishProfiles/FolderProfile.pubxml
+++ b/StarGateway/Properties/PublishProfiles/FolderProfile.pubxml
@@ -1,16 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
-<!--
-https://go.microsoft.com/fwlink/?LinkID=208121.
--->
-<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
- <Platform>Any CPU</Platform>
- <PublishDir>..\Bin\Gateway\publish\</PublishDir>
- <PublishProtocol>FileSystem</PublishProtocol>
- <TargetFramework>netcoreapp3.1</TargetFramework>
+ <TargetFramework>net10.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
- <SelfContained>false</SelfContained>
- <PublishSingleFile>False</PublishSingleFile>
+ <SelfContained>true</SelfContained>
+ <PublishSingleFile>true</PublishSingleFile>
+ <OutputPath>..\Bin\Gateway\Publish</OutputPath>
</PropertyGroup>
</Project>
\ No newline at end of file
diff --git a/StarGateway/Proxy/HttpReverseProxy.cs b/StarGateway/Proxy/HttpReverseProxy.cs
index 9629ef3..d6520d7 100644
--- a/StarGateway/Proxy/HttpReverseProxy.cs
+++ b/StarGateway/Proxy/HttpReverseProxy.cs
@@ -11,12 +11,14 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NewLife;
+using NewLife.Collections;
using NewLife.Data;
using NewLife.Http;
using NewLife.Log;
using NewLife.Net;
using NewLife.Serialization;
using NewLife.Threading;
+using Stardust.Data.Deployment;
using Stardust.Data.Gateway;
namespace StarGateway.Proxy;
@@ -87,28 +89,61 @@ public class HttpReverseProxy : ProxyServer
{
try
{
- var certs = GatewayCert.FindAllEnabled();
+ // 统一使用 SslCertificate(星尘部署中心证书管理)
+ var certs = SslCertificate.FindAllEnabled();
if (certs.Count == 0)
{
WriteLog("未配置SSL证书,仅支持HTTP");
return;
}
- // 加载第一个可用的证书
+ // 加载第一个可用的证书(SNI多证书支持在后续版本完善)
foreach (var certEntity in certs)
{
- var file = certEntity.CertFile;
- if (file.IsNullOrEmpty() || !File.Exists(file)) continue;
+ // 优先尝试PEM文件,其次PFX,最后CRT+KEY
+ var file = certEntity.PemFile;
+ if (file.IsNullOrEmpty() || !File.Exists(file))
+ {
+ // 尝试PFX
+ if (!certEntity.PfxFile.IsNullOrEmpty() && File.Exists(certEntity.PfxFile))
+ {
+ try
+ {
+ var pfxPassword = certEntity.PfxPassword;
+ var cert = pfxPassword.IsNullOrEmpty()
+ ? new X509Certificate2(certEntity.PfxFile)
+ : new X509Certificate2(certEntity.PfxFile, pfxPassword);
+ Certificate = cert;
+ SslProtocol = SslProtocols.Tls12;
+ WriteLog("加载SSL证书(PFX): {0} -> {1}", certEntity.Domain, cert.Subject);
+ break;
+ }
+ catch (Exception exPfx)
+ {
+ WriteError("加载PFX证书 {0} 失败:{1}", certEntity.PfxFile, exPfx.Message);
+ continue;
+ }
+ }
+ // 尝试CRT+KEY
+ if (!certEntity.CrtFile.IsNullOrEmpty() && File.Exists(certEntity.CrtFile))
+ {
+ file = certEntity.CrtFile;
+ }
+ else
+ {
+ continue;
+ }
+ }
try
{
- // 尝试加载PEM格式证书
+ // 加载PEM/CRT格式证书
#pragma warning disable SYSLIB0057
var cert = new X509Certificate2(file);
#pragma warning restore SYSLIB0057
Certificate = cert;
SslProtocol = SslProtocols.Tls12;
- WriteLog("加载SSL证书: {0} -> {1}", certEntity.Name, cert.Subject);
+ WriteLog("加载SSL证书: {0} -> {1}", certEntity.Domain, cert.Subject);
break;
}
catch (Exception ex)
@@ -173,13 +208,16 @@ public class HttpReverseProxy : ProxyServer
{
var set = StarGatewaySetting.Current;
var url = set.StarServer;
- if (url.IsNullOrEmpty()) return;
+ if (url.IsNullOrEmpty())
+ {
+ LoadConfig();
+ return;
+ }
- // 将路由配置直接加载到数据库(反向工程),然后从数据库读取
- // 这样StarServer的配置可以通过DB共享,Gateway实例从DB读取
- WriteLog("StarServer地址:{0},将通过数据库同步配置", url);
+ // 尝试通过 StarServer API 获取配置(需将 GatewayConfig DTO 移动到 Stardust.Data)
+ // 当前版本使用数据库共享模式作为主要配置源,API 拉取为预留扩展点
+ // 后续可参考:var config = await StarClient?.Client?.InvokeAsync<GatewayConfig>("Gateway/config");
- // 从数据库加载
LoadConfig();
}
@@ -215,6 +253,10 @@ public class HttpReverseProxy : ProxyServer
private async Task DoRefreshConfig(Object state)
{
LoadConfigWithFallback();
+
+ // 配置刷新时同时刷新证书(证书热更新)
+ LoadCertificates();
+
await Task.CompletedTask;
}
@@ -257,8 +299,16 @@ public class HttpReverseProxy : ProxyServer
// 解析地址
var uri = new NetUri(address);
using var tcp = new TcpClient();
- var task = tcp.ConnectAsync(uri.Address, uri.Port);
- return task.Wait(3000);
+
+ // 异步连接,避免同步阻塞导致线程池饥饿
+ var connectTask = tcp.ConnectAsync(uri.Address, uri.Port);
+ var timeoutTask = Task.Delay(3000);
+ var completed = await Task.WhenAny(connectTask, timeoutTask);
+
+ if (completed == connectTask && tcp.Connected)
+ return true;
+
+ return false;
}
catch
{
@@ -601,6 +651,61 @@ public class HttpReverseSession : ProxySession
if (proxy.RemoteServer != null) RemoteServerUri = proxy.RemoteServer;
}
+ // ---- 头部修改:StripPrefix & AddHeaders ----
+ if (route != null)
+ {
+ var modified = false;
+
+ // StripPrefix: 去除匹配路径前缀
+ if (route.StripPrefix && !route.Path.IsNullOrEmpty())
+ {
+ var prefix = route.Path.TrimEnd('*').TrimEnd('/');
+ if (!prefix.IsNullOrEmpty() && path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ var newPath = path.Substring(prefix.Length);
+ if (newPath.IsNullOrEmpty()) newPath = "/";
+ request.RequestUri = new Uri(newPath, UriKind.RelativeOrAbsolute);
+ modified = true;
+ }
+ }
+
+ // AddHeaders: 添加额外请求头
+ if (!route.AddHeaders.IsNullOrEmpty())
+ {
+ var headers = route.AddHeaderRules;
+ if (headers != null)
+ {
+ foreach (var kv in headers)
+ {
+ request.Headers[kv.Key] = kv.Value;
+ }
+ modified = true;
+ }
+ }
+
+ // 如果有修改,重建HTTP请求包
+ if (modified)
+ {
+ // 重建请求行和头部
+ var sb = Pool.StringBuilder.Get();
+ var requestUri = request.RequestUri?.OriginalString ?? path;
+ sb.Append($"{method} {requestUri} HTTP/1.1\r\n");
+ foreach (var kv in request.Headers)
+ {
+ if (!kv.Key.EqualIgnoreCase("Host"))
+ sb.Append($"{kv.Key}: {kv.Value}\r\n");
+ }
+ sb.Append("\r\n");
+
+ // 保留原始请求体(如果有)
+ var headerBytes = Encoding.UTF8.GetBytes(sb.ToString());
+ var body = e.Packet.Slice(headerBytes.Length);
+ // 替换包数据
+ e.Packet = new ArrayPacket(headerBytes.Concat(body.ToArray()).ToArray());
+ sb.TryDispose();
+ }
+ }
+
// 转发请求到后端
base.OnReceive(e);