From f017a153a33ad939e01e3a9f3906cb9c5c235073 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Mon, 3 Aug 2026 13:42:43 +0800 Subject: [PATCH] feat: mirror platform credentials securely --- .../PlatformIdentityProjectionService.java | 15 ++++++++++++++- .../PlatformCredentialRepository.java | 9 +++++++++ .../domain/PlatformCredential.java | 12 ++++++++++++ .../JdbcPlatformIdentityRepository.java | 16 +++++++++++++++- .../security/config/SecurityConfig.java | 7 +++++++ .../PlatformIdentityProjectionServiceTest.java | 18 ++++++++++++++++++ .../JdbcPlatformIdentityRepositoryTest.java | 17 +++++++++++++++-- 7 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformCredentialRepository.java create mode 100644 src/main/java/com/yau/digitalrmb/platformintegration/domain/PlatformCredential.java 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 69f2865..bbfd5ac 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,9 @@ 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; import org.springframework.transaction.annotation.Transactional; @@ -19,13 +21,16 @@ public class PlatformIdentityProjectionService { private final UserMapper userMapper; private final PlatformUserSnapshotMapper snapshotMapper; private final JdbcTemplate jdbcTemplate; + private final PasswordEncoder passwordEncoder; public PlatformIdentityProjectionService(UserMapper userMapper, PlatformUserSnapshotMapper snapshotMapper, - JdbcTemplate jdbcTemplate) { + JdbcTemplate jdbcTemplate, + PasswordEncoder passwordEncoder) { this.userMapper = userMapper; this.snapshotMapper = snapshotMapper; this.jdbcTemplate = jdbcTemplate; + this.passwordEncoder = passwordEncoder; } @Transactional @@ -35,6 +40,14 @@ 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) { diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformCredentialRepository.java b/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformCredentialRepository.java new file mode 100644 index 0000000..bbe0a5e --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformCredentialRepository.java @@ -0,0 +1,9 @@ +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/domain/PlatformCredential.java b/src/main/java/com/yau/digitalrmb/platformintegration/domain/PlatformCredential.java new file mode 100644 index 0000000..69e6d43 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/platformintegration/domain/PlatformCredential.java @@ -0,0 +1,12 @@ +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"); + } + } +} 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 95cf6b8..10f6404 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java @@ -1,7 +1,9 @@ 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; @@ -16,7 +18,7 @@ import java.util.ArrayList; import java.util.Optional; @Repository -public class JdbcPlatformIdentityRepository implements PlatformIdentityRepository { +public class JdbcPlatformIdentityRepository implements PlatformIdentityRepository, PlatformCredentialRepository { 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 @@ -46,6 +48,18 @@ 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/security/config/SecurityConfig.java b/src/main/java/com/yau/digitalrmb/security/config/SecurityConfig.java index 927548e..d62901a 100644 --- a/src/main/java/com/yau/digitalrmb/security/config/SecurityConfig.java +++ b/src/main/java/com/yau/digitalrmb/security/config/SecurityConfig.java @@ -19,6 +19,8 @@ import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; @@ -28,6 +30,11 @@ import java.util.UUID; @EnableWebSecurity @EnableConfigurationProperties(SecurityProperties.class) public class SecurityConfig { + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + @Bean public JwtEncoder jwtEncoder(SecurityProperties properties) { return new NimbusJwtEncoder(new ImmutableSecret(secretKey(properties))); diff --git a/src/test/java/com/yau/digitalrmb/identity/PlatformIdentityProjectionServiceTest.java b/src/test/java/com/yau/digitalrmb/identity/PlatformIdentityProjectionServiceTest.java index 034ba88..5e6e908 100644 --- a/src/test/java/com/yau/digitalrmb/identity/PlatformIdentityProjectionServiceTest.java +++ b/src/test/java/com/yau/digitalrmb/identity/PlatformIdentityProjectionServiceTest.java @@ -2,12 +2,14 @@ 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; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; +import org.springframework.security.crypto.password.PasswordEncoder; import javax.sql.DataSource; import java.time.Instant; @@ -23,6 +25,9 @@ class PlatformIdentityProjectionServiceTest { @Autowired private DataSource dataSource; + @Autowired + private PasswordEncoder passwordEncoder; + @Test void projectionIsIdempotentAndOwnsExactlyOneRole() { PlatformActor teacher = new PlatformActor(101L, 1L, "t001", "教师甲", PlatformRole.TEACHER, @@ -40,4 +45,17 @@ class PlatformIdentityProjectionServiceTest { assertThat(role).isEqualTo("TEACHER"); assertThat(userName).isEqualTo("t001"); } + + @Test + void credentialProjectionStoresOnlyBcryptPasswordHash() { + PlatformActor actor = new PlatformActor(301L, 3L, "tzs001", "教师", PlatformRole.TEACHER, + Instant.parse("2026-01-01T00:00:00Z")); + + projectionService.project(new PlatformCredential(actor, "123qwe")); + + String passwordHash = new JdbcTemplate(dataSource) + .queryForObject("SELECT password_hash FROM sys_user WHERE id = 301", String.class); + assertThat(passwordHash).startsWith("$2"); + assertThat(passwordEncoder.matches("123qwe", passwordHash)).isTrue(); + } } 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 1cbf706..07eb98e 100644 --- a/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepositoryTest.java +++ b/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepositoryTest.java @@ -1,6 +1,7 @@ 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; @@ -16,7 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; class JdbcPlatformIdentityRepositoryTest { private DataSource dataSource; - private PlatformIdentityRepository repository; + private JdbcPlatformIdentityRepository repository; @BeforeEach void setUp() throws Exception { @@ -52,8 +53,20 @@ class JdbcPlatformIdentityRepositoryTest { assertThat(repository.findByPlatformUserId(202L)).isEmpty(); } + @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), 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), PASSWORD VARCHAR(128), 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)"); }