237 lines
12 KiB
Java
237 lines
12 KiB
Java
package com.baiyue.auth.service;
|
|
|
|
import com.baiyue.auth.client.AuthorizationLoader;
|
|
import com.baiyue.auth.client.AuthorizationSnapshot;
|
|
import com.baiyue.auth.config.AuthSecurityProperties;
|
|
import com.baiyue.auth.dto.LoginRequest;
|
|
import com.baiyue.auth.entity.AuthAccount;
|
|
import com.baiyue.auth.repository.AuthAccountRepository;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.mockito.ArgumentCaptor;
|
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
import org.springframework.context.annotation.Bean;
|
|
import org.springframework.context.annotation.Configuration;
|
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
|
import org.springframework.security.oauth2.jwt.Jwt;
|
|
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
|
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
|
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
|
|
|
import jakarta.annotation.Resource;
|
|
import java.time.Duration;
|
|
import java.util.Set;
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.ArgumentMatchers.anyString;
|
|
import static org.mockito.Mockito.never;
|
|
import static org.mockito.Mockito.verify;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
@SpringJUnitConfig(classes = {AuthService.class, AuthServiceSmallJwtTest.TestConfig.class})
|
|
class AuthServiceSmallJwtTest {
|
|
@MockBean private AuthAccountRepository repository;
|
|
@MockBean private PasswordEncoder passwordEncoder;
|
|
@MockBean private JwtEncoder jwtEncoder;
|
|
@MockBean private AuthorizationLoader authorizationLoader;
|
|
@MockBean private TokenSessionStore tokenSessionStore;
|
|
@Resource private AuthService service;
|
|
|
|
@Configuration(proxyBeanMethods = false)
|
|
static class TestConfig {
|
|
@Bean
|
|
com.baiyue.auth.config.UnifiedJwtProperties signingProperties() {
|
|
return new com.baiyue.auth.config.UnifiedJwtProperties("unused", "current");
|
|
}
|
|
@Bean
|
|
AuthSecurityProperties properties() {
|
|
return new AuthSecurityProperties("auth-service", "lastleg-api",
|
|
Duration.ofMinutes(30),
|
|
Duration.ofDays(7), 5, Duration.ofMinutes(10));
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void signsUnifiedClaimsAndStoresSession() {
|
|
AuthAccount account = new AuthAccount();
|
|
account.setId(42L);
|
|
account.setUsername("operator");
|
|
account.setPasswordHash("hash");
|
|
account.setAccountStatus("ACTIVE");
|
|
account.setDefaultTenantId(0L);
|
|
account.setOperatorTenantId(0L);
|
|
account.setDefaultMembershipId(84L);
|
|
account.setDefaultAppCode("PLATFORM");
|
|
account.setTokenVersion(1);
|
|
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(42L, 0L, 84L, "PLATFORM",
|
|
"WEB", Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR"), Set.of("platform:order:list"), 1L, 900L);
|
|
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
|
|
|
|
when(repository.findByUsername("operator")).thenReturn(account);
|
|
when(passwordEncoder.matches("secret", "hash")).thenReturn(true);
|
|
when(authorizationLoader.load(account)).thenReturn(snapshot);
|
|
when(jwtEncoder.encode(any())).thenReturn(encoded);
|
|
when(encoded.getTokenValue()).thenReturn("small-token");
|
|
|
|
var response = service.login(new LoginRequest("operator", "secret"));
|
|
|
|
ArgumentCaptor<JwtEncoderParameters> jwt = ArgumentCaptor.forClass(JwtEncoderParameters.class);
|
|
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)
|
|
.containsEntry("overseas_warehouse_id", 900L)
|
|
.doesNotContainKeys("roles", "authorities", "permissions", "scope");
|
|
assertThat(jwt.getValue().getClaims().getAudience()).containsExactly("lastleg-api");
|
|
assertThat(jwt.getValue().getJwsHeader().getAlgorithm()).isEqualTo(SignatureAlgorithm.RS256);
|
|
assertThat(jwt.getValue().getJwsHeader().getKeyId()).isEqualTo("current");
|
|
verify(tokenSessionStore).store(any(), org.mockito.ArgumentMatchers.eq(42L),
|
|
org.mockito.ArgumentMatchers.eq(Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR")),
|
|
org.mockito.ArgumentMatchers.eq(Set.of("platform:order:list")), anyString(), any(),
|
|
org.mockito.ArgumentMatchers.eq(Duration.ofMinutes(30)),
|
|
org.mockito.ArgumentMatchers.eq(Duration.ofDays(7)));
|
|
assertThat(response.roles()).containsExactly("OVERSEAS_WAREHOUSE_PDA_OPERATOR");
|
|
assertThat(response.authorities()).containsExactly("platform:order:list");
|
|
}
|
|
|
|
@Test
|
|
void mainlandPhoneIsResolvedAsTheSinglePhoneLoginIdentifier() {
|
|
AuthAccount account = new AuthAccount();
|
|
account.setId(42L);
|
|
account.setUsername("operator");
|
|
account.setPhone("13800138000");
|
|
account.setPasswordHash("hash");
|
|
account.setAccountStatus("ACTIVE");
|
|
account.setDefaultTenantId(0L);
|
|
account.setOperatorTenantId(0L);
|
|
account.setDefaultMembershipId(84L);
|
|
account.setDefaultAppCode("PLATFORM");
|
|
account.setTokenVersion(1);
|
|
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(42L, 0L, 84L, "PLATFORM",
|
|
"WEB", Set.of(), Set.of(), 1L);
|
|
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
|
|
when(repository.findByPhone("13800138000")).thenReturn(account);
|
|
when(passwordEncoder.matches("secret", "hash")).thenReturn(true);
|
|
when(authorizationLoader.load(account)).thenReturn(snapshot);
|
|
when(jwtEncoder.encode(any())).thenReturn(encoded);
|
|
when(encoded.getTokenValue()).thenReturn("small-token");
|
|
|
|
service.login(new LoginRequest("13800138000", "secret"));
|
|
|
|
verify(repository).findByPhone("13800138000");
|
|
verify(repository, never()).findByUsername("13800138000");
|
|
}
|
|
|
|
@Test
|
|
void placeholderNumberIsNeverResolvedAsAMobileLoginIdentifier() {
|
|
when(repository.findByUsername("11111111111")).thenReturn(null);
|
|
|
|
assertThatThrownBy(() -> service.login(new LoginRequest("11111111111", "secret")))
|
|
.isInstanceOf(com.baiyue.common.core.exception.BizException.class)
|
|
.hasMessageContaining("登录标识或密码错误");
|
|
|
|
verify(repository).findByUsername("11111111111");
|
|
verify(repository, never()).findByPhone("11111111111");
|
|
}
|
|
|
|
@Test
|
|
void refreshRotatesUnifiedTokenFromCurrentAuthorizationSnapshot() {
|
|
AuthAccount account = new AuthAccount();
|
|
account.setId(52L);
|
|
account.setUsername("merchant");
|
|
account.setAccountStatus("ACTIVE");
|
|
account.setDefaultTenantId(10001L);
|
|
account.setOperatorTenantId(10001L);
|
|
account.setDefaultMembershipId(94L);
|
|
account.setDefaultAppCode("MERCHANT");
|
|
account.setTokenVersion(3);
|
|
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(52L, 10001L, 94L, "MERCHANT",
|
|
"WEB", Set.of("MERCHANT_ADMIN"), Set.of("merchant:order:list"), 3L);
|
|
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
|
|
|
|
when(tokenSessionStore.consume("refresh-old")).thenReturn(
|
|
new TokenSessionStore.RefreshSession(52L, "refresh-session", SessionStateService.fingerprint(account, snapshot)));
|
|
when(repository.findById(52L)).thenReturn(account);
|
|
when(authorizationLoader.load(account)).thenReturn(snapshot);
|
|
when(jwtEncoder.encode(any())).thenReturn(encoded);
|
|
when(encoded.getTokenValue()).thenReturn("domestic-refreshed");
|
|
|
|
var response = service.refresh("refresh-old");
|
|
|
|
assertThat(response.accessToken()).isEqualTo("domestic-refreshed");
|
|
assertThat(response.tokenType()).isEqualTo("Bearer");
|
|
assertThat(response.expiresIn()).isEqualTo(1800);
|
|
assertThat(response.refreshToken()).isNotBlank().isNotEqualTo("refresh-old");
|
|
assertThat(response.refreshExpiresIn()).isEqualTo(604800);
|
|
assertThat(response.roles()).containsExactly("MERCHANT_ADMIN");
|
|
assertThat(response.authorities()).containsExactly("merchant:order:list");
|
|
verify(tokenSessionStore).consume("refresh-old");
|
|
verify(tokenSessionStore).store(anyString(), org.mockito.ArgumentMatchers.eq(52L),
|
|
org.mockito.ArgumentMatchers.eq(Set.of("MERCHANT_ADMIN")),
|
|
org.mockito.ArgumentMatchers.eq(Set.of("merchant:order:list")),
|
|
anyString(), org.mockito.ArgumentMatchers.eq(response.refreshToken()),
|
|
org.mockito.ArgumentMatchers.eq(Duration.ofMinutes(30)),
|
|
org.mockito.ArgumentMatchers.eq(Duration.ofDays(7)));
|
|
verify(authorizationLoader).load(account);
|
|
}
|
|
|
|
@Test
|
|
void consumedRefreshTokenCannotMintAnotherDomesticOrOverseasPair() {
|
|
when(tokenSessionStore.consume("refresh-consumed")).thenReturn(null);
|
|
|
|
assertThatThrownBy(() -> service.refresh("refresh-consumed"))
|
|
.isInstanceOf(com.baiyue.common.core.exception.BizException.class)
|
|
.hasMessageContaining("刷新令牌无效或已过期");
|
|
|
|
verify(repository, never()).findById(any());
|
|
verify(jwtEncoder, never()).encode(any());
|
|
}
|
|
|
|
@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"), 2L);
|
|
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");
|
|
|
|
AuthService.ImpersonationTokenResponse response = service.exchangeImpersonationTicket("one-time-code");
|
|
|
|
assertThat(response.roles()).containsExactly("MERCHANT_OWNER");
|
|
assertThat(response.authorities()).containsExactly("merchant:order:list");
|
|
|
|
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")),
|
|
anyString(), org.mockito.ArgumentMatchers.eq(Duration.ofMinutes(15)));
|
|
}
|
|
}
|