diff --git a/docs/superpowers/plans/2026-08-03-local-password-mirror-implementation.md b/docs/superpowers/plans/2026-08-03-local-password-mirror-implementation.md new file mode 100644 index 0000000..6a2e2e6 --- /dev/null +++ b/docs/superpowers/plans/2026-08-03-local-password-mirror-implementation.md @@ -0,0 +1,99 @@ +# PEVC 式本地密码镜像 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 让智云教师/学生在本系统完成 SSO 或 CAS 后,以其智云账号和密码直接登录本系统;本地只保存 BCrypt 密码哈希。 + +**Architecture:** 保留现有主平台 Token/CAS 身份校验。验证成功后,独立的只读凭据查询从 `core_user.PASSWORD` 获取当前密码(不进入 Token、日志或 API),投影服务立即 BCrypt 编码并更新 `sys_user.password_hash`。本地 `/api/v1/auth/login` 仅校验本地哈希并签发本系统 JWT。 + +**Tech Stack:** Java 17, Spring Boot 4.1, Spring Security BCryptPasswordEncoder, MyBatis-Plus, Flyway, MySQL. + +## Global Constraints + +- 不修改 `E:\javawork\tianze-pro`;主平台数据源保持只读 SELECT。 +- 密码明文只在读取、BCrypt 编码和校验期间短暂存在,禁止日志、响应、Token、快照表或迁移脚本保存。 +- 仅支持从智云识别出的 `TEACHER`、`STUDENT`;无用户/角色 CRUD API。 +- 参照 PEVC 的 `UserDataMigration` 与 `AccountService`:外部密码 BCrypt 后本地保存,本地登录只校验本地密码哈希。 + +--- + +### Task 1: 主平台只读凭据查询与密码投影 + +**Files:** +- Create: `src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformCredentialRepository.java` +- Create: `src/main/java/com/yau/digitalrmb/platformintegration/domain/PlatformCredential.java` +- Modify: `src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java` +- Modify: `src/main/java/com/yau/digitalrmb/identity/application/PlatformIdentityProjectionService.java` +- Test: `src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepositoryTest.java` +- Test: `src/test/java/com/yau/digitalrmb/identity/PlatformIdentityProjectionServiceTest.java` + +**Interfaces:** +- Produces `Optional findCredential(long platformUserId)`. +- `PlatformCredential` contains `PlatformActor actor` and `String rawPassword`; it has no `toString` logging implementation. +- `void project(PlatformCredential credential)` stores `passwordEncoder.encode(credential.rawPassword())` in `sys_user.password_hash`. + +- [ ] Write failing tests proving the credentials query only returns enabled teacher/student records and that projection stores a BCrypt value matching the source password. +- [ ] Run the targeted tests; expect compilation failure for missing credential types. +- [ ] Implement the secondary SELECT for `core_user.PASSWORD`, the BCrypt `PasswordEncoder` bean, and projection overload. The existing actor-only projection remains for metadata sync and must not change password hashes. +- [ ] Run `mvn '-Dtest=JdbcPlatformIdentityRepositoryTest,PlatformIdentityProjectionServiceTest' test -DforkCount=0 -B`; expect success. +- [ ] Commit with `feat: mirror platform credentials securely`. + +### Task 2: 生产本地用户名密码登录 + +**Files:** +- Create: `src/main/java/com/yau/digitalrmb/security/application/LocalAccountAuthenticationService.java` +- Modify: `src/main/java/com/yau/digitalrmb/security/interfaces/AuthController.java` +- Delete: `src/main/java/com/yau/digitalrmb/security/interfaces/BootstrapLoginController.java` +- Delete: `src/main/java/com/yau/digitalrmb/security/application/BootstrapAdminAuthenticator.java` +- Modify: `src/main/java/com/yau/digitalrmb/security/config/SecurityConfig.java` +- Test: `src/test/java/com/yau/digitalrmb/security/AuthControllerTest.java` + +**Interfaces:** +- `JwtTokenService.Token login(String username, String rawPassword)` resolves enabled `sys_user`, checks BCrypt, reads `platform_user_snapshot`, and issues a role-bearing JWT. +- `POST /api/v1/auth/login` accepts existing `LoginRequest` and returns `LoginResponse`; it is anonymously reachable in every profile. + +- [ ] Replace the bootstrap-login regression test with a failing test that projects `tzs001` using source password `123qwe`, posts it to `/api/v1/auth/login`, and expects an access token. +- [ ] Run `mvn -Dtest=AuthControllerTest test -DforkCount=0 -B`; expect failure because the production login service does not exist. +- [ ] Implement local lookup, BCrypt verification, `TEACHER`/`STUDENT` role extraction, and the controller endpoint. Return `UNAUTHORIZED` for unknown, disabled, wrong-password, or non-projected users. Remove the bootstrap-only controller so no duplicate route exists. +- [ ] Run `mvn '-Dtest=AuthControllerTest,CurrentUserAndLogoutTest' test -DforkCount=0 -B`; expect success. +- [ ] Commit with `feat: add local password login`. + +### Task 3: SSO/CAS 密码初始化与 tzs001 启动初始化 + +**Files:** +- Modify: `src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java` +- Modify: `src/main/java/com/yau/digitalrmb/platformintegration/interfaces/CasAuthenticationController.java` +- Create: `src/main/java/com/yau/digitalrmb/identity/application/PlatformCredentialInitializer.java` +- Modify: `src/main/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationProperties.java` +- Modify: `src/main/resources/application-local.yml` +- Test: `src/test/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoControllerTest.java` +- Test: `src/test/java/com/yau/digitalrmb/identity/PlatformCredentialInitializerTest.java` + +**Interfaces:** +- Both successful SSO and CAS flows call `credentialRepository.findCredential(actor.platformUserId())` followed by `projectionService.project(credential)` before issuing an exchange code. +- `platform-integration.local-login.initial-accounts` defaults to an empty list; local profile contains `tzs001` so startup initializes it from the platform read-only database. + +- [ ] Write failing tests proving SSO invokes credential projection and the initializer only initializes configured accounts. +- [ ] Run target tests; expect missing initializer behavior. +- [ ] Implement no-logging credential projection in both flows and the explicit configurable initializer. It must skip absent/disabled/non-teacher/non-student accounts without creating local users. +- [ ] Run `mvn '-Dtest=PlatformSsoControllerTest,PlatformCredentialInitializerTest,AuthControllerTest' test -DforkCount=0 -B`; expect success. +- [ ] Commit with `feat: initialize local passwords from platform login`. + +### Task 4: Verification and operations documentation + +**Files:** +- Modify: `README.md` +- Modify: `docs/superpowers/specs/2026-08-03-platform-sso-readonly-design.md` +- Test: `src/test/java/com/yau/digitalrmb/security/EndToEndAuthenticationFlowTest.java` + +- [ ] Write an end-to-end failing test: source credential → SSO projection → local `/login` → `/me`; assert no raw password occurs in snapshots or JWT claims. +- [ ] Implement only documentation and any wiring exposed by that test. +- [ ] Document that local profile initializes `tzs001` from the platform DB; never document or source-control a password. +- [ ] Run `mvn test -DforkCount=0 -B` and `mvn package -DskipTests -B`; expect zero test failures and a generated executable JAR. +- [ ] Commit with `docs: document local password mirror login`. + +## Plan Self-Review + +- PEVC alignment: Task 1 matches external credential migration with BCrypt; Task 2 matches PEVC local `AccountService.login`; Task 3 ensures SSO/CAS create or refresh local credentials. +- Password handling: every task forbids raw password persistence outside the source read and BCrypt transformation. +- Scope: no change is made to the upstream platform or to public user/role CRUD.