feat(phase3): 收口父子单与全链路验收
This commit is contained in:
@@ -13,7 +13,7 @@ import org.springframework.stereotype.Component;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 授权快照加载器,调用 IAM 服务并校验返回身份上下文,为认证令牌组装权限声明。
|
||||
* 授权快照加载器,调用 IAM 服务并校验返回身份上下文,为 Redis 会话组装授权快照。
|
||||
*/
|
||||
@Component
|
||||
public class AuthorizationLoader {
|
||||
|
||||
@@ -32,7 +32,7 @@ import java.util.UUID;
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 统一认证应用服务,编排密码校验、IAM 权限快照加载、JWT 签发、刷新令牌轮换与注销事务。
|
||||
* 统一认证应用服务,编排密码校验、IAM 权限快照加载、小 JWT 签发、刷新令牌轮换与注销事务。
|
||||
*/
|
||||
@Service
|
||||
public class AuthService {
|
||||
@@ -117,8 +117,6 @@ public class AuthService {
|
||||
.claim("membership_id", account.getDefaultMembershipId())
|
||||
.claim("app_code", account.getDefaultAppCode())
|
||||
.claim("client_type", authorization.getClientType())
|
||||
.claim("roles", authorization.getRoles())
|
||||
.claim("permissions", authorization.getPermissions())
|
||||
.claim("token_version", account.getTokenVersion())
|
||||
.claim("password_change_required", Boolean.TRUE.equals(account.getPasswordChangeRequired()))
|
||||
.build();
|
||||
@@ -128,7 +126,7 @@ public class AuthService {
|
||||
byte[] random = new byte[48];
|
||||
secureRandom.nextBytes(random);
|
||||
String rawRefresh = Base64.getUrlEncoder().withoutPadding().encodeToString(random);
|
||||
tokenSessionStore.store(jwtId, account.getId(), rawRefresh,
|
||||
tokenSessionStore.store(jwtId, account.getId(), authorization.getRoles(), authorization.getPermissions(), rawRefresh,
|
||||
properties.accessTokenTtl(), properties.refreshTokenTtl());
|
||||
return new TokenResponse("Bearer", accessToken, properties.accessTokenTtl().toSeconds(),
|
||||
rawRefresh, properties.refreshTokenTtl().toSeconds());
|
||||
@@ -174,13 +172,13 @@ public class AuthService {
|
||||
.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("roles", authorization.getRoles())
|
||||
.claim("permissions", authorization.getPermissions()).claim("token_version", account.getTokenVersion())
|
||||
.claim("client_type", authorization.getClientType()).claim("token_version", 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();
|
||||
String accessToken = jwtEncoder.encode(JwtEncoderParameters.from(JwsHeader.with(MacAlgorithm.HS256).build(), claims)).getTokenValue();
|
||||
tokenSessionStore.storeAccessOnly(jwtId, account.getId(), Duration.ofMinutes(15));
|
||||
tokenSessionStore.storeAccessOnly(jwtId, account.getId(), authorization.getRoles(),
|
||||
authorization.getPermissions(), Duration.ofMinutes(15));
|
||||
return new ImpersonationTokenResponse("Bearer", accessToken, 900);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,13 +41,15 @@ public class TokenSessionStore {
|
||||
keyPrefix = RedisTokenKeys.normalize(keyPrefix);
|
||||
}
|
||||
|
||||
public void store(String jwtId, Long userId, String rawRefreshToken,
|
||||
Duration accessTtl, Duration refreshTtl) {
|
||||
public void store(String jwtId, Long userId, Set<String> roles, Set<String> permissions,
|
||||
String rawRefreshToken, Duration accessTtl, Duration refreshTtl) {
|
||||
String accessKey = RedisTokenKeys.access(keyPrefix, jwtId);
|
||||
String refreshKey = RedisTokenKeys.refresh(keyPrefix, sha256(rawRefreshToken));
|
||||
String userKey = userSessionsKey(userId);
|
||||
try {
|
||||
redisTemplate.opsForValue().set(accessKey, userId.toString(), accessTtl);
|
||||
String accessValue = objectMapper.writeValueAsString(
|
||||
new AccessSession(1, userId, normalized(roles), normalized(permissions)));
|
||||
redisTemplate.opsForValue().set(accessKey, accessValue, accessTtl);
|
||||
String refreshValue = objectMapper.writeValueAsString(new RefreshSession(userId, jwtId));
|
||||
redisTemplate.opsForValue().set(refreshKey, refreshValue, refreshTtl);
|
||||
SetOperations<String, String> sets = redisTemplate.opsForSet();
|
||||
@@ -59,10 +61,13 @@ public class TokenSessionStore {
|
||||
}
|
||||
}
|
||||
|
||||
public void storeAccessOnly(String jwtId, Long userId, Duration ttl) {
|
||||
public void storeAccessOnly(String jwtId, Long userId, Set<String> roles,
|
||||
Set<String> permissions, Duration ttl) {
|
||||
try {
|
||||
String accessKey = RedisTokenKeys.access(keyPrefix, jwtId);
|
||||
redisTemplate.opsForValue().set(accessKey, userId.toString(), ttl);
|
||||
String accessValue = objectMapper.writeValueAsString(
|
||||
new AccessSession(1, userId, normalized(roles), normalized(permissions)));
|
||||
redisTemplate.opsForValue().set(accessKey, accessValue, ttl);
|
||||
redisTemplate.opsForSet().add(userSessionsKey(userId), accessKey);
|
||||
redisTemplate.expire(userSessionsKey(userId), ttl);
|
||||
} catch (Exception exception) {
|
||||
@@ -134,7 +139,7 @@ public class TokenSessionStore {
|
||||
while (cursor.hasNext()) {
|
||||
String key = cursor.next();
|
||||
String value = redisTemplate.opsForValue().get(key);
|
||||
if ((!refresh && userId.toString().equals(value)) || (refresh && belongsTo(value, userId))) {
|
||||
if ((!refresh && accessBelongsTo(value, userId)) || (refresh && belongsTo(value, userId))) {
|
||||
result.add(key);
|
||||
}
|
||||
}
|
||||
@@ -149,6 +154,16 @@ public class TokenSessionStore {
|
||||
catch (Exception ignored) { return false; }
|
||||
}
|
||||
|
||||
private boolean accessBelongsTo(String value, Long userId) {
|
||||
if (value == null || value.isBlank()) return false;
|
||||
try { return userId.equals(objectMapper.readValue(value, AccessSession.class).userId()); }
|
||||
catch (Exception ignored) { return false; }
|
||||
}
|
||||
|
||||
private static Set<String> normalized(Set<String> values) {
|
||||
return values == null ? Set.of() : Set.copyOf(values);
|
||||
}
|
||||
|
||||
private String userSessionsKey(Long userId) {
|
||||
return keyPrefix + ":user-sessions:" + userId;
|
||||
}
|
||||
@@ -168,6 +183,12 @@ public class TokenSessionStore {
|
||||
}
|
||||
|
||||
public record RefreshSession(Long userId, String accessJwtId) { }
|
||||
public record AccessSession(int version, Long userId, Set<String> roles, Set<String> permissions) {
|
||||
public AccessSession {
|
||||
roles = normalized(roles);
|
||||
permissions = normalized(permissions);
|
||||
}
|
||||
}
|
||||
public record ImpersonationTicket(String ticketId, Long targetAuthUserId, Long tenantId,
|
||||
Long membershipId, Long actorUserId, Long actorMembershipId,
|
||||
String reason) { }
|
||||
|
||||
Reference in New Issue
Block a user