优化
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
package com.baiyue.iam.client;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
@FeignClient(name = "event-bridge-service", contextId = "iamSecurityEventBridgeClient",
|
||||
url = "${baiyue.security-event.event-bridge-url:}")
|
||||
public interface SecurityEventBridgeClient {
|
||||
@PostMapping("/internal/event-bridge/events")
|
||||
ApiResponse<Acknowledgement> receive(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody EventEnvelope envelope);
|
||||
|
||||
record Acknowledgement(String eventId, String result, String receivedAt) { }
|
||||
}
|
||||
@@ -42,6 +42,14 @@ public final class AuthorizationDtos {
|
||||
private Long permissionVersion;
|
||||
private Long overseasWarehouseId;
|
||||
private Long domesticWarehouseId;
|
||||
private Long tenantVersion;
|
||||
|
||||
public SnapshotResponse(Long userId, Long tenantId, Long membershipId, String appCode,
|
||||
String clientType, Set<String> roles, Set<String> permissions,
|
||||
Long permissionVersion, Long overseasWarehouseId, Long domesticWarehouseId) {
|
||||
this(userId, tenantId, membershipId, appCode, clientType, roles, permissions,
|
||||
permissionVersion, overseasWarehouseId, domesticWarehouseId, 0L);
|
||||
}
|
||||
|
||||
public SnapshotResponse(Long userId, Long tenantId, Long membershipId, String appCode,
|
||||
String clientType, Set<String> roles, Set<String> permissions,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.baiyue.iam.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** Rows from one MVCC statement; all identity/authorization facts share one read view. */
|
||||
@Data
|
||||
public class AuthorizationSnapshotRow {
|
||||
private String roleCode;
|
||||
private String permissionCode;
|
||||
private Long permissionVersion;
|
||||
private Long tenantVersion;
|
||||
private Long overseasWarehouseId;
|
||||
private Long domesticWarehouseId;
|
||||
}
|
||||
@@ -12,105 +12,27 @@ import java.util.List;
|
||||
@Mapper
|
||||
public interface AuthorizationQueryMapper {
|
||||
@Select("""
|
||||
SELECT COUNT(1)
|
||||
FROM iam_user u
|
||||
JOIN iam_membership m ON m.user_id = u.id
|
||||
WHERE u.auth_user_id = #{userId}
|
||||
AND u.status = 'ACTIVE' AND u.deleted = 0
|
||||
AND m.id = #{membershipId} AND m.tenant_id = #{tenantId}
|
||||
AND m.status = 'ACTIVE' AND m.deleted = 0
|
||||
AND (m.tenant_id = 0 OR EXISTS (
|
||||
SELECT 1 FROM iam_tenant t WHERE t.id = m.tenant_id AND t.deleted = 0
|
||||
AND t.status IN ('ACTIVE', 'PROFILE_REQUIRED', 'PENDING', 'REJECTED')
|
||||
))
|
||||
SELECT r.role_code AS roleCode, p.permission_code AS permissionCode,
|
||||
COALESCE(sv.permission_version,0) AS permissionVersion,
|
||||
COALESCE(tv.status_version,0) AS tenantVersion,
|
||||
m.overseas_warehouse_id AS overseasWarehouseId,
|
||||
m.domestic_warehouse_id AS domesticWarehouseId
|
||||
FROM iam_user u
|
||||
JOIN iam_membership m ON m.user_id=u.id
|
||||
LEFT JOIN iam_tenant t ON t.id=m.tenant_id AND t.deleted=0
|
||||
LEFT JOIN iam_membership_role mr ON mr.membership_id=m.id AND mr.tenant_id=m.tenant_id
|
||||
LEFT JOIN iam_role r ON r.id=mr.role_id AND (r.tenant_id=0 OR r.tenant_id=m.tenant_id)
|
||||
AND r.app_code=#{appCode} AND r.status='ACTIVE' AND r.deleted=0
|
||||
LEFT JOIN iam_role_permission rp ON rp.role_id=r.id
|
||||
LEFT JOIN iam_permission p ON p.id=rp.permission_id AND (p.tenant_id=0 OR p.tenant_id=m.tenant_id)
|
||||
AND p.app_code=#{appCode} AND p.status='ACTIVE' AND p.deleted=0
|
||||
LEFT JOIN iam_security_subject_version sv ON sv.auth_user_id=u.auth_user_id
|
||||
LEFT JOIN iam_security_tenant_version tv ON tv.tenant_id=m.tenant_id
|
||||
WHERE u.auth_user_id=#{userId} AND u.status='ACTIVE' AND u.deleted=0
|
||||
AND m.id=#{membershipId} AND m.tenant_id=#{tenantId} AND m.status='ACTIVE' AND m.deleted=0
|
||||
AND (m.tenant_id=0 OR t.status IN ('ACTIVE','PROFILE_REQUIRED','PENDING','REJECTED'))
|
||||
""")
|
||||
int countActiveIdentity(@Param("userId") long userId,
|
||||
@Param("tenantId") long tenantId,
|
||||
@Param("membershipId") long membershipId);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT r.role_code
|
||||
FROM iam_user u
|
||||
JOIN iam_membership m ON m.user_id = u.id
|
||||
JOIN iam_membership_role mr ON mr.membership_id = m.id AND mr.tenant_id = m.tenant_id
|
||||
JOIN iam_role r ON r.id = mr.role_id
|
||||
WHERE u.auth_user_id = #{userId}
|
||||
AND m.id = #{membershipId} AND m.tenant_id = #{tenantId}
|
||||
AND (r.tenant_id = 0 OR r.tenant_id = #{tenantId})
|
||||
AND r.app_code = #{appCode}
|
||||
AND r.status = 'ACTIVE' AND r.deleted = 0
|
||||
ORDER BY r.role_code
|
||||
""")
|
||||
List<String> findRoles(@Param("userId") long userId,
|
||||
@Param("tenantId") long tenantId,
|
||||
@Param("membershipId") long membershipId,
|
||||
@Param("appCode") String appCode);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT p.permission_code
|
||||
FROM iam_user u
|
||||
JOIN iam_membership m ON m.user_id = u.id
|
||||
JOIN iam_membership_role mr ON mr.membership_id = m.id AND mr.tenant_id = m.tenant_id
|
||||
JOIN iam_role r ON r.id = mr.role_id
|
||||
JOIN iam_role_permission rp ON rp.role_id = r.id
|
||||
JOIN iam_permission p ON p.id = rp.permission_id
|
||||
WHERE u.auth_user_id = #{userId}
|
||||
AND m.id = #{membershipId} AND m.tenant_id = #{tenantId}
|
||||
AND (r.tenant_id = 0 OR r.tenant_id = #{tenantId})
|
||||
AND r.app_code = #{appCode} AND r.status = 'ACTIVE' AND r.deleted = 0
|
||||
AND (p.tenant_id = 0 OR p.tenant_id = #{tenantId})
|
||||
AND p.app_code = #{appCode} AND p.status = 'ACTIVE' AND p.deleted = 0
|
||||
ORDER BY p.permission_code
|
||||
""")
|
||||
List<String> findPermissions(@Param("userId") long userId,
|
||||
@Param("tenantId") long tenantId,
|
||||
@Param("membershipId") long membershipId,
|
||||
@Param("appCode") String appCode);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT r.role_code
|
||||
FROM iam_user u
|
||||
JOIN iam_membership m ON m.user_id = u.id
|
||||
JOIN iam_membership_role mr ON mr.membership_id = m.id AND mr.tenant_id = m.tenant_id
|
||||
JOIN iam_role r ON r.id = mr.role_id
|
||||
WHERE u.auth_user_id = #{userId} AND u.status = 'ACTIVE' AND u.deleted = 0
|
||||
AND m.tenant_id = #{tenantId} AND m.status = 'ACTIVE' AND m.deleted = 0
|
||||
AND (r.tenant_id = 0 OR r.tenant_id = #{tenantId})
|
||||
AND r.app_code = #{appCode} AND r.status = 'ACTIVE' AND r.deleted = 0
|
||||
ORDER BY r.role_code
|
||||
""")
|
||||
List<String> findRolesByIdentity(@Param("userId") long userId,
|
||||
@Param("tenantId") long tenantId,
|
||||
@Param("appCode") String appCode);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT p.permission_code
|
||||
FROM iam_user u
|
||||
JOIN iam_membership m ON m.user_id = u.id
|
||||
JOIN iam_membership_role mr ON mr.membership_id = m.id AND mr.tenant_id = m.tenant_id
|
||||
JOIN iam_role r ON r.id = mr.role_id
|
||||
JOIN iam_role_permission rp ON rp.role_id = r.id
|
||||
JOIN iam_permission p ON p.id = rp.permission_id
|
||||
WHERE u.auth_user_id = #{userId} AND u.status = 'ACTIVE' AND u.deleted = 0
|
||||
AND m.tenant_id = #{tenantId} AND m.status = 'ACTIVE' AND m.deleted = 0
|
||||
AND (r.tenant_id = 0 OR r.tenant_id = #{tenantId})
|
||||
AND r.app_code = #{appCode} AND r.status = 'ACTIVE' AND r.deleted = 0
|
||||
AND (p.tenant_id = 0 OR p.tenant_id = #{tenantId})
|
||||
AND p.app_code = #{appCode} AND p.status = 'ACTIVE' AND p.deleted = 0
|
||||
ORDER BY p.permission_code
|
||||
""")
|
||||
List<String> findPermissionsByIdentity(@Param("userId") long userId,
|
||||
@Param("tenantId") long tenantId,
|
||||
@Param("appCode") String appCode);
|
||||
|
||||
@Select("SELECT COALESCE(MAX(permission_version),0) FROM iam_security_subject_version WHERE auth_user_id=#{userId}")
|
||||
long findPermissionVersion(@Param("userId") long userId);
|
||||
|
||||
@Select("SELECT overseas_warehouse_id FROM iam_membership WHERE id=#{membershipId} AND tenant_id=#{tenantId} AND status='ACTIVE' AND deleted=0")
|
||||
Long findOverseasWarehouseId(@Param("tenantId") long tenantId,
|
||||
@Param("membershipId") long membershipId);
|
||||
|
||||
@Select("SELECT domestic_warehouse_id FROM iam_membership WHERE id=#{membershipId} AND tenant_id=#{tenantId} AND status='ACTIVE' AND deleted=0")
|
||||
Long findDomesticWarehouseId(@Param("tenantId") long tenantId,
|
||||
@Param("membershipId") long membershipId);
|
||||
List<com.baiyue.iam.dto.AuthorizationSnapshotRow> findSnapshot(
|
||||
@Param("userId") long userId, @Param("tenantId") long tenantId,
|
||||
@Param("membershipId") long membershipId, @Param("appCode") String appCode);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ public interface IamMembershipRoleMapper extends BaseMapper<IamMembershipRole> {
|
||||
FROM iam_membership_role mr
|
||||
JOIN iam_membership m ON m.id = mr.membership_id AND m.tenant_id = mr.tenant_id
|
||||
JOIN iam_user u ON u.id = m.user_id
|
||||
WHERE mr.tenant_id = #{tenantId}
|
||||
JOIN iam_role r ON r.id = mr.role_id
|
||||
WHERE (#{tenantId} = 0 OR r.tenant_id = #{tenantId})
|
||||
AND mr.role_id = #{roleId}
|
||||
AND m.deleted = 0
|
||||
AND u.deleted = 0
|
||||
|
||||
@@ -69,7 +69,7 @@ public class RbacRepository {
|
||||
public Set<Long> findRoleIdsByPermission(long tenantId, Long permissionId) {
|
||||
List<IamRolePermission> rows = rolePermissionMapper.selectList(
|
||||
new LambdaQueryWrapper<IamRolePermission>()
|
||||
.eq(IamRolePermission::getTenantId, tenantId)
|
||||
.eq(tenantId != 0, IamRolePermission::getTenantId, tenantId)
|
||||
.eq(IamRolePermission::getPermissionId, permissionId));
|
||||
Set<Long> result = new LinkedHashSet<>();
|
||||
rows.forEach(row -> result.add(row.getRoleId()));
|
||||
|
||||
@@ -24,7 +24,7 @@ public class AuthorizationSnapshotService {
|
||||
private AuthorizationQueryMapper mapper;
|
||||
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Transactional(readOnly = true, isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public SnapshotResponse load(SnapshotRequest request) {
|
||||
requireRequest(request);
|
||||
String appCode = request.getAppCode().trim().toUpperCase(java.util.Locale.ROOT);
|
||||
@@ -37,25 +37,24 @@ public class AuthorizationSnapshotService {
|
||||
if ("PLATFORM".equals(appCode) && request.getTenantId() != 0) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "平台账号必须属于平台租户");
|
||||
}
|
||||
if (mapper.countActiveIdentity(request.getUserId(), request.getTenantId(), request.getMembershipId()) == 0) {
|
||||
var rows = mapper.findSnapshot(request.getUserId(), request.getTenantId(), request.getMembershipId(), appCode);
|
||||
if (rows.isEmpty()) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "IAM 用户或成员身份无效");
|
||||
}
|
||||
|
||||
Set<String> roles = normalizedSet(mapper.findRoles(
|
||||
request.getUserId(), request.getTenantId(), request.getMembershipId(), appCode));
|
||||
Set<String> permissions = normalizedSet(mapper.findPermissions(
|
||||
request.getUserId(), request.getTenantId(), request.getMembershipId(), appCode));
|
||||
Set<String> roles = normalizedSet(rows.stream().map(com.baiyue.iam.dto.AuthorizationSnapshotRow::getRoleCode).toList());
|
||||
Set<String> permissions = normalizedSet(rows.stream().map(com.baiyue.iam.dto.AuthorizationSnapshotRow::getPermissionCode).toList());
|
||||
var identity = rows.get(0);
|
||||
return new SnapshotResponse(request.getUserId(), request.getTenantId(), request.getMembershipId(),
|
||||
appCode, appCode, Set.copyOf(roles), Set.copyOf(permissions),
|
||||
mapper.findPermissionVersion(request.getUserId()),
|
||||
mapper.findOverseasWarehouseId(request.getTenantId(), request.getMembershipId()),
|
||||
mapper.findDomesticWarehouseId(request.getTenantId(), request.getMembershipId()));
|
||||
identity.getPermissionVersion(), identity.getOverseasWarehouseId(),
|
||||
identity.getDomesticWarehouseId(), identity.getTenantVersion());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据已认证请求主体重新计算有效权限,而不是复用 JWT 中可能已经过期的权限声明。
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
@Transactional(readOnly = true, isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public Set<String> currentPermissions(AuthenticatedUser user, String appCode) {
|
||||
if (user == null || user.userId() == null || user.tenantId() == null || user.membershipId() == null) {
|
||||
throw new BizException(ResultCode.UNAUTHORIZED, "Authentication identity is incomplete / 登录身份不完整");
|
||||
|
||||
@@ -41,7 +41,7 @@ public class MemberService {
|
||||
@Resource
|
||||
private AuthAccountClient authClient;
|
||||
@Resource
|
||||
private SecurityEventOutboxService securityEvents;
|
||||
private SecurityVersionService securityVersions;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
@@ -106,8 +106,7 @@ public class MemberService {
|
||||
membership.setMemberType(request.memberType()); membership.setMemberNo(request.memberNo());
|
||||
membership.setStatus("ACTIVE"); membership.setJoinedAt(LocalDateTime.now()); repository.insert(membership);
|
||||
repository.replaceRoles(tenantId, membershipId, request.roleIds());
|
||||
securityEvents.userStatus(authId, tenantId, "MERCHANT", "ACTIVE");
|
||||
securityEvents.permissionChanged(authId, tenantId, "MERCHANT", "INITIAL_AUTHORIZATION");
|
||||
securityVersions.subjectChanged(authId);
|
||||
return response(tenantId, membership, user);
|
||||
} catch (RuntimeException exception) {
|
||||
try {
|
||||
@@ -146,6 +145,7 @@ public class MemberService {
|
||||
if (!new HashSet<>(currentRoleIds).equals(new HashSet<>(request.roleIds()))) {
|
||||
call(() -> authClient.revokeSessions(internalToken,
|
||||
new AuthAccountClient.RevokeAccountSessionsRequest(Set.of(user.getAuthUserId()))));
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
}
|
||||
return response(tenantId, membership);
|
||||
}
|
||||
@@ -169,9 +169,7 @@ public class MemberService {
|
||||
membership.setStatus(request.status());
|
||||
membership.setLeftAt("DISABLED".equals(request.status()) ? LocalDateTime.now() : null);
|
||||
user.setStatus(request.status()); repository.saveUser(user); repository.save(membership);
|
||||
securityEvents.userStatus(user.getAuthUserId(), tenantId, "MERCHANT", request.status());
|
||||
securityEvents.permissionChanged(user.getAuthUserId(), tenantId, "MERCHANT",
|
||||
"MEMBER_STATUS_CHANGED");
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
} catch (RuntimeException localFailure) {
|
||||
try {
|
||||
authClient.status(internalToken, user.getAuthUserId(), new AuthAccountClient.StatusRequest(oldStatus));
|
||||
|
||||
@@ -43,7 +43,7 @@ public class MerchantOnboardingService {
|
||||
@Resource private IamTenantReviewMapper reviewMapper;
|
||||
@Resource private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@Resource private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@Resource private SecurityEventOutboxService securityEvents;
|
||||
@Resource private SecurityVersionService securityVersions;
|
||||
@Resource private CustomerCodeRepository customerCodes;
|
||||
|
||||
@Transactional
|
||||
@@ -82,7 +82,7 @@ public class MerchantOnboardingService {
|
||||
tenant.setDeleted(false);
|
||||
tenant.setVersion(0);
|
||||
tenantMapper.insert(tenant);
|
||||
securityEvents.tenantStatus(tenantId, tenant.getStatus());
|
||||
securityVersions.tenantChanged(tenantId);
|
||||
|
||||
IamUser user = new IamUser();
|
||||
user.setId(userId);
|
||||
@@ -114,8 +114,7 @@ public class MerchantOnboardingService {
|
||||
membershipRole.setTenantId(tenantId);
|
||||
membershipRole.setCreatedAt(LocalDateTime.now());
|
||||
membershipRoleMapper.insert(membershipRole);
|
||||
securityEvents.userStatus(request.authUserId(), tenantId, "MERCHANT", "ACTIVE");
|
||||
securityEvents.permissionChanged(request.authUserId(), tenantId, "MERCHANT", "INITIAL_AUTHORIZATION");
|
||||
securityVersions.subjectChanged(request.authUserId());
|
||||
|
||||
if (StringUtils.hasText(request.address())) {
|
||||
IamTenantAddress address = new IamTenantAddress();
|
||||
|
||||
@@ -39,7 +39,7 @@ public class PlatformInternalUserService {
|
||||
@Resource
|
||||
private AuthAccountClient authClient;
|
||||
@Resource
|
||||
private SecurityEventOutboxService securityEvents;
|
||||
private SecurityVersionService securityVersions;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
@@ -77,7 +77,7 @@ public class PlatformInternalUserService {
|
||||
repository.insertUser(user);
|
||||
repository.insert(membership);
|
||||
repository.replaceRoles(PLATFORM_TENANT_ID, membershipId, roleIds);
|
||||
publishInitialSecurityProjection(user);
|
||||
initializeSecurityVersion(user);
|
||||
return response(membership, user, request.username(), roleIds);
|
||||
} catch (RuntimeException localFailure) {
|
||||
compensateCreate(authUserId, localFailure);
|
||||
@@ -116,6 +116,7 @@ public class PlatformInternalUserService {
|
||||
repository.save(membership);
|
||||
repository.replaceRoles(PLATFORM_TENANT_ID, membershipId, roleIds);
|
||||
if (rolesChanged && !displayNameChanged) revokeSessions(user.getAuthUserId());
|
||||
if (rolesChanged) securityVersions.subjectChanged(user.getAuthUserId());
|
||||
} catch (RuntimeException localFailure) {
|
||||
if (displayNameChanged) compensateProfile(user.getAuthUserId(), oldAccount.displayName(), localFailure);
|
||||
throw localFailure;
|
||||
@@ -146,9 +147,7 @@ public class PlatformInternalUserService {
|
||||
user.setStatus(request.status());
|
||||
repository.saveUser(user);
|
||||
repository.save(membership);
|
||||
securityEvents.userStatus(user.getAuthUserId(), PLATFORM_TENANT_ID, "PLATFORM", request.status());
|
||||
securityEvents.permissionChanged(user.getAuthUserId(), PLATFORM_TENANT_ID, "PLATFORM",
|
||||
"PLATFORM_INTERNAL_USER_STATUS_CHANGED");
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
return response(membership, user, authAccount.username(), roleIds);
|
||||
} catch (RuntimeException localFailure) {
|
||||
compensateStatus(user.getAuthUserId(), authAccount.status(), localFailure);
|
||||
@@ -178,8 +177,7 @@ public class PlatformInternalUserService {
|
||||
repository.replaceRoles(PLATFORM_TENANT_ID, membershipId, roleIds);
|
||||
if (!new java.util.HashSet<>(currentRoleIds).equals(new java.util.HashSet<>(roleIds))) {
|
||||
revokeSessions(user.getAuthUserId());
|
||||
securityEvents.permissionChanged(user.getAuthUserId(), PLATFORM_TENANT_ID, "PLATFORM",
|
||||
"MEMBER_ROLES_CHANGED");
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
}
|
||||
AuthAccountClient.AccountSummary authAccount = account(user.getAuthUserId());
|
||||
return response(membership, user, authAccount.username(), roleIds);
|
||||
@@ -204,14 +202,12 @@ public class PlatformInternalUserService {
|
||||
IamMembership membership = newMembership(membershipId, user.getId(), memberNo);
|
||||
repository.insert(membership);
|
||||
repository.replaceRoles(PLATFORM_TENANT_ID, membershipId, roleIds);
|
||||
publishInitialSecurityProjection(user);
|
||||
initializeSecurityVersion(user);
|
||||
return response(membership, user, authAccount.username(), roleIds);
|
||||
}
|
||||
|
||||
private void publishInitialSecurityProjection(IamUser user) {
|
||||
securityEvents.userStatus(user.getAuthUserId(), PLATFORM_TENANT_ID, "PLATFORM", user.getStatus());
|
||||
securityEvents.permissionChanged(user.getAuthUserId(), PLATFORM_TENANT_ID, "PLATFORM",
|
||||
"INITIAL_AUTHORIZATION");
|
||||
private void initializeSecurityVersion(IamUser user) {
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
}
|
||||
|
||||
private InternalUserResponse response(IamMembership membership) {
|
||||
|
||||
@@ -44,7 +44,7 @@ public class PlatformUserManagementService {
|
||||
@Resource
|
||||
private AuthAccountClient authClient;
|
||||
@Resource
|
||||
private SecurityEventOutboxService securityEvents;
|
||||
private SecurityVersionService securityVersions;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
@@ -107,8 +107,7 @@ public class PlatformUserManagementService {
|
||||
membership.setLeftAt("DISABLED".equals(request.status()) ? LocalDateTime.now() : null);
|
||||
members.insert(membership);
|
||||
members.replaceRoles(tenantId, membershipId, roleIds);
|
||||
securityEvents.userStatus(authUserId, tenantId, appCode, request.status());
|
||||
securityEvents.permissionChanged(authUserId, tenantId, appCode, "INITIAL_AUTHORIZATION");
|
||||
securityVersions.subjectChanged(authUserId);
|
||||
return response(membership);
|
||||
} catch (RuntimeException localFailure) {
|
||||
compensateCreate(authUserId, localFailure);
|
||||
@@ -170,8 +169,7 @@ public class PlatformUserManagementService {
|
||||
new AuthAccountClient.RevokeAccountSessionsRequest(Set.of(user.getAuthUserId()))));
|
||||
}
|
||||
if (rolesChanged || warehouseChanged || scopeChanged) {
|
||||
securityEvents.permissionChanged(user.getAuthUserId(), newTenantId,
|
||||
appCode(newTenantId), "PLATFORM_USER_AUTHORIZATION_CHANGED");
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
}
|
||||
} catch (RuntimeException localFailure) {
|
||||
compensateUpdate(user.getAuthUserId(), membershipId, oldTenantId, oldAccount,
|
||||
@@ -204,10 +202,7 @@ public class PlatformUserManagementService {
|
||||
user.setStatus(request.status());
|
||||
members.saveUser(user);
|
||||
members.save(membership);
|
||||
securityEvents.userStatus(user.getAuthUserId(), membership.getTenantId(),
|
||||
appCode(membership.getTenantId()), request.status());
|
||||
securityEvents.permissionChanged(user.getAuthUserId(), membership.getTenantId(),
|
||||
appCode(membership.getTenantId()), "PLATFORM_USER_STATUS_CHANGED");
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
} catch (RuntimeException localFailure) {
|
||||
call(() -> authClient.status(internalToken, user.getAuthUserId(),
|
||||
new AuthAccountClient.StatusRequest(oldAccount.status())));
|
||||
|
||||
@@ -33,7 +33,7 @@ public class RbacService {
|
||||
@Resource private RbacRepository repository;
|
||||
@Resource private MemberRepository memberRepository;
|
||||
@Resource private AuthAccountClient authClient;
|
||||
@Resource private SecurityEventOutboxService securityEvents;
|
||||
@Resource private SecurityVersionService securityVersions;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
@@ -186,6 +186,7 @@ public class RbacService {
|
||||
affectedAccounts.addAll(repository.findAssignedAuthUserIds(tenantId, roleId));
|
||||
}
|
||||
revokeSessions(List.copyOf(affectedAccounts));
|
||||
affectedAccounts.forEach(id -> securityVersions.subjectChanged(id));
|
||||
return permission;
|
||||
}
|
||||
|
||||
@@ -240,7 +241,7 @@ public class RbacService {
|
||||
var user = memberRepository.findUser(membership.getUserId());
|
||||
if (user == null || user.getAuthUserId() == null) throw sessionRevocationUnavailable();
|
||||
revokeSessions(List.of(user.getAuthUserId()));
|
||||
securityEvents.permissionChanged(user.getAuthUserId(), tenantId, appCode, "MEMBER_ROLES_CHANGED");
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,12 +261,13 @@ public class RbacService {
|
||||
repository.replaceRolePermissions(tenantId, request.roleId(), request.permissionIds());
|
||||
List<Long> affected = repository.findAssignedAuthUserIds(tenantId, request.roleId());
|
||||
revokeSessions(affected);
|
||||
affected.forEach(userId -> securityEvents.permissionChanged(userId, tenantId, appCode,
|
||||
"ROLE_PERMISSIONS_CHANGED"));
|
||||
affected.forEach(userId -> securityVersions.subjectChanged(userId));
|
||||
}
|
||||
|
||||
private void revokeAssignedSessions(long tenantId, Long roleId) {
|
||||
revokeSessions(repository.findAssignedAuthUserIds(tenantId, roleId));
|
||||
List<Long> affected = repository.findAssignedAuthUserIds(tenantId, roleId);
|
||||
revokeSessions(affected);
|
||||
affected.forEach(id -> securityVersions.subjectChanged(id));
|
||||
}
|
||||
|
||||
private void revokeSessions(List<Long> accountIds) {
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventEnvelopeValidator;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
import com.baiyue.iam.client.SecurityEventBridgeClient;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/** IAM 安全事件至少一次投递器,支持过期租约回收、退避重试和死信。 */
|
||||
@Component
|
||||
@Slf4j
|
||||
public class SecurityEventOutboxRelay {
|
||||
@Resource private JdbcTemplate jdbc;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private SecurityEventBridgeClient client;
|
||||
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
||||
@Value("${baiyue.security-event.max-attempts:12}") private int maxAttempts;
|
||||
private final String workerId = UUID.randomUUID().toString();
|
||||
|
||||
@Scheduled(fixedDelayString = "${baiyue.security-event.fixed-delay-ms:5000}")
|
||||
public void relay() {
|
||||
recoverExpiredLeases();
|
||||
for (String eventId : candidates()) deliver(eventId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deliver(String eventId) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int claimed = jdbc.update("UPDATE iam_security_event_outbox SET status='SENDING',lease_owner=?,"
|
||||
+ "lease_until=?,updated_at=? WHERE event_id=? AND status='PENDING' AND next_retry_at<=?",
|
||||
workerId, now.plusMinutes(2), now, eventId, now);
|
||||
if (claimed != 1) return;
|
||||
String json = jdbc.queryForObject("SELECT payload_json FROM iam_security_event_outbox "
|
||||
+ "WHERE event_id=? AND status='SENDING' AND lease_owner=?", String.class, eventId, workerId);
|
||||
try {
|
||||
EventEnvelope envelope = objectMapper.readValue(json, EventEnvelope.class);
|
||||
EventEnvelopeValidator.requireValid(envelope);
|
||||
if (envelope.sourceRegion() != RegionCode.CN || envelope.targetRegion() != RegionCode.OVERSEAS) {
|
||||
throw new IllegalStateException("IAM 安全事件必须从 CN 投影到 OVERSEAS");
|
||||
}
|
||||
var response = client.receive(internalToken, envelope);
|
||||
if (response == null || !response.successful() || response.data() == null
|
||||
|| !eventId.equals(response.data().eventId())) {
|
||||
throw new IllegalStateException("事件桥未确认 IAM 安全事件");
|
||||
}
|
||||
jdbc.update("UPDATE iam_security_event_outbox SET status='SENT',sent_at=?,lease_owner=NULL,"
|
||||
+ "lease_until=NULL,last_error=NULL,updated_at=? WHERE event_id=? AND lease_owner=?",
|
||||
LocalDateTime.now(), LocalDateTime.now(), eventId, workerId);
|
||||
} catch (Exception exception) {
|
||||
fail(eventId, exception.getMessage());
|
||||
log.warn("IAM 安全事件投递失败,eventId={}, reason={}", eventId, exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> candidates() {
|
||||
return jdbc.queryForList("SELECT event_id FROM iam_security_event_outbox WHERE status='PENDING' "
|
||||
+ "AND next_retry_at<=NOW(3) ORDER BY created_at,event_id LIMIT 50", String.class);
|
||||
}
|
||||
|
||||
private void recoverExpiredLeases() {
|
||||
jdbc.update("UPDATE iam_security_event_outbox SET status='PENDING',lease_owner=NULL,lease_until=NULL,"
|
||||
+ "next_retry_at=NOW(3),updated_at=NOW(3) WHERE status='SENDING' AND lease_until<NOW(3)");
|
||||
}
|
||||
|
||||
private void fail(String eventId, String message) {
|
||||
Integer attempts = jdbc.queryForObject("SELECT attempt_count FROM iam_security_event_outbox WHERE event_id=?",
|
||||
Integer.class, eventId);
|
||||
int next = (attempts == null ? 0 : attempts) + 1;
|
||||
String status = next >= maxAttempts ? "DEAD" : "PENDING";
|
||||
long delay = Math.min(1800, 5L << Math.min(next, 8));
|
||||
jdbc.update("UPDATE iam_security_event_outbox SET status=?,attempt_count=?,next_retry_at=?,"
|
||||
+ "lease_owner=NULL,lease_until=NULL,last_error=?,updated_at=? WHERE event_id=? AND lease_owner=?",
|
||||
status, next, LocalDateTime.now().plusSeconds(delay), truncate(message), LocalDateTime.now(), eventId, workerId);
|
||||
}
|
||||
|
||||
private String truncate(String value) {
|
||||
String text = value == null ? "unknown" : value.replaceAll("[\\r\\n\\t]+", " ");
|
||||
return text.length() > 1000 ? text.substring(0, 1000) : text;
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventIdentifiers;
|
||||
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||
import com.baiyue.common.event.EventPayloadHash;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.common.event.IamSecurityEventsV1;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
import com.baiyue.iam.mapper.AuthorizationQueryMapper;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** 在 IAM 状态事务内递增安全版本并写入可靠事件发送箱。 */
|
||||
@Service
|
||||
public class SecurityEventOutboxService {
|
||||
private static final List<String> OVERSEAS_AUTHORITY_PREFIXES = List.of(
|
||||
"platform:overseas-warehouse:", "platform:overseas-business:",
|
||||
"platform:overseas-master-data:", "merchant:warehouse:", "merchant:overseas-warehouse:",
|
||||
"merchant:cross-border:", "platform:finance:", "lastleg:");
|
||||
@Resource private JdbcTemplate jdbc;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private AuthorizationQueryMapper authorizationQueries;
|
||||
|
||||
public void userStatus(long authUserId, long tenantId, String appCode, String status) {
|
||||
long version = bump(authUserId);
|
||||
write(EventTypes.IAM_USER_STATUS_CHANGED, "USER", String.valueOf(authUserId), tenantId, version,
|
||||
new IamSecurityEventsV1.UserStatusChanged(authUserId, tenantId, appCode, status, version, Instant.now()));
|
||||
}
|
||||
|
||||
public void tenantStatus(long tenantId, String status) {
|
||||
jdbc.update("INSERT INTO iam_security_tenant_version(tenant_id,status_version) VALUES (?,1) "
|
||||
+ "ON DUPLICATE KEY UPDATE status_version=status_version+1", tenantId);
|
||||
long version = jdbc.queryForObject(
|
||||
"SELECT status_version FROM iam_security_tenant_version WHERE tenant_id=?", Long.class, tenantId);
|
||||
String securityStatus = "ACTIVE".equals(status) ? "ACTIVE" : "DISABLED";
|
||||
write(EventTypes.IAM_TENANT_STATUS_CHANGED, "TENANT", String.valueOf(tenantId), tenantId, version,
|
||||
new IamSecurityEventsV1.TenantStatusChanged(tenantId, securityStatus, Instant.now()));
|
||||
}
|
||||
|
||||
public void permissionChanged(long authUserId, long tenantId, String appCode, String reason) {
|
||||
long version = bump(authUserId);
|
||||
Set<String> roles = Set.copyOf(authorizationQueries.findRolesByIdentity(authUserId, tenantId, appCode));
|
||||
Set<String> authorities = authorizationQueries.findPermissionsByIdentity(authUserId, tenantId, appCode)
|
||||
.stream().filter(permission -> permission != null && OVERSEAS_AUTHORITY_PREFIXES.stream()
|
||||
.anyMatch(permission::startsWith)).collect(Collectors.toUnmodifiableSet());
|
||||
write(EventTypes.IAM_PERMISSION_VERSION_CHANGED, "AUTHORIZATION", String.valueOf(authUserId), tenantId, version,
|
||||
new IamSecurityEventsV1.PermissionVersionChanged(authUserId, tenantId, appCode, version, reason,
|
||||
Instant.now(), roles, authorities));
|
||||
}
|
||||
|
||||
private long bump(long authUserId) {
|
||||
jdbc.update("INSERT INTO iam_security_subject_version(auth_user_id,permission_version) VALUES (?,1) "
|
||||
+ "ON DUPLICATE KEY UPDATE permission_version=permission_version+1", authUserId);
|
||||
return jdbc.queryForObject("SELECT permission_version FROM iam_security_subject_version WHERE auth_user_id=?",
|
||||
Long.class, authUserId);
|
||||
}
|
||||
|
||||
private void write(String type, String aggregateType, String aggregateId, long tenantId,
|
||||
long aggregateVersion, Object value) {
|
||||
JsonNode payload = objectMapper.valueToTree(value);
|
||||
String hash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payload));
|
||||
String flowId = EventIdentifiers.newUuid();
|
||||
EventEnvelope envelope = new EventEnvelope(EventIdentifiers.newUuid(), type, IamSecurityEventsV1.VERSION,
|
||||
Instant.now(), RegionCode.CN, RegionCode.OVERSEAS, String.valueOf(tenantId), aggregateType,
|
||||
aggregateId, aggregateVersion, null, flowId, flowId, null, payload, hash).requireValid();
|
||||
try {
|
||||
jdbc.update("INSERT INTO iam_security_event_outbox(event_id,event_type,aggregate_type,aggregate_id,payload_json) "
|
||||
+ "VALUES (?,?,?,?,CAST(? AS JSON))", envelope.eventId(), type, aggregateType, aggregateId,
|
||||
objectMapper.writeValueAsString(envelope));
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("IAM 安全事件写入失败", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/** Durable security epochs commit or roll back with their IAM mutation, with no projection relay. */
|
||||
@Service
|
||||
@Transactional(propagation = Propagation.MANDATORY)
|
||||
public class SecurityVersionService {
|
||||
@Resource private JdbcTemplate jdbc;
|
||||
|
||||
public void subjectChanged(long authUserId) {
|
||||
jdbc.update("INSERT INTO iam_security_subject_version(auth_user_id,permission_version) VALUES (?,1) "
|
||||
+ "ON DUPLICATE KEY UPDATE permission_version=permission_version+1", authUserId);
|
||||
}
|
||||
|
||||
public void tenantChanged(long tenantId) {
|
||||
jdbc.update("INSERT INTO iam_security_tenant_version(tenant_id,status_version) VALUES (?,1) "
|
||||
+ "ON DUPLICATE KEY UPDATE status_version=status_version+1", tenantId);
|
||||
}
|
||||
}
|
||||
+6
-8
@@ -44,7 +44,7 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
@Resource
|
||||
private AuthAccountClient authClient;
|
||||
@Resource
|
||||
private SecurityEventOutboxService securityEvents;
|
||||
private SecurityVersionService securityVersions;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
private SecureRandom random = new SecureRandom();
|
||||
@@ -115,7 +115,7 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
membership.setUpdatedBy(operatorId);
|
||||
members.insert(membership);
|
||||
members.replaceRoles(tenant.getId(), membershipId, List.of(ownerRole.getId()));
|
||||
publishInitialSecurityProjection(authUserId, tenant.getId());
|
||||
initializeSecurityVersion(authUserId, tenant.getId());
|
||||
return result;
|
||||
} catch (RuntimeException localFailure) {
|
||||
compensate(context, result);
|
||||
@@ -156,7 +156,7 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
String password = temporaryPassword();
|
||||
String username = createAuthAccount(user.getAuthUserId(), password, tenant, owner.getId(),
|
||||
user.getPhone(), user.getDisplayName());
|
||||
publishInitialSecurityProjection(user.getAuthUserId(), tenant.getId());
|
||||
initializeSecurityVersion(user.getAuthUserId(), tenant.getId());
|
||||
return ProvisioningResult.created(user.getAuthUserId(), username, password);
|
||||
}
|
||||
if (!"ACTIVE".equals(account.status())
|
||||
@@ -171,8 +171,7 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
List<Long> repairedRoleIds = new java.util.ArrayList<>(roleIds);
|
||||
repairedRoleIds.add(ownerRole.getId());
|
||||
members.replaceRoles(tenant.getId(), owner.getId(), repairedRoleIds.stream().distinct().toList());
|
||||
securityEvents.permissionChanged(user.getAuthUserId(), tenant.getId(), "MERCHANT",
|
||||
"MEMBER_ROLES_CHANGED");
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
}
|
||||
return ProvisioningResult.existing();
|
||||
}
|
||||
@@ -232,9 +231,8 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
}
|
||||
}
|
||||
|
||||
private void publishInitialSecurityProjection(long authUserId, long tenantId) {
|
||||
securityEvents.userStatus(authUserId, tenantId, "MERCHANT", "ACTIVE");
|
||||
securityEvents.permissionChanged(authUserId, tenantId, "MERCHANT", "INITIAL_AUTHORIZATION");
|
||||
private void initializeSecurityVersion(long authUserId, long tenantId) {
|
||||
securityVersions.subjectChanged(authUserId);
|
||||
}
|
||||
|
||||
private void compensate(long authUserId) {
|
||||
|
||||
@@ -76,7 +76,7 @@ public class TenantService {
|
||||
@Resource
|
||||
private TenantAdministratorProvisioningService administratorProvisioning;
|
||||
@Resource
|
||||
private SecurityEventOutboxService securityEvents;
|
||||
private SecurityVersionService securityVersions;
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "PLATFORM_CUSTOMER_OWNER_PASSWORD_RESET", resourceType = "TENANT",
|
||||
@@ -194,7 +194,7 @@ public class TenantService {
|
||||
tenant.setCreatedBy(operatorId);
|
||||
tenant.setUpdatedBy(operatorId);
|
||||
repository.insertTenant(tenant);
|
||||
securityEvents.tenantStatus(tenant.getId(), tenant.getStatus());
|
||||
securityVersions.tenantChanged(tenant.getId());
|
||||
dimensionRuleInitialization.initialize(tenant.getId(), operatorId);
|
||||
overseasStorageFeeInitialization.initialize(tenant.getId(), operatorId, tenant.getTenantName());
|
||||
var administrator = administratorProvisioning.provisionPlatformCreatedTenant(tenant, operatorId);
|
||||
@@ -302,7 +302,7 @@ public class TenantService {
|
||||
tenant.setApprovedAt(approve ? LocalDateTime.now() : null);
|
||||
tenant.setApprovedBy(approve ? reviewerId : null);
|
||||
repository.saveTenant(tenant);
|
||||
securityEvents.tenantStatus(tenant.getId(), tenant.getStatus());
|
||||
securityVersions.tenantChanged(tenant.getId());
|
||||
for (var qualification : profileRepository.findQualifications(tenant.getId())) {
|
||||
if ("PENDING".equals(qualification.getStatus())) {
|
||||
qualification.setStatus(approve ? "APPROVED" : "REJECTED");
|
||||
@@ -424,7 +424,7 @@ public class TenantService {
|
||||
tenant.setStatus(request.status());
|
||||
repository.saveTenant(tenant);
|
||||
revokeMerchantSessions(tenantId);
|
||||
securityEvents.tenantStatus(tenantId, request.status());
|
||||
securityVersions.tenantChanged(tenantId);
|
||||
return response(tenant);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
spring:
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: ${IAM_DB_URL:jdbc:mysql://127.0.0.1:3306/baiyue_iam?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true}
|
||||
username: ${IAM_DB_USERNAME:root}
|
||||
password: ${IAM_DB_PASSWORD:123456}
|
||||
@@ -1,15 +0,0 @@
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
config:
|
||||
namespace: ${NACOS_NAMESPACE:test}
|
||||
group: ${NACOS_GROUP:BAIYUE}
|
||||
encode: UTF-8
|
||||
discovery:
|
||||
namespace: ${NACOS_NAMESPACE:test}
|
||||
group: ${NACOS_GROUP:BAIYUE}
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: ${IAM_TEST_DB_URL:jdbc:mysql://${TEST_DB_HOST:8.163.30.128}:${TEST_DB_PORT:3306}/baiyue_iam?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true}
|
||||
username: ${IAM_TEST_DB_USERNAME:${TEST_DB_USERNAME:root}}
|
||||
password: ${IAM_TEST_DB_PASSWORD:${TEST_DB_PASSWORD}}
|
||||
@@ -2,29 +2,28 @@ server:
|
||||
port: ${IAM_PORT:9201}
|
||||
spring:
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||
active: ${SPRING_PROFILES_ACTIVE:test}
|
||||
application:
|
||||
name: iam-service
|
||||
config:
|
||||
import:
|
||||
- optional:nacos:baiyue-common.yml?group=BAIYUE&refreshEnabled=true
|
||||
- optional:nacos:${spring.application.name}.yml?group=BAIYUE&refreshEnabled=true
|
||||
- nacos:baiyue-common.yml?group=${NACOS_GROUP:BAIYUE}&refreshEnabled=true
|
||||
- nacos:${spring.application.name}.yml?group=${NACOS_GROUP:BAIYUE}&refreshEnabled=true
|
||||
cloud:
|
||||
nacos:
|
||||
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
server-addr: ${NACOS_SERVER_ADDR}
|
||||
username: ${NACOS_USERNAME}
|
||||
password: ${NACOS_PASSWORD}
|
||||
discovery:
|
||||
enabled: ${NACOS_DISCOVERY_ENABLED:true}
|
||||
register-enabled: ${NACOS_REGISTER_ENABLED:true}
|
||||
group: ${NACOS_GROUP:BAIYUE}
|
||||
namespace: ${NACOS_NAMESPACE:public}
|
||||
namespace: ${NACOS_NAMESPACE}
|
||||
config:
|
||||
namespace: ${NACOS_NAMESPACE:public}
|
||||
namespace: ${NACOS_NAMESPACE}
|
||||
encode: UTF-8
|
||||
baiyue:
|
||||
security-event:
|
||||
event-bridge-url: ${SECURITY_EVENT_BRIDGE_URL:http://event-bridge-overseas:9312}
|
||||
customer-dimension-initialization:
|
||||
fixed-delay-ms: ${CUSTOMER_DIMENSION_INITIALIZATION_FIXED_DELAY_MS:5000}
|
||||
max-attempts: ${CUSTOMER_DIMENSION_INITIALIZATION_MAX_ATTEMPTS:12}
|
||||
@@ -36,6 +35,5 @@ baiyue:
|
||||
auth:
|
||||
url: ${AUTH_SERVICE_URL:}
|
||||
security:
|
||||
jwt-secret: ${JWT_SECRET:change-me-to-at-least-32-bytes-long-before-start}
|
||||
internal-service-token: ${INTERNAL_SERVICE_TOKEN:change-me-to-an-internal-token-at-least-32-bytes-long}
|
||||
internal-service-token: ${INTERNAL_SERVICE_TOKEN}
|
||||
management.endpoints.web.exposure.include: health,info,prometheus
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
USE baiyue_iam;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_security_subject_version (
|
||||
auth_user_id BIGINT NOT NULL COMMENT '认证账号ID',
|
||||
permission_version BIGINT NOT NULL DEFAULT 0 COMMENT '权限版本,身份或授权变化时单调递增',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (auth_user_id)
|
||||
) ENGINE=InnoDB COMMENT='安全主体权限版本';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_security_tenant_version (
|
||||
tenant_id BIGINT NOT NULL COMMENT '客户ID',
|
||||
status_version BIGINT NOT NULL DEFAULT 0 COMMENT '客户状态单调版本',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (tenant_id)
|
||||
) ENGINE=InnoDB COMMENT='客户状态事件版本';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_security_event_outbox (
|
||||
event_id VARCHAR(64) NOT NULL COMMENT '全局事件ID,消费者幂等键',
|
||||
event_type VARCHAR(64) NOT NULL COMMENT '安全事件类型',
|
||||
aggregate_type VARCHAR(32) NOT NULL COMMENT 'USER、TENANT或AUTHORIZATION',
|
||||
aggregate_id VARCHAR(64) NOT NULL COMMENT '聚合标识',
|
||||
payload_json JSON NOT NULL COMMENT '标准跨区事件信封,不含密码、令牌和权限全集',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '发送状态:PENDING、SENDING、SENT、DEAD',
|
||||
attempt_count INT NOT NULL DEFAULT 0 COMMENT '发送尝试次数',
|
||||
next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '下次重试时间',
|
||||
lease_owner VARCHAR(128) NULL COMMENT '租约持有者',
|
||||
lease_until DATETIME(3) NULL COMMENT '租约到期时间',
|
||||
last_error VARCHAR(1000) NULL COMMENT '最近一次错误',
|
||||
sent_at DATETIME(3) NULL COMMENT '发送成功时间',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (event_id),
|
||||
KEY idx_security_outbox_publish (status,next_retry_at,lease_until,created_at),
|
||||
KEY idx_security_outbox_aggregate (aggregate_type,aggregate_id,created_at)
|
||||
) ENGINE=InnoDB COMMENT='IAM可靠安全事件本地发送箱';
|
||||
@@ -0,0 +1,16 @@
|
||||
USE baiyue_iam;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_security_subject_version (
|
||||
auth_user_id BIGINT NOT NULL COMMENT '认证账号ID',
|
||||
permission_version BIGINT NOT NULL DEFAULT 0 COMMENT '权限版本,身份或授权变化时单调递增',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (auth_user_id)
|
||||
) ENGINE=InnoDB COMMENT='安全主体权限版本';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_security_tenant_version (
|
||||
tenant_id BIGINT NOT NULL COMMENT '客户ID',
|
||||
status_version BIGINT NOT NULL DEFAULT 0 COMMENT '客户状态单调版本',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (tenant_id)
|
||||
) ENGINE=InnoDB COMMENT='客户状态持久化版本';
|
||||
|
||||
@@ -869,6 +869,37 @@ paths:
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/internal/iam/authorization-snapshot:
|
||||
post:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: Read a consistent authoritative authorization snapshot
|
||||
description: Internal service credential required; no public Gateway route. Identity, permissions, warehouse bindings and monotonic subject/customer versions are read by one MVCC SELECT statement, without cross-statement snapshot mixing. Auth fingerprints this state for login, refresh and every Access validation.
|
||||
security: []
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/InternalServiceToken'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [userId, tenantId, membershipId, appCode]
|
||||
properties:
|
||||
userId: {type: integer, format: int64}
|
||||
tenantId: {type: integer, format: int64}
|
||||
membershipId: {type: integer, format: int64}
|
||||
appCode: {type: string, enum: [PLATFORM, MERCHANT]}
|
||||
responses:
|
||||
'200':
|
||||
description: Consistent snapshot or business error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/ApiResponse'}
|
||||
- type: object
|
||||
properties:
|
||||
data: {$ref: '#/components/schemas/AuthorizationSnapshot'}
|
||||
/internal/iam/merchant-registrations/{authUserId}:
|
||||
get:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
@@ -1747,3 +1778,18 @@ components:
|
||||
properties:
|
||||
roleId: {type: integer, format: int64}
|
||||
permissionIds: {type: array, items: {type: integer, format: int64}}
|
||||
AuthorizationSnapshot:
|
||||
type: object
|
||||
required: [userId, tenantId, membershipId, appCode, clientType, roles, permissions, permissionVersion, tenantVersion]
|
||||
properties:
|
||||
userId: {type: integer, format: int64}
|
||||
tenantId: {type: integer, format: int64}
|
||||
membershipId: {type: integer, format: int64}
|
||||
appCode: {type: string, enum: [PLATFORM, MERCHANT]}
|
||||
clientType: {type: string}
|
||||
roles: {type: array, items: {type: string}}
|
||||
permissions: {type: array, items: {type: string}}
|
||||
permissionVersion: {type: integer, format: int64, description: Persistent subject security epoch}
|
||||
tenantVersion: {type: integer, format: int64, description: Persistent customer status epoch}
|
||||
overseasWarehouseId: {type: integer, format: int64, nullable: true}
|
||||
domesticWarehouseId: {type: integer, format: int64, nullable: true}
|
||||
|
||||
Reference in New Issue
Block a user