diff --git a/src/main/java/com/yau/digitalrmb/identity/application/PlatformCredentialInitializer.java b/src/main/java/com/yau/digitalrmb/identity/application/PlatformCredentialInitializer.java new file mode 100644 index 0000000..e3ffee6 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/identity/application/PlatformCredentialInitializer.java @@ -0,0 +1,36 @@ +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/platformintegration/config/PlatformIntegrationProperties.java b/src/main/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationProperties.java index eae0250..d930797 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationProperties.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationProperties.java @@ -10,6 +10,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; import java.time.Duration; +import java.util.List; @Getter @Setter @@ -26,6 +27,8 @@ public class PlatformIntegrationProperties { private Frontend frontend = new Frontend(); @Valid private Sync sync = new Sync(); + @Valid + private LocalLogin localLogin = new LocalLogin(); @Getter @Setter @@ -79,4 +82,10 @@ public class PlatformIntegrationProperties { @NotNull 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/interfaces/CasAuthenticationController.java b/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/CasAuthenticationController.java index 9e5a2ae..0ffbee0 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/CasAuthenticationController.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/CasAuthenticationController.java @@ -3,6 +3,7 @@ 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; @@ -22,17 +23,20 @@ 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(); @@ -52,7 +56,7 @@ public class CasAuthenticationController { String account = ticketValidator.validate(ticket); PlatformActor actor = identityRepository.findBySchoolAccount(account) .orElseThrow(() -> new BusinessException(ErrorCode.UNAUTHORIZED, "用户无权访问本系统")); - projectionService.project(actor); + credentialRepository.findCredential(actor.platformUserId()).ifPresent(projectionService::project); 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 60f464e..7285a58 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java @@ -2,6 +2,7 @@ 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; @@ -18,14 +19,17 @@ 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(); @@ -34,7 +38,7 @@ public class PlatformSsoController { @GetMapping("/sso") public ResponseEntity loginFromPlatform(@RequestParam("token") String token) { VerifiedPlatformToken verified = tokenVerifier.verify(token); - projectionService.project(verified.actor()); + credentialRepository.findCredential(verified.actor().platformUserId()).ifPresent(projectionService::project); String exchangeCode = exchangeCodeService.issue(verified.actor().platformUserId()); String location = UriComponentsBuilder.fromUriString(frontend.getCallbackUrl()) .queryParam("code", exchangeCode).build().encode().toUriString(); diff --git a/src/test/java/com/yau/digitalrmb/identity/PlatformCredentialInitializerTest.java b/src/test/java/com/yau/digitalrmb/identity/PlatformCredentialInitializerTest.java new file mode 100644 index 0000000..14f1fad --- /dev/null +++ b/src/test/java/com/yau/digitalrmb/identity/PlatformCredentialInitializerTest.java @@ -0,0 +1,41 @@ +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/platformintegration/interfaces/PlatformSsoControllerTest.java b/src/test/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoControllerTest.java index b2743b1..4851348 100644 --- a/src/test/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoControllerTest.java +++ b/src/test/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoControllerTest.java @@ -2,9 +2,11 @@ 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; @@ -16,6 +18,7 @@ import java.time.Instant; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -24,6 +27,7 @@ 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(); @@ -31,13 +35,15 @@ 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, projection, exchangeCodes, properties)).build(); + MockMvc mvc = MockMvcBuilders.standaloneSetup(new PlatformSsoController(verifier, credentials, 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")); } }