From 031a8a39986e63609197dd054a279465975bbc7e Mon Sep 17 00:00:00 2001 From: chenyuan Date: Mon, 3 Aug 2026 14:21:07 +0800 Subject: [PATCH] feat: align local accounts with pevc --- README.md | 4 +- ...2026-08-03-platform-sso-readonly-design.md | 15 ++++--- .../PlatformCredentialInitializer.java | 36 ---------------- .../PlatformIdentityProjectionService.java | 14 +------ .../PlatformCredentialRepository.java | 9 ---- .../config/PlatformIntegrationProperties.java | 8 ---- .../domain/PlatformCredential.java | 17 -------- .../JdbcPlatformIdentityRepository.java | 16 +------- .../CasAuthenticationController.java | 8 +--- .../interfaces/PlatformSsoController.java | 10 +---- .../V3__seed_tzs001_local_student.sql | 12 ++++++ .../PlatformCredentialInitializerTest.java | 41 ------------------- ...PlatformIdentityProjectionServiceTest.java | 11 +++-- .../domain/PlatformCredentialTest.java | 19 --------- .../JdbcPlatformIdentityRepositoryTest.java | 14 +------ .../interfaces/PlatformSsoControllerTest.java | 8 +--- .../security/AuthControllerTest.java | 19 +-------- 17 files changed, 36 insertions(+), 225 deletions(-) delete mode 100644 src/main/java/com/yau/digitalrmb/identity/application/PlatformCredentialInitializer.java delete mode 100644 src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformCredentialRepository.java delete mode 100644 src/main/java/com/yau/digitalrmb/platformintegration/domain/PlatformCredential.java create mode 100644 src/main/resources/db/migration/V3__seed_tzs001_local_student.sql delete mode 100644 src/test/java/com/yau/digitalrmb/identity/PlatformCredentialInitializerTest.java delete mode 100644 src/test/java/com/yau/digitalrmb/platformintegration/domain/PlatformCredentialTest.java diff --git a/README.md b/README.md index 255bc10..a972d63 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ - 主平台单点登录:主平台跳转至 `GET /api/v1/auth/sso?token=...`。后端以主平台教师或学生档案的 `add_time` 动态 HMAC 密钥验证 Token,生成一次性 `code` 并跳转至前端回调地址。 - 独立访问:浏览器访问 `GET /api/v1/auth/cas/login`,跳转学校 CAS;回调校验成功后生成同样的一次性 `code`。 -- 本地账号密码:`POST /api/v1/auth/login` 使用主平台账号和密码直接登录本系统。成功的主平台 SSO/CAS 登录会从主平台只读库读取该账号的密码,并立即以 BCrypt 哈希写入本地;本系统不保存或返回密码明文。 +- 本地账号密码:`POST /api/v1/auth/login` 仅校验本系统保存的 BCrypt 密码哈希。数据库迁移内置学生账号 `tzs001`;SSO/CAS 仅同步身份与角色,不读取主平台密码。 - 会话兑换:`POST /api/v1/auth/session/exchange`,请求体为 `{"code":"..."}`,返回本系统访问 JWT 和刷新令牌。 本系统只接受教师和学生两类身份。平台账号与角色只能通过内部投影写入;没有用户、角色或用户角色的管理接口。 @@ -37,4 +37,4 @@ mvn spring-boot:run -Dspring-boot.run.profiles=local -Dspring-boot.run.arguments 本地 Swagger: -`local` 配置启动时会尝试初始化 `tzs001` 的本地密码哈希;该账号必须存在于主平台且属于教师或学生。密码变更后,用户下一次通过 SSO/CAS 登录会自动刷新本地哈希。 +首次启动时,Flyway V3 迁移会创建或更新 `tzs001`(平台用户 ID `487`、`STUDENT`)。迁移文件只保存 BCrypt 哈希,不含密码明文。新 SSO/CAS 用户会获得随机 BCrypt 密码;如需为其开通本地密码登录,必须通过后续受控的本地账号初始化流程完成。 diff --git a/docs/superpowers/specs/2026-08-03-platform-sso-readonly-design.md b/docs/superpowers/specs/2026-08-03-platform-sso-readonly-design.md index 6e482a6..96bc000 100644 --- a/docs/superpowers/specs/2026-08-03-platform-sso-readonly-design.md +++ b/docs/superpowers/specs/2026-08-03-platform-sso-readonly-design.md @@ -31,7 +31,6 @@ ``` 主平台 ──携带自定义 Token 跳转──> digital-rmb-backend ──签发本系统 JWT──> 前端 学校 CAS ──CAS Ticket 认证────────> digital-rmb-backend ──签发本系统 JWT──> 前端 -主平台只读库 ──密码明文(瞬时)─────> BCrypt ──> sys_user.password_hash digital-rmb-backend ──SELECT only──────────────────────────> 主平台数据库 ``` @@ -44,7 +43,7 @@ digital-rmb-backend ──SELECT only────────────── 1. 主平台既有跳转链接访问 `GET /api/v1/auth/sso?token=...`; 2. 后端按 Token 中未验签的用户 ID 和身份类型定位主平台候选记录; 3. 后端读取候选教师或学生记录的 `addTime`,按主平台算法完成 HMAC 验签; -4. 后端读取主平台用户状态、教师/学生资料、角色及密码,刷新本地只读快照,并将密码立即 BCrypt 编码为本地哈希; +4. 后端读取主平台用户状态、教师/学生资料及角色,刷新本地只读快照;首次投影的本地用户生成随机 BCrypt 密码; 5. 后端生成仅一次、短时有效的登录兑换码,重定向到配置的前端回调地址; 6. 前端调用 `POST /api/v1/auth/session/exchange` 兑换本系统 JWT 与刷新令牌。 @@ -54,17 +53,17 @@ digital-rmb-backend ──SELECT only────────────── 2. 后端将浏览器重定向至学校 CAS 登录页,并使用固定的回调地址作为 CAS `service`; 3. CAS 将 `ticket` 回调至 `GET /api/v1/auth/cas/callback`; 4. 后端调用学校 CAS 的 `serviceValidate` 接口验证 Ticket,取得学校账号; -5. 后端以学校账号查询主平台 `core_user`,仅接受教师或学生,再刷新快照及本地密码哈希; +5. 后端以学校账号查询主平台 `core_user`,仅接受教师或学生,再刷新快照; 6. 后端创建登录兑换码,前端兑换本系统 JWT 与刷新令牌。 用户已在学校 CAS 或主平台登录时,第 2 步无感完成;没有 CAS 会话时,用户在学校认证页输入统一账号密码。 ### 4.3 本地账号密码登录 -1. 用户调用 `POST /api/v1/auth/login`,提交主平台账号与密码; +1. 用户调用 `POST /api/v1/auth/login`,提交已由本系统初始化的账号与密码; 2. 本系统仅以 `sys_user.password_hash` 的 BCrypt 哈希校验密码,成功后签发本系统 JWT; -3. 初次使用前,用户通过主平台 SSO/CAS 成功登录时会初始化本地哈希;`local` 环境还会在启动时为配置账号 `tzs001` 尝试初始化; -4. 密码变更后,下一次成功 SSO/CAS 登录会刷新本地哈希。密码明文绝不进入快照表、JWT、响应、日志或迁移脚本。 +3. Flyway V3 迁移内置 `tzs001`(平台用户 ID `487`、`STUDENT`)的 BCrypt 哈希、快照和角色关联; +4. SSO/CAS 自动建号仅生成随机 BCrypt 密码,不读取主平台密码。密码明文绝不进入快照表、JWT、响应、日志或迁移脚本。 ### 4.4 退出 @@ -110,14 +109,14 @@ CAS `service` 地址必须固定并使用 HTTPS。Ticket 仅能向配置的 CAS 后端使用独立的主平台只读账号,仅对 `core_user`、`student`、`teacher` 及必要关联表授予 `SELECT`。禁止授予 `INSERT`、`UPDATE`、`DELETE`、`CREATE`、`ALTER` 或 DDL 权限。 -本系统只允许主平台教师和学生使用本地账号密码登录。密码哈希仅由内部主平台凭据投影写入;不提供用户、角色、用户角色或密码的管理接口。主平台数据库账号除身份表外还需对 `core_user.PASSWORD` 保有只读 `SELECT` 权限。 +本系统只允许教师和学生使用本地账号密码登录。内置账号通过 Flyway 迁移写入 BCrypt 哈希;SSO/CAS 自动建号使用随机 BCrypt 密码,不提供用户、角色、用户角色或密码的管理接口。主平台数据库账号只需读取身份和角色所需字段,不读取 `core_user.PASSWORD`。 ## 7. 对外接口 - `GET /api/v1/auth/sso?token=...`:主平台单点登录入口; - `GET /api/v1/auth/cas/login`:发起学校 CAS 认证; - `GET /api/v1/auth/cas/callback?ticket=...`:固定 CAS 回调; -- `POST /api/v1/auth/login`:以已初始化的主平台账号密码直接登录; +- `POST /api/v1/auth/login`:以本系统已初始化的账号密码直接登录; - `POST /api/v1/auth/session/exchange`:以一次性兑换码换取本系统 JWT; - `GET /api/v1/auth/me`:读取当前用户与教师/学生角色; - `POST /api/v1/auth/logout`:撤销本系统会话。 diff --git a/src/main/java/com/yau/digitalrmb/identity/application/PlatformCredentialInitializer.java b/src/main/java/com/yau/digitalrmb/identity/application/PlatformCredentialInitializer.java deleted file mode 100644 index e3ffee6..0000000 --- a/src/main/java/com/yau/digitalrmb/identity/application/PlatformCredentialInitializer.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.yau.digitalrmb.identity.application; - -import com.yau.digitalrmb.platformintegration.application.PlatformCredentialRepository; -import com.yau.digitalrmb.platformintegration.application.PlatformIdentityRepository; -import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component -@Profile("local") -public class PlatformCredentialInitializer implements ApplicationRunner { - private final PlatformIntegrationProperties.LocalLogin properties; - private final PlatformIdentityRepository identityRepository; - private final PlatformCredentialRepository credentialRepository; - private final PlatformIdentityProjectionService projectionService; - - public PlatformCredentialInitializer(PlatformIntegrationProperties properties, - PlatformIdentityRepository identityRepository, - PlatformCredentialRepository credentialRepository, - PlatformIdentityProjectionService projectionService) { - this.properties = properties.getLocalLogin(); - this.identityRepository = identityRepository; - this.credentialRepository = credentialRepository; - this.projectionService = projectionService; - } - - @Override - public void run(ApplicationArguments args) { - properties.getInitialAccounts().stream().filter(account -> account != null && !account.isBlank()) - .forEach(account -> identityRepository.findBySchoolAccount(account) - .flatMap(actor -> credentialRepository.findCredential(actor.platformUserId())) - .ifPresent(projectionService::project)); - } -} diff --git a/src/main/java/com/yau/digitalrmb/identity/application/PlatformIdentityProjectionService.java b/src/main/java/com/yau/digitalrmb/identity/application/PlatformIdentityProjectionService.java index bbfd5ac..db118ca 100644 --- a/src/main/java/com/yau/digitalrmb/identity/application/PlatformIdentityProjectionService.java +++ b/src/main/java/com/yau/digitalrmb/identity/application/PlatformIdentityProjectionService.java @@ -5,7 +5,6 @@ import com.yau.digitalrmb.identity.infrastructure.persistence.entity.UserEntity; import com.yau.digitalrmb.identity.infrastructure.persistence.mapper.PlatformUserSnapshotMapper; import com.yau.digitalrmb.identity.infrastructure.persistence.mapper.UserMapper; import com.yau.digitalrmb.platformintegration.domain.PlatformActor; -import com.yau.digitalrmb.platformintegration.domain.PlatformCredential; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -13,11 +12,10 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.util.UUID; @Service public class PlatformIdentityProjectionService { - private static final String EXTERNAL_SSO_ONLY_PASSWORD = "{noop}EXTERNAL_SSO_ONLY"; - private final UserMapper userMapper; private final PlatformUserSnapshotMapper snapshotMapper; private final JdbcTemplate jdbcTemplate; @@ -40,21 +38,13 @@ public class PlatformIdentityProjectionService { projectRole(actor); } - @Transactional - public void project(PlatformCredential credential) { - project(credential.actor()); - UserEntity user = userMapper.selectById(credential.actor().platformUserId()); - user.setPasswordHash(passwordEncoder.encode(credential.rawPassword())); - userMapper.updateById(user); - } - private void projectUser(PlatformActor actor) { UserEntity user = userMapper.selectById(actor.platformUserId()); if (user == null) { user = new UserEntity(); user.setId(actor.platformUserId()); user.setUsername(actor.account()); - user.setPasswordHash(EXTERNAL_SSO_ONLY_PASSWORD); + user.setPasswordHash(passwordEncoder.encode(UUID.randomUUID().toString())); user.setEnabled(true); userMapper.insert(user); return; diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformCredentialRepository.java b/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformCredentialRepository.java deleted file mode 100644 index bbe0a5e..0000000 --- a/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformCredentialRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.yau.digitalrmb.platformintegration.application; - -import com.yau.digitalrmb.platformintegration.domain.PlatformCredential; - -import java.util.Optional; - -public interface PlatformCredentialRepository { - Optional findCredential(long platformUserId); -} diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationProperties.java b/src/main/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationProperties.java index d930797..56cd9a2 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationProperties.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationProperties.java @@ -10,7 +10,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; import java.time.Duration; -import java.util.List; @Getter @Setter @@ -27,8 +26,6 @@ public class PlatformIntegrationProperties { private Frontend frontend = new Frontend(); @Valid private Sync sync = new Sync(); - @Valid - private LocalLogin localLogin = new LocalLogin(); @Getter @Setter @@ -83,9 +80,4 @@ public class PlatformIntegrationProperties { private Duration fixedDelay = Duration.ofMinutes(15); } - @Getter - @Setter - public static class LocalLogin { - private List initialAccounts = List.of("tzs001"); - } } diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/domain/PlatformCredential.java b/src/main/java/com/yau/digitalrmb/platformintegration/domain/PlatformCredential.java deleted file mode 100644 index 60b4bba..0000000 --- a/src/main/java/com/yau/digitalrmb/platformintegration/domain/PlatformCredential.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.yau.digitalrmb.platformintegration.domain; - -import java.util.Objects; - -public record PlatformCredential(PlatformActor actor, String rawPassword) { - public PlatformCredential { - Objects.requireNonNull(actor, "actor must not be null"); - if (rawPassword == null || rawPassword.isBlank()) { - throw new IllegalArgumentException("rawPassword must not be blank"); - } - } - - @Override - public String toString() { - return "PlatformCredential[actor=" + actor + ", rawPassword=]"; - } -} diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java index 10f6404..95cf6b8 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java @@ -1,9 +1,7 @@ package com.yau.digitalrmb.platformintegration.infrastructure; import com.yau.digitalrmb.platformintegration.application.PlatformIdentityRepository; -import com.yau.digitalrmb.platformintegration.application.PlatformCredentialRepository; import com.yau.digitalrmb.platformintegration.domain.PlatformActor; -import com.yau.digitalrmb.platformintegration.domain.PlatformCredential; import com.yau.digitalrmb.platformintegration.domain.PlatformRole; import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.beans.factory.annotation.Qualifier; @@ -18,7 +16,7 @@ import java.util.ArrayList; import java.util.Optional; @Repository -public class JdbcPlatformIdentityRepository implements PlatformIdentityRepository, PlatformCredentialRepository { +public class JdbcPlatformIdentityRepository implements PlatformIdentityRepository { private static final String TEACHER_QUERY = """ SELECT cu.ID, cu.CODE, cu.NAME, t.teacher_id AS profile_id, t.add_time AS signing_time FROM core_user cu JOIN teacher t ON t.user_id = cu.ID @@ -48,18 +46,6 @@ public class JdbcPlatformIdentityRepository implements PlatformIdentityRepositor return findBy("cu.CODE = :value", schoolAccount); } - @Override - public Optional findCredential(long platformUserId) { - return findByPlatformUserId(platformUserId).flatMap(actor -> jdbcClient.sql(""" - SELECT PASSWORD FROM core_user - WHERE ID = :userId AND PASSWORD IS NOT NULL AND PASSWORD <> '' - """) - .param("userId", platformUserId) - .query(String.class) - .optional() - .map(password -> new PlatformCredential(actor, password))); - } - @Override public List findChangedSince(Instant watermark) { String predicate = "(cu.update_Time > :watermark OR cu.CREATE_TIME > :watermark)"; diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/CasAuthenticationController.java b/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/CasAuthenticationController.java index 520ea14..9e5a2ae 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/CasAuthenticationController.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/CasAuthenticationController.java @@ -3,7 +3,6 @@ package com.yau.digitalrmb.platformintegration.interfaces; import com.yau.digitalrmb.identity.application.PlatformIdentityProjectionService; import com.yau.digitalrmb.platformintegration.application.CasTicketValidator; import com.yau.digitalrmb.platformintegration.application.PlatformIdentityRepository; -import com.yau.digitalrmb.platformintegration.application.PlatformCredentialRepository; import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties; import com.yau.digitalrmb.platformintegration.domain.PlatformActor; import com.yau.digitalrmb.security.application.LoginExchangeCodeService; @@ -23,20 +22,17 @@ public class CasAuthenticationController { private final CasTicketValidator ticketValidator; private final PlatformIdentityRepository identityRepository; private final PlatformIdentityProjectionService projectionService; - private final PlatformCredentialRepository credentialRepository; private final LoginExchangeCodeService exchangeCodeService; private final PlatformIntegrationProperties.Cas cas; private final PlatformIntegrationProperties.Frontend frontend; public CasAuthenticationController(CasTicketValidator ticketValidator, PlatformIdentityRepository identityRepository, - PlatformCredentialRepository credentialRepository, PlatformIdentityProjectionService projectionService, LoginExchangeCodeService exchangeCodeService, PlatformIntegrationProperties properties) { this.ticketValidator = ticketValidator; this.identityRepository = identityRepository; - this.credentialRepository = credentialRepository; this.projectionService = projectionService; this.exchangeCodeService = exchangeCodeService; this.cas = properties.getCas(); @@ -56,9 +52,7 @@ public class CasAuthenticationController { String account = ticketValidator.validate(ticket); PlatformActor actor = identityRepository.findBySchoolAccount(account) .orElseThrow(() -> new BusinessException(ErrorCode.UNAUTHORIZED, "用户无权访问本系统")); - credentialRepository.findCredential(actor.platformUserId()) - .ifPresentOrElse(projectionService::project, - () -> { throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户密码初始化失败"); }); + projectionService.project(actor); String exchangeCode = exchangeCodeService.issue(actor.platformUserId()); String location = UriComponentsBuilder.fromUriString(frontend.getCallbackUrl()) .queryParam("code", exchangeCode).build().encode().toUriString(); diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java b/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java index 9b5b560..60f464e 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java @@ -2,12 +2,9 @@ package com.yau.digitalrmb.platformintegration.interfaces; import com.yau.digitalrmb.identity.application.PlatformIdentityProjectionService; import com.yau.digitalrmb.platformintegration.application.PlatformTokenVerifier; -import com.yau.digitalrmb.platformintegration.application.PlatformCredentialRepository; import com.yau.digitalrmb.platformintegration.application.VerifiedPlatformToken; import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties; import com.yau.digitalrmb.security.application.LoginExchangeCodeService; -import com.yau.digitalrmb.shared.api.ErrorCode; -import com.yau.digitalrmb.shared.exception.BusinessException; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -21,17 +18,14 @@ import org.springframework.web.util.UriComponentsBuilder; public class PlatformSsoController { private final PlatformTokenVerifier tokenVerifier; private final PlatformIdentityProjectionService projectionService; - private final PlatformCredentialRepository credentialRepository; private final LoginExchangeCodeService exchangeCodeService; private final PlatformIntegrationProperties.Frontend frontend; public PlatformSsoController(PlatformTokenVerifier tokenVerifier, - PlatformCredentialRepository credentialRepository, PlatformIdentityProjectionService projectionService, LoginExchangeCodeService exchangeCodeService, PlatformIntegrationProperties properties) { this.tokenVerifier = tokenVerifier; - this.credentialRepository = credentialRepository; this.projectionService = projectionService; this.exchangeCodeService = exchangeCodeService; this.frontend = properties.getFrontend(); @@ -40,9 +34,7 @@ public class PlatformSsoController { @GetMapping("/sso") public ResponseEntity loginFromPlatform(@RequestParam("token") String token) { VerifiedPlatformToken verified = tokenVerifier.verify(token); - credentialRepository.findCredential(verified.actor().platformUserId()) - .ifPresentOrElse(projectionService::project, - () -> { throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户密码初始化失败"); }); + projectionService.project(verified.actor()); String exchangeCode = exchangeCodeService.issue(verified.actor().platformUserId()); String location = UriComponentsBuilder.fromUriString(frontend.getCallbackUrl()) .queryParam("code", exchangeCode).build().encode().toUriString(); diff --git a/src/main/resources/db/migration/V3__seed_tzs001_local_student.sql b/src/main/resources/db/migration/V3__seed_tzs001_local_student.sql new file mode 100644 index 0000000..da21cd4 --- /dev/null +++ b/src/main/resources/db/migration/V3__seed_tzs001_local_student.sql @@ -0,0 +1,12 @@ +INSERT INTO sys_user (id, username, password_hash, enabled, created_at, updated_at, created_by, updated_by, deleted) +VALUES (487, 'tzs001', '$2a$10$ufcw5KFHtOmLzxAsV4C.MuIDOErMlw0iw5J5hc8OMzaTx0u9QYxG6', TRUE, + CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'SYSTEM', 'SYSTEM', FALSE) +ON DUPLICATE KEY UPDATE username = VALUES(username), password_hash = VALUES(password_hash), enabled = TRUE, + updated_at = CURRENT_TIMESTAMP, updated_by = 'SYSTEM', deleted = FALSE; + +INSERT INTO platform_user_snapshot (platform_user_id, account, display_name, role_key, source_updated_at, synced_at) +VALUES (487, 'tzs001', 'tzs001', 'STUDENT', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) +ON DUPLICATE KEY UPDATE account = VALUES(account), display_name = VALUES(display_name), role_key = VALUES(role_key), + source_updated_at = CURRENT_TIMESTAMP, synced_at = CURRENT_TIMESTAMP; + +INSERT IGNORE INTO sys_user_role (user_id, role_id) VALUES (487, 1002); diff --git a/src/test/java/com/yau/digitalrmb/identity/PlatformCredentialInitializerTest.java b/src/test/java/com/yau/digitalrmb/identity/PlatformCredentialInitializerTest.java deleted file mode 100644 index 14f1fad..0000000 --- a/src/test/java/com/yau/digitalrmb/identity/PlatformCredentialInitializerTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.yau.digitalrmb.identity; - -import com.yau.digitalrmb.identity.application.PlatformCredentialInitializer; -import com.yau.digitalrmb.identity.application.PlatformIdentityProjectionService; -import com.yau.digitalrmb.platformintegration.application.PlatformCredentialRepository; -import com.yau.digitalrmb.platformintegration.application.PlatformIdentityRepository; -import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties; -import com.yau.digitalrmb.platformintegration.domain.PlatformActor; -import com.yau.digitalrmb.platformintegration.domain.PlatformCredential; -import com.yau.digitalrmb.platformintegration.domain.PlatformRole; -import org.junit.jupiter.api.Test; -import org.springframework.boot.DefaultApplicationArguments; - -import java.time.Instant; -import java.util.List; -import java.util.Optional; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -class PlatformCredentialInitializerTest { - @Test - void initializesOnlyConfiguredPlatformAccount() throws Exception { - PlatformIdentityRepository identities = mock(PlatformIdentityRepository.class); - PlatformCredentialRepository credentials = mock(PlatformCredentialRepository.class); - PlatformIdentityProjectionService projection = mock(PlatformIdentityProjectionService.class); - PlatformIntegrationProperties properties = new PlatformIntegrationProperties(); - properties.getLocalLogin().setInitialAccounts(List.of("tzs001")); - PlatformActor actor = new PlatformActor(301L, 3L, "tzs001", "教师", PlatformRole.TEACHER, - Instant.parse("2026-01-01T00:00:00Z")); - PlatformCredential credential = new PlatformCredential(actor, "123qwe"); - when(identities.findBySchoolAccount("tzs001")).thenReturn(Optional.of(actor)); - when(credentials.findCredential(301L)).thenReturn(Optional.of(credential)); - - new PlatformCredentialInitializer(properties, identities, credentials, projection) - .run(new DefaultApplicationArguments()); - - verify(projection).project(credential); - } -} diff --git a/src/test/java/com/yau/digitalrmb/identity/PlatformIdentityProjectionServiceTest.java b/src/test/java/com/yau/digitalrmb/identity/PlatformIdentityProjectionServiceTest.java index 5e6e908..9ce2f5a 100644 --- a/src/test/java/com/yau/digitalrmb/identity/PlatformIdentityProjectionServiceTest.java +++ b/src/test/java/com/yau/digitalrmb/identity/PlatformIdentityProjectionServiceTest.java @@ -2,7 +2,6 @@ package com.yau.digitalrmb.identity; import com.yau.digitalrmb.identity.application.PlatformIdentityProjectionService; import com.yau.digitalrmb.platformintegration.domain.PlatformActor; -import com.yau.digitalrmb.platformintegration.domain.PlatformCredential; import com.yau.digitalrmb.platformintegration.domain.PlatformRole; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -47,15 +46,15 @@ class PlatformIdentityProjectionServiceTest { } @Test - void credentialProjectionStoresOnlyBcryptPasswordHash() { - PlatformActor actor = new PlatformActor(301L, 3L, "tzs001", "教师", PlatformRole.TEACHER, + void ssoProjectionCreatesUserWithRandomBcryptPassword() { + PlatformActor actor = new PlatformActor(302L, 3L, "sso-user", "教师", PlatformRole.TEACHER, Instant.parse("2026-01-01T00:00:00Z")); - projectionService.project(new PlatformCredential(actor, "123qwe")); + projectionService.project(actor); String passwordHash = new JdbcTemplate(dataSource) - .queryForObject("SELECT password_hash FROM sys_user WHERE id = 301", String.class); + .queryForObject("SELECT password_hash FROM sys_user WHERE id = 302", String.class); assertThat(passwordHash).startsWith("$2"); - assertThat(passwordEncoder.matches("123qwe", passwordHash)).isTrue(); + assertThat(passwordEncoder.matches("EXTERNAL_SSO_ONLY", passwordHash)).isFalse(); } } diff --git a/src/test/java/com/yau/digitalrmb/platformintegration/domain/PlatformCredentialTest.java b/src/test/java/com/yau/digitalrmb/platformintegration/domain/PlatformCredentialTest.java deleted file mode 100644 index 40eb47d..0000000 --- a/src/test/java/com/yau/digitalrmb/platformintegration/domain/PlatformCredentialTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.yau.digitalrmb.platformintegration.domain; - -import org.junit.jupiter.api.Test; - -import java.time.Instant; - -import static org.assertj.core.api.Assertions.assertThat; - -class PlatformCredentialTest { - @Test - void stringRepresentationRedactsRawPassword() { - PlatformActor actor = new PlatformActor(301L, 3L, "tzs001", "教师", PlatformRole.TEACHER, - Instant.parse("2026-01-01T00:00:00Z")); - - PlatformCredential credential = new PlatformCredential(actor, "123qwe"); - - assertThat(credential.toString()).contains("").doesNotContain("123qwe"); - } -} diff --git a/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepositoryTest.java b/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepositoryTest.java index 07eb98e..2feb202 100644 --- a/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepositoryTest.java +++ b/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepositoryTest.java @@ -1,7 +1,6 @@ package com.yau.digitalrmb.platformintegration.infrastructure; import com.yau.digitalrmb.platformintegration.application.PlatformIdentityRepository; -import com.yau.digitalrmb.platformintegration.domain.PlatformCredential; import com.yau.digitalrmb.platformintegration.domain.PlatformActor; import com.yau.digitalrmb.platformintegration.domain.PlatformRole; import org.h2.jdbcx.JdbcDataSource; @@ -54,19 +53,8 @@ class JdbcPlatformIdentityRepositoryTest { } @Test - void resolvesCredentialOnlyForEnabledSupportedPlatformUsers() throws Exception { - execute("INSERT INTO core_user(ID, CODE, NAME, PASSWORD, STATE, JOB_TYPE1, DEL_FLAG) VALUES (301, 'tzs001', '教师', '123qwe', 'S1', 'JT_S_02', 0)"); - execute("INSERT INTO teacher(teacher_id, user_id, teacher_status, add_time) VALUES (3, 301, 1, '2026-01-01 00:00:00')"); - - PlatformCredential credential = repository.findCredential(301L).orElseThrow(); - - assertThat(credential.actor().account()).isEqualTo("tzs001"); - assertThat(credential.rawPassword()).isEqualTo("123qwe"); - assertThat(repository.findCredential(999L)).isEmpty(); - } - private void createSchema() throws Exception { - execute("CREATE TABLE core_user(ID BIGINT PRIMARY KEY, CODE VARCHAR(64), NAME VARCHAR(64), PASSWORD VARCHAR(128), STATE VARCHAR(16), JOB_TYPE1 VARCHAR(16), DEL_FLAG INT)"); + execute("CREATE TABLE core_user(ID BIGINT PRIMARY KEY, CODE VARCHAR(64), NAME VARCHAR(64), STATE VARCHAR(16), JOB_TYPE1 VARCHAR(16), DEL_FLAG INT)"); execute("CREATE TABLE teacher(teacher_id BIGINT PRIMARY KEY, user_id BIGINT, teacher_status INT, add_time TIMESTAMP)"); execute("CREATE TABLE student(student_id BIGINT PRIMARY KEY, user_id BIGINT, student_status INT, add_time TIMESTAMP)"); } diff --git a/src/test/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoControllerTest.java b/src/test/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoControllerTest.java index 4851348..4d7facd 100644 --- a/src/test/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoControllerTest.java +++ b/src/test/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoControllerTest.java @@ -2,11 +2,9 @@ package com.yau.digitalrmb.platformintegration.interfaces; import com.yau.digitalrmb.identity.application.PlatformIdentityProjectionService; import com.yau.digitalrmb.platformintegration.application.PlatformTokenVerifier; -import com.yau.digitalrmb.platformintegration.application.PlatformCredentialRepository; import com.yau.digitalrmb.platformintegration.application.VerifiedPlatformToken; import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties; import com.yau.digitalrmb.platformintegration.domain.PlatformActor; -import com.yau.digitalrmb.platformintegration.domain.PlatformCredential; import com.yau.digitalrmb.platformintegration.domain.PlatformRole; import com.yau.digitalrmb.security.application.LoginExchangeCodeService; import org.junit.jupiter.api.Test; @@ -27,7 +25,6 @@ class PlatformSsoControllerTest { @Test void ssoRedirectDoesNotLeakIncomingToken() throws Exception { PlatformTokenVerifier verifier = mock(PlatformTokenVerifier.class); - PlatformCredentialRepository credentials = mock(PlatformCredentialRepository.class); PlatformIdentityProjectionService projection = mock(PlatformIdentityProjectionService.class); LoginExchangeCodeService exchangeCodes = mock(LoginExchangeCodeService.class); PlatformIntegrationProperties properties = new PlatformIntegrationProperties(); @@ -35,15 +32,14 @@ class PlatformSsoControllerTest { PlatformActor actor = new PlatformActor(101L, 1L, "t001", "教师甲", PlatformRole.TEACHER, Instant.parse("2026-01-01T00:00:00Z")); when(verifier.verify(anyString())).thenReturn(new VerifiedPlatformToken(actor, "fingerprint")); - when(credentials.findCredential(101L)).thenReturn(java.util.Optional.of(new PlatformCredential(actor, "123qwe"))); when(exchangeCodes.issue(101L)).thenReturn("one-time-code"); - MockMvc mvc = MockMvcBuilders.standaloneSetup(new PlatformSsoController(verifier, credentials, projection, exchangeCodes, properties)).build(); + MockMvc mvc = MockMvcBuilders.standaloneSetup(new PlatformSsoController(verifier, projection, exchangeCodes, properties)).build(); mvc.perform(get("/api/v1/auth/sso").param("token", "incoming-platform-token")) .andExpect(status().isFound()) .andExpect(header().string("Location", "https://rmb.example.edu/sso-callback?code=one-time-code")) .andExpect(header().string("Cache-Control", "no-store")) .andExpect(header().string("Referrer-Policy", "no-referrer")); - verify(projection).project(new PlatformCredential(actor, "123qwe")); + verify(projection).project(actor); } } diff --git a/src/test/java/com/yau/digitalrmb/security/AuthControllerTest.java b/src/test/java/com/yau/digitalrmb/security/AuthControllerTest.java index 6d41629..928496d 100644 --- a/src/test/java/com/yau/digitalrmb/security/AuthControllerTest.java +++ b/src/test/java/com/yau/digitalrmb/security/AuthControllerTest.java @@ -1,15 +1,10 @@ package com.yau.digitalrmb.security; import com.jayway.jsonpath.JsonPath; -import com.yau.digitalrmb.identity.application.PlatformIdentityProjectionService; -import com.yau.digitalrmb.platformintegration.domain.PlatformActor; -import com.yau.digitalrmb.platformintegration.domain.PlatformCredential; -import com.yau.digitalrmb.platformintegration.domain.PlatformRole; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; @@ -26,16 +21,6 @@ class AuthControllerTest { @Autowired private MockMvc mvc; - @Autowired - private PlatformIdentityProjectionService projectionService; - - @BeforeEach - void setUp() { - PlatformActor actor = new PlatformActor(301L, 3L, "tzs001", "教师", PlatformRole.TEACHER, - java.time.Instant.parse("2026-01-01T00:00:00Z")); - projectionService.project(new PlatformCredential(actor, "123qwe")); - } - @Test void localPasswordLoginIssuesTokenAndCanReadCurrentUser() throws Exception { String body = "{\"username\":\"tzs001\",\"password\":\"123qwe\"}"; @@ -51,7 +36,7 @@ class AuthControllerTest { .header("Authorization", "Bearer " + token)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.account").value("tzs001")) - .andExpect(jsonPath("$.data.roles[0]").value("TEACHER")); + .andExpect(jsonPath("$.data.roles[0]").value("STUDENT")); } @Test