plan: M1 Phase 3 登录(net+auth+credential+LoginWindow) 实现计划

This commit is contained in:
gaozheng 2026-06-07 20:48:13 +08:00
parent 519d0ed1df
commit 0a3d41689f
1 changed files with 95 additions and 0 deletions

View File

@ -0,0 +1,95 @@
# M1 Phase 3登录net + auth + credential + LoginWindow实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development。Steps 用 `- [ ]`
**Goal:** 桌面客户端真实登录:验证码 + RSA 加密密码 + login2 → token,凭证安全存储,登录成功进入工作台。登录页样式参考现有 web。
**Architecture:** `net/` 层(ApiClient/AuthService/RsaEncryptor/Credential)+ `view/login/`(LoginWindow)。复用真实 API(`http://tenant.geomative.cn/pop-api`)。
**Tech Stack:** Qt6(Widgets/Network)/ OpenSSL(RSA)/ QtKeychain / gtest。统一 Release,构建经 `external/dev.bat`,改 CMake 后先重配置。app 启动前先 `taskkill /IM geopro_desktop.exe /F`
## 已确认的实站事实(本会话抓取)
- 基址 `http://tenant.geomative.cn/pop-api`;认证头 `geomativeauthorization: Geomative <token>`(不透明会话令牌)。
- 流程:① `GET /business/system/personalUser/getImageCode` → 验证码图 + `codeId``POST /business/system/personalUser/verifyCodeCheck {code,codeId}``POST /admin/tenant/auth/login2 {username, password=RSA加密, checkCode}` → 响应 `data.accessToken`(值即 `"Geomative <hash>"`)。
- 密码加密 = **RSA-2048**(密文 258 字节)。
- 登录页:账号登录(用户名/密码/图形验证码/记住一个月)+ 手机/邮箱登录(M1 不做);浅色 `#F5F7FD` + 左 hero banner。
## ⚠️ 唯一外部阻塞:RSA 公钥(非静态字面量)
公钥不在任何静态 JS chunk,也不在 window 全局。**获取办法(三选一,Task 4 前完成)**:
1. **DevTools 断点**(推荐):登录页 F12 → Sources → 对 `login2` 的 XHR 设 XHR breakpoint(URL 含 `login2`)→ 正常登录一次(输真验证码)→ 命中断点后在调用栈里找到 `JSEncrypt` 实例,控制台执行 `其实例.getPublicKey()` 复制 PEM。
2. 或在 Console 注入 hook 后登录:`(()=>{const o=window.…})` —— 因 JSEncrypt 为打包模块,需先在 Sources 里 `Ctrl+P` 打开含 `setPublicKey` 的 chunk 下断点取实参。
3. 或问后端/前端同事直接要公钥 PEM。
拿到后存入项目配置(见 Task 4),**严禁进 git 的同时**——公钥可入库(非私钥),写进 `resources` 或常量即可。
---
## Task 1RSA 加密器OpenSSL临时密钥对自测
**Files:** `src/net/CMakeLists.txt`、`src/net/crypto/RsaEncryptor.{hpp,cpp}`、`tests/net/test_rsa.cpp`、`src/CMakeLists.txt`、`tests/CMakeLists.txt`、`vcpkg.json`(加 `openssl`)
- [ ] **Step 1:** `vcpkg.json` deps 加 `"openssl"`。建 `src/net/CMakeLists.txt`:
```cmake
find_package(OpenSSL REQUIRED)
add_library(geopro_net STATIC crypto/RsaEncryptor.cpp)
target_include_directories(geopro_net PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(geopro_net PUBLIC OpenSSL::SSL OpenSSL::Crypto)
target_compile_features(geopro_net PUBLIC cxx_std_17)
set_target_properties(geopro_net PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF)
```
`src/CMakeLists.txt``add_subdirectory(net)`(data 之后、app 之前)。
- [ ] **Step 2:** 失败测试 `tests/net/test_rsa.cpp`:用 OpenSSL 生成临时 RSA-2048 密钥对 → 用公钥 PEM 构造 `RsaEncryptor``encryptBase64("hello")` → 用私钥解密 → 断言 == "hello";断言密文 base64 解码后 256 字节。在 `tests/CMakeLists.txt` 注册 + 链 `geopro_net`(及 OpenSSL,供测试解密)。
- [ ] **Step 3:** 配置+编译失败。
- [ ] **Step 4:** 实现 `RsaEncryptor`:
- `RsaEncryptor(const std::string& publicKeyPem)`:`BIO_new_mem_buf` + `PEM_read_bio_PUBKEY``EVP_PKEY*`(RAII,析构 `EVP_PKEY_free`)。
- `std::string encryptBase64(const std::string& plain) const`:`EVP_PKEY_CTX` + `EVP_PKEY_encrypt_init` + `EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING)`(JSEncrypt 默认 PKCS#1 v1.5)→ encrypt → base64 编码(用 OpenSSL `EVP_EncodeBlock` 或自实现)。
- 头文件不暴露 OpenSSL 类型(pImpl 或仅 std::string 接口)。
- [ ] **Step 5:** 编译+ctest `-R Rsa` → PASS。
- [ ] **Step 6:** 提交 `feat(net): RSA 加密器(OpenSSL, PKCS1v1.5, base64)`
---
## Task 2CredentialQtKeychain 凭证存储)
**Files:** `src/net/Credential.{hpp,cpp}`、`tests/net/test_credential.cpp`、`vcpkg.json`? (QtKeychain 走 FetchContent,见下)、`src/net/CMakeLists.txt`
> QtKeychain 依赖 Qt → 走 **FetchContent 对接官方 Qt**(同 ADS,不走 vcpkg)。在顶层 `CMakeLists.txt` 加 FetchContent qtkeychain(GIT_TAG 0.14.3),net 链 `Qt6Keychain` + `Qt6::Core`
- [ ] 同步 TDD:`Credential::save(service,key,token)` / `load` / `clear`,用 QtKeychain 的同步 Job 或 ReadPasswordJob/WritePasswordJob(事件循环驱动,测试用 QEventLoop)。测试写入→读出→清除往返。提交 `feat(net): Credential(QtKeychain 凭证存储)`
---
## Task 3ApiClientQtNetwork
**Files:** `src/net/ApiClient.{hpp,cpp}`、`tests/net/test_apiclient.cpp`
- [ ] `ApiClient(baseUrl)`:`get(path)` / `post(path, jsonBody)`(QNetworkAccessManager,QEventLoop 同步等待或回调);自动注入 `geomativeauthorization` 头(若已设 token);返回 `{status, jsonBody}`;401 处理钩子。可测部分:URL 拼接、头注入(用本地 mock/QHttpServer 或仅测构造逻辑)。提交 `feat(net): ApiClient(QtNetwork, geomativeauthorization 注入)`
---
## Task 4AuthService编排登录流程+ 公钥接入
**Files:** `src/net/AuthService.{hpp,cpp}`、`resources/rsa_public_key.pem`(填入 Task 0 取得的公钥)、`tests/net/test_auth.cpp`(mock)
- [ ] `AuthService(ApiClient&)`:`fetchCaptcha()→{imageBytes,codeId}`、`verifyCaptcha(code,codeId)`、`login(username,password,code,codeId)→{ok,token,error}`(内部 RSA 加密密码、调 login2、取 `data.accessToken`)。公钥从 `resources/rsa_public_key.pem` 读。mock ApiClient 测编排逻辑。**真实公钥**(Task 0)填入后,做一次真实连通自测(需真验证码,手工)。提交 `feat(net): AuthService 登录编排 + RSA 公钥接入`
---
## Task 5LoginWindow视图,样式参考 web+ 接入启动
**Files:** `src/view/login/LoginWindow.{hpp,cpp}`、`src/app/main.cpp`(改:先 LoginWindow,成功后开工作台)、`src/app/CMakeLists.txt`(链 geopro_net + view)
- [ ] LoginWindow:左 hero 图 + 右表单(用户名/密码/验证码图(点刷新=重取 getImageCode)/验证码输入/记住一个月/立即登录)。浅色 `#F5F7FD`。点登录→AuthService.login→成功存 token(Credential)+ 关登录窗、开 MainWindow 工作台;失败提示。"记住"→持久化 token,下次启动静默进入(token 有效期/refresh 见 spec §8.3,不确定则到期重登)。
- [ ] app 启动流程:有有效 token→直接工作台;否则 LoginWindow。
- [ ] 构建+部署+启动,**人工验证**:输账号 sydk/123456 + 验证码 → 登录成功进工作台(需 Task 0 公钥到位)。提交 `feat(app): LoginWindow + 启动登录流程`
---
## Self-Review 备注
- 覆盖 spec §8(登录全流程、QtKeychain、§8.3 公钥/token 前置)。
- 铁律:net 可用 Qt/OpenSSL,不依赖 VTK;core 仍纯净。
- RSA 公钥是唯一硬阻塞,Task 4 前必须取得(DevTools 断点)。
- QtKeychain 走 FetchContent 对接官方 Qt(不走 vcpkg,避免双 Qt)。