You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
digital-rmb-backend/docs/superpowers/plans/2026-08-03-local-password-m...

100 lines
7.5 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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<PlatformCredential> 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.