7.0 KiB
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 前完成):
- DevTools 断点(推荐):登录页 F12 → Sources → 对
login2的 XHR 设 XHR breakpoint(URL 含login2)→ 正常登录一次(输真验证码)→ 命中断点后在调用栈里找到JSEncrypt实例,控制台执行其实例.getPublicKey()复制 PEM。 - 或在 Console 注入 hook 后登录:
(()=>{const o=window.…})—— 因 JSEncrypt 为打包模块,需先在 Sources 里Ctrl+P打开含setPublicKey的 chunk 下断点取实参。 - 或问后端/前端同事直接要公钥 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.jsondeps 加"openssl"。建src/net/CMakeLists.txt:
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 编码(用 OpenSSLEVP_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)。