Merge branch 'test' into test-lastlag

# Conflicts:
#	auth/src/main/java/com/baiyue/auth/service/AuthService.java
#	auth/src/test/java/com/baiyue/auth/service/AuthServiceSmallJwtTest.java
This commit is contained in:
2026-08-14 09:24:12 +08:00
53 changed files with 1272 additions and 357 deletions
@@ -30,6 +30,7 @@ import java.time.LocalDateTime;
import java.util.Base64;
import java.util.UUID;
import java.time.Duration;
import java.util.List;
/**
* 统一认证应用服务,编排密码校验、IAM 权限快照加载、小 JWT 签发、刷新令牌轮换与注销事务。
@@ -108,7 +109,8 @@ public class AuthService {
String jwtId = UUID.randomUUID().toString();
Instant expiresAt = issuedAt.plus(properties.accessTokenTtl());
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer(properties.issuer()).id(jwtId).issuedAt(issuedAt).expiresAt(expiresAt)
.issuer(properties.issuer()).audience(List.of(properties.audience()))
.id(jwtId).issuedAt(issuedAt).expiresAt(expiresAt)
.subject(account.getId().toString())
.claim("user_id", account.getId())
.claim("username", account.getUsername())
@@ -119,6 +121,10 @@ public class AuthService {
.claim("client_type", authorization.getClientType())
.claim("token_version", account.getTokenVersion())
.claim("password_change_required", Boolean.TRUE.equals(account.getPasswordChangeRequired()))
.claim("userId", account.getId())
.claim("tenantId", account.getDefaultTenantId())
.claim("appCode", account.getDefaultAppCode())
.claim("permissionVersion", account.getTokenVersion())
.build();
String accessToken = jwtEncoder.encode(JwtEncoderParameters.from(
signingHeader(), claims)).getTokenValue();
@@ -167,12 +173,16 @@ public class AuthService {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(Duration.ofMinutes(15));
String jwtId = UUID.randomUUID().toString();
JwtClaimsSet claims = JwtClaimsSet.builder().issuer(properties.issuer()).id(jwtId)
JwtClaimsSet claims = JwtClaimsSet.builder().issuer(properties.issuer())
.audience(List.of(properties.audience())).id(jwtId)
.issuedAt(issuedAt).expiresAt(expiresAt).subject(account.getId().toString())
.claim("user_id", account.getId()).claim("username", account.getUsername())
.claim("tenant_id", account.getDefaultTenantId()).claim("operator_tenant_id", account.getOperatorTenantId())
.claim("membership_id", account.getDefaultMembershipId()).claim("app_code", "MERCHANT")
.claim("client_type", authorization.getClientType()).claim("token_version", account.getTokenVersion())
.claim("userId", account.getId()).claim("tenantId", account.getDefaultTenantId())
.claim("appCode", "MERCHANT")
.claim("permissionVersion", account.getTokenVersion())
.claim("impersonated", true).claim("impersonation_id", ticket.ticketId())
.claim("actor_user_id", ticket.actorUserId()).claim("actor_membership_id", ticket.actorMembershipId())
.claim("password_change_required", Boolean.TRUE.equals(account.getPasswordChangeRequired())).build();
@@ -47,7 +47,7 @@ class AuthServiceSmallJwtTest {
}
@Test
void signsDomesticHmacClaimsAndStoresSession() {
void signsCrossRegionClaimsAndStoresSession() {
AuthAccount account = new AuthAccount();
account.setId(42L);
account.setUsername("operator");
@@ -59,7 +59,7 @@ class AuthServiceSmallJwtTest {
account.setDefaultAppCode("PLATFORM");
account.setTokenVersion(1);
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(42L, 0L, 84L, "PLATFORM",
"WEB", Set.of("PLATFORM_ADMIN"), Set.of("platform:order:list"), 7L);
"WEB", Set.of("PLATFORM_ADMIN"), Set.of("platform:order:list"));
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
when(repository.findByUsername("operator")).thenReturn(account);
@@ -74,10 +74,13 @@ class AuthServiceSmallJwtTest {
verify(jwtEncoder).encode(jwt.capture());
assertThat(jwt.getValue().getClaims().getClaims())
.containsEntry("user_id", 42L)
.containsEntry("userId", 42L)
.containsEntry("tenantId", 0L)
.containsEntry("appCode", "PLATFORM")
.containsEntry("permissionVersion", 1)
.containsEntry("membership_id", 84L)
.doesNotContainKeys("userId", "tenantId", "appCode", "roles", "authorities",
"permissionVersion", "permissions", "scope");
assertThat(jwt.getValue().getClaims().getAudience()).isNull();
.doesNotContainKeys("roles", "authorities", "permissions", "scope");
assertThat(jwt.getValue().getClaims().getAudience()).containsExactly("lastleg-api");
assertThat(jwt.getValue().getJwsHeader().getAlgorithm()).isEqualTo(MacAlgorithm.HS256);
assertThat(jwt.getValue().getJwsHeader().getKeyId()).isNull();
verify(tokenSessionStore).store(any(), org.mockito.ArgumentMatchers.eq(42L),
@@ -86,4 +89,42 @@ class AuthServiceSmallJwtTest {
org.mockito.ArgumentMatchers.eq(Duration.ofMinutes(30)),
org.mockito.ArgumentMatchers.eq(Duration.ofDays(7)));
}
@Test
void impersonationTokenAlsoKeepsRolesAndPermissionsOutOfJwt() {
AuthAccount account = new AuthAccount();
account.setId(43L);
account.setUsername("merchant-owner");
account.setAccountStatus("ACTIVE");
account.setDefaultTenantId(10001L);
account.setOperatorTenantId(0L);
account.setDefaultMembershipId(85L);
account.setDefaultAppCode("MERCHANT");
account.setTokenVersion(2);
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(43L, 10001L, 85L, "MERCHANT",
"WEB", Set.of("MERCHANT_OWNER"), Set.of("merchant:order:list"));
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
TokenSessionStore.ImpersonationTicket ticket = new TokenSessionStore.ImpersonationTicket(
"ticket-1", 43L, 10001L, 85L, 42L, 84L, "support");
when(tokenSessionStore.consumeImpersonationTicket("one-time-code")).thenReturn(ticket);
when(repository.findById(43L)).thenReturn(account);
when(authorizationLoader.load(account)).thenReturn(snapshot);
when(jwtEncoder.encode(any())).thenReturn(encoded);
when(encoded.getTokenValue()).thenReturn("small-impersonation-token");
service.exchangeImpersonationTicket("one-time-code");
ArgumentCaptor<JwtEncoderParameters> jwt = ArgumentCaptor.forClass(JwtEncoderParameters.class);
verify(jwtEncoder).encode(jwt.capture());
assertThat(jwt.getValue().getClaims().getClaims())
.containsEntry("appCode", "MERCHANT")
.containsEntry("impersonated", true)
.containsEntry("membership_id", 85L)
.doesNotContainKeys("roles", "authorities", "permissions", "scope");
verify(tokenSessionStore).storeAccessOnly(any(), org.mockito.ArgumentMatchers.eq(43L),
org.mockito.ArgumentMatchers.eq(Set.of("MERCHANT_OWNER")),
org.mockito.ArgumentMatchers.eq(Set.of("merchant:order:list")),
org.mockito.ArgumentMatchers.eq(Duration.ofMinutes(15)));
}
}