fix: project overseas authorization outside jwt

This commit is contained in:
2026-09-07 11:37:06 +08:00
parent 6b479d1a68
commit 4963889024
33 changed files with 382 additions and 92 deletions
@@ -4,6 +4,7 @@ import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.time.Instant;
import java.util.Set;
public final class LastlegSecurityDtos {
private LastlegSecurityDtos() { }
@@ -14,10 +15,17 @@ public final class LastlegSecurityDtos {
@NotNull @Min(0) Long tenantId,
@NotNull @Min(0) Long permissionVersion) { }
public record ValidationResponse(boolean valid, FailureReason failureReason) {
public static ValidationResponse accepted() { return new ValidationResponse(true, null); }
public record ValidationResponse(boolean valid, FailureReason failureReason,
Set<String> roles, Set<String> authorities) {
public ValidationResponse {
roles = roles == null ? Set.of() : Set.copyOf(roles);
authorities = authorities == null ? Set.of() : Set.copyOf(authorities);
}
public static ValidationResponse accepted(Set<String> roles, Set<String> authorities) {
return new ValidationResponse(true, null, roles, authorities);
}
public static ValidationResponse rejected(FailureReason reason) {
return new ValidationResponse(false, reason);
return new ValidationResponse(false, reason, Set.of(), Set.of());
}
}
@@ -19,6 +19,7 @@ import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
@Service
public class LastlegSecurityProjectionService {
@@ -103,7 +104,7 @@ public class LastlegSecurityProjectionService {
requireStatus(value.status());
yield new ProjectionInput("USER", String.valueOf(value.authUserId()), value.tenantId(),
value.status(), value.permissionVersion(), envelope.aggregateVersion(),
payloadHash, envelope.eventId(), value.changedAt());
payloadHash, envelope.eventId(), value.changedAt(), Set.of(), Set.of());
}
case EventTypes.IAM_TENANT_STATUS_CHANGED -> {
var value = objectMapper.treeToValue(envelope.payload(),
@@ -114,7 +115,7 @@ public class LastlegSecurityProjectionService {
requireStatus(value.status());
yield new ProjectionInput("TENANT", String.valueOf(value.tenantId()), value.tenantId(),
value.status(), null, envelope.aggregateVersion(), payloadHash,
envelope.eventId(), value.changedAt());
envelope.eventId(), value.changedAt(), Set.of(), Set.of());
}
case EventTypes.IAM_PERMISSION_VERSION_CHANGED -> {
var value = objectMapper.treeToValue(envelope.payload(),
@@ -122,9 +123,10 @@ public class LastlegSecurityProjectionService {
requireIdentity(envelope, "AUTHORIZATION", value.authUserId(), value.tenantId());
requirePayload(value.appCode(), value.changedAt(), value.permissionVersion(),
envelope.aggregateVersion());
requireAuthorizationSnapshot(value.roles(), value.authorities());
yield new ProjectionInput("AUTHORIZATION", String.valueOf(value.authUserId()), value.tenantId(),
null, value.permissionVersion(), envelope.aggregateVersion(), payloadHash,
envelope.eventId(), value.changedAt());
envelope.eventId(), value.changedAt(), value.roles(), value.authorities());
}
default -> throw new IllegalArgumentException("unsupported type");
};
@@ -158,6 +160,14 @@ public class LastlegSecurityProjectionService {
}
}
private static void requireAuthorizationSnapshot(Set<String> roles, Set<String> authorities) {
if (roles == null || authorities == null
|| roles.stream().anyMatch(value -> value == null || value.isBlank())
|| authorities.stream().anyMatch(value -> value == null || value.isBlank())) {
throw new BizException(ResultCode.BAD_REQUEST, "IAM 海外授权快照无效");
}
}
private ProjectionRow findForUpdate(String type, String id) {
List<ProjectionRow> rows = jdbc.query(
"SELECT aggregate_version,payload_hash FROM ll_security_projection "
@@ -167,15 +177,24 @@ public class LastlegSecurityProjectionService {
}
private void upsert(ProjectionInput value) {
final String rolesJson;
final String authoritiesJson;
try {
rolesJson = objectMapper.writeValueAsString(value.roles());
authoritiesJson = objectMapper.writeValueAsString(value.authorities());
} catch (Exception exception) {
throw new IllegalStateException("海外授权快照序列化失败", exception);
}
jdbc.update("INSERT INTO ll_security_projection(aggregate_type,aggregate_id,tenant_id,security_status,"
+ "permission_version,aggregate_version,payload_hash,last_event_id,changed_at,updated_at) "
+ "VALUES (?,?,?,?,?,?,?,?,?,NOW(3)) ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id),"
+ "permission_version,roles_json,authorities_json,aggregate_version,payload_hash,last_event_id,changed_at,updated_at) "
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,NOW(3)) ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id),"
+ "security_status=VALUES(security_status),permission_version=VALUES(permission_version),"
+ "roles_json=VALUES(roles_json),authorities_json=VALUES(authorities_json),"
+ "aggregate_version=VALUES(aggregate_version),payload_hash=VALUES(payload_hash),"
+ "last_event_id=VALUES(last_event_id),changed_at=VALUES(changed_at),updated_at=NOW(3)",
value.aggregateType(), value.aggregateId(), value.tenantId(), value.status(),
value.permissionVersion(), value.aggregateVersion(), value.payloadHash(), value.eventId(),
value.changedAt());
value.permissionVersion(), rolesJson, authoritiesJson, value.aggregateVersion(),
value.payloadHash(), value.eventId(), value.changedAt());
}
private record InboxRow(String eventType, String payloadHash, String status) { }
@@ -184,5 +203,6 @@ public class LastlegSecurityProjectionService {
public record ProjectionChanged(ProjectionInput projection) { }
public record ProjectionInput(String aggregateType, String aggregateId, long tenantId, String status,
Long permissionVersion, long aggregateVersion, String payloadHash,
String eventId, Instant changedAt) { }
String eventId, Instant changedAt, Set<String> roles,
Set<String> authorities) { }
}
@@ -3,17 +3,22 @@ package com.baiyue.lastleg.security.service;
import com.baiyue.lastleg.security.dto.LastlegSecurityDtos.FailureReason;
import com.baiyue.lastleg.security.dto.LastlegSecurityDtos.ValidationRequest;
import com.baiyue.lastleg.security.dto.LastlegSecurityDtos.ValidationResponse;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
@Service
public class LastlegSecurityValidationService {
private final JdbcTemplate jdbc;
private final ObjectMapper objectMapper;
public LastlegSecurityValidationService(JdbcTemplate jdbc) {
public LastlegSecurityValidationService(JdbcTemplate jdbc, ObjectMapper objectMapper) {
this.jdbc = jdbc;
this.objectMapper = objectMapper;
}
public ValidationResponse validate(ValidationRequest request) {
@@ -34,10 +39,10 @@ public class LastlegSecurityValidationService {
}
}
List<Long> projectedVersions = jdbc.query(
"SELECT MAX(permission_version) FROM ll_security_projection "
+ "WHERE aggregate_id=? AND tenant_id=? "
+ "AND aggregate_type IN ('USER','AUTHORIZATION') HAVING COUNT(permission_version)>0",
(rs, row) -> rs.getLong(1), String.valueOf(request.userId()), request.tenantId());
"SELECT permission_version FROM ll_security_projection "
+ "WHERE aggregate_type='AUTHORIZATION' AND aggregate_id=? AND tenant_id=?",
(rs, row) -> rs.getLong(1),
String.valueOf(request.userId()), request.tenantId());
if (projectedVersions.isEmpty()) {
return ValidationResponse.rejected(FailureReason.SECURITY_PROJECTION_MISSING);
}
@@ -48,7 +53,19 @@ public class LastlegSecurityValidationService {
if (request.permissionVersion() > projectedVersion) {
return ValidationResponse.rejected(FailureReason.SECURITY_PROJECTION_NOT_READY);
}
return ValidationResponse.accepted();
List<String> snapshots = jdbc.query(
"SELECT CONCAT(roles_json, CHAR(10), authorities_json) FROM ll_security_projection "
+ "WHERE aggregate_type='AUTHORIZATION' AND aggregate_id=? AND tenant_id=?",
(rs, row) -> rs.getString(1), String.valueOf(request.userId()), request.tenantId());
if (snapshots.isEmpty() || snapshots.get(0) == null || !snapshots.get(0).contains("\n")) {
return ValidationResponse.rejected(FailureReason.SECURITY_PROJECTION_MISSING);
}
try {
String[] values = snapshots.get(0).split("\n", 2);
return ValidationResponse.accepted(readSet(values[0]), readSet(values[1]));
} catch (Exception exception) {
return ValidationResponse.rejected(FailureReason.SECURITY_PROJECTION_MISSING);
}
}
private String status(String type, String id, long tenantId) {
@@ -57,4 +74,9 @@ public class LastlegSecurityValidationService {
(rs, row) -> rs.getString(1), type, id, tenantId);
return values.isEmpty() ? null : values.get(0);
}
private Set<String> readSet(String json) throws Exception {
if (json == null || json.isBlank()) return Set.of();
return objectMapper.readValue(json, new TypeReference<Set<String>>() { });
}
}