feat: mirror platform credentials securely

master
chenyuan 4 weeks ago
parent 5de5b5d5c6
commit f017a153a3

@ -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) {

@ -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<PlatformCredential> findCredential(long platformUserId);
}

@ -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");
}
}
}

@ -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<PlatformCredential> 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<PlatformActor> findChangedSince(Instant watermark) {
String predicate = "(cu.update_Time > :watermark OR cu.CREATE_TIME > :watermark)";

@ -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<SecurityContext>(secretKey(properties)));

@ -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();
}
}

@ -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)");
}

Loading…
Cancel
Save