From a9eb46d70095921e15745292894f668aff272d56 Mon Sep 17 00:00:00 2001 From: "jiazheng.zhao" <2624533894@qq.com> Date: Tue, 4 Aug 2026 09:47:08 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=E5=8F=91=E8=A1=8C=E8=B4=A7=E5=B8=81?= =?UTF-8?q?=E7=9A=84=E5=8A=9F=E8=83=BD=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 6 + .../yau/digitalrmb/DigitalRmbApplication.java | 2 - .../InstitutionApplicationPackageResult.java | 44 ++++++ ...titutionApplicationVerificationResult.java | 40 +++++ ...itutionApplicationVerificationService.java | 102 ++++++++++++ .../application/InstitutionDigestResult.java | 38 +++++ ...stitutionIdentifierConfirmationResult.java | 29 ++++ .../InstitutionIdentifierStepService.java | 124 +++++++++++++++ ...stitutionInformationPreparationResult.java | 50 ++++++ .../application/InstitutionKeyPairResult.java | 57 +++++++ .../application/InstitutionKeyService.java | 147 ++++++++++++++++++ .../application/InstitutionKeySubject.java | 38 +++++ .../InstitutionPublicKeyResult.java | 67 ++++++++ .../InstitutionSignatureResult.java | 56 +++++++ .../InstitutionTrainingErrorRecorder.java | 61 ++++++++ .../InstitutionIdentifierApplication.java | 140 +++++++++++++++++ .../InstitutionIdentifierRepository.java | 17 ++ .../InstitutionIdentityCryptography.java | 8 + .../domain/InstitutionSm2KeyPair.java | 14 ++ ...nstitutionIdentifierApplicationEntity.java | 25 +++ ...nstitutionIdentifierApplicationMapper.java | 7 + ...stitutionIdentifierOperationLogEntity.java | 31 ++++ ...stitutionIdentifierOperationLogMapper.java | 8 + ...titutionIdentifierTrainingErrorEntity.java | 38 +++++ ...titutionIdentifierTrainingErrorMapper.java | 8 + .../InstitutionPrivateKeyCipher.java | 60 +++++++ .../InstitutionSm2KeyAuditEntity.java | 35 +++++ .../InstitutionSm2KeyAuditMapper.java | 8 + .../InstitutionSm2KeyEntity.java | 31 ++++ .../InstitutionSm2KeyMapper.java | 8 + ...ybatisInstitutionIdentifierRepository.java | 132 ++++++++++++++++ .../SmInstitutionIdentityCryptography.java | 109 +++++++++++++ .../ConfirmInstitutionIdentifierRequest.java | 28 ++++ .../GenerateInstitutionIdentifierRequest.java | 12 ++ .../dto/InstitutionIdentifierStepRequest.java | 26 ++++ .../VerifyInstitutionApplicationRequest.java | 28 ++++ .../rest/InstitutionIdentifierController.java | 136 ++++++++++++++++ .../application/CasTicketValidator.java | 2 + .../application/PlatformTokenVerifier.java | 2 + .../JdbcPlatformIdentityRepository.java | 2 + .../PlatformReadOnlyDataSourceConfig.java | 4 + .../CasAuthenticationController.java | 2 + .../interfaces/PlatformSsoController.java | 2 + .../security/application/JwtTokenService.java | 21 ++- .../shared/web/GlobalExceptionHandler.java | 21 ++- src/main/resources/application-test.yml | 1 + src/main/resources/application.yml | 2 + src/main/resources/schema.sql | 73 +++++++++ .../PlatformReadOnlyDataSourceConfigTest.java | 1 + 49 files changed, 1894 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationPackageResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationService.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionDigestResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierConfirmationResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierStepService.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionInformationPreparationResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyPairResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyService.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeySubject.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionPublicKeyResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionSignatureResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionTrainingErrorRecorder.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierApplication.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierRepository.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentityCryptography.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionSm2KeyPair.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierApplicationEntity.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierApplicationMapper.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogEntity.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogMapper.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorEntity.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorMapper.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionPrivateKeyCipher.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditEntity.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditMapper.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyEntity.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyMapper.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisInstitutionIdentifierRepository.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/SmInstitutionIdentityCryptography.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/ConfirmInstitutionIdentifierRequest.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/GenerateInstitutionIdentifierRequest.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/InstitutionIdentifierStepRequest.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/VerifyInstitutionApplicationRequest.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/InstitutionIdentifierController.java diff --git a/pom.xml b/pom.xml index 918239d..b0e4355 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,7 @@ 1.8 3.5.17 1.8.0 + 1.70 @@ -71,6 +72,11 @@ lombok true + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + com.h2database h2 diff --git a/src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java b/src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java index 74edcfb..93d0329 100644 --- a/src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java +++ b/src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java @@ -2,11 +2,9 @@ package com.yau.digitalrmb; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication -@ConfigurationPropertiesScan @EnableScheduling public class DigitalRmbApplication { diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationPackageResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationPackageResult.java new file mode 100644 index 0000000..c6e2676 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationPackageResult.java @@ -0,0 +1,44 @@ +package com.yau.digitalrmb.institutionidentity.application; + +public class InstitutionApplicationPackageResult { + private final long applicationId; + private final String status; + private final String bankCode; + private final String timestamp; + private final String digest; + private final String signature; + + public InstitutionApplicationPackageResult(long applicationId, String status, String bankCode, String timestamp, + String digest, String signature) { + this.applicationId = applicationId; + this.status = status; + this.bankCode = bankCode; + this.timestamp = timestamp; + this.digest = digest; + this.signature = signature; + } + + public long getApplicationId() { + return applicationId; + } + + public String getStatus() { + return status; + } + + public String getBankCode() { + return bankCode; + } + + public String getTimestamp() { + return timestamp; + } + + public String getDigest() { + return digest; + } + + public String getSignature() { + return signature; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationResult.java new file mode 100644 index 0000000..8076e9b --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationResult.java @@ -0,0 +1,40 @@ +package com.yau.digitalrmb.institutionidentity.application; + +public class InstitutionApplicationVerificationResult { + private final long applicationId; + private final String status; + private final String verificationAlgorithm; + private final String digestAlgorithm; + private final String signatureHashAlgorithm; + private final String verificationKeyId; + private final String originalText; + private final String submittedDigest; + private final String recalculatedDigest; + private final boolean signatureValid; + private final boolean digestMatches; + private final boolean verified; + private final String signatureConclusion; + private final String integrityConclusion; + + public InstitutionApplicationVerificationResult(long applicationId, String status, String verificationAlgorithm, + String verificationKeyId, String originalText, String submittedDigest, String recalculatedDigest, + boolean signatureValid, boolean digestMatches) { + this.applicationId = applicationId; this.status = status; + this.verificationAlgorithm = verificationAlgorithm; this.digestAlgorithm = "SM3"; + this.signatureHashAlgorithm = "SM3"; this.verificationKeyId = verificationKeyId; this.originalText = originalText; + this.submittedDigest = submittedDigest; this.recalculatedDigest = recalculatedDigest; this.signatureValid = signatureValid; + this.digestMatches = digestMatches; this.verified = signatureValid && digestMatches; + this.signatureConclusion = signatureValid ? "公钥与商业银行第二私钥匹配,身份真实性验证通过" : "签名验证失败,无法确认申请方身份"; + this.integrityConclusion = digestMatches ? "重算摘要与申请摘要一致,数据完整性验证通过" : "重算摘要与申请摘要不一致,申请数据可能已被篡改"; + } + public long getApplicationId() { return applicationId; } + public String getStatus() { return status; } + public String getVerificationAlgorithm() { return verificationAlgorithm; } + public String getDigestAlgorithm() { return digestAlgorithm; } + public String getSignatureHashAlgorithm() { return signatureHashAlgorithm; } + public String getVerificationKeyId() { return verificationKeyId; } + public String getOriginalText() { return originalText; } public String getSubmittedDigest() { return submittedDigest; } + public String getRecalculatedDigest() { return recalculatedDigest; } public boolean isSignatureValid() { return signatureValid; } + public boolean isDigestMatches() { return digestMatches; } public boolean isVerified() { return verified; } + public String getSignatureConclusion() { return signatureConclusion; } public String getIntegrityConclusion() { return integrityConclusion; } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationService.java new file mode 100644 index 0000000..1dd07e7 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationService.java @@ -0,0 +1,102 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierApplication; +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierRepository; +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography; +import com.yau.digitalrmb.shared.api.ErrorCode; +import com.yau.digitalrmb.shared.exception.BusinessException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; + +@Service +public class InstitutionApplicationVerificationService { + @Resource + private InstitutionIdentityCryptography cryptography; + + @Resource + private InstitutionKeyService keyService; + + @Resource + private InstitutionIdentifierRepository repository; + + @Resource + private InstitutionTrainingErrorRecorder errorRecorder; + + @Transactional + public InstitutionApplicationVerificationResult verify(long applicationId, String verificationKeyId, + InstitutionKeySubject subject, String operator) { + InstitutionIdentifierApplication application = requireApplication(applicationId); + if (!InstitutionKeyService.BANK_SECOND_KEY.equals(verificationKeyId)) { + recordAndThrow(application, "WRONG_KEY", "VERIFY", verificationKeyId, + "验证必须选择商业银行第二密钥的公钥", operator); + } + requireStatus(application, InstitutionIdentifierApplication.Status.PACKAGED, + "当前步骤不是央行验证,不能操作", operator); + InstitutionIdentifierApplication.Status expectedStatus = application.getStatus(); + String originalText = application.getBankCode() + "|" + application.getTimestamp(); + String recalculatedDigest = cryptography.sm3(originalText); + boolean signatureValid = keyService.verifyCommercialBank(subject, application.getDigest(), + application.getBankSignature()); + boolean digestMatches = recalculatedDigest.equalsIgnoreCase(application.getDigest()); + invokeDomain(() -> application.recordVerification(verificationKeyId, signatureValid, digestMatches)); + repository.update(application, expectedStatus, "VERIFY", + signatureValid && digestMatches ? "SM2身份验证和SM3完整性验证通过" : "申请验证失败", + operator); + return new InstitutionApplicationVerificationResult(applicationId, application.getStatus().name(), + "SM2_WITH_SM3_VERIFY_AND_SM3_RECALCULATE", verificationKeyId, originalText, + application.getDigest(), recalculatedDigest, signatureValid, digestMatches); + } + + @Transactional + public InstitutionIdentifierConfirmationResult confirm(long applicationId, String signingKeyId, + InstitutionKeySubject subject, String operator) { + InstitutionIdentifierApplication application = requireApplication(applicationId); + if (!InstitutionKeyService.CENTRAL_FIRST_KEY.equals(signingKeyId)) { + recordAndThrow(application, "WRONG_KEY", "CONFIRM", signingKeyId, + "签名确权必须选择中央银行第一密钥", operator); + } + requireStatus(application, InstitutionIdentifierApplication.Status.VERIFIED, + "申请尚未验证通过,不能进行央行签名确权", operator); + InstitutionIdentifierApplication.Status expectedStatus = application.getStatus(); + String digest = application.getDigest(); + String identifier = "ORG_" + digest.substring(0, 12); + String centralBankSignature = keyService.signCentralBank(subject, digest); + invokeDomain(() -> application.issue(signingKeyId, identifier, centralBankSignature)); + repository.update(application, expectedStatus, "CONFIRM", + "中央银行第一私钥完成SM2签名确权并签发机构标识", operator); + return new InstitutionIdentifierConfirmationResult(applicationId, application.getStatus().name(), + signingKeyId, centralBankSignature, identifier, digest); + } + + private InstitutionIdentifierApplication requireApplication(long applicationId) { + return repository.findById(applicationId).orElseThrow(() -> + new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "机构标识申请不存在")); + } + + private void requireStatus(InstitutionIdentifierApplication application, + InstitutionIdentifierApplication.Status expectedStatus, + String message, String operator) { + if (application.getStatus() != expectedStatus) { + recordAndThrow(application, "INVALID_STEP", expectedStatus.name(), null, + message + ",当前状态:" + application.getStatus().name(), operator); + } + } + + private void recordAndThrow(InstitutionIdentifierApplication application, String errorType, String operationStep, + String providedKeyId, String message, String operator) { + int errorSequence = errorRecorder.record(application.getId(), errorType, operationStep, providedKeyId, + message, operator); + throw new BusinessException(ErrorCode.VALIDATION_ERROR, + message + ",本次实训第" + errorSequence + "次错误"); + } + + private static void invokeDomain(Runnable operation) { + try { + operation.run(); + } catch (IllegalArgumentException | IllegalStateException exception) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, exception.getMessage()); + } + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionDigestResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionDigestResult.java new file mode 100644 index 0000000..1729bbb --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionDigestResult.java @@ -0,0 +1,38 @@ +package com.yau.digitalrmb.institutionidentity.application; + +public class InstitutionDigestResult { + private final long applicationId; + private final String status; + private final String algorithm; + private final String concatenatedText; + private final String digest; + + public InstitutionDigestResult(long applicationId, String status, String algorithm, String concatenatedText, + String digest) { + this.applicationId = applicationId; + this.status = status; + this.algorithm = algorithm; + this.concatenatedText = concatenatedText; + this.digest = digest; + } + + public long getApplicationId() { + return applicationId; + } + + public String getStatus() { + return status; + } + + public String getAlgorithm() { + return algorithm; + } + + public String getConcatenatedText() { + return concatenatedText; + } + + public String getDigest() { + return digest; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierConfirmationResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierConfirmationResult.java new file mode 100644 index 0000000..e60e5c6 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierConfirmationResult.java @@ -0,0 +1,29 @@ +package com.yau.digitalrmb.institutionidentity.application; + +public class InstitutionIdentifierConfirmationResult { + private final long applicationId; + private final String status; + private final String signingAlgorithm; + private final String signatureHashAlgorithm; + private final String signatureAlgorithm; + private final String signingKeyId; + private final String centralBankSignature; + private final String institutionIdentifier; + private final String fullDigest; + public InstitutionIdentifierConfirmationResult(long applicationId, String status, String signingKeyId, + String centralBankSignature, String institutionIdentifier, String fullDigest) { + this.applicationId = applicationId; this.status = status; + this.signingAlgorithm = "SM2"; this.signatureHashAlgorithm = "SM3"; + this.signatureAlgorithm = "SM2_WITH_SM3"; + this.signingKeyId = signingKeyId; this.centralBankSignature = centralBankSignature; + this.institutionIdentifier = institutionIdentifier; this.fullDigest = fullDigest; + } + public long getApplicationId() { return applicationId; } + public String getStatus() { return status; } + public String getSigningAlgorithm() { return signingAlgorithm; } + public String getSignatureHashAlgorithm() { return signatureHashAlgorithm; } + public String getSignatureAlgorithm() { return signatureAlgorithm; } + public String getSigningKeyId() { return signingKeyId; } + public String getCentralBankSignature() { return centralBankSignature; } public String getInstitutionIdentifier() { return institutionIdentifier; } + public String getFullDigest() { return fullDigest; } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierStepService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierStepService.java new file mode 100644 index 0000000..484b76b --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierStepService.java @@ -0,0 +1,124 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierApplication; +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierRepository; +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography; +import com.yau.digitalrmb.shared.api.ErrorCode; +import com.yau.digitalrmb.shared.exception.BusinessException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.time.Clock; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +@Service +public class InstitutionIdentifierStepService { + private static final DateTimeFormatter TIMESTAMP = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + + @Resource + private InstitutionIdentityCryptography cryptography; + + @Resource + private InstitutionKeyService keyService; + + @Resource + private InstitutionIdentifierRepository repository; + + @Resource + private InstitutionTrainingErrorRecorder errorRecorder; + + private final Clock clock = Clock.systemUTC(); + + @Transactional + public InstitutionInformationPreparationResult prepare(String bankCode, String operator) { + String normalized = normalize(bankCode); + int trainingRound = repository.nextTrainingRound(normalized, operator); + String timestamp = LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC).format(TIMESTAMP); + InstitutionIdentifierApplication application = InstitutionIdentifierApplication.prepare(normalized, timestamp); + long applicationId = repository.save(application, trainingRound, operator); + return new InstitutionInformationPreparationResult(applicationId, application.getStatus().name(), normalized, + timestamp, normalized + "|" + timestamp, trainingRound, 0); + } + + @Transactional + public InstitutionDigestResult digest(long applicationId, String operator) { + InstitutionIdentifierApplication application = requireApplication(applicationId); + requireStatus(application, InstitutionIdentifierApplication.Status.PREPARED, "当前步骤不是摘要计算,不能操作", operator); + InstitutionIdentifierApplication.Status expectedStatus = application.getStatus(); + String text = application.getBankCode() + "|" + application.getTimestamp(); + String digest = cryptography.sm3(text); + invokeDomain(() -> application.recordDigest(digest)); + repository.update(application, expectedStatus, "DIGEST", "使用SM3计算并保存业务摘要", operator); + return new InstitutionDigestResult(applicationId, application.getStatus().name(), "SM3", text, digest); + } + + @Transactional + public InstitutionSignatureResult sign(long applicationId, String keyId, InstitutionKeySubject subject, + String operator) { + InstitutionIdentifierApplication application = requireApplication(applicationId); + if (!InstitutionKeyService.BANK_SECOND_KEY.equals(keyId)) { + recordAndThrow(application, "WRONG_KEY", "SIGN", keyId, + "签名必须选择商业银行第二密钥", operator); + } + requireStatus(application, InstitutionIdentifierApplication.Status.DIGESTED, + "当前步骤不是商业银行签名,不能操作", operator); + InstitutionIdentifierApplication.Status expectedStatus = application.getStatus(); + String signature = keyService.signCommercialBank(subject, application.getDigest()); + invokeDomain(() -> application.recordBankSignature(signature)); + repository.update(application, expectedStatus, "SIGN", "使用商业银行第二私钥完成SM2签名", operator); + return new InstitutionSignatureResult(applicationId, application.getStatus().name(), "SM2", keyId, + "COMMERCIAL_BANK_SECOND_PRIVATE_KEY", signature); + } + + @Transactional + public InstitutionApplicationPackageResult packageApplication(long applicationId, String operator) { + InstitutionIdentifierApplication application = requireApplication(applicationId); + requireStatus(application, InstitutionIdentifierApplication.Status.SIGNED, "当前步骤不是申请组装,不能操作", operator); + InstitutionIdentifierApplication.Status expectedStatus = application.getStatus(); + invokeDomain(application::packageApplication); + repository.update(application, expectedStatus, "PACKAGE", "组装机构标识申请材料并提交央行待验证", operator); + return new InstitutionApplicationPackageResult(applicationId, application.getStatus().name(), + application.getBankCode(), application.getTimestamp(), application.getDigest(), + application.getBankSignature()); + } + + private InstitutionIdentifierApplication requireApplication(long applicationId) { + return repository.findById(applicationId).orElseThrow(() -> + new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "机构标识申请不存在")); + } + + private void requireStatus(InstitutionIdentifierApplication application, + InstitutionIdentifierApplication.Status expectedStatus, + String message, String operator) { + if (application.getStatus() != expectedStatus) { + recordAndThrow(application, "INVALID_STEP", expectedStatus.name(), null, + message + ",当前状态:" + application.getStatus().name(), operator); + } + } + + private void recordAndThrow(InstitutionIdentifierApplication application, String errorType, String operationStep, + String providedKeyId, String message, String operator) { + int errorSequence = errorRecorder.record(application.getId(), errorType, operationStep, providedKeyId, + message, operator); + invalid(message + ",本次实训第" + errorSequence + "次错误"); + } + + private static void invokeDomain(Runnable operation) { + try { + operation.run(); + } catch (IllegalArgumentException | IllegalStateException exception) { + invalid(exception.getMessage()); + } + } + + private static String normalize(String value) { + return value.trim().toUpperCase(); + } + + private static void invalid(String message) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, message); + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionInformationPreparationResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionInformationPreparationResult.java new file mode 100644 index 0000000..a15a49e --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionInformationPreparationResult.java @@ -0,0 +1,50 @@ +package com.yau.digitalrmb.institutionidentity.application; + +public class InstitutionInformationPreparationResult { + private final long applicationId; + private final String status; + private final String bankCode; + private final String timestamp; + private final String concatenatedText; + private final int trainingRound; + private final int scoringCriteria; + + public InstitutionInformationPreparationResult(long applicationId, String status, String bankCode, String timestamp, + String concatenatedText, int trainingRound, int scoringCriteria) { + this.applicationId = applicationId; + this.status = status; + this.bankCode = bankCode; + this.timestamp = timestamp; + this.concatenatedText = concatenatedText; + this.trainingRound = trainingRound; + this.scoringCriteria = scoringCriteria; + } + + public long getApplicationId() { + return applicationId; + } + + public String getStatus() { + return status; + } + + public String getBankCode() { + return bankCode; + } + + public String getTimestamp() { + return timestamp; + } + + public String getConcatenatedText() { + return concatenatedText; + } + + public int getTrainingRound() { + return trainingRound; + } + + public int getScoringCriteria() { + return scoringCriteria; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyPairResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyPairResult.java new file mode 100644 index 0000000..cf02d54 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyPairResult.java @@ -0,0 +1,57 @@ +package com.yau.digitalrmb.institutionidentity.application; + +public class InstitutionKeyPairResult { + private final long recordId; + private final String keyId; + private final String name; + private final String owner; + private final String purpose; + private final long userId; + private final long schoolId; + private final long classId; + private final String algorithm; + private final String signatureHashAlgorithm; + private final String signatureAlgorithm; + private final String curve; + private final String publicKeyFormat; + private final String privateKeyFormat; + private final String publicKey; + private final String privateKey; + + public InstitutionKeyPairResult(long recordId, String keyId, String name, String owner, String purpose, + InstitutionKeySubject subject, String publicKey, String privateKey) { + this.recordId = recordId; + this.keyId = keyId; + this.name = name; + this.owner = owner; + this.purpose = purpose; + this.userId = subject.getUserId(); + this.schoolId = subject.getSchoolId(); + this.classId = subject.getClassId(); + this.algorithm = "SM2"; + this.signatureHashAlgorithm = "SM3"; + this.signatureAlgorithm = "SM2_WITH_SM3"; + this.curve = "sm2p256v1"; + this.publicKeyFormat = "UNCOMPRESSED_EC_POINT_HEX"; + this.privateKeyFormat = "SM2_PRIVATE_SCALAR_HEX"; + this.publicKey = publicKey; + this.privateKey = privateKey; + } + + public long getRecordId() { return recordId; } + public String getKeyId() { return keyId; } + public String getName() { return name; } + public String getOwner() { return owner; } + public String getPurpose() { return purpose; } + public long getUserId() { return userId; } + public long getSchoolId() { return schoolId; } + public long getClassId() { return classId; } + public String getAlgorithm() { return algorithm; } + public String getSignatureHashAlgorithm() { return signatureHashAlgorithm; } + public String getSignatureAlgorithm() { return signatureAlgorithm; } + public String getCurve() { return curve; } + public String getPublicKeyFormat() { return publicKeyFormat; } + public String getPrivateKeyFormat() { return privateKeyFormat; } + public String getPublicKey() { return publicKey; } + public String getPrivateKey() { return privateKey; } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyService.java new file mode 100644 index 0000000..6a18245 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyService.java @@ -0,0 +1,147 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography; +import com.yau.digitalrmb.institutionidentity.domain.InstitutionSm2KeyPair; +import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionPrivateKeyCipher; +import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionSm2KeyAuditEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionSm2KeyAuditMapper; +import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionSm2KeyEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionSm2KeyMapper; +import com.yau.digitalrmb.shared.api.ErrorCode; +import com.yau.digitalrmb.shared.exception.BusinessException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.time.LocalDateTime; + +@Service +public class InstitutionKeyService { + public static final String BANK_SECOND_KEY = "BANK_SECOND_SM2_KEY"; + public static final String CENTRAL_FIRST_KEY = "CENTRAL_FIRST_SM2_KEY"; + + @Resource + private InstitutionIdentityCryptography cryptography; + @Resource + private InstitutionPrivateKeyCipher privateKeyCipher; + @Resource + private InstitutionSm2KeyMapper keyMapper; + @Resource + private InstitutionSm2KeyAuditMapper auditMapper; + + @Transactional + public InstitutionKeyPairResult commercialBankKey(InstitutionKeySubject subject, String operator) { + return findOrCreate(subject, BANK_SECOND_KEY, "商业银行第二密钥", "COMMERCIAL_BANK", + "SIGN_APPLICATION", operator); + } + + @Transactional + public InstitutionKeyPairResult centralBankKey(InstitutionKeySubject subject, String operator) { + return findOrCreate(subject, CENTRAL_FIRST_KEY, "中央银行第一密钥", "CENTRAL_BANK", + "SIGN_IDENTIFIER", operator); + } + + @Transactional(readOnly = true) + public String signCommercialBank(InstitutionKeySubject subject, String message) { + return cryptography.sign(privateKey(subject, BANK_SECOND_KEY), message); + } + + @Transactional(readOnly = true) + public boolean verifyCommercialBank(InstitutionKeySubject subject, String message, String signature) { + return cryptography.verify(required(subject, BANK_SECOND_KEY).getPublicKey(), message, signature); + } + + @Transactional(readOnly = true) + public String signCentralBank(InstitutionKeySubject subject, String message) { + return cryptography.sign(privateKey(subject, CENTRAL_FIRST_KEY), message); + } + + @Transactional + public void logicalDelete(InstitutionKeySubject subject, String keyId, String operator) { + InstitutionSm2KeyEntity key = required(subject, keyId); + LambdaUpdateWrapper update = new LambdaUpdateWrapper() + .eq(InstitutionSm2KeyEntity::getId, key.getId()) + .eq(InstitutionSm2KeyEntity::getDeleted, false) + .set(InstitutionSm2KeyEntity::getDeleted, true) + .set(InstitutionSm2KeyEntity::getUpdatedAt, LocalDateTime.now()) + .set(InstitutionSm2KeyEntity::getUpdatedBy, operator); + if (keyMapper.update(null, update) != 1) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, "密钥已删除或状态已变化"); + } + audit(key, subject, "DELETE", "逻辑删除机构SM2密钥", operator); + } + + private InstitutionKeyPairResult findOrCreate(InstitutionKeySubject subject, String keyId, String name, + String owner, String purpose, String operator) { + InstitutionSm2KeyEntity key = find(subject, keyId); + if (key == null) { + InstitutionSm2KeyPair pair = cryptography.generateSm2KeyPair(); + key = new InstitutionSm2KeyEntity(); + key.setKeyId(keyId); + key.setKeyName(name); + key.setKeyOwner(owner); + key.setKeyPurpose(purpose); + key.setUserId(subject.getUserId()); + key.setSchoolId(subject.getSchoolId()); + key.setClassId(subject.getClassId()); + key.setPublicKey(pair.getPublicKey()); + key.setEncryptedPrivateKey(privateKeyCipher.encrypt(pair.getPrivateKey())); + LocalDateTime now = LocalDateTime.now(); + key.setCreatedAt(now); + key.setUpdatedAt(now); + key.setCreatedBy(operator); + key.setUpdatedBy(operator); + key.setDeleted(false); + keyMapper.insert(key); + audit(key, subject, "CREATE", "生成并保存机构SM2密钥对", operator); + } else { + audit(key, subject, "VIEW_PRIVATE_KEY", "受控查看机构SM2公私钥", operator); + } + return result(key, subject); + } + + private String privateKey(InstitutionKeySubject subject, String keyId) { + return privateKeyCipher.decrypt(required(subject, keyId).getEncryptedPrivateKey()); + } + + private InstitutionSm2KeyEntity required(InstitutionKeySubject subject, String keyId) { + InstitutionSm2KeyEntity key = find(subject, keyId); + if (key == null) { + throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "当前用户、学校和班级下的密钥不存在,请先获取密钥"); + } + return key; + } + + private InstitutionSm2KeyEntity find(InstitutionKeySubject subject, String keyId) { + return keyMapper.selectOne(new LambdaQueryWrapper() + .eq(InstitutionSm2KeyEntity::getKeyId, keyId) + .eq(InstitutionSm2KeyEntity::getUserId, subject.getUserId()) + .eq(InstitutionSm2KeyEntity::getSchoolId, subject.getSchoolId()) + .eq(InstitutionSm2KeyEntity::getClassId, subject.getClassId()) + .eq(InstitutionSm2KeyEntity::getDeleted, false) + .orderByDesc(InstitutionSm2KeyEntity::getCreatedAt) + .last("LIMIT 1")); + } + + private InstitutionKeyPairResult result(InstitutionSm2KeyEntity key, InstitutionKeySubject subject) { + return new InstitutionKeyPairResult(key.getId(), key.getKeyId(), key.getKeyName(), key.getKeyOwner(), + key.getKeyPurpose(), subject, key.getPublicKey(), privateKeyCipher.decrypt(key.getEncryptedPrivateKey())); + } + + private void audit(InstitutionSm2KeyEntity key, InstitutionKeySubject subject, String operation, + String detail, String operator) { + InstitutionSm2KeyAuditEntity audit = new InstitutionSm2KeyAuditEntity(); + audit.setKeyRecordId(key.getId()); + audit.setKeyId(key.getKeyId()); + audit.setOperation(operation); + audit.setUserId(subject.getUserId()); + audit.setSchoolId(subject.getSchoolId()); + audit.setClassId(subject.getClassId()); + audit.setOperationDetail(detail); + audit.setCreatedAt(LocalDateTime.now()); + audit.setCreatedBy(operator); + auditMapper.insert(audit); + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeySubject.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeySubject.java new file mode 100644 index 0000000..c1e6ae6 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeySubject.java @@ -0,0 +1,38 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import com.yau.digitalrmb.shared.api.ErrorCode; +import com.yau.digitalrmb.shared.exception.BusinessException; +import org.springframework.security.oauth2.jwt.Jwt; + +public class InstitutionKeySubject { + private final long userId; + private final long schoolId; + private final long classId; + + public InstitutionKeySubject(long userId, long schoolId, long classId) { + this.userId = userId; + this.schoolId = schoolId; + this.classId = classId; + } + + public static InstitutionKeySubject from(Jwt jwt) { + return new InstitutionKeySubject(requiredLong(jwt, "userId"), requiredLong(jwt, "schoolId"), + requiredLong(jwt, "classId")); + } + + private static long requiredLong(Jwt jwt, String claim) { + Object value = jwt.getClaims().get(claim); + if (value == null) { + throw new BusinessException(ErrorCode.UNAUTHORIZED, "Token缺少" + claim); + } + try { + return Long.parseLong(String.valueOf(value)); + } catch (NumberFormatException exception) { + throw new BusinessException(ErrorCode.UNAUTHORIZED, "Token中的" + claim + "格式不正确"); + } + } + + public long getUserId() { return userId; } + public long getSchoolId() { return schoolId; } + public long getClassId() { return classId; } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionPublicKeyResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionPublicKeyResult.java new file mode 100644 index 0000000..334c796 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionPublicKeyResult.java @@ -0,0 +1,67 @@ +package com.yau.digitalrmb.institutionidentity.application; + +public class InstitutionPublicKeyResult { + private final String keyId; + private final String name; + private final String owner; + private final String purpose; + private final String algorithm; + private final String signatureHashAlgorithm; + private final String signatureAlgorithm; + private final String curve; + private final String publicKeyFormat; + private final String publicKey; + + public InstitutionPublicKeyResult(String keyId, String name, String owner, String purpose, String publicKey) { + this.keyId = keyId; + this.name = name; + this.owner = owner; + this.purpose = purpose; + this.algorithm = "SM2"; + this.signatureHashAlgorithm = "SM3"; + this.signatureAlgorithm = "SM2_WITH_SM3"; + this.curve = "sm2p256v1"; + this.publicKeyFormat = "UNCOMPRESSED_EC_POINT_HEX"; + this.publicKey = publicKey; + } + + public String getKeyId() { + return keyId; + } + + public String getName() { + return name; + } + + public String getOwner() { + return owner; + } + + public String getPurpose() { + return purpose; + } + + public String getAlgorithm() { + return algorithm; + } + + public String getSignatureHashAlgorithm() { + return signatureHashAlgorithm; + } + + public String getSignatureAlgorithm() { + return signatureAlgorithm; + } + + public String getCurve() { + return curve; + } + + public String getPublicKeyFormat() { + return publicKeyFormat; + } + + public String getPublicKey() { + return publicKey; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionSignatureResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionSignatureResult.java new file mode 100644 index 0000000..8dde9d2 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionSignatureResult.java @@ -0,0 +1,56 @@ +package com.yau.digitalrmb.institutionidentity.application; + +public class InstitutionSignatureResult { + private final long applicationId; + private final String status; + private final String algorithm; + private final String signatureHashAlgorithm; + private final String signatureAlgorithm; + private final String keyId; + private final String keyPurpose; + private final String signature; + + public InstitutionSignatureResult(long applicationId, String status, String algorithm, String keyId, + String keyPurpose, String signature) { + this.applicationId = applicationId; + this.status = status; + this.algorithm = algorithm; + this.signatureHashAlgorithm = "SM3"; + this.signatureAlgorithm = "SM2_WITH_SM3"; + this.keyId = keyId; + this.keyPurpose = keyPurpose; + this.signature = signature; + } + + public long getApplicationId() { + return applicationId; + } + + public String getStatus() { + return status; + } + + public String getAlgorithm() { + return algorithm; + } + + public String getSignatureHashAlgorithm() { + return signatureHashAlgorithm; + } + + public String getSignatureAlgorithm() { + return signatureAlgorithm; + } + + public String getKeyId() { + return keyId; + } + + public String getKeyPurpose() { + return keyPurpose; + } + + public String getSignature() { + return signature; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionTrainingErrorRecorder.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionTrainingErrorRecorder.java new file mode 100644 index 0000000..692f967 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionTrainingErrorRecorder.java @@ -0,0 +1,61 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationMapper; +import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierTrainingErrorEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierTrainingErrorMapper; +import com.yau.digitalrmb.shared.api.ErrorCode; +import com.yau.digitalrmb.shared.exception.BusinessException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.time.LocalDateTime; + +@Service +public class InstitutionTrainingErrorRecorder { + @Resource + private InstitutionIdentifierApplicationMapper applicationMapper; + + @Resource + private InstitutionIdentifierTrainingErrorMapper errorMapper; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public int record(long applicationId, String errorType, String operationStep, String providedKeyId, + String errorMessage, String operator) { + InstitutionIdentifierApplicationEntity application = applicationMapper.selectById(applicationId); + if (application == null || Boolean.TRUE.equals(application.getDeleted())) { + throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "机构标识申请不存在"); + } + + LambdaUpdateWrapper increment = + new LambdaUpdateWrapper() + .eq(InstitutionIdentifierApplicationEntity::getId, applicationId) + .eq(InstitutionIdentifierApplicationEntity::getScoringCriteria, + application.getScoringCriteria()) + .eq(InstitutionIdentifierApplicationEntity::getDeleted, false) + .setSql("scoring_criteria = scoring_criteria + 1") + .set(InstitutionIdentifierApplicationEntity::getUpdatedAt, LocalDateTime.now()) + .set(InstitutionIdentifierApplicationEntity::getUpdatedBy, operator); + if (applicationMapper.update(null, increment) != 1) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, "错误次数已变化,请重新操作"); + } + + int errorSequence = application.getScoringCriteria() + 1; + InstitutionIdentifierTrainingErrorEntity error = new InstitutionIdentifierTrainingErrorEntity(); + error.setApplicationId(applicationId); + error.setTrainingRound(application.getTrainingRound()); + error.setErrorSequence(errorSequence); + error.setErrorType(errorType); + error.setOperationStep(operationStep); + error.setApplicationStatus(application.getStatus()); + error.setProvidedKeyId(providedKeyId); + error.setErrorMessage(errorMessage); + error.setCreatedAt(LocalDateTime.now()); + error.setCreatedBy(operator); + errorMapper.insert(error); + return errorSequence; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierApplication.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierApplication.java new file mode 100644 index 0000000..8fc9501 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierApplication.java @@ -0,0 +1,140 @@ +package com.yau.digitalrmb.institutionidentity.domain; + +import java.util.regex.Pattern; + +public class InstitutionIdentifierApplication { + private static final Pattern BANK_CODE = Pattern.compile("[A-Z0-9]{8,32}"); + + public enum Status { + PREPARED, + DIGESTED, + SIGNED, + PACKAGED, + VERIFIED, + VERIFICATION_FAILED, + ISSUED, + REJECTED + } + + private final Long id; + private final String bankCode; + private final String timestamp; + private String digest; + private String bankSignature; + private Status status; + private String verificationKeyId; + private Boolean signatureValid; + private Boolean digestMatches; + private String signingKeyId; + private String institutionIdentifier; + private String centralBankSignature; + + public InstitutionIdentifierApplication(Long id, String bankCode, String timestamp, String digest, + String bankSignature, Status status, String verificationKeyId, + Boolean signatureValid, Boolean digestMatches, String signingKeyId, + String institutionIdentifier, String centralBankSignature) { + validateBase(bankCode, timestamp); + this.id = id; + this.bankCode = bankCode; + this.timestamp = timestamp; + this.digest = digest; + this.bankSignature = bankSignature; + this.status = status; + this.verificationKeyId = verificationKeyId; + this.signatureValid = signatureValid; + this.digestMatches = digestMatches; + this.signingKeyId = signingKeyId; + this.institutionIdentifier = institutionIdentifier; + this.centralBankSignature = centralBankSignature; + } + + public static InstitutionIdentifierApplication prepare(String bankCode, String timestamp) { + return new InstitutionIdentifierApplication(null, bankCode, timestamp, null, null, Status.PREPARED, + null, null, null, null, null, null); + } + + public static InstitutionIdentifierApplication restore(Long id, String bankCode, String timestamp, String digest, + String bankSignature, Status status, String verificationKeyId, Boolean signatureValid, + Boolean digestMatches, String signingKeyId, String institutionIdentifier, String centralBankSignature) { + return new InstitutionIdentifierApplication(id, bankCode, timestamp, digest, bankSignature, status, + verificationKeyId, signatureValid, digestMatches, signingKeyId, institutionIdentifier, + centralBankSignature); + } + + public void recordDigest(String digest) { + requireStatus(Status.PREPARED, "当前步骤不是摘要计算,不能操作"); + if (digest == null || !digest.matches("[0-9A-F]{64}")) { + throw new IllegalArgumentException("SM3摘要格式不正确"); + } + this.digest = digest; + this.status = Status.DIGESTED; + } + + public void recordBankSignature(String bankSignature) { + requireStatus(Status.DIGESTED, "当前步骤不是商业银行签名,不能操作"); + if (bankSignature == null || !bankSignature.matches("[0-9A-F]+")) { + throw new IllegalArgumentException("银行SM2签名格式不正确"); + } + this.bankSignature = bankSignature; + this.status = Status.SIGNED; + } + + public void packageApplication() { + requireStatus(Status.SIGNED, "当前步骤不是申请组装,不能操作"); + this.status = Status.PACKAGED; + } + + public void recordVerification(String verificationKeyId, boolean signatureValid, boolean digestMatches) { + requireStatus(Status.PACKAGED, "当前步骤不是央行验证,不能操作"); + this.verificationKeyId = verificationKeyId; + this.signatureValid = signatureValid; + this.digestMatches = digestMatches; + this.status = signatureValid && digestMatches ? Status.VERIFIED : Status.VERIFICATION_FAILED; + } + + public void issue(String signingKeyId, String institutionIdentifier, String centralBankSignature) { + requireStatus(Status.VERIFIED, "申请尚未验证通过,不能进行央行签名确权"); + if (signingKeyId == null || institutionIdentifier == null || centralBankSignature == null) { + throw new IllegalArgumentException("签发信息不能为空"); + } + this.signingKeyId = signingKeyId; + this.institutionIdentifier = institutionIdentifier; + this.centralBankSignature = centralBankSignature; + this.status = Status.ISSUED; + } + + public void reject() { + if (status == Status.ISSUED || status == Status.REJECTED) { + throw new IllegalStateException("申请状态不允许拒绝"); + } + status = Status.REJECTED; + } + + private void requireStatus(Status expected, String message) { + if (status != expected) { + throw new IllegalStateException(message + ",当前状态:" + status.name()); + } + } + + private static void validateBase(String bankCode, String timestamp) { + if (bankCode == null || !BANK_CODE.matcher(bankCode).matches()) { + throw new IllegalArgumentException("银行代码格式不正确"); + } + if (timestamp == null || !timestamp.matches("\\d{14}")) { + throw new IllegalArgumentException("时间戳格式不正确"); + } + } + + public Long getId() { return id; } + public String getBankCode() { return bankCode; } + public String getTimestamp() { return timestamp; } + public String getDigest() { return digest; } + public String getBankSignature() { return bankSignature; } + public Status getStatus() { return status; } + public String getVerificationKeyId() { return verificationKeyId; } + public Boolean getSignatureValid() { return signatureValid; } + public Boolean getDigestMatches() { return digestMatches; } + public String getSigningKeyId() { return signingKeyId; } + public String getInstitutionIdentifier() { return institutionIdentifier; } + public String getCentralBankSignature() { return centralBankSignature; } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierRepository.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierRepository.java new file mode 100644 index 0000000..61a33c8 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierRepository.java @@ -0,0 +1,17 @@ +package com.yau.digitalrmb.institutionidentity.domain; + +import java.util.Optional; + +public interface InstitutionIdentifierRepository { + long save(InstitutionIdentifierApplication application, int trainingRound, String operator); + + Optional findById(long applicationId); + + void update(InstitutionIdentifierApplication application, + InstitutionIdentifierApplication.Status expectedStatus, + String operation, + String operationDetail, + String operator); + + int nextTrainingRound(String bankCode, String operator); +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentityCryptography.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentityCryptography.java new file mode 100644 index 0000000..78c0766 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentityCryptography.java @@ -0,0 +1,8 @@ +package com.yau.digitalrmb.institutionidentity.domain; + +public interface InstitutionIdentityCryptography { + String sm3(String text); + InstitutionSm2KeyPair generateSm2KeyPair(); + String sign(String privateKey, String message); + boolean verify(String publicKey, String message, String signature); +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionSm2KeyPair.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionSm2KeyPair.java new file mode 100644 index 0000000..b413271 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionSm2KeyPair.java @@ -0,0 +1,14 @@ +package com.yau.digitalrmb.institutionidentity.domain; + +public class InstitutionSm2KeyPair { + private final String privateKey; + private final String publicKey; + + public InstitutionSm2KeyPair(String privateKey, String publicKey) { + this.privateKey = privateKey; + this.publicKey = publicKey; + } + + public String getPrivateKey() { return privateKey; } + public String getPublicKey() { return publicKey; } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierApplicationEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierApplicationEntity.java new file mode 100644 index 0000000..43fc890 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierApplicationEntity.java @@ -0,0 +1,25 @@ +package com.yau.digitalrmb.institutionidentity.infrastructure; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yau.digitalrmb.shared.infrastructure.persistence.AuditableEntity; +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +@TableName("institution_identifier_application") +public class InstitutionIdentifierApplicationEntity extends AuditableEntity { + @TableField("bank_code") private String bankCode; + @TableField("request_timestamp") private String requestTimestamp; + private String digest; + @TableField("bank_signature") private String bankSignature; + private String status; + @TableField("verification_key_id") private String verificationKeyId; + @TableField("signature_valid") private Boolean signatureValid; + @TableField("digest_matches") private Boolean digestMatches; + @TableField("signing_key_id") private String signingKeyId; + @TableField("institution_identifier") private String institutionIdentifier; + @TableField("central_bank_signature") private String centralBankSignature; + @TableField("training_round") private Integer trainingRound; + @TableField("scoring_criteria") private Integer scoringCriteria; +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierApplicationMapper.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierApplicationMapper.java new file mode 100644 index 0000000..46d83af --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierApplicationMapper.java @@ -0,0 +1,7 @@ +package com.yau.digitalrmb.institutionidentity.infrastructure; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface InstitutionIdentifierApplicationMapper extends BaseMapper { } diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogEntity.java new file mode 100644 index 0000000..a2bad64 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogEntity.java @@ -0,0 +1,31 @@ +package com.yau.digitalrmb.institutionidentity.infrastructure; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@TableName("institution_identifier_operation_log") +public class InstitutionIdentifierOperationLogEntity { + @TableId(type = IdType.ASSIGN_ID) + private Long id; + @TableField("application_id") + private Long applicationId; + private String operation; + @TableField("from_status") + private String fromStatus; + @TableField("to_status") + private String toStatus; + @TableField("operation_detail") + private String operationDetail; + @TableField("created_at") + private LocalDateTime createdAt; + @TableField("created_by") + private String createdBy; +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogMapper.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogMapper.java new file mode 100644 index 0000000..baa7dfc --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogMapper.java @@ -0,0 +1,8 @@ +package com.yau.digitalrmb.institutionidentity.infrastructure; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface InstitutionIdentifierOperationLogMapper extends BaseMapper { +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorEntity.java new file mode 100644 index 0000000..d330d6e --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorEntity.java @@ -0,0 +1,38 @@ +package com.yau.digitalrmb.institutionidentity.infrastructure; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@TableName("institution_identifier_training_error") +public class InstitutionIdentifierTrainingErrorEntity { + @TableId(type = IdType.ASSIGN_ID) + private Long id; + @TableField("application_id") + private Long applicationId; + @TableField("training_round") + private Integer trainingRound; + @TableField("error_sequence") + private Integer errorSequence; + @TableField("error_type") + private String errorType; + @TableField("operation_step") + private String operationStep; + @TableField("application_status") + private String applicationStatus; + @TableField("provided_key_id") + private String providedKeyId; + @TableField("error_message") + private String errorMessage; + @TableField("created_at") + private LocalDateTime createdAt; + @TableField("created_by") + private String createdBy; +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorMapper.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorMapper.java new file mode 100644 index 0000000..72a22da --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorMapper.java @@ -0,0 +1,8 @@ +package com.yau.digitalrmb.institutionidentity.infrastructure; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface InstitutionIdentifierTrainingErrorMapper extends BaseMapper { +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionPrivateKeyCipher.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionPrivateKeyCipher.java new file mode 100644 index 0000000..9e3ae20 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionPrivateKeyCipher.java @@ -0,0 +1,60 @@ +package com.yau.digitalrmb.institutionidentity.infrastructure; + +import com.yau.digitalrmb.security.config.SecurityProperties; +import org.springframework.stereotype.Component; + +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Base64; + +@Component +public class InstitutionPrivateKeyCipher { + private static final int IV_LENGTH = 12; + private static final int TAG_LENGTH = 128; + private final SecretKeySpec encryptionKey; + + public InstitutionPrivateKeyCipher(SecurityProperties properties) { + try { + byte[] digest = MessageDigest.getInstance("SHA-256") + .digest(properties.getJwt().getSecret().getBytes(StandardCharsets.UTF_8)); + this.encryptionKey = new SecretKeySpec(digest, "AES"); + } catch (Exception exception) { + throw new IllegalStateException("机构私钥加密组件初始化失败", exception); + } + } + + public String encrypt(String privateKey) { + try { + byte[] iv = new byte[IV_LENGTH]; + new SecureRandom().nextBytes(iv); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new GCMParameterSpec(TAG_LENGTH, iv)); + byte[] encrypted = cipher.doFinal(privateKey.getBytes(StandardCharsets.US_ASCII)); + byte[] result = new byte[iv.length + encrypted.length]; + System.arraycopy(iv, 0, result, 0, iv.length); + System.arraycopy(encrypted, 0, result, iv.length, encrypted.length); + return Base64.getEncoder().encodeToString(result); + } catch (Exception exception) { + throw new IllegalStateException("机构私钥加密失败", exception); + } + } + + public String decrypt(String encryptedPrivateKey) { + try { + byte[] value = Base64.getDecoder().decode(encryptedPrivateKey); + byte[] iv = new byte[IV_LENGTH]; + byte[] encrypted = new byte[value.length - IV_LENGTH]; + System.arraycopy(value, 0, iv, 0, iv.length); + System.arraycopy(value, iv.length, encrypted, 0, encrypted.length); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, encryptionKey, new GCMParameterSpec(TAG_LENGTH, iv)); + return new String(cipher.doFinal(encrypted), StandardCharsets.US_ASCII); + } catch (Exception exception) { + throw new IllegalStateException("机构私钥解密失败", exception); + } + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditEntity.java new file mode 100644 index 0000000..644278b --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditEntity.java @@ -0,0 +1,35 @@ +package com.yau.digitalrmb.institutionidentity.infrastructure; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@TableName("institution_sm2_key_audit") +public class InstitutionSm2KeyAuditEntity { + @TableId(type = IdType.ASSIGN_ID) + private Long id; + @TableField("key_record_id") + private Long keyRecordId; + @TableField("key_id") + private String keyId; + private String operation; + @TableField("user_id") + private Long userId; + @TableField("school_id") + private Long schoolId; + @TableField("class_id") + private Long classId; + @TableField("operation_detail") + private String operationDetail; + @TableField("created_at") + private LocalDateTime createdAt; + @TableField("created_by") + private String createdBy; +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditMapper.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditMapper.java new file mode 100644 index 0000000..d2f30ed --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditMapper.java @@ -0,0 +1,8 @@ +package com.yau.digitalrmb.institutionidentity.infrastructure; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface InstitutionSm2KeyAuditMapper extends BaseMapper { +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyEntity.java new file mode 100644 index 0000000..1caaeda --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyEntity.java @@ -0,0 +1,31 @@ +package com.yau.digitalrmb.institutionidentity.infrastructure; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yau.digitalrmb.shared.infrastructure.persistence.AuditableEntity; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@TableName("institution_sm2_key") +public class InstitutionSm2KeyEntity extends AuditableEntity { + @TableField("key_id") + private String keyId; + @TableField("key_name") + private String keyName; + @TableField("key_owner") + private String keyOwner; + @TableField("key_purpose") + private String keyPurpose; + @TableField("user_id") + private Long userId; + @TableField("school_id") + private Long schoolId; + @TableField("class_id") + private Long classId; + @TableField("public_key") + private String publicKey; + @TableField("encrypted_private_key") + private String encryptedPrivateKey; +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyMapper.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyMapper.java new file mode 100644 index 0000000..c50e2ea --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyMapper.java @@ -0,0 +1,8 @@ +package com.yau.digitalrmb.institutionidentity.infrastructure; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface InstitutionSm2KeyMapper extends BaseMapper { +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisInstitutionIdentifierRepository.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisInstitutionIdentifierRepository.java new file mode 100644 index 0000000..6faed36 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisInstitutionIdentifierRepository.java @@ -0,0 +1,132 @@ +package com.yau.digitalrmb.institutionidentity.infrastructure; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierApplication; +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierRepository; +import com.yau.digitalrmb.shared.api.ErrorCode; +import com.yau.digitalrmb.shared.exception.BusinessException; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.Optional; + +@Repository +public class MybatisInstitutionIdentifierRepository implements InstitutionIdentifierRepository { + @Resource + private InstitutionIdentifierApplicationMapper mapper; + + @Resource + private InstitutionIdentifierOperationLogMapper operationLogMapper; + + @Override + public long save(InstitutionIdentifierApplication application, int trainingRound, String operator) { + InstitutionIdentifierApplicationEntity entity = toEntity(application); + entity.setTrainingRound(trainingRound); + entity.setScoringCriteria(0); + LocalDateTime now = LocalDateTime.now(); + entity.setCreatedAt(now); + entity.setUpdatedAt(now); + entity.setCreatedBy(operator); + entity.setUpdatedBy(operator); + entity.setDeleted(false); + mapper.insert(entity); + insertLog(entity.getId(), "PREPARE", null, application.getStatus().name(), + "创建机构标识申请并生成业务时间戳", operator); + return entity.getId(); + } + + @Override + public Optional findById(long applicationId) { + InstitutionIdentifierApplicationEntity entity = mapper.selectOne( + new LambdaQueryWrapper() + .eq(InstitutionIdentifierApplicationEntity::getId, applicationId) + .eq(InstitutionIdentifierApplicationEntity::getDeleted, false)); + return Optional.ofNullable(entity).map(this::toDomain); + } + + @Override + public void update(InstitutionIdentifierApplication application, + InstitutionIdentifierApplication.Status expectedStatus, + String operation, + String operationDetail, + String operator) { + LambdaUpdateWrapper update = + new LambdaUpdateWrapper() + .eq(InstitutionIdentifierApplicationEntity::getId, application.getId()) + .eq(InstitutionIdentifierApplicationEntity::getStatus, expectedStatus.name()) + .eq(InstitutionIdentifierApplicationEntity::getDeleted, false) + .set(InstitutionIdentifierApplicationEntity::getDigest, application.getDigest()) + .set(InstitutionIdentifierApplicationEntity::getBankSignature, application.getBankSignature()) + .set(InstitutionIdentifierApplicationEntity::getStatus, application.getStatus().name()) + .set(InstitutionIdentifierApplicationEntity::getVerificationKeyId, + application.getVerificationKeyId()) + .set(InstitutionIdentifierApplicationEntity::getSignatureValid, + application.getSignatureValid()) + .set(InstitutionIdentifierApplicationEntity::getDigestMatches, + application.getDigestMatches()) + .set(InstitutionIdentifierApplicationEntity::getSigningKeyId, application.getSigningKeyId()) + .set(InstitutionIdentifierApplicationEntity::getInstitutionIdentifier, + application.getInstitutionIdentifier()) + .set(InstitutionIdentifierApplicationEntity::getCentralBankSignature, + application.getCentralBankSignature()) + .set(InstitutionIdentifierApplicationEntity::getUpdatedAt, LocalDateTime.now()) + .set(InstitutionIdentifierApplicationEntity::getUpdatedBy, operator); + if (mapper.update(null, update) != 1) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, "申请状态已变化,不能重复或越级操作"); + } + insertLog(application.getId(), operation, expectedStatus.name(), application.getStatus().name(), + operationDetail, operator); + } + + @Override + public int nextTrainingRound(String bankCode, String operator) { + InstitutionIdentifierApplicationEntity latest = mapper.selectOne( + new LambdaQueryWrapper() + .eq(InstitutionIdentifierApplicationEntity::getBankCode, bankCode) + .eq(InstitutionIdentifierApplicationEntity::getCreatedBy, operator) + .eq(InstitutionIdentifierApplicationEntity::getDeleted, false) + .orderByDesc(InstitutionIdentifierApplicationEntity::getTrainingRound) + .last("LIMIT 1")); + return latest == null || latest.getTrainingRound() == null ? 1 : latest.getTrainingRound() + 1; + } + + private InstitutionIdentifierApplicationEntity toEntity(InstitutionIdentifierApplication application) { + InstitutionIdentifierApplicationEntity entity = new InstitutionIdentifierApplicationEntity(); + entity.setId(application.getId()); + entity.setBankCode(application.getBankCode()); + entity.setRequestTimestamp(application.getTimestamp()); + entity.setDigest(application.getDigest()); + entity.setBankSignature(application.getBankSignature()); + entity.setStatus(application.getStatus().name()); + entity.setVerificationKeyId(application.getVerificationKeyId()); + entity.setSignatureValid(application.getSignatureValid()); + entity.setDigestMatches(application.getDigestMatches()); + entity.setSigningKeyId(application.getSigningKeyId()); + entity.setInstitutionIdentifier(application.getInstitutionIdentifier()); + entity.setCentralBankSignature(application.getCentralBankSignature()); + return entity; + } + + private InstitutionIdentifierApplication toDomain(InstitutionIdentifierApplicationEntity entity) { + return InstitutionIdentifierApplication.restore(entity.getId(), entity.getBankCode(), + entity.getRequestTimestamp(), entity.getDigest(), entity.getBankSignature(), + InstitutionIdentifierApplication.Status.valueOf(entity.getStatus()), entity.getVerificationKeyId(), + entity.getSignatureValid(), entity.getDigestMatches(), entity.getSigningKeyId(), + entity.getInstitutionIdentifier(), entity.getCentralBankSignature()); + } + + private void insertLog(Long applicationId, String operation, String fromStatus, String toStatus, + String operationDetail, String operator) { + InstitutionIdentifierOperationLogEntity log = new InstitutionIdentifierOperationLogEntity(); + log.setApplicationId(applicationId); + log.setOperation(operation); + log.setFromStatus(fromStatus); + log.setToStatus(toStatus); + log.setOperationDetail(operationDetail); + log.setCreatedAt(LocalDateTime.now()); + log.setCreatedBy(operator); + operationLogMapper.insert(log); + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/SmInstitutionIdentityCryptography.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/SmInstitutionIdentityCryptography.java new file mode 100644 index 0000000..a991f9e --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/SmInstitutionIdentityCryptography.java @@ -0,0 +1,109 @@ +package com.yau.digitalrmb.institutionidentity.infrastructure; + +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography; +import com.yau.digitalrmb.institutionidentity.domain.InstitutionSm2KeyPair; +import org.bouncycastle.asn1.gm.GMNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.digests.SM3Digest; +import org.bouncycastle.crypto.generators.ECKeyPairGenerator; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECKeyGenerationParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.ParametersWithID; +import org.bouncycastle.crypto.params.ParametersWithRandom; +import org.bouncycastle.crypto.signers.SM2Signer; +import org.springframework.stereotype.Component; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; + +@Component +public class SmInstitutionIdentityCryptography implements InstitutionIdentityCryptography { + private static final byte[] USER_ID = "1234567812345678".getBytes(StandardCharsets.US_ASCII); + private static final ECDomainParameters DOMAIN; + + static { + X9ECParameters curve = GMNamedCurves.getByName("sm2p256v1"); + DOMAIN = new ECDomainParameters(curve.getCurve(), curve.getG(), curve.getN(), curve.getH()); + } + + @Override + public String sm3(String text) { + byte[] input = text.getBytes(StandardCharsets.UTF_8); + SM3Digest digest = new SM3Digest(); + digest.update(input, 0, input.length); + byte[] output = new byte[digest.getDigestSize()]; + digest.doFinal(output, 0); + return hex(output); + } + + @Override + public InstitutionSm2KeyPair generateSm2KeyPair() { + ECKeyPairGenerator generator = new ECKeyPairGenerator(); + generator.init(new ECKeyGenerationParameters(DOMAIN, new SecureRandom())); + AsymmetricCipherKeyPair keyPair = generator.generateKeyPair(); + ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) keyPair.getPrivate(); + ECPublicKeyParameters publicKey = (ECPublicKeyParameters) keyPair.getPublic(); + return new InstitutionSm2KeyPair(leftPad(privateKey.getD().toString(16).toUpperCase(), 64), + hex(publicKey.getQ().getEncoded(false))); + } + + @Override + public String sign(String privateKey, String message) { + ECPrivateKeyParameters key = new ECPrivateKeyParameters(new BigInteger(privateKey, 16), DOMAIN); + SM2Signer signer = new SM2Signer(); + signer.init(true, new ParametersWithID(new ParametersWithRandom(key, new SecureRandom()), USER_ID)); + byte[] bytes = message.getBytes(StandardCharsets.US_ASCII); + signer.update(bytes, 0, bytes.length); + try { + return hex(signer.generateSignature()); + } catch (Exception exception) { + throw new IllegalStateException("SM2签名计算失败", exception); + } + } + + @Override + public boolean verify(String publicKey, String message, String signature) { + try { + ECPublicKeyParameters key = new ECPublicKeyParameters(DOMAIN.getCurve().decodePoint(fromHex(publicKey)), + DOMAIN); + SM2Signer signer = new SM2Signer(); + signer.init(false, new ParametersWithID(key, USER_ID)); + byte[] bytes = message.getBytes(StandardCharsets.US_ASCII); + signer.update(bytes, 0, bytes.length); + return signer.verifySignature(fromHex(signature)); + } catch (RuntimeException exception) { + return false; + } + } + + private static byte[] fromHex(String value) { + if (value == null || (value.length() & 1) == 1) { + throw new IllegalArgumentException("非法十六进制"); + } + byte[] bytes = new byte[value.length() / 2]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) Integer.parseInt(value.substring(i * 2, i * 2 + 2), 16); + } + return bytes; + } + + private static String leftPad(String value, int length) { + StringBuilder result = new StringBuilder(length); + for (int i = value.length(); i < length; i++) { + result.append('0'); + } + return result.append(value).toString(); + } + + private static String hex(byte[] bytes) { + StringBuilder result = new StringBuilder(bytes.length * 2); + for (byte value : bytes) { + result.append(String.format("%02X", value)); + } + return result.toString(); + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/ConfirmInstitutionIdentifierRequest.java b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/ConfirmInstitutionIdentifierRequest.java new file mode 100644 index 0000000..efd695e --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/ConfirmInstitutionIdentifierRequest.java @@ -0,0 +1,28 @@ +package com.yau.digitalrmb.institutionidentity.interfaces.dto; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +public class ConfirmInstitutionIdentifierRequest { + @NotNull(message = "申请编号不能为空") + private Long applicationId; + + @NotBlank(message = "签名密钥不能为空") + private String signingKeyId; + + public Long getApplicationId() { + return applicationId; + } + + public void setApplicationId(Long applicationId) { + this.applicationId = applicationId; + } + + public String getSigningKeyId() { + return signingKeyId; + } + + public void setSigningKeyId(String signingKeyId) { + this.signingKeyId = signingKeyId; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/GenerateInstitutionIdentifierRequest.java b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/GenerateInstitutionIdentifierRequest.java new file mode 100644 index 0000000..1401ae3 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/GenerateInstitutionIdentifierRequest.java @@ -0,0 +1,12 @@ +package com.yau.digitalrmb.institutionidentity.interfaces.dto; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +public class GenerateInstitutionIdentifierRequest { + @NotBlank(message = "银行代码不能为空") + @Pattern(regexp = "[A-Za-z0-9]{8,32}", message = "银行代码必须为8至32位字母或数字") + private String bankCode; + public String getBankCode() { return bankCode; } + public void setBankCode(String bankCode) { this.bankCode = bankCode; } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/InstitutionIdentifierStepRequest.java b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/InstitutionIdentifierStepRequest.java new file mode 100644 index 0000000..1a829a1 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/InstitutionIdentifierStepRequest.java @@ -0,0 +1,26 @@ +package com.yau.digitalrmb.institutionidentity.interfaces.dto; + +import javax.validation.constraints.NotNull; + +public class InstitutionIdentifierStepRequest { + @NotNull(message = "申请编号不能为空") + private Long applicationId; + + private String keyId; + + public Long getApplicationId() { + return applicationId; + } + + public void setApplicationId(Long applicationId) { + this.applicationId = applicationId; + } + + public String getKeyId() { + return keyId; + } + + public void setKeyId(String keyId) { + this.keyId = keyId; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/VerifyInstitutionApplicationRequest.java b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/VerifyInstitutionApplicationRequest.java new file mode 100644 index 0000000..209a473 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/VerifyInstitutionApplicationRequest.java @@ -0,0 +1,28 @@ +package com.yau.digitalrmb.institutionidentity.interfaces.dto; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +public class VerifyInstitutionApplicationRequest { + @NotNull(message = "申请编号不能为空") + private Long applicationId; + + @NotBlank(message = "验证密钥不能为空") + private String verificationKeyId; + + public Long getApplicationId() { + return applicationId; + } + + public void setApplicationId(Long applicationId) { + this.applicationId = applicationId; + } + + public String getVerificationKeyId() { + return verificationKeyId; + } + + public void setVerificationKeyId(String verificationKeyId) { + this.verificationKeyId = verificationKeyId; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/InstitutionIdentifierController.java b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/InstitutionIdentifierController.java new file mode 100644 index 0000000..3bb23ad --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/InstitutionIdentifierController.java @@ -0,0 +1,136 @@ +package com.yau.digitalrmb.institutionidentity.interfaces.rest; + +import com.yau.digitalrmb.institutionidentity.application.InstitutionIdentifierStepService; +import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService; +import com.yau.digitalrmb.institutionidentity.application.InstitutionApplicationVerificationResult; +import com.yau.digitalrmb.institutionidentity.application.InstitutionApplicationVerificationService; +import com.yau.digitalrmb.institutionidentity.application.InstitutionIdentifierConfirmationResult; +import com.yau.digitalrmb.institutionidentity.application.InstitutionInformationPreparationResult; +import com.yau.digitalrmb.institutionidentity.application.InstitutionDigestResult; +import com.yau.digitalrmb.institutionidentity.application.InstitutionSignatureResult; +import com.yau.digitalrmb.institutionidentity.application.InstitutionApplicationPackageResult; +import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyPairResult; +import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; +import com.yau.digitalrmb.institutionidentity.interfaces.dto.GenerateInstitutionIdentifierRequest; +import com.yau.digitalrmb.institutionidentity.interfaces.dto.InstitutionIdentifierStepRequest; +import com.yau.digitalrmb.institutionidentity.interfaces.dto.VerifyInstitutionApplicationRequest; +import com.yau.digitalrmb.institutionidentity.interfaces.dto.ConfirmInstitutionIdentifierRequest; +import com.yau.digitalrmb.shared.api.ApiResponse; +import com.yau.digitalrmb.shared.web.TraceIdFilter; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.MDC; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; +@RestController +@RequestMapping("/api/v1/institution-identifiers") +@Tag(name = "投放机构标识") +public class InstitutionIdentifierController { + @Resource + private InstitutionIdentifierStepService stepService; + + @Resource + private InstitutionKeyService keyService; + + @Resource + private InstitutionApplicationVerificationService verificationService; + + @PostMapping("/keys/commercial-bank") + @Operation(summary = "获取商业银行SM2密钥对", + description = "按Token中的userId、schoolId、classId生成或读取商业银行密钥对,私钥加密保存。") + public ApiResponse commercialBankKey( + @AuthenticationPrincipal Jwt jwt, Authentication authentication) { + return ApiResponse.success(keyService.commercialBankKey(InstitutionKeySubject.from(jwt), + authentication.getName()), traceId()); + } + + @DeleteMapping("/keys/commercial-bank") + @Operation(summary = "删除商业银行SM2密钥对", + description = "仅逻辑删除当前Token中的userId、schoolId、classId所对应的商业银行密钥。") + public ApiResponse deleteCommercialBankKey( + @AuthenticationPrincipal Jwt jwt, Authentication authentication) { + keyService.logicalDelete(InstitutionKeySubject.from(jwt), InstitutionKeyService.BANK_SECOND_KEY, + authentication.getName()); + return ApiResponse.success(null, traceId()); + } + + @PostMapping("/keys/central-bank") + @Operation(summary = "获取中央银行SM2密钥对", + description = "按Token中的userId、schoolId、classId生成或读取中央银行密钥对,私钥加密保存。") + public ApiResponse centralBankKey( + @AuthenticationPrincipal Jwt jwt, Authentication authentication) { + return ApiResponse.success(keyService.centralBankKey(InstitutionKeySubject.from(jwt), + authentication.getName()), traceId()); + } + + @DeleteMapping("/keys/central-bank") + @Operation(summary = "删除中央银行SM2密钥对", + description = "仅逻辑删除当前Token中的userId、schoolId、classId所对应的中央银行密钥。") + public ApiResponse deleteCentralBankKey( + @AuthenticationPrincipal Jwt jwt, Authentication authentication) { + keyService.logicalDelete(InstitutionKeySubject.from(jwt), InstitutionKeyService.CENTRAL_FIRST_KEY, + authentication.getName()); + return ApiResponse.success(null, traceId()); + } + + @PostMapping("/steps/prepare") + @Operation(summary = "创建申请并获取业务信息", description = "创建PREPARED状态申请并保存数据库,返回后续步骤使用的applicationId。") + public ApiResponse prepare( + @Valid @RequestBody GenerateInstitutionIdentifierRequest request, Authentication authentication) { + return ApiResponse.success(stepService.prepare(request.getBankCode(), authentication.getName()), traceId()); + } + + @PostMapping("/steps/digest") + @Operation(summary = "使用SM3计算摘要", description = "仅PREPARED状态可操作,完成后保存摘要并进入DIGESTED状态。") + public ApiResponse digest( + @Valid @RequestBody InstitutionIdentifierStepRequest request, Authentication authentication) { + return ApiResponse.success(stepService.digest(request.getApplicationId(), authentication.getName()), traceId()); + } + + @PostMapping("/steps/sign") + @Operation(summary = "使用商业银行第二私钥进行SM2签名", description = "仅DIGESTED状态可操作,完成后保存签名并进入SIGNED状态。") + public ApiResponse sign( + @Valid @RequestBody InstitutionIdentifierStepRequest request, + @AuthenticationPrincipal Jwt jwt, Authentication authentication) { + return ApiResponse.success(stepService.sign(request.getApplicationId(), request.getKeyId(), + InstitutionKeySubject.from(jwt), authentication.getName()), traceId()); + } + + @PostMapping("/steps/package") + @Operation(summary = "组装机构标识申请", description = "仅SIGNED状态可操作,完成后进入PACKAGED待央行验证状态。") + public ApiResponse packageApplication( + @Valid @RequestBody InstitutionIdentifierStepRequest request, Authentication authentication) { + return ApiResponse.success(stepService.packageApplication(request.getApplicationId(), authentication.getName()), + traceId()); + } + + @PostMapping("/applications/verification") + @Operation(summary = "查看并保存机构标识申请验证过程", description = "仅PACKAGED状态可操作;保存SM2验签和SM3完整性验证结果。") + public ApiResponse verifyApplication( + @Valid @RequestBody VerifyInstitutionApplicationRequest request, + @AuthenticationPrincipal Jwt jwt, Authentication authentication) { + return ApiResponse.success(verificationService.verify(request.getApplicationId(), + request.getVerificationKeyId(), InstitutionKeySubject.from(jwt), authentication.getName()), traceId()); + } + + @PostMapping("/applications/confirmation") + @Operation(summary = "央行签名确权并组装机构标识", description = "仅VERIFIED状态可操作;保存央行签名和机构标识并进入ISSUED状态。") + public ApiResponse confirmIdentifier( + @Valid @RequestBody ConfirmInstitutionIdentifierRequest request, + @AuthenticationPrincipal Jwt jwt, Authentication authentication) { + return ApiResponse.success(verificationService.confirm(request.getApplicationId(), request.getSigningKeyId(), + InstitutionKeySubject.from(jwt), authentication.getName()), traceId()); + } + + private String traceId() { return MDC.get(TraceIdFilter.MDC_KEY); } +} diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/application/CasTicketValidator.java b/src/main/java/com/yau/digitalrmb/platformintegration/application/CasTicketValidator.java index 23098d8..e603f4e 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/application/CasTicketValidator.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/application/CasTicketValidator.java @@ -4,6 +4,7 @@ import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperti import com.yau.digitalrmb.shared.api.ErrorCode; import com.yau.digitalrmb.shared.exception.BusinessException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import org.springframework.http.ResponseEntity; import org.springframework.http.client.SimpleClientHttpRequestFactory; @@ -21,6 +22,7 @@ import java.io.StringReader; import java.net.URI; @Component +@ConditionalOnProperty(prefix = "platform-integration", name = "enabled", havingValue = "true") public class CasTicketValidator { private final PlatformIntegrationProperties.Cas properties; private final RestTemplate restTemplate; diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifier.java b/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifier.java index 921b0f7..efbc87c 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifier.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifier.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties; import com.yau.digitalrmb.platformintegration.domain.PlatformActor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import javax.crypto.Mac; @@ -17,6 +18,7 @@ import java.time.Instant; import java.util.Base64; @Component +@ConditionalOnProperty(prefix = "platform-integration", name = "enabled", havingValue = "true") public class PlatformTokenVerifier { private static final Duration CLOCK_SKEW = Duration.ofSeconds(30); 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 9a6a282..eb9fd76 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java @@ -4,6 +4,7 @@ import com.yau.digitalrmb.platformintegration.application.PlatformIdentityReposi import com.yau.digitalrmb.platformintegration.domain.PlatformActor; import com.yau.digitalrmb.platformintegration.domain.PlatformRole; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; @@ -19,6 +20,7 @@ import java.util.ArrayList; import java.util.Optional; @Repository +@ConditionalOnProperty(prefix = "platform-integration", name = "enabled", havingValue = "true") 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 " diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfig.java b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfig.java index c7d3459..abd1069 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfig.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfig.java @@ -4,6 +4,8 @@ import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -11,6 +13,8 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import javax.sql.DataSource; @Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(PlatformIntegrationProperties.class) +@ConditionalOnProperty(prefix = "platform-integration", name = "enabled", havingValue = "true") public class PlatformReadOnlyDataSourceConfig { @Bean(name = "platformReadOnlyDataSource", destroyMethod = "close") public HikariDataSource platformReadOnlyDataSource(PlatformIntegrationProperties properties) { 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..aac5ff8 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/CasAuthenticationController.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/CasAuthenticationController.java @@ -8,6 +8,7 @@ import com.yau.digitalrmb.platformintegration.domain.PlatformActor; import com.yau.digitalrmb.security.application.LoginExchangeCodeService; import com.yau.digitalrmb.shared.api.ErrorCode; import com.yau.digitalrmb.shared.exception.BusinessException; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -17,6 +18,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.UriComponentsBuilder; @RestController +@ConditionalOnProperty(prefix = "platform-integration", name = "enabled", havingValue = "true") @RequestMapping("/api/v1/auth/cas") public class CasAuthenticationController { private final CasTicketValidator ticketValidator; 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..3879d14 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java @@ -5,6 +5,7 @@ import com.yau.digitalrmb.platformintegration.application.PlatformTokenVerifier; import com.yau.digitalrmb.platformintegration.application.VerifiedPlatformToken; import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties; import com.yau.digitalrmb.security.application.LoginExchangeCodeService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.UriComponentsBuilder; @RestController +@ConditionalOnProperty(prefix = "platform-integration", name = "enabled", havingValue = "true") @RequestMapping("/api/v1/auth") public class PlatformSsoController { private final PlatformTokenVerifier tokenVerifier; diff --git a/src/main/java/com/yau/digitalrmb/security/application/JwtTokenService.java b/src/main/java/com/yau/digitalrmb/security/application/JwtTokenService.java index ef35c26..c3da4ff 100644 --- a/src/main/java/com/yau/digitalrmb/security/application/JwtTokenService.java +++ b/src/main/java/com/yau/digitalrmb/security/application/JwtTokenService.java @@ -35,17 +35,28 @@ public class JwtTokenService { } public Token issueFor(long platformUserId, String account, Set roles) { + return issueFor(platformUserId, null, null, account, roles); + } + + public Token issueFor(long userId, Long schoolId, Long classId, String account, Set roles) { Instant issuedAt = Instant.now(); Instant expiresAt = issuedAt.plus(properties.getJwt().getAccessTokenTtl()); List authorities = roles.stream().sorted().map(role -> "ROLE_" + role).collect(Collectors.toList()); - JwtClaimsSet claims = JwtClaimsSet.builder() - .subject(String.valueOf(platformUserId)) + JwtClaimsSet.Builder claims = JwtClaimsSet.builder() + .subject(String.valueOf(userId)) .issuedAt(issuedAt) .expiresAt(expiresAt) + .claim("userId", userId) .claim("preferred_username", account) - .claim("roles", authorities) - .build(); - Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(JwsHeader.with(MacAlgorithm.HS256).build(), claims)); + .claim("roles", authorities); + if (schoolId != null) { + claims.claim("schoolId", schoolId); + } + if (classId != null) { + claims.claim("classId", classId); + } + Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(JwsHeader.with(MacAlgorithm.HS256).build(), + claims.build())); return new Token(jwt.getTokenValue(), properties.getJwt().getAccessTokenTtl().getSeconds()); } diff --git a/src/main/java/com/yau/digitalrmb/shared/web/GlobalExceptionHandler.java b/src/main/java/com/yau/digitalrmb/shared/web/GlobalExceptionHandler.java index f52fb6f..6ed89c3 100644 --- a/src/main/java/com/yau/digitalrmb/shared/web/GlobalExceptionHandler.java +++ b/src/main/java/com/yau/digitalrmb/shared/web/GlobalExceptionHandler.java @@ -9,6 +9,7 @@ import org.slf4j.MDC; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; @@ -26,8 +27,9 @@ public class GlobalExceptionHandler { } @ExceptionHandler(BusinessException.class) - public ApiResponse handleBusiness(BusinessException exception) { - return ApiResponse.failure(exception.getErrorCode(), exception.getMessage(), traceId()); + public ResponseEntity> handleBusiness(BusinessException exception) { + ApiResponse response = ApiResponse.failure(exception.getErrorCode(), exception.getMessage(), traceId()); + return ResponseEntity.status(httpStatus(exception.getErrorCode())).body(response); } @ExceptionHandler(PlatformTokenException.class) @@ -43,6 +45,21 @@ public class GlobalExceptionHandler { return ApiResponse.failure(ErrorCode.INTERNAL_ERROR, "系统内部错误", traceId()); } + private static HttpStatus httpStatus(ErrorCode errorCode) { + switch (errorCode) { + case VALIDATION_ERROR: + return HttpStatus.BAD_REQUEST; + case UNAUTHORIZED: + return HttpStatus.UNAUTHORIZED; + case FORBIDDEN: + return HttpStatus.FORBIDDEN; + case RESOURCE_NOT_FOUND: + return HttpStatus.NOT_FOUND; + default: + return HttpStatus.INTERNAL_SERVER_ERROR; + } + } + private String traceId() { String traceId = MDC.get(TraceIdFilter.MDC_KEY); return traceId == null ? UUID.randomUUID().toString() : traceId; diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 487192c..1bbcd72 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -9,6 +9,7 @@ security: secret: 0123456789012345678901234567890123456789012345678901234567890123 access-token-ttl: PT30M platform-integration: + enabled: true datasource: url: jdbc:h2:mem:platform;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE username: sa diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 199be9a..e6342f7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,6 @@ spring: + profiles: + active: dev application: name: digital-rmb-backend datasource: diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index b749a73..4d5165d 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -51,6 +51,39 @@ CREATE TABLE IF NOT EXISTS auth_refresh_token ( revoked_at TIMESTAMP NULL ); +CREATE TABLE IF NOT EXISTS institution_sm2_key ( + id BIGINT PRIMARY KEY, + key_id VARCHAR(64) NOT NULL, + key_name VARCHAR(64) NOT NULL, + key_owner VARCHAR(32) NOT NULL, + key_purpose VARCHAR(32) NOT NULL, + user_id BIGINT NOT NULL, + school_id BIGINT NOT NULL, + class_id BIGINT NOT NULL, + public_key VARCHAR(256) NOT NULL, + encrypted_private_key VARCHAR(512) NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + created_by VARCHAR(64) NOT NULL, + updated_by VARCHAR(64) NOT NULL, + deleted BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE IF NOT EXISTS institution_sm2_key_audit ( + id BIGINT PRIMARY KEY, + key_record_id BIGINT NOT NULL, + key_id VARCHAR(64) NOT NULL, + operation VARCHAR(32) NOT NULL, + user_id BIGINT NOT NULL, + school_id BIGINT NOT NULL, + class_id BIGINT NOT NULL, + operation_detail VARCHAR(512) NOT NULL, + created_at TIMESTAMP NOT NULL, + created_by VARCHAR(64) NOT NULL, + CONSTRAINT fk_institution_key_audit_key + FOREIGN KEY (key_record_id) REFERENCES institution_sm2_key(id) +); + INSERT IGNORE INTO sys_role (id, name, created_at, updated_at, created_by, updated_by, deleted) VALUES (1001, 'TEACHER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'SYSTEM', 'SYSTEM', FALSE), (1002, 'STUDENT', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'SYSTEM', 'SYSTEM', FALSE); @@ -67,3 +100,43 @@ ON DUPLICATE KEY UPDATE account = VALUES(account), display_name = VALUES(display source_updated_at = CURRENT_TIMESTAMP, synced_at = CURRENT_TIMESTAMP; INSERT IGNORE INTO sys_user_role (user_id, role_id) VALUES (487, 1002); +-- +-- CREATE TABLE IF NOT EXISTS institution_identifier_application ( +-- id BIGINT PRIMARY KEY, +-- bank_code VARCHAR(32) NOT NULL, +-- request_timestamp CHAR(14) NOT NULL, +-- digest CHAR(64), +-- bank_signature VARCHAR(512), +-- status VARCHAR(32) NOT NULL, +-- verification_key_id VARCHAR(64), +-- signature_valid BOOLEAN, +-- digest_matches BOOLEAN, +-- signing_key_id VARCHAR(64), +-- institution_identifier VARCHAR(32), +-- central_bank_signature VARCHAR(512), +-- created_at TIMESTAMP NOT NULL, +-- updated_at TIMESTAMP NOT NULL, +-- created_by VARCHAR(64) NOT NULL, +-- updated_by VARCHAR(64) NOT NULL, +-- deleted BOOLEAN NOT NULL DEFAULT FALSE, +-- CONSTRAINT uk_institution_identifier_bank UNIQUE (bank_code) +-- ); +-- +-- CREATE INDEX IF NOT EXISTS idx_institution_identifier_status +-- ON institution_identifier_application(status); +-- +-- CREATE TABLE IF NOT EXISTS institution_identifier_operation_log ( +-- id BIGINT PRIMARY KEY, +-- application_id BIGINT NOT NULL, +-- operation VARCHAR(32) NOT NULL, +-- from_status VARCHAR(32), +-- to_status VARCHAR(32) NOT NULL, +-- operation_detail VARCHAR(1024) NOT NULL, +-- created_at TIMESTAMP NOT NULL, +-- created_by VARCHAR(64) NOT NULL, +-- CONSTRAINT fk_identifier_operation_application +-- FOREIGN KEY (application_id) REFERENCES institution_identifier_application(id) +-- ); +-- +-- CREATE INDEX IF NOT EXISTS idx_identifier_operation_application +-- ON institution_identifier_operation_log(application_id); diff --git a/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfigTest.java b/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfigTest.java index c30412a..6d0f3f3 100644 --- a/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfigTest.java +++ b/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfigTest.java @@ -15,6 +15,7 @@ class PlatformReadOnlyDataSourceConfigTest { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(PropertiesConfiguration.class, PlatformReadOnlyDataSourceConfig.class)) .withPropertyValues( + "platform-integration.enabled=true", "platform-integration.datasource.url=jdbc:h2:mem:platform_config", "platform-integration.datasource.username=sa", "platform-integration.datasource.password=test-password", From 1a65c4d426a87dcf46ee3d1bd1ae2ac34de0ff86 Mon Sep 17 00:00:00 2001 From: "jiazheng.zhao" <2624533894@qq.com> Date: Tue, 4 Aug 2026 11:24:28 +0800 Subject: [PATCH 2/2] fix: remove merge conflict markers --- .../interfaces/PlatformSsoController.java | 5 ----- src/main/resources/application-test.yml | 8 -------- 2 files changed, 13 deletions(-) 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 196ed9d..10be3ba 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java @@ -4,13 +4,8 @@ import com.yau.digitalrmb.identity.application.LocalSsoAccountService; import com.yau.digitalrmb.platformintegration.application.PlatformTokenVerifier; import com.yau.digitalrmb.platformintegration.application.VerifiedPlatformToken; import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties; -<<<<<<< HEAD -import com.yau.digitalrmb.security.application.LoginExchangeCodeService; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -======= import com.yau.digitalrmb.security.application.JwtTokenService; import io.swagger.v3.oas.annotations.Operation; ->>>>>>> origin/master import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 2316a2e..36a3a99 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -9,14 +9,6 @@ security: secret: 0123456789012345678901234567890123456789012345678901234567890123 access-token-ttl: PT30M platform-integration: -<<<<<<< HEAD - enabled: true - datasource: - url: jdbc:h2:mem:platform;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE - username: sa - password: test-password -======= ->>>>>>> origin/master token: link-secret-key: local-token-sso-test-secret-key-123456 frontend: