feat(phase3): 收口父子单与全链路验收

This commit is contained in:
2026-08-10 12:23:43 +08:00
parent b294695169
commit 75d5fd4e26
55 changed files with 1110 additions and 115 deletions
@@ -27,13 +27,31 @@ class JwtClaimsMapperTest {
.claim("roles", List.of("ADMIN"))
.build();
BaiyueJwtAuthenticationToken authentication = new JwtClaimsMapper().map(jwt);
AccessTokenSession session = new AccessTokenSession(1, 42L,
java.util.Set.of("ADMIN"), java.util.Set.of("order:read", "order:create"));
BaiyueJwtAuthenticationToken authentication = new JwtClaimsMapper().map(jwt, session);
assertThat(authentication.getPrincipal().userId()).isEqualTo(42L);
assertThat(authentication.getPrincipal().tenantId()).isEqualTo(7L);
assertThat(authentication.getAuthorities()).extracting("authority")
.contains("order:read", "order:create", "ROLE_ADMIN", "APP_MERCHANT");
}
@Test
void ignoresAuthorizationClaimsInJwt() {
Jwt jwt = Jwt.withTokenValue("token").header("alg", "none").subject("42")
.issuedAt(Instant.now()).expiresAt(Instant.now().plusSeconds(60))
.claim("user_id", 42L).claim("tenant_id", 0L).claim("app_code", "PLATFORM")
.claim("permissions", List.of("platform:forged"))
.claim("roles", List.of("FORGED")).build();
BaiyueJwtAuthenticationToken authentication = new JwtClaimsMapper().map(jwt,
new AccessTokenSession(1, 42L, java.util.Set.of(), java.util.Set.of("platform:real")));
assertThat(authentication.getAuthorities()).extracting("authority")
.contains("platform:real", "APP_PLATFORM")
.doesNotContain("platform:forged", "ROLE_FORGED");
}
@Test
void fallsBackToNumericSubjectForUserId() {
Jwt jwt = Jwt.withTokenValue("token")
@@ -4,6 +4,7 @@ import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.oauth2.jwt.Jwt;
import java.time.Instant;
@@ -18,7 +19,10 @@ class RedisAccessTokenSessionValidatorTest {
@Test
void acceptsActiveSession() {
StringRedisTemplate redis = mock(StringRedisTemplate.class);
when(redis.hasKey("baiyue:auth:access:jti-1")).thenReturn(true);
ValueOperations<String, String> values = mock(ValueOperations.class);
when(redis.opsForValue()).thenReturn(values);
when(values.get("baiyue:auth:access:jti-1"))
.thenReturn("{\"version\":1,\"userId\":1,\"roles\":[\"ADMIN\"],\"permissions\":[\"order:read\"]}");
assertThatCode(() -> new RedisAccessTokenSessionValidator(redis, "baiyue:auth")
.validate(jwt("jti-1"))).doesNotThrowAnyException();
@@ -27,7 +31,9 @@ class RedisAccessTokenSessionValidatorTest {
@Test
void rejectsRevokedSession() {
StringRedisTemplate redis = mock(StringRedisTemplate.class);
when(redis.hasKey("baiyue:auth:access:jti-1")).thenReturn(false);
ValueOperations<String, String> values = mock(ValueOperations.class);
when(redis.opsForValue()).thenReturn(values);
when(values.get("baiyue:auth:access:jti-1")).thenReturn(null);
assertThatThrownBy(() -> new RedisAccessTokenSessionValidator(redis, "baiyue:auth")
.validate(jwt("jti-1")))
@@ -38,7 +44,9 @@ class RedisAccessTokenSessionValidatorTest {
@Test
void failsClosedWhenRedisIsUnavailable() {
StringRedisTemplate redis = mock(StringRedisTemplate.class);
when(redis.hasKey("baiyue:auth:access:jti-1"))
ValueOperations<String, String> values = mock(ValueOperations.class);
when(redis.opsForValue()).thenReturn(values);
when(values.get("baiyue:auth:access:jti-1"))
.thenThrow(new RedisConnectionFailureException("offline"));
assertThatThrownBy(() -> new RedisAccessTokenSessionValidator(redis, "baiyue:auth")
@@ -47,9 +55,37 @@ class RedisAccessTokenSessionValidatorTest {
.hasMessageContaining("unavailable");
}
@Test
void rejectsSessionForAnotherUser() {
StringRedisTemplate redis = mock(StringRedisTemplate.class);
ValueOperations<String, String> values = mock(ValueOperations.class);
when(redis.opsForValue()).thenReturn(values);
when(values.get("baiyue:auth:access:jti-1"))
.thenReturn("{\"version\":1,\"userId\":2,\"roles\":[],\"permissions\":[]}");
assertThatThrownBy(() -> new RedisAccessTokenSessionValidator(redis, "baiyue:auth")
.validate(jwt("jti-1")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("identity mismatch");
}
@Test
void rejectsMalformedSessionJson() {
StringRedisTemplate redis = mock(StringRedisTemplate.class);
ValueOperations<String, String> values = mock(ValueOperations.class);
when(redis.opsForValue()).thenReturn(values);
when(values.get("baiyue:auth:access:jti-1")).thenReturn("not-json");
assertThatThrownBy(() -> new RedisAccessTokenSessionValidator(redis, "baiyue:auth")
.validate(jwt("jti-1")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Invalid access token session");
}
private Jwt jwt(String jwtId) {
return Jwt.withTokenValue("token").header("alg", "none")
.subject("1").claim("jti", jwtId).issuedAt(Instant.now())
.claim("user_id", 1L)
.expiresAt(Instant.now().plusSeconds(60)).build();
}
}