plan: M1 Phase 3 登录(net+auth+credential+LoginWindow) 实现计划
This commit is contained in:
parent
519d0ed1df
commit
0a3d41689f
|
|
@ -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 1:RSA 加密器(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 2:Credential(QtKeychain 凭证存储)
|
||||||
|
|
||||||
|
**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 3:ApiClient(QtNetwork)
|
||||||
|
|
||||||
|
**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 4:AuthService(编排登录流程)+ 公钥接入
|
||||||
|
|
||||||
|
**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 5:LoginWindow(视图,样式参考 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)。
|
||||||
Loading…
Reference in New Issue