## 云同步设置 — 服务端化改造方案 ### 1. 当前架构概述 #### 1.1 项目层次关系 ``` src/ ├── Hua.Todo.Application/ ← 共享层(服务端 & 客户端公用) │ ├── CloudSync/ ← 云同步业务逻辑 │ │ ├── Auth/ ← 认证/授权 │ │ ├── Models/ ← DTO(纯数据合约,两端可用) │ │ └── Services/ ← 业务服务 │ ├── Data/ ← EF Core DbContext │ └── DynamicApi/ ← 动态 API 中间件 │ ├── Hua.Todo.Host/ ← ASP.NET 服务端(云同步接口的提供方) │ └── Program.cs │ ├── AddApplicationServices() ← Todo CRUD 服务 │ └── AddCloudSyncServer() ← 云同步认证/授权/端点 **仅服务端** │ └── Hua.Todo.Maui/ ← 桌面/移动客户端(嵌入 WebView) └── MauiProgram.cs └── AddApplicationServices() ← **仅** Todo CRUD 服务 (不注册 AddCloudSyncServer,不暴露云同步端点) ``` #### 1.2 两种部署模式 **模式 A:Host 独立服务端(开发/部署)** ``` ┌───────────────────────────────┐ │ Vue 前端 (Vite :5174) │ │ cloudClient │ │ baseURL = serverUrl(外部) │ 直连外部 │ → /auth/login │ ────────→ 外部云同步服务 │ probeReachability() │ 直连外部 │ → fetch(serverUrl) │ ────────→ ├───────────────────────────────┤ │ proxy /api │ ├───────────────────────────────┤ │ ASP.NET Host (:5173) │ │ AddApplicationServices() │ Todo CRUD │ AddCloudSyncServer() │ 云同步端点 ← **服务端** │ ├─ /api/* (本地 Todo) │ │ └─ /auth/* (云同步) │ │ /tasks/* │ │ /cloud-sync/probe (新增) │ └───────────────────────────────┘ ``` **模式 B:MAUI 嵌入式 WebView(桌面端/移动端)** ``` ┌───────────────────────────────┐ │ MAUI WebView │ │ ┌─────────────────────────┐ │ │ │ Vue 前端 (静态托管) │ │ │ │ cloudClient │ │ │ │ baseURL = 同源 │ │ 同源请求 │ │ → /api/* (本地 Todo) │──┼───────→ Embedded WebServer │ │ → /auth/* (❌ 不可用) │ │ (不暴露云同步端点) │ └─────────────────────────┘ │ ├───────────────────────────────┤ │ Embedded WebServer (:5057) │ │ AddApplicationServices() │ 仅 Todo CRUD │ AddCloudSyncServer() │ ❌ 未注册 │ UseDynamicApi() │ 仅本地 Todo API │ MapCloudSyncEndpoints() │ ❌ 未映射 └───────────────────────────────┘ ``` > **关键区别**:`Hua.Todo.Application` 被两方引用,但云同步能力**仅在 Host 端通过 `AddCloudSyncServer()` 激活**。MAUI 端只使用 `AddApplicationServices()` 获取 Todo CRUD 能力,**不暴露也不应暴露云同步端点**。 #### 1.3 核心问题 1. 前端的"保存并探测"使用**浏览器端直接 `fetch()`** 探测外部 URL,受 CORS、证书、网络隔离限制。 2. 前端的"登录"通过 `cloudClient` 直连外部 `serverUrl`,session token 存在前端 `sessionStorage`,与 Host 脱节。 3. 在 Host 模式下,Host 本身就是云同步服务,但前端并不知道,额外配置了外部 `serverUrl`。 ### 2. 改造目标 | 操作 | 当前 | 目标 | |---|---|---| | **保存并探测** | 浏览器 `fetch()` 直连 | 前端 → Host 接口 → Application CloudProbeService → 返回结果 | | **登录** | 前端直连 `serverUrl` | 前端 → Host `/auth/login` | | **后续云同步请求** | `cloudClient` 直连 `serverUrl` | 统一走 Host(同源请求) | ### 3. 变更详情 #### 3.1 新增:服务端探测接口 > **注意**:`CloudProbeService` 和服务端点注册均在 `AddCloudSyncServer()` 路径下,MAUI 端不受影响。 **Application 层新增 `CloudProbeService`** 位置:`src/Hua.Todo.Application/CloudSync/Services/CloudProbeService.cs` 职责:接收目标 URL,从服务端发起 HTTP 探测,返回探测结果。 **新增 DTO**(位置:`src/Hua.Todo.Application/CloudSync/Models/AdminDtos.cs`,或新建 `CloudSync/Models/ProbeDtos.cs`) DTO 是纯数据合约,放在 Application 共享层无副作用,MAUI 端即使不调用也不会产生依赖。 ```csharp /// /// 服务端探测请求。 /// public class ProbeRequest { public string TargetUrl { get; set; } = string.Empty; } /// /// 服务端探测响应。 /// public class ProbeResponse { public bool IsReachable { get; set; } public int? HttpStatus { get; set; } public bool IsHttps { get; set; } public string Title { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; /// /// success | warn | error /// public string Type { get; set; } = "error"; } ``` **DI 注册(`CloudSyncServiceCollectionExtensions.AddCloudSyncServer()`)** 在 `AddCloudSyncServer()` 方法末尾追加,确保 `CloudProbeService` 仅在服务端 DI 容器中可用: ```csharp // CloudSyncServiceCollectionExtensions.AddCloudSyncServer() 末尾新增: services.AddHttpClient("ProbeClient", client => { client.DefaultRequestHeaders.UserAgent.ParseAdd("HuaTodo-Probe/1.0"); }); services.AddScoped(); ``` > **为什么放在 `AddCloudSyncServer()` 而非 `AddApplicationServices()`**: > - `AddApplicationServices()` 被 MAUI 端调用,MAUI 不需要也不会启动云同步探测 > - `AddCloudSyncServer()` 仅在 Host 的 `Program.cs` 中被调用,确保服务仅注册在服务端 **端点注册(`CloudSyncEndpointExtensions.MapCloudSyncEndpoints()`)** 在 `MapCloudSyncEndpoints()` 方法内部新增端点组: ```csharp var probe = app.MapGroup("/cloud-sync").WithTags("CloudSync - Setup"); probe.MapPost("/probe", ProbeAsync).AllowAnonymous(); ``` 端点实现: ```csharp private static async Task ProbeAsync( ProbeRequest request, CloudProbeService probeService, CancellationToken cancellationToken) { if (request == null || string.IsNullOrWhiteSpace(request.TargetUrl)) { return CloudApiErrors.BadRequest("TargetUrl is required."); } var result = await probeService.ProbeAsync(request.TargetUrl, cancellationToken); return Results.Json(result); } ``` **前端修改** `src/Hua.Todo.Web/src/components/CloudSyncSettingsDialog.vue` 删除 `probeReachability()` 函数(原 L235-L280),`saveServerUrl()` 中改为: ```typescript // 旧:前端直接 fetch // probeResult.value = await probeReachability(serverUrlSaved.value); // 新:调用 Host 的探测接口 const response = await cloudClient.post('/cloud-sync/probe', { targetUrl: serverUrlSaved.value, }); probeResult.value = { type: response.data.type as ProbeType, title: response.data.title, desc: response.data.description, }; ``` #### 3.2 修改:登录统一走 Host **核心思路**:Host 自身就是云同步服务方,`/auth/login` 已实现完整认证。前端只需改为走 Host 同源请求。 **前端 `cloudClient.ts` 改造**(`src/Hua.Todo.Web/src/api/cloudClient.ts`) 移除对外部 `serverUrl` 的依赖: ```typescript // 改造前(L36-L39): // const { serverUrl } = CloudSyncStorage.loadSettings(); // if (serverUrl) { config.baseURL = serverUrl; } // 改造后:不设 baseURL,axios 使用浏览器默认同源 // cloudClient 请求直接打到当前 Host:/auth/* , /tasks/* , /cloud-sync/* ``` > **同源适配说明**: > - **Host 模式**(Vite dev):Vue 运行在 :5174,`/api` 走 proxy 到 Host(:5173),云同步端点 `/auth/*` 需配 Vite proxy 或直接用 Host 地址 > - **MAUI 模式**(WebView):Vue 部署在 MAUI 嵌入服务器同源,不需要额外代理 > - 具体方案:在 Vite dev 时增加 proxy 规则将 `/auth`、`/tasks`、`/cloud-sync` 也 proxy 到 Host;或在 cloudClient 中按环境设置 baseURL **前端 `CloudSyncSettingsDialog.vue` 的 `login()` 无需修改**: `cloudSyncApi.login()` 调用 `cloudClient.post('/auth/login', ...)`,自动打到 Host。 **Host 端 `CloudAuthService.LoginAsync` 已有完整实现**: - 创建 `UserSessionEntity` 写入 DB(session 管理在 Host) - 返回 `LoginResponse`(`AccessToken` = DB SessionId) - Session 验证由 `SessionAuthenticationHandler` 查询 DB 完成 #### 3.3 Vite 代理配置补充 Vite dev 模式下,除了已有的 `/api` 代理,需**补充云同步端点的代理规则**(`vite.config.ts`): ```typescript server: { port: 5174, proxy: { '/api': { target: 'http://localhost:5173', changeOrigin: true, secure: false, }, // 新增:云同步端点代理到 Host '/auth': { target: 'http://localhost:5173', changeOrigin: true, secure: false, }, '/tasks': { target: 'http://localhost:5173', changeOrigin: true, secure: false, }, '/sync': { target: 'http://localhost:5173', changeOrigin: true, secure: false, }, '/security': { target: 'http://localhost:5173', changeOrigin: true, secure: false, }, '/cloud-sync': { target: 'http://localhost:5173', changeOrigin: true, secure: false, }, }, }, ``` > **MAUI 模式无需此配置**:MAUI 使用 `vite build --mode maui`,静态部署到嵌入服务器同源。 #### 3.4 `serverUrl` 字段的语义变化 | 维度 | 改造前 | 改造后 | |---|---|---| | **用途** | 云同步 API 的 base URL(前端直连) | 仅用于"服务端探测"的目标地址 | | **存储位置** | `localStorage` | 不变(探测时使用) | | **影响登录/Todo 拉取** | 是(前端用它做 baseURL) | 否(走 Host 自身端点) | ### 4. 需要变更的文件清单 | 层 | 文件 | 变更类型 | 说明 | |---|---|---|---| | Application | `Services/CloudProbeService.cs` | **新增** | 服务端探测逻辑,注册于 `AddCloudSyncServer()` | | Application | `Models/AdminDtos.cs`(或新 `Models/ProbeDtos.cs`) | **新增 DTO** | `ProbeRequest` / `ProbeResponse`(纯数据合约) | | Application | `CloudSyncEndpointExtensions.cs` | **修改** | 注册 `POST /cloud-sync/probe` | | Application | `CloudSyncServiceCollectionExtensions.cs` | **修改** | `AddCloudSyncServer()` 内注册 `CloudProbeService` + `HttpClient` | | Host | `Program.cs` | **无需修改** | `MapCloudSyncEndpoints()` 自动包含新端点 | | Web(Vue) | `vite.config.ts` | **修改** | 补充云同步端点的 dev proxy | | Web(Vue) | `api/cloudClient.ts` | **修改** | 移除外部 `serverUrl` baseURL | | Web(Vue) | `api/cloudSync.ts` | **修改** | 新增 `probeServerUrl()` 方法 | | Web(Vue) | `components/CloudSyncSettingsDialog.vue` | **修改** | 删除 `probeReachability()`,改用 API 调用 | | 层 | 文件 | 变更 | 说明 | |---|---|---|---| | MAUI | `MauiProgram.cs` | **无需修改** | 不注册 `AddCloudSyncServer()` | | MAUI | `EmbeddedWebServerService.cs` | **无需修改** | 不映射 `MapCloudSyncEndpoints()` | ### 5. 端点路由变更汇总 | 路由 | 方法 | 变更 | 可用环境 | |---|---|---|---| | `/cloud-sync/probe` | POST | **新增** | Host(Vite dev proxy)/ MAUI(同源请求到 Host) | | `/auth/login` | POST | 无变更 | 同上 | | `/auth/step-up` | POST | 无变更 | 同上 | | `/tasks/` | GET | 无变更 | 同上 | | `/security/policy` | GET | 无变更 | 同上 | ### 6. 服务端/客户端职责边界总结 ``` ┌──────────────────────────────────────────────────────────────┐ │ Hua.Todo.Application (共享层) │ │ │ │ AddApplicationServices() AddCloudSyncServer() │ │ ├─ TodoDbContext ├─ CloudAuthService │ │ ├─ TaskRepository ├─ CloudAdminService │ │ ├─ TaskService ├─ CloudTaskSyncService │ │ ├─ DynamicApi ├─ SecurityPolicyService │ │ └─ DTOs (Models/*) ├─ CloudProbeService (新) │ │ ↑ 纯数据合约,两端安全 ├─ Authentication/Policy │ │ └─ MapCloudSyncEndpoints │ │ │ │ Host 注册: AddApplicationServices + AddCloudSyncServer │ │ MAUI 注册: AddApplicationServices only │ └──────────────────────────────────────────────────────────────┘ ``` ### 7. 安全考量 | 风险点 | 缓解措施 | |---|---| | SSRF(探测端点攻击内网) | `CloudProbeService` 限制目标 URL 必须是 HTTP/HTTPS 公网地址,禁止探测 localhost/内网 IP | | 探测请求被滥用 | 加频率限制(如每分钟 3 次),仅允许同源请求 | | MAUI 端不暴露云同步端点 | `AddCloudSyncServer()` 仅在 Host 调用,MAUI 无法访问云同步端点 |