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

7.0 KiB
Raw Blame History

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 → 验证码图 + codeIdPOST /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.txtsrc/net/crypto/RsaEncryptor.{hpp,cpp}tests/net/test_rsa.cppsrc/CMakeLists.txttests/CMakeLists.txtvcpkg.json(加 openssl)

  • Step 1: vcpkg.json deps 加 "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.txtadd_subdirectory(net)(data 之后、app 之前)。

  • Step 2: 失败测试 tests/net/test_rsa.cpp:用 OpenSSL 生成临时 RSA-2048 密钥对 → 用公钥 PEM 构造 RsaEncryptorencryptBase64("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_PUBKEYEVP_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.cppvcpkg.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)。