diff --git a/README.md b/README.md index 639afa3..cabb109 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ 开发规范与模块目录约定见 [开发规范](docs/DEVELOPMENT_GUIDE.md)。 +## 运行环境 + +- JDK 8(已按 Temurin 8 验证) +- Spring Boot 2.7.18、Spring Security 5.7、MyBatis-Plus 3.5.17、Springdoc 1.8.0 +- MySQL 8 + ## 登录方式 - 主平台单点登录:主平台跳转至 `GET /api/v1/auth/sso?token=...`。后端以主平台教师或学生档案的 `add_time` 动态 HMAC 密钥验证 Token,生成一次性 `code` 并跳转至前端回调地址。 diff --git a/docs/DEVELOPMENT_GUIDE.md b/docs/DEVELOPMENT_GUIDE.md index 854fab1..11532ee 100644 --- a/docs/DEVELOPMENT_GUIDE.md +++ b/docs/DEVELOPMENT_GUIDE.md @@ -2,6 +2,10 @@ 本文档是本项目新增模块、接口和数据表时的统一约定。目标是在业务持续扩展时保持领域边界清晰、认证安全、数据可演进。 +## 运行时版本 + +项目运行与构建统一使用 JDK 8,框架基线为 Spring Boot 2.7.18、Spring Security 5.7、MyBatis-Plus 3.5.17 和 Springdoc 1.8.0。新增代码不得使用 Java 9 及以上的语言特性或标准库 API;Servlet 与 Bean Validation 使用 `javax.*` 包。 + ## 1. 架构总览 项目采用 DDD 的“按限界上下文分包”方式。一个顶层模块代表一个业务能力,而不是一个技术层。 @@ -104,7 +108,7 @@ infrastructure → domain / application 定义的端口 ## 6. API 与异常规范 - REST Controller 放在 `.interfaces.rest`,路径统一以 `/api/v1/` 开头。 -- 请求 DTO 使用 `jakarta.validation` 注解;Controller 参数标注 `@Valid`。 +- 请求 DTO 使用 `javax.validation` 注解;Controller 参数标注 `@Valid`。 - 对外成功响应统一使用 `ApiResponse`,不要自行定义不一致的响应包装格式。 - 可预期业务错误抛出 `BusinessException` 并使用 `ErrorCode`;不要在 Controller 中 `try/catch` 后吞掉异常。 - 接口响应只返回 DTO,不返回 MyBatis Entity、密码哈希、刷新令牌摘要或主平台 Token。 diff --git a/docs/superpowers/plans/2026-08-03-java8-downgrade.md b/docs/superpowers/plans/2026-08-03-java8-downgrade.md index 16ca13a..bb3a427 100644 --- a/docs/superpowers/plans/2026-08-03-java8-downgrade.md +++ b/docs/superpowers/plans/2026-08-03-java8-downgrade.md @@ -10,7 +10,7 @@ ## Global Constraints -- Maven 必须以 `release 8` 编译;不得保留 Java 9+ 或 Java 17 语言/API 依赖。 +- Maven 必须以 JDK 8 以及 `source`/`target` 8 编译;不得保留 Java 9+ 或 Java 17 语言/API 依赖。 - 保持本系统 JWT、本地用户名密码登录、SSO/CAS、Flyway V3 用户初始化和 Swagger 地址不变。 - 不修改主平台工程,不输出任何密码或 Token。 @@ -23,10 +23,10 @@ - Modify: `src/main/java/com/yau/digitalrmb/shared/infrastructure/persistence/ApplicationDataSourceConfig.java` - Test: `mvn -DskipTests compile -B` -- [ ] 将父版本切换为 `2.7.18`,`java.version` 切换为 `1.8`,设置 `maven.compiler.release=8`。 -- [ ] 将 Web/Flyway/MyBatis-Plus/Springdoc 依赖替换为 Boot 2 对应 artifact:`spring-boot-starter-web`、`flyway-core`、`mybatis-plus-boot-starter`、`springdoc-openapi-ui:1.8.0`;移除 Boot 4 专用依赖。 -- [ ] 将 `DataSourceProperties` 导入改为 Boot 2 包路径。 -- [ ] 运行 `mvn -DskipTests compile -B`,以编译报错清单驱动后续兼容改造。 +- [x] 将父版本切换为 `2.7.18`,`java.version` 切换为 `1.8`,设置 `maven.compiler.source` 与 `maven.compiler.target` 为 `1.8`。 +- [x] 将 Web/Flyway/MyBatis-Plus/Springdoc 依赖替换为 Boot 2 对应 artifact:`spring-boot-starter-web`、`flyway-core`、`mybatis-plus-boot-starter`、`springdoc-openapi-ui:1.8.0`;移除 Boot 4 专用依赖。 +- [x] 将 `DataSourceProperties` 导入改为 Boot 2 包路径。 +- [x] 使用 JDK 8 完成编译并按报错清单完成后续兼容改造。 ### Task 2: 改造 Java 17、Spring 6 与 Jakarta 代码 @@ -34,11 +34,11 @@ - Modify: 所有 `src/main/java/**` 中的 record、text block、`JdbcClient`、`jakarta.*` 和 Java 9+ API 调用 - Modify: 对应 `src/test/java/**` 测试与 Boot 4 测试导入 -- [ ] 将所有 record 变为带构造器、getter、`equals`/`hashCode` 的 Java 8 类;请求 DTO 提供 JavaBean getter,保留 Bean Validation。 -- [ ] 用 `NamedParameterJdbcTemplate` 替换 `JdbcClient`;用普通字符串替换 text block 和 `String.formatted`。 -- [ ] 将 Jakarta 包改为 `javax.*`,将 Boot 4 `AutoConfigureMockMvc` 包改为 Boot 2 包。 -- [ ] 用 `Collections`、显式 `switch`、`StringBuilder`/自定义十六进制转换替换 Java 9+ API。 -- [ ] 运行 `mvn test -DforkCount=0 -B`。 +- [x] 将所有 record 变为带构造器、getter、`equals`/`hashCode` 的 Java 8 类;请求 DTO 提供 JavaBean getter,保留 Bean Validation。 +- [x] 用 `NamedParameterJdbcTemplate` 替换 `JdbcClient`;用普通字符串替换 text block 和 `String.formatted`。 +- [x] 将 Jakarta 包改为 `javax.*`,将 Boot 4 `AutoConfigureMockMvc` 包改为 Boot 2 包。 +- [x] 用 `Collections`、显式 `switch`、`StringBuilder`/自定义十六进制转换替换 Java 9+ API。 +- [x] 使用 JDK 8 运行 `mvn clean test -B`,19 项测试通过。 ### Task 3: Java 8 运行时验证与文档 @@ -46,6 +46,6 @@ - Modify: `README.md` - Modify: `docs/DEVELOPMENT_GUIDE.md` -- [ ] 使用 JDK 8 执行 `java -version`、`mvn test` 和打包后的 JAR 启动验证。 -- [ ] 更新 README 与开发规范的 Java/Spring Boot 版本要求。 -- [ ] 运行 `mvn package -DskipTests -B`,确认可执行包生成成功。 +- [x] 使用 JDK 8 执行 `java -version`、`mvn test` 和 Web 健康检查启动验证。 +- [x] 更新 README 与开发规范的 Java/Spring Boot 版本要求。 +- [x] 使用 JDK 8 运行 `mvn package -DskipTests -B`,确认可执行包生成成功。 diff --git a/pom.xml b/pom.xml index 605b83f..20e2ad6 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ org.springframework.boot spring-boot-starter-parent - 4.1.0 + 2.7.18 @@ -18,15 +18,17 @@ 数字人民币教学仿真后端 - 17 + 1.8 + 1.8 + 1.8 3.5.17 - 3.0.3 + 1.8.0 org.springframework.boot - spring-boot-starter-webmvc + spring-boot-starter-web org.springframework.boot @@ -44,22 +46,18 @@ org.springframework.boot spring-boot-starter-actuator - - org.springframework.boot - spring-boot-starter-flyway - org.flywaydb - flyway-mysql + flyway-core com.baomidou - mybatis-plus-spring-boot4-starter + mybatis-plus-boot-starter ${mybatis-plus.version} com.baomidou - mybatis-plus-jsqlparser + mybatis-plus-jsqlparser-4.9 ${mybatis-plus.version} @@ -69,7 +67,7 @@ org.springdoc - springdoc-openapi-starter-webmvc-ui + springdoc-openapi-ui ${springdoc.version} @@ -87,11 +85,6 @@ spring-boot-starter-test test - - org.springframework.boot - spring-boot-starter-webmvc-test - test - org.springframework.security spring-security-test diff --git a/src/main/java/com/yau/digitalrmb/identity/application/PlatformIdentityProjectionService.java b/src/main/java/com/yau/digitalrmb/identity/application/PlatformIdentityProjectionService.java index db118ca..6a619fd 100644 --- a/src/main/java/com/yau/digitalrmb/identity/application/PlatformIdentityProjectionService.java +++ b/src/main/java/com/yau/digitalrmb/identity/application/PlatformIdentityProjectionService.java @@ -80,9 +80,6 @@ public class PlatformIdentityProjectionService { } private long roleId(PlatformActor actor) { - return switch (actor.role()) { - case TEACHER -> 1001L; - case STUDENT -> 1002L; - }; + return actor.role().name().equals("TEACHER") ? 1001L : 1002L; } } diff --git a/src/main/java/com/yau/digitalrmb/issuance/domain/model/IssuanceApplicationId.java b/src/main/java/com/yau/digitalrmb/issuance/domain/model/IssuanceApplicationId.java index 842d622..bc0667a 100644 --- a/src/main/java/com/yau/digitalrmb/issuance/domain/model/IssuanceApplicationId.java +++ b/src/main/java/com/yau/digitalrmb/issuance/domain/model/IssuanceApplicationId.java @@ -1,4 +1,14 @@ package com.yau.digitalrmb.issuance.domain.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; import java.util.Objects; import java.util.UUID; -public record IssuanceApplicationId(UUID value) { public IssuanceApplicationId { Objects.requireNonNull(value, "value must not be null"); } } + +@Getter +@EqualsAndHashCode +public class IssuanceApplicationId { + private final UUID value; + public IssuanceApplicationId(UUID value) { this.value = Objects.requireNonNull(value, "value must not be null"); } + public UUID value() { return value; } +} 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 27acbe5..23098d8 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/application/CasTicketValidator.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/application/CasTicketValidator.java @@ -5,6 +5,9 @@ import com.yau.digitalrmb.shared.api.ErrorCode; import com.yau.digitalrmb.shared.exception.BusinessException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import org.w3c.dom.Document; import org.xml.sax.InputSource; @@ -16,24 +19,27 @@ import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; import java.io.StringReader; import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; @Component public class CasTicketValidator { private final PlatformIntegrationProperties.Cas properties; - private final HttpClient httpClient; + private final RestTemplate restTemplate; @Autowired public CasTicketValidator(PlatformIntegrationProperties properties) { - this(properties.getCas(), HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build()); + this(properties.getCas(), createRestTemplate()); } - CasTicketValidator(PlatformIntegrationProperties.Cas properties, HttpClient httpClient) { + private static RestTemplate createRestTemplate() { + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + requestFactory.setConnectTimeout(5000); + requestFactory.setReadTimeout(5000); + return new RestTemplate(requestFactory); + } + + CasTicketValidator(PlatformIntegrationProperties.Cas properties, RestTemplate restTemplate) { this.properties = properties; - this.httpClient = httpClient; + this.restTemplate = restTemplate; } public String validate(String ticket) { @@ -42,12 +48,11 @@ public class CasTicketValidator { .queryParam("service", properties.getCallbackUrl()) .queryParam("ticket", ticket) .build().encode().toUri(); - HttpRequest request = HttpRequest.newBuilder(uri).GET().timeout(Duration.ofSeconds(5)).build(); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() < 200 || response.statusCode() >= 300) { + ResponseEntity response = restTemplate.getForEntity(uri, String.class); + if (response.getStatusCodeValue() < 200 || response.getStatusCodeValue() >= 300) { throw rejected(); } - return parseAccount(response.body()); + return parseAccount(response.getBody()); } catch (BusinessException exception) { throw exception; } catch (Exception exception) { @@ -66,13 +71,13 @@ public class CasTicketValidator { factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); factory.setXIncludeAware(false); factory.setExpandEntityReferences(false); - var builder = factory.newDocumentBuilder(); + javax.xml.parsers.DocumentBuilder builder = factory.newDocumentBuilder(); builder.setErrorHandler(new DefaultHandler()); Document document = builder.parse(new InputSource(new StringReader(xml))); String account = (String) XPathFactory.newInstance().newXPath().evaluate( "string(//*[local-name()='authenticationSuccess']/*[local-name()='user'])", document, XPathConstants.STRING); - if (account == null || account.isBlank()) { + if (account == null || account.trim().isEmpty()) { throw rejected(); } return account.trim(); 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 2659a79..921b0f7 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifier.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifier.java @@ -15,7 +15,6 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.Base64; -import java.util.HexFormat; @Component public class PlatformTokenVerifier { @@ -114,10 +113,9 @@ public class PlatformTokenVerifier { } private void verifyIdentityClaim(JsonNode payload, PlatformActor actor) { - String expected = switch (actor.role()) { - case TEACHER -> properties.getTeacherClaimValue(); - case STUDENT -> properties.getStudentClaimValue(); - }; + String expected = actor.role().name().equals("TEACHER") + ? properties.getTeacherClaimValue() + : properties.getStudentClaimValue(); if (!expected.equals(payload.path(String.valueOf(actor.profileId())).asText())) { throw invalid(); } @@ -136,7 +134,15 @@ public class PlatformTokenVerifier { private String fingerprint(String rawToken) throws Exception { byte[] digest = MessageDigest.getInstance("SHA-256").digest(rawToken.getBytes(StandardCharsets.UTF_8)); - return HexFormat.of().formatHex(digest); + StringBuilder fingerprint = new StringBuilder(digest.length * 2); + for (byte value : digest) { + String hex = Integer.toHexString(value & 0xff); + if (hex.length() == 1) { + fingerprint.append('0'); + } + fingerprint.append(hex); + } + return fingerprint.toString(); } private PlatformTokenException invalid() { diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/application/VerifiedPlatformToken.java b/src/main/java/com/yau/digitalrmb/platformintegration/application/VerifiedPlatformToken.java index 939fa7e..b4aa863 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/application/VerifiedPlatformToken.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/application/VerifiedPlatformToken.java @@ -1,6 +1,13 @@ package com.yau.digitalrmb.platformintegration.application; import com.yau.digitalrmb.platformintegration.domain.PlatformActor; +import lombok.EqualsAndHashCode; +import lombok.Getter; -public record VerifiedPlatformToken(PlatformActor actor, String fingerprint) { +@Getter +@EqualsAndHashCode +public class VerifiedPlatformToken { + private final PlatformActor actor; private final String fingerprint; + public VerifiedPlatformToken(PlatformActor actor, String fingerprint) { this.actor = actor; this.fingerprint = fingerprint; } + public PlatformActor actor() { return actor; } public String fingerprint() { return fingerprint; } } diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationProperties.java b/src/main/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationProperties.java index 56cd9a2..7868a99 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationProperties.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationProperties.java @@ -1,9 +1,9 @@ package com.yau.digitalrmb.platformintegration.config; -import jakarta.validation.Valid; -import jakarta.validation.constraints.AssertTrue; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/domain/PlatformActor.java b/src/main/java/com/yau/digitalrmb/platformintegration/domain/PlatformActor.java index 01b6990..ab3c9d2 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/domain/PlatformActor.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/domain/PlatformActor.java @@ -1,12 +1,13 @@ package com.yau.digitalrmb.platformintegration.domain; +import lombok.EqualsAndHashCode; +import lombok.Getter; import java.time.Instant; -public record PlatformActor( - long platformUserId, - long profileId, - String account, - String displayName, - PlatformRole role, - Instant tokenSigningTime) { +@Getter +@EqualsAndHashCode +public class PlatformActor { + private final long platformUserId; private final long profileId; private final String account; private final String displayName; private final PlatformRole role; private final Instant tokenSigningTime; + public PlatformActor(long platformUserId, long profileId, String account, String displayName, PlatformRole role, Instant tokenSigningTime) { this.platformUserId = platformUserId; this.profileId = profileId; this.account = account; this.displayName = displayName; this.role = role; this.tokenSigningTime = tokenSigningTime; } + public long platformUserId() { return platformUserId; } public long profileId() { return profileId; } public String account() { return account; } public String displayName() { return displayName; } public PlatformRole role() { return role; } public Instant tokenSigningTime() { return tokenSigningTime; } } diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java index 95cf6b8..9a6a282 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java @@ -3,8 +3,11 @@ package com.yau.digitalrmb.platformintegration.infrastructure; import com.yau.digitalrmb.platformintegration.application.PlatformIdentityRepository; import com.yau.digitalrmb.platformintegration.domain.PlatformActor; import com.yau.digitalrmb.platformintegration.domain.PlatformRole; -import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Repository; import java.sql.ResultSet; @@ -17,23 +20,22 @@ import java.util.Optional; @Repository public class JdbcPlatformIdentityRepository implements PlatformIdentityRepository { - private static final String TEACHER_QUERY = """ - SELECT cu.ID, cu.CODE, cu.NAME, t.teacher_id AS profile_id, t.add_time AS signing_time - FROM core_user cu JOIN teacher t ON t.user_id = cu.ID - WHERE cu.JOB_TYPE1 = 'JT_S_02' AND cu.STATE = 'S1' AND cu.DEL_FLAG = 0 - AND t.teacher_status = 1 AND t.add_time IS NOT NULL AND %s - """; - private static final String STUDENT_QUERY = """ - SELECT cu.ID, cu.CODE, cu.NAME, s.student_id AS profile_id, s.add_time AS signing_time - FROM core_user cu JOIN student s ON s.user_id = cu.ID - WHERE cu.JOB_TYPE1 = 'JT_S_03' AND cu.STATE = 'S1' AND cu.DEL_FLAG = 0 - AND s.student_status = 1 AND s.add_time IS NOT NULL AND %s - """; + private static final String TEACHER_QUERY = + "SELECT cu.ID, cu.CODE, cu.NAME, t.teacher_id AS profile_id, t.add_time AS signing_time " + + "FROM core_user cu JOIN teacher t ON t.user_id = cu.ID " + + "WHERE cu.JOB_TYPE1 = 'JT_S_02' AND cu.STATE = 'S1' AND cu.DEL_FLAG = 0 " + + "AND t.teacher_status = 1 AND t.add_time IS NOT NULL AND %s"; + private static final String STUDENT_QUERY = + "SELECT cu.ID, cu.CODE, cu.NAME, s.student_id AS profile_id, s.add_time AS signing_time " + + "FROM core_user cu JOIN student s ON s.user_id = cu.ID " + + "WHERE cu.JOB_TYPE1 = 'JT_S_03' AND cu.STATE = 'S1' AND cu.DEL_FLAG = 0 " + + "AND s.student_status = 1 AND s.add_time IS NOT NULL AND %s"; - private final JdbcClient jdbcClient; + private final NamedParameterJdbcTemplate jdbcTemplate; - public JdbcPlatformIdentityRepository(@Qualifier("platformJdbcClient") JdbcClient jdbcClient) { - this.jdbcClient = jdbcClient; + public JdbcPlatformIdentityRepository( + @Qualifier("platformNamedParameterJdbcTemplate") NamedParameterJdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; } @Override @@ -50,28 +52,37 @@ public class JdbcPlatformIdentityRepository implements PlatformIdentityRepositor public List findChangedSince(Instant watermark) { String predicate = "(cu.update_Time > :watermark OR cu.CREATE_TIME > :watermark)"; Timestamp since = Timestamp.from(watermark); - List actors = new ArrayList<>(queryAll(TEACHER_QUERY.formatted(predicate), since, PlatformRole.TEACHER)); - actors.addAll(queryAll(STUDENT_QUERY.formatted(predicate), since, PlatformRole.STUDENT)); + List actors = new ArrayList( + queryAll(String.format(TEACHER_QUERY, predicate), since, PlatformRole.TEACHER)); + actors.addAll(queryAll(String.format(STUDENT_QUERY, predicate), since, PlatformRole.STUDENT)); return actors; } private Optional findBy(String predicate, Object value) { - Optional teacher = query(TEACHER_QUERY.formatted(predicate), value, PlatformRole.TEACHER); - return teacher.isPresent() ? teacher : query(STUDENT_QUERY.formatted(predicate), value, PlatformRole.STUDENT); + Optional teacher = query(String.format(TEACHER_QUERY, predicate), value, PlatformRole.TEACHER); + return teacher.isPresent() ? teacher : query(String.format(STUDENT_QUERY, predicate), value, PlatformRole.STUDENT); } private Optional query(String sql, Object value, PlatformRole role) { - return jdbcClient.sql(sql) - .param("value", value) - .query((resultSet, rowNumber) -> map(resultSet, role)) - .optional(); + List actors = jdbcTemplate.query(sql, + new MapSqlParameterSource("value", value), rowMapper(role)); + if (actors.size() > 1) { + throw new IncorrectResultSizeDataAccessException(1, actors.size()); + } + return actors.isEmpty() ? Optional.empty() : Optional.of(actors.get(0)); } private List queryAll(String sql, Timestamp watermark, PlatformRole role) { - return jdbcClient.sql(sql) - .param("watermark", watermark) - .query((resultSet, rowNumber) -> map(resultSet, role)) - .list(); + return jdbcTemplate.query(sql, new MapSqlParameterSource("watermark", watermark), rowMapper(role)); + } + + private RowMapper rowMapper(final PlatformRole role) { + return new RowMapper() { + @Override + public PlatformActor mapRow(ResultSet resultSet, int rowNumber) throws SQLException { + return map(resultSet, role); + } + }; } private PlatformActor map(ResultSet resultSet, PlatformRole role) throws SQLException { 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 e3adf36..c7d3459 100644 --- a/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfig.java +++ b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfig.java @@ -6,7 +6,7 @@ import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperti import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import javax.sql.DataSource; @@ -24,8 +24,9 @@ public class PlatformReadOnlyDataSourceConfig { return new HikariDataSource(config); } - @Bean(name = "platformJdbcClient") - public JdbcClient platformJdbcClient(@Qualifier("platformReadOnlyDataSource") DataSource dataSource) { - return JdbcClient.create(dataSource); + @Bean(name = "platformNamedParameterJdbcTemplate") + public NamedParameterJdbcTemplate platformNamedParameterJdbcTemplate( + @Qualifier("platformReadOnlyDataSource") DataSource dataSource) { + return new NamedParameterJdbcTemplate(dataSource); } } 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 ec51e13..ef35c26 100644 --- a/src/main/java/com/yau/digitalrmb/security/application/JwtTokenService.java +++ b/src/main/java/com/yau/digitalrmb/security/application/JwtTokenService.java @@ -12,6 +12,8 @@ import org.springframework.stereotype.Service; import java.time.Instant; import java.util.List; import java.util.Set; +import java.util.Collections; +import java.util.stream.Collectors; @Service public class JwtTokenService { @@ -27,15 +29,15 @@ public class JwtTokenService { Instant issuedAt = Instant.now(); Instant expiresAt = issuedAt.plus(properties.getJwt().getAccessTokenTtl()); JwtClaimsSet claims = JwtClaimsSet.builder().subject(username).issuedAt(issuedAt).expiresAt(expiresAt) - .claim("roles", List.of("ROLE_ADMIN")).build(); + .claim("roles", Collections.singletonList("ROLE_ADMIN")).build(); Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(JwsHeader.with(MacAlgorithm.HS256).build(), claims)); - return new Token(jwt.getTokenValue(), properties.getJwt().getAccessTokenTtl().toSeconds()); + return new Token(jwt.getTokenValue(), properties.getJwt().getAccessTokenTtl().getSeconds()); } public Token issueFor(long platformUserId, String account, Set roles) { Instant issuedAt = Instant.now(); Instant expiresAt = issuedAt.plus(properties.getJwt().getAccessTokenTtl()); - List authorities = roles.stream().sorted().map(role -> "ROLE_" + role).toList(); + List authorities = roles.stream().sorted().map(role -> "ROLE_" + role).collect(Collectors.toList()); JwtClaimsSet claims = JwtClaimsSet.builder() .subject(String.valueOf(platformUserId)) .issuedAt(issuedAt) @@ -44,8 +46,14 @@ public class JwtTokenService { .claim("roles", authorities) .build(); Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(JwsHeader.with(MacAlgorithm.HS256).build(), claims)); - return new Token(jwt.getTokenValue(), properties.getJwt().getAccessTokenTtl().toSeconds()); + return new Token(jwt.getTokenValue(), properties.getJwt().getAccessTokenTtl().getSeconds()); } - public record Token(String accessToken, long expiresIn) { } + public static class Token { + private final String accessToken; + private final long expiresIn; + public Token(String accessToken, long expiresIn) { this.accessToken = accessToken; this.expiresIn = expiresIn; } + public String accessToken() { return accessToken; } + public long expiresIn() { return expiresIn; } + } } diff --git a/src/main/java/com/yau/digitalrmb/security/application/LocalAccountAuthenticationService.java b/src/main/java/com/yau/digitalrmb/security/application/LocalAccountAuthenticationService.java index 94a8c0d..65c3cc8 100644 --- a/src/main/java/com/yau/digitalrmb/security/application/LocalAccountAuthenticationService.java +++ b/src/main/java/com/yau/digitalrmb/security/application/LocalAccountAuthenticationService.java @@ -10,7 +10,7 @@ import com.yau.digitalrmb.shared.exception.BusinessException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; -import java.util.Set; +import java.util.Collections; @Service public class LocalAccountAuthenticationService { @@ -41,7 +41,7 @@ public class LocalAccountAuthenticationService { if (snapshot == null) { throw invalidCredentials(); } - return jwtTokenService.issueFor(user.getId(), snapshot.getAccount(), Set.of(snapshot.getRoleKey())); + return jwtTokenService.issueFor(user.getId(), snapshot.getAccount(), Collections.singleton(snapshot.getRoleKey())); } private BusinessException invalidCredentials() { diff --git a/src/main/java/com/yau/digitalrmb/security/application/LoginExchangeCodeService.java b/src/main/java/com/yau/digitalrmb/security/application/LoginExchangeCodeService.java index 63dddd5..4da3dbe 100644 --- a/src/main/java/com/yau/digitalrmb/security/application/LoginExchangeCodeService.java +++ b/src/main/java/com/yau/digitalrmb/security/application/LoginExchangeCodeService.java @@ -12,7 +12,6 @@ import java.security.SecureRandom; import java.sql.Timestamp; import java.time.Instant; import java.util.Base64; -import java.util.HexFormat; import java.util.List; @Service @@ -53,8 +52,16 @@ public class LoginExchangeCodeService { static String hash(String value) { try { - return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256") - .digest(value.getBytes(StandardCharsets.UTF_8))); + byte[] digest = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)); + StringBuilder hash = new StringBuilder(digest.length * 2); + for (byte item : digest) { + String hex = Integer.toHexString(item & 0xff); + if (hex.length() == 1) { + hash.append('0'); + } + hash.append(hex); + } + return hash.toString(); } catch (Exception exception) { throw new IllegalStateException("SHA-256 is unavailable", exception); } diff --git a/src/main/java/com/yau/digitalrmb/security/config/SecurityConfig.java b/src/main/java/com/yau/digitalrmb/security/config/SecurityConfig.java index d62901a..3df3699 100644 --- a/src/main/java/com/yau/digitalrmb/security/config/SecurityConfig.java +++ b/src/main/java/com/yau/digitalrmb/security/config/SecurityConfig.java @@ -49,8 +49,8 @@ public class SecurityConfig { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http.csrf(AbstractHttpConfigurer::disable) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/actuator/health", "/api/v1/auth/login", "/api/v1/auth/sso", + .authorizeRequests(authorize -> authorize + .antMatchers("/actuator/health", "/api/v1/auth/login", "/api/v1/auth/sso", "/api/v1/auth/cas/**", "/api/v1/auth/session/exchange", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html") .permitAll() @@ -66,7 +66,7 @@ public class SecurityConfig { return new SecretKeySpec(properties.getJwt().getSecret().getBytes(StandardCharsets.UTF_8), "HmacSHA256"); } - private static void writeError(jakarta.servlet.http.HttpServletResponse response, ErrorCode code, int status) + private static void writeError(javax.servlet.http.HttpServletResponse response, ErrorCode code, int status) throws java.io.IOException { response.setStatus(status); response.setContentType(MediaType.APPLICATION_JSON_VALUE); diff --git a/src/main/java/com/yau/digitalrmb/security/config/SecurityProperties.java b/src/main/java/com/yau/digitalrmb/security/config/SecurityProperties.java index 98c7745..26858ae 100644 --- a/src/main/java/com/yau/digitalrmb/security/config/SecurityProperties.java +++ b/src/main/java/com/yau/digitalrmb/security/config/SecurityProperties.java @@ -1,6 +1,6 @@ package com.yau.digitalrmb.security.config; -import jakarta.annotation.PostConstruct; +import javax.annotation.PostConstruct; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; diff --git a/src/main/java/com/yau/digitalrmb/security/interfaces/AuthController.java b/src/main/java/com/yau/digitalrmb/security/interfaces/AuthController.java index a5e0961..968b9ae 100644 --- a/src/main/java/com/yau/digitalrmb/security/interfaces/AuthController.java +++ b/src/main/java/com/yau/digitalrmb/security/interfaces/AuthController.java @@ -10,7 +10,7 @@ import com.yau.digitalrmb.shared.api.ApiResponse; import com.yau.digitalrmb.shared.api.ErrorCode; import com.yau.digitalrmb.shared.exception.BusinessException; import com.yau.digitalrmb.shared.web.TraceIdFilter; -import jakarta.validation.Valid; +import javax.validation.Valid; import org.slf4j.MDC; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.jwt.Jwt; @@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Set; +import java.util.Collections; @RestController @RequestMapping("/api/v1/auth") @@ -57,7 +58,7 @@ public class AuthController { throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户身份不存在"); } JwtTokenService.Token accessToken = tokenService.issueFor(platformUserId, snapshot.getAccount(), - Set.of(snapshot.getRoleKey())); + Collections.singleton(snapshot.getRoleKey())); String refreshToken = refreshTokenService.issue(platformUserId); return ApiResponse.success(new SessionResponse(accessToken.accessToken(), refreshToken, "Bearer", accessToken.expiresIn()), MDC.get(TraceIdFilter.MDC_KEY)); @@ -71,7 +72,7 @@ public class AuthController { throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户身份不存在"); } return ApiResponse.success(new CurrentUserResponse(platformUserId, snapshot.getAccount(), snapshot.getDisplayName(), - List.of(snapshot.getRoleKey())), MDC.get(TraceIdFilter.MDC_KEY)); + Collections.singletonList(snapshot.getRoleKey())), MDC.get(TraceIdFilter.MDC_KEY)); } @PostMapping("/logout") diff --git a/src/main/java/com/yau/digitalrmb/security/interfaces/CurrentUserResponse.java b/src/main/java/com/yau/digitalrmb/security/interfaces/CurrentUserResponse.java index 2b916b6..eb74b4f 100644 --- a/src/main/java/com/yau/digitalrmb/security/interfaces/CurrentUserResponse.java +++ b/src/main/java/com/yau/digitalrmb/security/interfaces/CurrentUserResponse.java @@ -1,6 +1,4 @@ package com.yau.digitalrmb.security.interfaces; - -import java.util.List; - -public record CurrentUserResponse(long platformUserId, String account, String displayName, List roles) { -} +import lombok.AllArgsConstructor; import lombok.Getter; import java.util.List; +@Getter @AllArgsConstructor +public class CurrentUserResponse { private final long platformUserId; private final String account; private final String displayName; private final List roles; } diff --git a/src/main/java/com/yau/digitalrmb/security/interfaces/ExchangeCodeRequest.java b/src/main/java/com/yau/digitalrmb/security/interfaces/ExchangeCodeRequest.java index bc80131..84addff 100644 --- a/src/main/java/com/yau/digitalrmb/security/interfaces/ExchangeCodeRequest.java +++ b/src/main/java/com/yau/digitalrmb/security/interfaces/ExchangeCodeRequest.java @@ -1,6 +1,4 @@ package com.yau.digitalrmb.security.interfaces; - -import jakarta.validation.constraints.NotBlank; - -public record ExchangeCodeRequest(@NotBlank String code) { -} +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import javax.validation.constraints.NotBlank; +@Getter @Setter @NoArgsConstructor @AllArgsConstructor +public class ExchangeCodeRequest { @NotBlank private String code; public String code() { return code; } } diff --git a/src/main/java/com/yau/digitalrmb/security/interfaces/LoginRequest.java b/src/main/java/com/yau/digitalrmb/security/interfaces/LoginRequest.java index a72ae92..4afcf06 100644 --- a/src/main/java/com/yau/digitalrmb/security/interfaces/LoginRequest.java +++ b/src/main/java/com/yau/digitalrmb/security/interfaces/LoginRequest.java @@ -1,5 +1,4 @@ package com.yau.digitalrmb.security.interfaces; - -import jakarta.validation.constraints.NotBlank; - -public record LoginRequest(@NotBlank String username, @NotBlank String password) { } +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import javax.validation.constraints.NotBlank; +@Getter @Setter @NoArgsConstructor @AllArgsConstructor +public class LoginRequest { @NotBlank private String username; @NotBlank private String password; public String username() { return username; } public String password() { return password; } } diff --git a/src/main/java/com/yau/digitalrmb/security/interfaces/LoginResponse.java b/src/main/java/com/yau/digitalrmb/security/interfaces/LoginResponse.java index a5aef5f..03c2c68 100644 --- a/src/main/java/com/yau/digitalrmb/security/interfaces/LoginResponse.java +++ b/src/main/java/com/yau/digitalrmb/security/interfaces/LoginResponse.java @@ -1,3 +1,4 @@ package com.yau.digitalrmb.security.interfaces; - -public record LoginResponse(String accessToken, String tokenType, long expiresIn) { } +import lombok.AllArgsConstructor; import lombok.Getter; +@Getter @AllArgsConstructor +public class LoginResponse { private final String accessToken; private final String tokenType; private final long expiresIn; public String accessToken() { return accessToken; } public String tokenType() { return tokenType; } public long expiresIn() { return expiresIn; } } diff --git a/src/main/java/com/yau/digitalrmb/security/interfaces/LogoutRequest.java b/src/main/java/com/yau/digitalrmb/security/interfaces/LogoutRequest.java index 6e2d166..f9ea783 100644 --- a/src/main/java/com/yau/digitalrmb/security/interfaces/LogoutRequest.java +++ b/src/main/java/com/yau/digitalrmb/security/interfaces/LogoutRequest.java @@ -1,6 +1,4 @@ package com.yau.digitalrmb.security.interfaces; - -import jakarta.validation.constraints.NotBlank; - -public record LogoutRequest(@NotBlank String refreshToken) { -} +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import javax.validation.constraints.NotBlank; +@Getter @Setter @NoArgsConstructor @AllArgsConstructor +public class LogoutRequest { @NotBlank private String refreshToken; public String refreshToken() { return refreshToken; } } diff --git a/src/main/java/com/yau/digitalrmb/security/interfaces/SessionResponse.java b/src/main/java/com/yau/digitalrmb/security/interfaces/SessionResponse.java index 47f585d..9748d9c 100644 --- a/src/main/java/com/yau/digitalrmb/security/interfaces/SessionResponse.java +++ b/src/main/java/com/yau/digitalrmb/security/interfaces/SessionResponse.java @@ -1,4 +1,4 @@ package com.yau.digitalrmb.security.interfaces; - -public record SessionResponse(String accessToken, String refreshToken, String tokenType, long expiresIn) { -} +import lombok.AllArgsConstructor; import lombok.Getter; +@Getter @AllArgsConstructor +public class SessionResponse { private final String accessToken; private final String refreshToken; private final String tokenType; private final long expiresIn; } diff --git a/src/main/java/com/yau/digitalrmb/shared/api/ApiResponse.java b/src/main/java/com/yau/digitalrmb/shared/api/ApiResponse.java index 913465d..601c611 100644 --- a/src/main/java/com/yau/digitalrmb/shared/api/ApiResponse.java +++ b/src/main/java/com/yau/digitalrmb/shared/api/ApiResponse.java @@ -1,19 +1,29 @@ package com.yau.digitalrmb.shared.api; +import lombok.Getter; + import java.time.Instant; -public record ApiResponse( - String code, - String message, - T data, - String traceId, - Instant timestamp -) { - public static ApiResponse success(T data, String traceId) { - return new ApiResponse<>(ErrorCode.SUCCESS.name(), "success", data, traceId, Instant.now()); - } +@Getter +public class ApiResponse { + private final String code; + private final String message; + private final T data; + private final String traceId; + private final Instant timestamp; - public static ApiResponse failure(ErrorCode errorCode, String message, String traceId) { - return new ApiResponse<>(errorCode.name(), message, null, traceId, Instant.now()); + public ApiResponse(String code, String message, T data, String traceId, Instant timestamp) { + this.code = code; + this.message = message; + this.data = data; + this.traceId = traceId; + this.timestamp = timestamp; } + public String code() { return code; } + public String message() { return message; } + public T data() { return data; } + public String traceId() { return traceId; } + public Instant timestamp() { return timestamp; } + public static ApiResponse success(T data, String traceId) { return new ApiResponse(ErrorCode.SUCCESS.name(), "success", data, traceId, Instant.now()); } + public static ApiResponse failure(ErrorCode errorCode, String message, String traceId) { return new ApiResponse(errorCode.name(), message, null, traceId, Instant.now()); } } diff --git a/src/main/java/com/yau/digitalrmb/shared/infrastructure/persistence/ApplicationDataSourceConfig.java b/src/main/java/com/yau/digitalrmb/shared/infrastructure/persistence/ApplicationDataSourceConfig.java index 744a8d0..4c74c79 100644 --- a/src/main/java/com/yau/digitalrmb/shared/infrastructure/persistence/ApplicationDataSourceConfig.java +++ b/src/main/java/com/yau/digitalrmb/shared/infrastructure/persistence/ApplicationDataSourceConfig.java @@ -3,7 +3,7 @@ package com.yau.digitalrmb.shared.infrastructure.persistence; import com.zaxxer.hikari.HikariDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.jdbc.autoconfigure.DataSourceProperties; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; diff --git a/src/main/java/com/yau/digitalrmb/shared/interfaces/DiagnosticController.java b/src/main/java/com/yau/digitalrmb/shared/interfaces/DiagnosticController.java index 8a7646a..27b660e 100644 --- a/src/main/java/com/yau/digitalrmb/shared/interfaces/DiagnosticController.java +++ b/src/main/java/com/yau/digitalrmb/shared/interfaces/DiagnosticController.java @@ -2,7 +2,7 @@ package com.yau.digitalrmb.shared.interfaces; import com.yau.digitalrmb.shared.api.ApiResponse; import com.yau.digitalrmb.shared.web.TraceIdFilter; -import jakarta.validation.constraints.NotBlank; +import javax.validation.constraints.NotBlank; import org.slf4j.MDC; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; 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 fc1daa7..f52fb6f 100644 --- a/src/main/java/com/yau/digitalrmb/shared/web/GlobalExceptionHandler.java +++ b/src/main/java/com/yau/digitalrmb/shared/web/GlobalExceptionHandler.java @@ -4,7 +4,7 @@ import com.yau.digitalrmb.shared.api.ApiResponse; import com.yau.digitalrmb.shared.api.ErrorCode; import com.yau.digitalrmb.shared.exception.BusinessException; import com.yau.digitalrmb.platformintegration.application.PlatformTokenException; -import jakarta.validation.ConstraintViolationException; +import javax.validation.ConstraintViolationException; import org.slf4j.MDC; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/com/yau/digitalrmb/shared/web/TraceIdFilter.java b/src/main/java/com/yau/digitalrmb/shared/web/TraceIdFilter.java index fccd3d6..4a94da3 100644 --- a/src/main/java/com/yau/digitalrmb/shared/web/TraceIdFilter.java +++ b/src/main/java/com/yau/digitalrmb/shared/web/TraceIdFilter.java @@ -1,9 +1,9 @@ package com.yau.digitalrmb.shared.web; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.slf4j.MDC; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; @@ -20,7 +20,7 @@ public class TraceIdFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String traceId = request.getHeader(HEADER_NAME); - if (traceId == null || traceId.isBlank()) { + if (traceId == null || traceId.trim().isEmpty()) { traceId = UUID.randomUUID().toString(); } MDC.put(MDC_KEY, traceId); diff --git a/src/test/java/com/yau/digitalrmb/platformintegration/application/CasTicketValidatorTest.java b/src/test/java/com/yau/digitalrmb/platformintegration/application/CasTicketValidatorTest.java index 82c77bb..b5d010b 100644 --- a/src/test/java/com/yau/digitalrmb/platformintegration/application/CasTicketValidatorTest.java +++ b/src/test/java/com/yau/digitalrmb/platformintegration/application/CasTicketValidatorTest.java @@ -1,12 +1,32 @@ package com.yau.digitalrmb.platformintegration.application; +import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties; import com.yau.digitalrmb.shared.exception.BusinessException; import org.junit.jupiter.api.Test; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.client.RestTemplate; +import org.springframework.http.client.SimpleClientHttpRequestFactory; + +import java.lang.reflect.Field; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class CasTicketValidatorTest { + @Test + void configuresFiveSecondTimeoutsForCasRequests() throws Exception { + PlatformIntegrationProperties properties = new PlatformIntegrationProperties(); + CasTicketValidator validator = new CasTicketValidator(properties); + Field restTemplateField = ReflectionUtils.findField(CasTicketValidator.class, "restTemplate"); + ReflectionUtils.makeAccessible(restTemplateField); + RestTemplate restTemplate = (RestTemplate) ReflectionUtils.getField(restTemplateField, validator); + + assertThat(restTemplate.getRequestFactory()).isInstanceOf(SimpleClientHttpRequestFactory.class); + SimpleClientHttpRequestFactory requestFactory = (SimpleClientHttpRequestFactory) restTemplate.getRequestFactory(); + assertThat(readIntField(requestFactory, "connectTimeout")).isEqualTo(5000); + assertThat(readIntField(requestFactory, "readTimeout")).isEqualTo(5000); + } + @Test void parsesSuccessfulCasAccountAndRejectsExternalEntityPayloads() { String success = "" @@ -18,4 +38,10 @@ class CasTicketValidatorTest { assertThat(CasTicketValidator.parseAccount(success)).isEqualTo("t001"); assertThatThrownBy(() -> CasTicketValidator.parseAccount(xxe)).isInstanceOf(BusinessException.class); } + + private int readIntField(Object target, String fieldName) throws Exception { + Field field = ReflectionUtils.findField(target.getClass(), fieldName); + ReflectionUtils.makeAccessible(field); + return (Integer) ReflectionUtils.getField(field, target); + } } diff --git a/src/test/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifierTest.java b/src/test/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifierTest.java index 2543c81..d237664 100644 --- a/src/test/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifierTest.java +++ b/src/test/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifierTest.java @@ -13,8 +13,10 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; import java.util.Base64; +import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -45,7 +47,7 @@ class PlatformTokenVerifierTest { @Override public List findChangedSince(Instant watermark) { - return List.of(); + return Collections.emptyList(); } }; verifier = new PlatformTokenVerifier(repository, properties); @@ -78,9 +80,14 @@ class PlatformTokenVerifierTest { private String issueToken(PlatformActor actor, Instant loginTime, String identityClaimValue) throws Exception { ObjectMapper mapper = new ObjectMapper(); - String header = encode(mapper.writeValueAsBytes(Map.of("alg", "HS256", "typ", "JWT"))); - String payload = encode(mapper.writeValueAsBytes(Map.of("aud", new String[]{String.valueOf(actor.platformUserId())}, - String.valueOf(actor.profileId()), identityClaimValue))); + Map headerClaims = new HashMap(); + headerClaims.put("alg", "HS256"); + headerClaims.put("typ", "JWT"); + Map payloadClaims = new HashMap(); + payloadClaims.put("aud", new String[]{String.valueOf(actor.platformUserId())}); + payloadClaims.put(String.valueOf(actor.profileId()), identityClaimValue); + String header = encode(mapper.writeValueAsBytes(headerClaims)); + String payload = encode(mapper.writeValueAsBytes(payloadClaims)); String unsigned = header + "." + payload; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(String.valueOf(actor.tokenSigningTime().toEpochMilli()).getBytes(StandardCharsets.UTF_8), "HmacSHA256")); diff --git a/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepositoryTest.java b/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepositoryTest.java index 2feb202..4c5b0bc 100644 --- a/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepositoryTest.java +++ b/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepositoryTest.java @@ -6,13 +6,16 @@ import com.yau.digitalrmb.platformintegration.domain.PlatformRole; import org.h2.jdbcx.JdbcDataSource; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import javax.sql.DataSource; -import java.sql.Timestamp; +import java.sql.Connection; +import java.sql.Statement; import java.time.Instant; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class JdbcPlatformIdentityRepositoryTest { private DataSource dataSource; @@ -26,7 +29,7 @@ class JdbcPlatformIdentityRepositoryTest { dataSource = source; execute("DROP ALL OBJECTS"); createSchema(); - repository = new JdbcPlatformIdentityRepository(JdbcClient.create(dataSource)); + repository = new JdbcPlatformIdentityRepository(new NamedParameterJdbcTemplate(dataSource)); } @Test @@ -34,7 +37,8 @@ class JdbcPlatformIdentityRepositoryTest { execute("INSERT INTO core_user(ID, CODE, NAME, STATE, JOB_TYPE1, DEL_FLAG) VALUES (101, 't001', '教师甲', 'S1', 'JT_S_02', 0)"); execute("INSERT INTO teacher(teacher_id, user_id, teacher_status, add_time) VALUES (1, 101, 1, '2026-01-01 00:00:00')"); - PlatformActor actor = repository.findByPlatformUserId(101L).orElseThrow(); + PlatformActor actor = repository.findByPlatformUserId(101L) + .orElseThrow(() -> new IllegalStateException("teacher not found")); assertThat(actor.account()).isEqualTo("t001"); assertThat(actor.role()).isEqualTo(PlatformRole.TEACHER); @@ -52,6 +56,17 @@ class JdbcPlatformIdentityRepositoryTest { assertThat(repository.findByPlatformUserId(202L)).isEmpty(); } + @Test + void rejectsDuplicateSchoolAccounts() throws Exception { + execute("INSERT INTO core_user(ID, CODE, NAME, STATE, JOB_TYPE1, DEL_FLAG) VALUES (301, 'duplicate', 'student one', 'S1', 'JT_S_03', 0)"); + execute("INSERT INTO core_user(ID, CODE, NAME, STATE, JOB_TYPE1, DEL_FLAG) VALUES (302, 'duplicate', 'student two', 'S1', 'JT_S_03', 0)"); + execute("INSERT INTO student(student_id, user_id, student_status, add_time) VALUES (11, 301, 1, '2026-01-01 00:00:00')"); + execute("INSERT INTO student(student_id, user_id, student_status, add_time) VALUES (12, 302, 1, '2026-01-01 00:00:00')"); + + assertThatThrownBy(() -> repository.findBySchoolAccount("duplicate")) + .isInstanceOf(IncorrectResultSizeDataAccessException.class); + } + @Test private void createSchema() throws Exception { execute("CREATE TABLE core_user(ID BIGINT PRIMARY KEY, CODE VARCHAR(64), NAME VARCHAR(64), STATE VARCHAR(16), JOB_TYPE1 VARCHAR(16), DEL_FLAG INT)"); @@ -60,7 +75,7 @@ class JdbcPlatformIdentityRepositoryTest { } private void execute(String sql) throws Exception { - try (var connection = dataSource.getConnection(); var statement = connection.createStatement()) { + try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) { statement.execute(sql); } } 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 8b3a25e..c30412a 100644 --- a/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfigTest.java +++ b/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfigTest.java @@ -7,7 +7,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Configuration; -import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import static org.assertj.core.api.Assertions.assertThat; @@ -27,12 +27,12 @@ class PlatformReadOnlyDataSourceConfigTest { "platform-integration.frontend.callback-url=https://rmb.example.edu/sso-callback"); @Test - void createsNamedReadOnlyDatasourceAndJdbcClient() { + void createsNamedReadOnlyDatasourceAndJdbcTemplate() { contextRunner.run(context -> { HikariDataSource dataSource = context.getBean("platformReadOnlyDataSource", HikariDataSource.class); assertThat(dataSource.isReadOnly()).isTrue(); - assertThat(context.getBean("platformJdbcClient", JdbcClient.class)).isNotNull(); + assertThat(context.getBean("platformNamedParameterJdbcTemplate", NamedParameterJdbcTemplate.class)).isNotNull(); }); } diff --git a/src/test/java/com/yau/digitalrmb/security/AuthControllerTest.java b/src/test/java/com/yau/digitalrmb/security/AuthControllerTest.java index 928496d..c6dcb08 100644 --- a/src/test/java/com/yau/digitalrmb/security/AuthControllerTest.java +++ b/src/test/java/com/yau/digitalrmb/security/AuthControllerTest.java @@ -3,7 +3,7 @@ package com.yau.digitalrmb.security; import com.jayway.jsonpath.JsonPath; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; diff --git a/src/test/java/com/yau/digitalrmb/security/CurrentUserAndLogoutTest.java b/src/test/java/com/yau/digitalrmb/security/CurrentUserAndLogoutTest.java index 9541af3..e5f7fd6 100644 --- a/src/test/java/com/yau/digitalrmb/security/CurrentUserAndLogoutTest.java +++ b/src/test/java/com/yau/digitalrmb/security/CurrentUserAndLogoutTest.java @@ -9,13 +9,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import java.time.Instant; -import java.util.Set; +import java.util.Collections; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -42,7 +42,7 @@ class CurrentUserAndLogoutTest { void setUp() { projectionService.project(new PlatformActor(101L, 1L, "t001", "教师甲", PlatformRole.TEACHER, Instant.parse("2026-01-01T00:00:00Z"))); - teacherJwt = jwtTokenService.issueFor(101L, "t001", Set.of("TEACHER")).accessToken(); + teacherJwt = jwtTokenService.issueFor(101L, "t001", Collections.singleton("TEACHER")).accessToken(); refreshToken = refreshTokenService.issue(101L); } diff --git a/src/test/java/com/yau/digitalrmb/shared/GlobalExceptionHandlerTest.java b/src/test/java/com/yau/digitalrmb/shared/GlobalExceptionHandlerTest.java index 5e19e4f..501e682 100644 --- a/src/test/java/com/yau/digitalrmb/shared/GlobalExceptionHandlerTest.java +++ b/src/test/java/com/yau/digitalrmb/shared/GlobalExceptionHandlerTest.java @@ -3,7 +3,7 @@ package com.yau.digitalrmb.shared; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc;