geopro/docs/superpowers/plans/2026-06-07-m1-phase3-login.md

96 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)。