fix:u盾绑定

This commit is contained in:
waner
2026-05-14 09:39:09 +08:00
parent c55ae9a35e
commit 34e4e274a8
19 changed files with 725 additions and 38 deletions
@@ -1,8 +1,6 @@
package com.cisd.tms.modules.auth.controller; package com.cisd.tms.modules.auth.controller;
import com.cisd.tms.common.api.ApiResponse; import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.auth.dto.AdminChangePasswordRequest; import com.cisd.tms.modules.auth.dto.AdminChangePasswordRequest;
import com.cisd.tms.modules.auth.dto.AuthUserListResponse; import com.cisd.tms.modules.auth.dto.AuthUserListResponse;
import com.cisd.tms.modules.auth.enums.AuthLevel; import com.cisd.tms.modules.auth.enums.AuthLevel;
@@ -93,7 +91,7 @@ public class AuthAdminController {
} }
@PostMapping("/roles/{roleCode}/ukeys/issue-sign") @PostMapping("/roles/{roleCode}/ukeys/issue-sign")
@Operation(summary = "签发并绑定其他管理员 UKey", description = "仅允许 SUPER_ADMIN FULL 会话登记其他管理员的 UKey。") @Operation(summary = "签发并绑定管理员 UKey", description = "仅允许 SUPER_ADMIN FULL 会话登记或重绑定管理员 UKey;超级管理员重绑定需传 ukeyBindingId。")
@ReplayProtected @ReplayProtected
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL) @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<UKeySignResult> issueUkeyBindingSign( public ApiResponse<UKeySignResult> issueUkeyBindingSign(
@@ -101,9 +99,6 @@ public class AuthAdminController {
@Valid @RequestBody UKeySignDTO request, @Valid @RequestBody UKeySignDTO request,
HttpServletRequest httpRequest HttpServletRequest httpRequest
) { ) {
if (RoleCode.SUPER_ADMIN.getCode().equals(roleCode)) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "use super-admin ukey binding endpoint");
}
return ApiResponse.success(authAdminService.issueUkeyBindingSign( return ApiResponse.success(authAdminService.issueUkeyBindingSign(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE), (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL), (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL),
@@ -24,4 +24,6 @@ public interface AuthSessionMapper extends BaseMapperX<AuthSessionEntity> {
@Param("authenticatedPrincipalsJson") String authenticatedPrincipalsJson, @Param("authenticatedPrincipalsJson") String authenticatedPrincipalsJson,
@Param("now") LocalDateTime now @Param("now") LocalDateTime now
); );
int deleteAll();
} }
@@ -12,4 +12,11 @@ public interface RoleUkeyBindingMapper extends BaseMapperX<RoleUkeyBindingEntity
List<RoleUkeyBindingEntity> selectActiveByRoleCodeAndUid(@Param("roleCode") String roleCode, @Param("uid") Integer uid); List<RoleUkeyBindingEntity> selectActiveByRoleCodeAndUid(@Param("roleCode") String roleCode, @Param("uid") Integer uid);
List<RoleUkeyBindingEntity> selectActiveByRoleCode(@Param("roleCode") String roleCode); List<RoleUkeyBindingEntity> selectActiveByRoleCode(@Param("roleCode") String roleCode);
List<RoleUkeyBindingEntity> selectActiveByUkeyIdentity(
@Param("ukeySerial") String ukeySerial,
@Param("ukeyPubkey") String ukeyPubkey
);
int deleteAll();
} }
@@ -21,4 +21,6 @@ public interface AuthSessionRepository {
void save(AuthSessionEntity entity); void save(AuthSessionEntity entity);
void update(AuthSessionEntity entity); void update(AuthSessionEntity entity);
void deleteAll();
} }
@@ -2,14 +2,21 @@ package com.cisd.tms.modules.auth.repository;
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface RoleUkeyBindingRepository { public interface RoleUkeyBindingRepository {
Optional<RoleUkeyBindingEntity> findById(Long id);
List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid); List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid);
List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode); List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode);
List<RoleUkeyBindingEntity> findActiveByUkeyIdentity(String ukeySerial, String ukeyPubkey);
void save(RoleUkeyBindingEntity entity); void save(RoleUkeyBindingEntity entity);
void update(RoleUkeyBindingEntity entity); void update(RoleUkeyBindingEntity entity);
void deleteAll();
} }
@@ -51,4 +51,9 @@ public class AuthSessionRepositoryImpl implements AuthSessionRepository {
public void update(AuthSessionEntity entity) { public void update(AuthSessionEntity entity) {
authSessionMapper.updateById(entity); authSessionMapper.updateById(entity);
} }
@Override
public void deleteAll() {
authSessionMapper.deleteAll();
}
} }
@@ -4,6 +4,7 @@ import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
import com.cisd.tms.modules.auth.mapper.RoleUkeyBindingMapper; import com.cisd.tms.modules.auth.mapper.RoleUkeyBindingMapper;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@Repository @Repository
@@ -15,6 +16,11 @@ public class RoleUkeyBindingRepositoryImpl implements RoleUkeyBindingRepository
this.roleUkeyBindingMapper = roleUkeyBindingMapper; this.roleUkeyBindingMapper = roleUkeyBindingMapper;
} }
@Override
public Optional<RoleUkeyBindingEntity> findById(Long id) {
return Optional.ofNullable(roleUkeyBindingMapper.selectById(id));
}
@Override @Override
public List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) { public List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
return roleUkeyBindingMapper.selectActiveByRoleCodeAndUid(roleCode, uid); return roleUkeyBindingMapper.selectActiveByRoleCodeAndUid(roleCode, uid);
@@ -25,6 +31,11 @@ public class RoleUkeyBindingRepositoryImpl implements RoleUkeyBindingRepository
return roleUkeyBindingMapper.selectActiveByRoleCode(roleCode); return roleUkeyBindingMapper.selectActiveByRoleCode(roleCode);
} }
@Override
public List<RoleUkeyBindingEntity> findActiveByUkeyIdentity(String ukeySerial, String ukeyPubkey) {
return roleUkeyBindingMapper.selectActiveByUkeyIdentity(ukeySerial, ukeyPubkey);
}
@Override @Override
public void save(RoleUkeyBindingEntity entity) { public void save(RoleUkeyBindingEntity entity) {
roleUkeyBindingMapper.insert(entity); roleUkeyBindingMapper.insert(entity);
@@ -34,4 +45,9 @@ public class RoleUkeyBindingRepositoryImpl implements RoleUkeyBindingRepository
public void update(RoleUkeyBindingEntity entity) { public void update(RoleUkeyBindingEntity entity) {
roleUkeyBindingMapper.updateById(entity); roleUkeyBindingMapper.updateById(entity);
} }
@Override
public void deleteAll() {
roleUkeyBindingMapper.deleteAll();
}
} }
@@ -0,0 +1,6 @@
package com.cisd.tms.modules.auth.service;
public interface AuthSecurityResetService {
void resetAfterMasterKeyInitialized();
}
@@ -136,6 +136,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
.sorted(Comparator .sorted(Comparator
.comparingInt((AuthUserListItemResponse item) -> roleOrder(item.getRoleCode())) .comparingInt((AuthUserListItemResponse item) -> roleOrder(item.getRoleCode()))
.thenComparing(AuthUserListItemResponse::getUid, Comparator.nullsLast(Integer::compareTo)) .thenComparing(AuthUserListItemResponse::getUid, Comparator.nullsLast(Integer::compareTo))
.thenComparingInt(this::ukeyDisplayOrder)
.thenComparing(AuthUserListItemResponse::getUkeyBindingId, Comparator.nullsLast(Long::compareTo)) .thenComparing(AuthUserListItemResponse::getUkeyBindingId, Comparator.nullsLast(Long::compareTo))
.thenComparing(AuthUserListItemResponse::getUkeySerialNo, Comparator.nullsLast(String::compareTo))) .thenComparing(AuthUserListItemResponse::getUkeySerialNo, Comparator.nullsLast(String::compareTo)))
.toList(); .toList();
@@ -180,19 +181,29 @@ public class AuthAdminServiceImpl implements AuthAdminService {
.stream() .stream()
.toList(); .toList();
if (bindings.isEmpty()) { if (bindings.isEmpty()) {
return Stream.of(toUserListItem(account, role, null)); return Stream.of(toUserListItem(account, role, null, null));
} }
return bindings.stream().map(binding -> toUserListItem(account, role, binding)); List<AuthUserListItemResponse> items = new ArrayList<>();
for (int index = 0; index < bindings.size(); index++) {
String bindingLabel = index == 0 ? "主U盾" : "备份U盾";
items.add(toUserListItem(account, role, bindings.get(index), bindingLabel));
}
return items.stream();
} }
private AuthUserListItemResponse toUserListItem(AuthFullAccountEntity account, RoleAccountEntity role, RoleUkeyBindingEntity binding) { private AuthUserListItemResponse toUserListItem(
AuthFullAccountEntity account,
RoleAccountEntity role,
RoleUkeyBindingEntity binding,
String bindingLabel
) {
String effectiveStatus = effectiveStatus(role, account); String effectiveStatus = effectiveStatus(role, account);
AuthUserListItemResponse item = new AuthUserListItemResponse(); AuthUserListItemResponse item = new AuthUserListItemResponse();
item.setRoleCode(account.getRoleCode()); item.setRoleCode(account.getRoleCode());
item.setRoleName(role == null ? roleDisplayName(account.getRoleCode()) : role.getDisplayName()); item.setRoleName(role == null ? roleDisplayName(account.getRoleCode()) : role.getDisplayName());
item.setUid(account.getUid()); item.setUid(account.getUid());
item.setUsername(account.getAccountName()); item.setUsername(account.getAccountName());
item.setDisplayName(account.getDisplayName()); item.setDisplayName(bindingLabel == null ? account.getDisplayName() : account.getDisplayName() + "-" + bindingLabel);
item.setStatus(effectiveStatus); item.setStatus(effectiveStatus);
item.setStatusName(statusName(effectiveStatus)); item.setStatusName(statusName(effectiveStatus));
item.setLastLoginAt(account.getLastLoginAt()); item.setLastLoginAt(account.getLastLoginAt());
@@ -203,6 +214,20 @@ public class AuthAdminServiceImpl implements AuthAdminService {
return item; return item;
} }
private int ukeyDisplayOrder(AuthUserListItemResponse item) {
String displayName = item.getDisplayName();
if (displayName == null) {
return 0;
}
if (displayName.endsWith("-主U盾")) {
return 0;
}
if (displayName.endsWith("-备份U盾")) {
return 1;
}
return 0;
}
private String effectiveStatus(RoleAccountEntity role, AuthFullAccountEntity account) { private String effectiveStatus(RoleAccountEntity role, AuthFullAccountEntity account) {
if (role != null && !RoleAccountStatus.ACTIVE.name().equals(role.getStatus())) { if (role != null && !RoleAccountStatus.ACTIVE.name().equals(role.getStatus())) {
return role.getStatus(); return role.getStatus();
@@ -287,6 +312,9 @@ public class AuthAdminServiceImpl implements AuthAdminService {
authFullAccountRepository.findByRoleCodeAndUid(targetRoleCode, uid) authFullAccountRepository.findByRoleCodeAndUid(targetRoleCode, uid)
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色完整账号不存在")); .orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色完整账号不存在"));
if (RoleCode.SUPER_ADMIN != targetRole) {
unbindActiveSeatBindings(targetRoleCode, uid);
}
RoleUkeyBindingEntity binding = findExistingActiveBinding(targetRoleCode, uid, ukeySerial, ukeyPubkey); RoleUkeyBindingEntity binding = findExistingActiveBinding(targetRoleCode, uid, ukeySerial, ukeyPubkey);
if (binding.getId() == null) { if (binding.getId() == null) {
binding.setId((long) Math.abs(Objects.hash(targetRoleCode, uid, ukeySerial, TraceIdUtil.newTraceId()))); binding.setId((long) Math.abs(Objects.hash(targetRoleCode, uid, ukeySerial, TraceIdUtil.newTraceId())));
@@ -302,6 +330,16 @@ public class AuthAdminServiceImpl implements AuthAdminService {
roleUkeyBindingRepository.update(binding); roleUkeyBindingRepository.update(binding);
} }
private void rebindSuperAdminUkey(
RoleUkeyBindingEntity binding,
String ukeySerial,
String ukeyPubkey,
String issuerSign
) {
applyBinding(binding, ukeySerial, ukeyPubkey, issuerSign);
roleUkeyBindingRepository.update(binding);
}
private RoleUkeyBindingEntity findExistingActiveBinding( private RoleUkeyBindingEntity findExistingActiveBinding(
String roleCode, String roleCode,
Integer uid, Integer uid,
@@ -315,6 +353,29 @@ public class AuthAdminServiceImpl implements AuthAdminService {
.orElseGet(RoleUkeyBindingEntity::new); .orElseGet(RoleUkeyBindingEntity::new);
} }
private void unbindActiveSeatBindings(String roleCode, Integer uid) {
for (RoleUkeyBindingEntity existing : roleUkeyBindingRepository.findActiveByRoleCodeAndUid(roleCode, uid)) {
existing.setStatus("UNBOUND");
existing.setUnboundAt(now());
roleUkeyBindingRepository.update(existing);
}
}
private RoleUkeyBindingEntity loadSuperAdminBindingForRebind(Integer uid, Long ukeyBindingId) {
if (ukeyBindingId == null) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "超级管理员UKey重绑定必须指定ukeyBindingId");
}
RoleUkeyBindingEntity binding = roleUkeyBindingRepository.findById(ukeyBindingId)
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "超级管理员UKey绑定记录不存在"));
boolean matchesSuperAdminSeat = RoleCode.SUPER_ADMIN.getCode().equals(binding.getRoleCode())
&& Objects.equals(uid, binding.getUid())
&& "ACTIVE".equals(binding.getStatus());
if (!matchesSuperAdminSeat) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "ukeyBindingId不属于目标超级管理员席位");
}
return binding;
}
@Override @Override
public UKeySignResult issueUkeyBindingSign( public UKeySignResult issueUkeyBindingSign(
String operatorRoleCode, String operatorRoleCode,
@@ -324,6 +385,17 @@ public class AuthAdminServiceImpl implements AuthAdminService {
) { ) {
RoleCode targetRole = resolveRoleCode(targetRoleCode); RoleCode targetRole = resolveRoleCode(targetRoleCode);
validateRoleUid(targetRole, request.getUid()); validateRoleUid(targetRole, request.getUid());
if (request.getUkeySerial() == null || request.getUkeySerial().isBlank()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "ukeySerial不能为空");
}
boolean bootstrapSuperAdmin = RoleCode.SUPER_ADMIN == targetRole && operatorRoleCode == null && operatorAuthLevel == null;
RoleUkeyBindingEntity superAdminRebindTarget = null;
if (RoleCode.SUPER_ADMIN == targetRole && !bootstrapSuperAdmin) {
superAdminRebindTarget = loadSuperAdminBindingForRebind(request.getUid(), request.getUkeyBindingId());
rejectUkeyBoundToAnotherBinding(request.getUkeySerial(), request.getPubKey(), superAdminRebindTarget.getId());
} else {
rejectUkeyBoundToDifferentSeat(targetRole.getCode(), request.getUid(), request.getUkeySerial(), request.getPubKey());
}
String authKeyPair = lmkService.exportIkPublicKeyHex(); String authKeyPair = lmkService.exportIkPublicKeyHex();
UKeySignDTO dto = new UKeySignDTO(); UKeySignDTO dto = new UKeySignDTO();
@@ -332,12 +404,12 @@ public class AuthAdminServiceImpl implements AuthAdminService {
String signValue = lmkService.signIk(toIssuePayload(dto, authKeyPair, targetRole.getCode())); String signValue = lmkService.signIk(toIssuePayload(dto, authKeyPair, targetRole.getCode()));
MasterKeyBackupPacket backupPacket = null; MasterKeyBackupPacket backupPacket = null;
if (request.getUkeySerial() == null || request.getUkeySerial().isBlank()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "ukeySerial不能为空");
}
if (RoleCode.SUPER_ADMIN == targetRole) { if (RoleCode.SUPER_ADMIN == targetRole) {
backupPacket = lmkService.buildBackupPacket(request.getUid()); backupPacket = lmkService.buildBackupPacket(request.getUid());
} }
if (superAdminRebindTarget != null) {
rebindSuperAdminUkey(superAdminRebindTarget, request.getUkeySerial(), request.getPubKey(), signValue);
} else {
bindUkey( bindUkey(
targetRole.getCode(), targetRole.getCode(),
request.getUid(), request.getUid(),
@@ -345,6 +417,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
request.getPubKey(), request.getPubKey(),
signValue signValue
); );
}
enableRoleAfterUkeyBindingIfReady(targetRole); enableRoleAfterUkeyBindingIfReady(targetRole);
return UKeySignResult.builder() return UKeySignResult.builder()
.sign(signValue) .sign(signValue)
@@ -352,6 +425,22 @@ public class AuthAdminServiceImpl implements AuthAdminService {
.build(); .build();
} }
private void rejectUkeyBoundToDifferentSeat(String roleCode, Integer uid, String ukeySerial, String ukeyPubkey) {
boolean boundToDifferentSeat = roleUkeyBindingRepository.findActiveByUkeyIdentity(ukeySerial, ukeyPubkey).stream()
.anyMatch(binding -> !Objects.equals(binding.getRoleCode(), roleCode) || !Objects.equals(binding.getUid(), uid));
if (boundToDifferentSeat) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "UKey已绑定到其他角色席位");
}
}
private void rejectUkeyBoundToAnotherBinding(String ukeySerial, String ukeyPubkey, Long allowedBindingId) {
boolean boundToAnotherBinding = roleUkeyBindingRepository.findActiveByUkeyIdentity(ukeySerial, ukeyPubkey).stream()
.anyMatch(binding -> !Objects.equals(binding.getId(), allowedBindingId));
if (boundToAnotherBinding) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "UKey已绑定到其他角色席位");
}
}
private void applyBinding( private void applyBinding(
RoleUkeyBindingEntity binding, RoleUkeyBindingEntity binding,
String ukeySerial, String ukeySerial,
@@ -0,0 +1,178 @@
package com.cisd.tms.modules.auth.service.impl;
import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleAccountEntity;
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.repository.AuthFullAccountRepository;
import com.cisd.tms.modules.auth.repository.AuthSessionRepository;
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.modules.auth.service.AuthSecurityResetService;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AuthSecurityResetServiceImpl implements AuthSecurityResetService {
private final RoleAccountRepository roleAccountRepository;
private final AuthFullAccountRepository authFullAccountRepository;
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
private final AuthSessionRepository authSessionRepository;
private final Clock clock;
public AuthSecurityResetServiceImpl(
RoleAccountRepository roleAccountRepository,
AuthFullAccountRepository authFullAccountRepository,
RoleUkeyBindingRepository roleUkeyBindingRepository,
AuthSessionRepository authSessionRepository,
Clock clock
) {
this.roleAccountRepository = roleAccountRepository;
this.authFullAccountRepository = authFullAccountRepository;
this.roleUkeyBindingRepository = roleUkeyBindingRepository;
this.authSessionRepository = authSessionRepository;
this.clock = clock;
}
@Override
@Transactional
public void resetAfterMasterKeyInitialized() {
roleUkeyBindingRepository.deleteAll();
authSessionRepository.deleteAll();
Map<String, RoleAccountEntity> rolesByCode = roleAccountRepository.findAll().stream()
.collect(Collectors.toMap(RoleAccountEntity::getRoleCode, Function.identity(), (left, right) -> left));
for (SeedRole seed : seedRoles()) {
RoleAccountEntity role = rolesByCode.getOrDefault(seed.roleCode(), new RoleAccountEntity());
boolean existing = role.getRoleCode() != null;
if (!existing) {
role.setId(seed.id());
}
role.setRoleCode(seed.roleCode());
role.setDisplayName(seed.displayName());
role.setRequiredUkeyCount(seed.requiredUkeyCount());
role.setStatus(seed.status());
if (existing) {
roleAccountRepository.update(role);
} else {
roleAccountRepository.save(role);
}
}
LocalDateTime current = LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC);
Map<String, AuthFullAccountEntity> accountsBySeat = authFullAccountRepository.findAll().stream()
.collect(Collectors.toMap(
account -> seatKey(account.getRoleCode(), account.getUid()),
Function.identity(),
(left, right) -> left
));
for (SeedFullAccount seed : seedFullAccounts()) {
AuthFullAccountEntity account = accountsBySeat.getOrDefault(seatKey(seed.roleCode(), seed.uid()), new AuthFullAccountEntity());
boolean existing = account.getRoleCode() != null;
if (!existing) {
account.setId(seed.id());
}
account.setRoleCode(seed.roleCode());
account.setUid(seed.uid());
account.setAccountName(seed.accountName());
account.setDisplayName(seed.displayName());
account.setPasswordHash(seed.passwordHash());
account.setPasswordSalt(seed.passwordSalt());
account.setStatus(RoleAccountStatus.ACTIVE.name());
account.setNeedChangePassword(Boolean.TRUE);
account.setPasswordChangedAt(current);
account.setFailedCount(0);
account.setLockedUntil(null);
account.setLastLoginAt(null);
account.setLastActiveAt(null);
if (existing) {
authFullAccountRepository.update(account);
} else {
authFullAccountRepository.save(account);
}
}
}
private String seatKey(String roleCode, Integer uid) {
return roleCode + "#" + uid;
}
private List<SeedRole> seedRoles() {
return List.of(
new SeedRole(11001L, RoleCode.SUPER_ADMIN.getCode(), "超级管理员", 2, RoleAccountStatus.ACTIVE.name()),
new SeedRole(11002L, RoleCode.KEY_ADMIN.getCode(), "密钥管理员", 1, RoleAccountStatus.ACTIVE.name()),
new SeedRole(11003L, RoleCode.AUDIT_ADMIN.getCode(), "审计管理员", 1, RoleAccountStatus.UNENABLED.name()),
new SeedRole(11004L, RoleCode.OPS_ADMIN.getCode(), "运维管理员", 1, RoleAccountStatus.UNENABLED.name())
);
}
private List<SeedFullAccount> seedFullAccounts() {
return List.of(
new SeedFullAccount(
12101L,
RoleCode.SUPER_ADMIN.getCode(),
1,
"super-admin-full-01",
"超级管理员UKey席位一",
"Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=",
"init-super-admin-salt-20260325"
),
new SeedFullAccount(
12102L,
RoleCode.SUPER_ADMIN.getCode(),
2,
"super-admin-full-02",
"超级管理员UKey席位二",
"Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=",
"init-super-admin-salt-20260325"
),
new SeedFullAccount(
12103L,
RoleCode.KEY_ADMIN.getCode(),
1,
"key-admin-full-01",
"密钥管理员UKey席位",
"/xqmDy3A/q9X5p7XWznIPSBabQ6bxTLAPgtHQ6Be33c=",
"init-key-admin-salt-20260325"
),
new SeedFullAccount(
12104L,
RoleCode.AUDIT_ADMIN.getCode(),
1,
"audit-admin-full-01",
"审计管理员UKey席位",
"j/J1LKyUOzVEJfbbHe2dwKBKNdabRk502olk/LWIwhg=",
"init-audit-admin-salt-20260325"
),
new SeedFullAccount(
12105L,
RoleCode.OPS_ADMIN.getCode(),
1,
"ops-admin-full-01",
"运维管理员UKey席位",
"2e9F4aCgsDb+AuD1LrBEqEg8yjy3ODo27UHtx06vj/A=",
"init-ops-admin-salt-20260325"
)
);
}
private record SeedRole(Long id, String roleCode, String displayName, int requiredUkeyCount, String status) {
}
private record SeedFullAccount(
Long id,
String roleCode,
int uid,
String accountName,
String displayName,
String passwordHash,
String passwordSalt
) {
}
}
@@ -21,6 +21,11 @@ public class UKeySignDTO {
@NotBlank(message = "ukeySerial不能为空") @NotBlank(message = "ukeySerial不能为空")
private String ukeySerial; private String ukeySerial;
/**
* 角色 UKey 绑定记录 ID。超级管理员主/备 UKey 重绑定时用于精确定位目标记录。
*/
private Long ukeyBindingId;
/** /**
* 角色内固定席位编号。 * 角色内固定席位编号。
*/ */
@@ -9,6 +9,7 @@ import com.cisd.tms.integration.crypto.pcie.jna.EccRefPublicKey;
import com.cisd.tms.integration.crypto.pcie.jna.EccSignature; import com.cisd.tms.integration.crypto.pcie.jna.EccSignature;
import com.cisd.tms.integration.crypto.pcie.model.*; import com.cisd.tms.integration.crypto.pcie.model.*;
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
import com.cisd.tms.modules.auth.service.AuthSecurityResetService;
import com.cisd.tms.modules.mk.common.LMKConstant; import com.cisd.tms.modules.mk.common.LMKConstant;
import com.cisd.tms.modules.mk.config.MasterKeyInitProperties; import com.cisd.tms.modules.mk.config.MasterKeyInitProperties;
import com.cisd.tms.modules.mk.dto.MasterKeyBackupMaterial; import com.cisd.tms.modules.mk.dto.MasterKeyBackupMaterial;
@@ -31,9 +32,9 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.util.encoders.Hex; import org.bouncycastle.util.encoders.Hex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/** /**
@@ -41,7 +42,6 @@ import org.springframework.stereotype.Service;
*/ */
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor
public class LmkServiceImpl implements LmkService { public class LmkServiceImpl implements LmkService {
// 对外接口只暴露两份备份包:packet 1 = {1,2},packet 2 = {2,3}。 // 对外接口只暴露两份备份包:packet 1 = {1,2},packet 2 = {2,3}。
@@ -67,9 +67,25 @@ public class LmkServiceImpl implements LmkService {
private final PcieCryptoService pcieCryptoService; private final PcieCryptoService pcieCryptoService;
private final MasterKeyInitProperties masterKeyInitProperties; private final MasterKeyInitProperties masterKeyInitProperties;
private final AuthSecurityResetService authSecurityResetService;
private final Object backupRoundLock = new Object(); private final Object backupRoundLock = new Object();
private CachedBackupRound cachedBackupRound; private CachedBackupRound cachedBackupRound;
@Autowired
public LmkServiceImpl(
PcieCryptoService pcieCryptoService,
MasterKeyInitProperties masterKeyInitProperties,
AuthSecurityResetService authSecurityResetService
) {
this.pcieCryptoService = pcieCryptoService;
this.masterKeyInitProperties = masterKeyInitProperties;
this.authSecurityResetService = authSecurityResetService;
}
public LmkServiceImpl(PcieCryptoService pcieCryptoService, MasterKeyInitProperties masterKeyInitProperties) {
this(pcieCryptoService, masterKeyInitProperties, () -> { });
}
@Override @Override
public MasterKeyStateResult initMasterKey() { public MasterKeyStateResult initMasterKey() {
if (pcieCryptoService.checkLmk()) { if (pcieCryptoService.checkLmk()) {
@@ -83,6 +99,7 @@ public class LmkServiceImpl implements LmkService {
MasterKeyStateResult result = new MasterKeyStateResult(); MasterKeyStateResult result = new MasterKeyStateResult();
result.setStatus(true); result.setStatus(true);
result.setSeedMac(Hex.toHexString(pcieCryptoService.initializeMasterKey(oldPin, newPin))); result.setSeedMac(Hex.toHexString(pcieCryptoService.initializeMasterKey(oldPin, newPin)));
authSecurityResetService.resetAfterMasterKeyInitialized();
return result; return result;
} }
@@ -79,4 +79,8 @@
AND expires_at IS NOT NULL AND expires_at IS NOT NULL
AND expires_at > #{now} AND expires_at > #{now}
</select> </select>
<delete id="deleteAll">
DELETE FROM tms_auth_session
</delete>
</mapper> </mapper>
@@ -34,7 +34,7 @@
WHERE role_code = #{roleCode} WHERE role_code = #{roleCode}
AND uid = #{uid} AND uid = #{uid}
AND status = 'ACTIVE' AND status = 'ACTIVE'
ORDER BY bound_at DESC, id DESC ORDER BY bound_at ASC, id ASC
</select> </select>
<select id="selectActiveByRoleCode" parameterType="string" resultMap="RoleUkeyBindingResultMap"> <select id="selectActiveByRoleCode" parameterType="string" resultMap="RoleUkeyBindingResultMap">
@@ -52,6 +52,28 @@
FROM tms_role_ukey_binding FROM tms_role_ukey_binding
WHERE role_code = #{roleCode} WHERE role_code = #{roleCode}
AND status = 'ACTIVE' AND status = 'ACTIVE'
ORDER BY uid ASC ORDER BY uid ASC, bound_at ASC, id ASC
</select> </select>
<select id="selectActiveByUkeyIdentity" resultMap="RoleUkeyBindingResultMap">
SELECT id,
role_code,
uid,
ukey_serial,
ukey_pubkey,
issuer_sign,
status,
bound_at,
unbound_at,
create_time,
update_time
FROM tms_role_ukey_binding
WHERE status = 'ACTIVE'
AND (ukey_serial = #{ukeySerial} OR ukey_pubkey = #{ukeyPubkey})
ORDER BY bound_at DESC, id DESC
</select>
<delete id="deleteAll">
DELETE FROM tms_role_ukey_binding
</delete>
</mapper> </mapper>
@@ -357,4 +357,40 @@ class AuthControllerTest {
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-KEY-001\""))); .andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-KEY-001\"")));
} }
@Test
void shouldIssueSuperAdminUkeyRebindThroughAdminEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
MasterKeyBackupPacket packet = new MasterKeyBackupPacket();
packet.setPacketIndex(1);
UKeySignResult response = UKeySignResult.builder().sign("ISSUE-SIGN-SUPER-REBIND").backupPacket(packet).build();
Mockito.when(authAdminService.issueUkeyBindingSign(
ArgumentMatchers.eq("SUPER_ADMIN"),
ArgumentMatchers.eq("FULL"),
ArgumentMatchers.eq("SUPER_ADMIN"),
ArgumentMatchers.any()
)).thenReturn(response);
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/roles/SUPER_ADMIN/ukeys/issue-sign")
.requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "SUPER_ADMIN")
.requestAttr(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"pubKey": "PUB-SUPER-NEW",
"ukeySerial": "UK-SUPER-NEW",
"uid": 1,
"ukeyBindingId": 1001
}
"""))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-SUPER-REBIND\"")))
.andExpect(content().string(containsString("\"packetIndex\":1")));
}
} }
@@ -49,9 +49,9 @@ class AuthAdminServiceTest {
AuthFullAccountEntity first = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow(); AuthFullAccountEntity first = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow();
AuthFullAccountEntity second = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow(); AuthFullAccountEntity second = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow();
Assertions.assertEquals("salt-reset-1", first.getPasswordSalt()); Assertions.assertEquals("salt-reset-1", first.getPasswordSalt());
Assertions.assertEquals("HASH:12345678:salt-reset-1", first.getPasswordHash()); Assertions.assertEquals("HASH:Sunyard@123:salt-reset-1", first.getPasswordHash());
Assertions.assertEquals("salt-reset-2", second.getPasswordSalt()); Assertions.assertEquals("salt-reset-2", second.getPasswordSalt());
Assertions.assertEquals("HASH:12345678:salt-reset-2", second.getPasswordHash()); Assertions.assertEquals("HASH:Sunyard@123:salt-reset-2", second.getPasswordHash());
Assertions.assertTrue(Boolean.TRUE.equals(first.getNeedChangePassword())); Assertions.assertTrue(Boolean.TRUE.equals(first.getNeedChangePassword()));
Assertions.assertTrue(Boolean.TRUE.equals(second.getNeedChangePassword())); Assertions.assertTrue(Boolean.TRUE.equals(second.getNeedChangePassword()));
} }
@@ -102,7 +102,43 @@ class AuthAdminServiceTest {
} }
@Test @Test
void shouldIssueSuperAdminBackupUkeyWithoutReplacingExistingBindingForSameUid() { void shouldRebindSuperAdminUkeyByBindingIdWithoutReplacingSiblingBinding() {
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository();
InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository();
roles.save(role(RoleCode.SUPER_ADMIN, RoleAccountStatus.ACTIVE));
accounts.save(account(RoleCode.SUPER_ADMIN, 1, "super-admin-01", "HASH-OLD", "SALT-OLD"));
RoleUkeyBindingEntity primary = binding(RoleCode.SUPER_ADMIN, 1, "UK-PRIMARY", "PUB-PRIMARY", "SIG-PRIMARY");
RoleUkeyBindingEntity backup = binding(RoleCode.SUPER_ADMIN, 1, "UK-BACKUP", "PUB-BACKUP", "SIG-BACKUP");
bindings.save(primary);
bindings.save(backup);
LmkService lmkService = Mockito.mock(LmkService.class);
Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
Mockito.when(lmkService.signIk(Mockito.anyString())).thenReturn("ISSUE-SIGN-REBIND");
MasterKeyBackupPacket packet = new MasterKeyBackupPacket();
packet.setPacketIndex(1);
Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet);
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey("PUB-BACKUP-NEW");
dto.setUkeySerial("UK-BACKUP-NEW");
dto.setUid(1);
dto.setUkeyBindingId(backup.getId());
UKeySignResult result = service(roles, accounts, bindings, lmkService)
.issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto);
Assertions.assertEquals("ISSUE-SIGN-REBIND", result.getSign());
Assertions.assertNotNull(result.getBackupPacket());
List<RoleUkeyBindingEntity> activeUidBindings = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1);
Assertions.assertEquals(2, activeUidBindings.size());
Assertions.assertTrue(activeUidBindings.stream()
.anyMatch(item -> primary.getId().equals(item.getId()) && "UK-PRIMARY".equals(item.getUkeySerial())));
Assertions.assertTrue(activeUidBindings.stream()
.anyMatch(item -> backup.getId().equals(item.getId()) && "UK-BACKUP-NEW".equals(item.getUkeySerial())));
}
@Test
void shouldRejectSuperAdminRebindWithoutBindingId() {
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository();
InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository();
@@ -110,25 +146,101 @@ class AuthAdminServiceTest {
accounts.save(account(RoleCode.SUPER_ADMIN, 1, "super-admin-01", "HASH-OLD", "SALT-OLD")); accounts.save(account(RoleCode.SUPER_ADMIN, 1, "super-admin-01", "HASH-OLD", "SALT-OLD"));
bindings.save(binding(RoleCode.SUPER_ADMIN, 1, "UK-PRIMARY", "PUB-PRIMARY", "SIG-PRIMARY")); bindings.save(binding(RoleCode.SUPER_ADMIN, 1, "UK-PRIMARY", "PUB-PRIMARY", "SIG-PRIMARY"));
LmkService lmkService = Mockito.mock(LmkService.class); LmkService lmkService = Mockito.mock(LmkService.class);
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey("PUB-NEW");
dto.setUkeySerial("UK-NEW");
dto.setUid(1);
BizException exception = Assertions.assertThrows(BizException.class, () -> service(roles, accounts, bindings, lmkService)
.issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto));
Assertions.assertEquals(ErrorCode.VALIDATE_FAILED.getCode(), exception.getCode());
Assertions.assertEquals("超级管理员UKey重绑定必须指定ukeyBindingId", exception.getMessage());
Mockito.verify(lmkService, Mockito.never()).buildBackupPacket(Mockito.anyInt());
Mockito.verify(lmkService, Mockito.never()).signIk(Mockito.anyString());
}
@Test
void shouldRebindSingleSeatAdminByReplacingExistingBinding() {
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository();
InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository();
roles.save(role(RoleCode.KEY_ADMIN, RoleAccountStatus.ACTIVE));
accounts.save(account(RoleCode.KEY_ADMIN, 1, "key-admin-01", "HASH-OLD", "SALT-OLD"));
bindings.save(binding(RoleCode.KEY_ADMIN, 1, "UK-OLD", "PUB-OLD", "SIG-OLD"));
LmkService lmkService = Mockito.mock(LmkService.class);
Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
Mockito.when(lmkService.signIk(Mockito.anyString())).thenReturn("ISSUE-SIGN-BACKUP"); Mockito.when(lmkService.signIk(Mockito.anyString())).thenReturn("ISSUE-SIGN-NEW");
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey("PUB-NEW");
dto.setUkeySerial("UK-NEW");
dto.setUid(1);
UKeySignResult result = service(roles, accounts, bindings, lmkService)
.issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.KEY_ADMIN.getCode(), dto);
Assertions.assertEquals("ISSUE-SIGN-NEW", result.getSign());
Assertions.assertNull(result.getBackupPacket());
List<RoleUkeyBindingEntity> activeBindings = bindings.findActiveByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1);
Assertions.assertEquals(1, activeBindings.size());
Assertions.assertEquals("UK-NEW", activeBindings.get(0).getUkeySerial());
Assertions.assertTrue(bindings.all().stream()
.anyMatch(item -> "UK-OLD".equals(item.getUkeySerial())
&& "UNBOUND".equals(item.getStatus())
&& item.getUnboundAt() != null));
}
@Test
void shouldBootstrapSuperAdminUkeyWithoutLoginContext() {
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository();
InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository();
roles.save(role(RoleCode.SUPER_ADMIN, RoleAccountStatus.ACTIVE));
accounts.save(account(RoleCode.SUPER_ADMIN, 1, "super-admin-01", "HASH-OLD", "SALT-OLD"));
LmkService lmkService = Mockito.mock(LmkService.class);
Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
Mockito.when(lmkService.signIk(Mockito.anyString())).thenReturn("ISSUE-SIGN-SUPER");
MasterKeyBackupPacket packet = new MasterKeyBackupPacket(); MasterKeyBackupPacket packet = new MasterKeyBackupPacket();
packet.setPacketIndex(1); packet.setPacketIndex(1);
Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet); Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet);
UKeySignDTO dto = new UKeySignDTO(); UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey("PUB-BACKUP"); dto.setPubKey("PUB-SUPER");
dto.setUkeySerial("UK-BACKUP"); dto.setUkeySerial("UK-SUPER");
dto.setUid(1); dto.setUid(1);
UKeySignResult result = service(roles, accounts, bindings, lmkService) UKeySignResult result = service(roles, accounts, bindings, lmkService)
.issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto); .issueUkeyBindingSign(null, null, RoleCode.SUPER_ADMIN.getCode(), dto);
Assertions.assertEquals("ISSUE-SIGN-BACKUP", result.getSign()); List<RoleUkeyBindingEntity> activeBindings = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1);
Assertions.assertEquals(1, activeBindings.size());
Assertions.assertEquals("UK-SUPER", activeBindings.get(0).getUkeySerial());
Assertions.assertEquals("ISSUE-SIGN-SUPER", result.getSign());
Assertions.assertNotNull(result.getBackupPacket()); Assertions.assertNotNull(result.getBackupPacket());
List<RoleUkeyBindingEntity> activeUidBindings = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1); }
Assertions.assertEquals(2, activeUidBindings.size());
Assertions.assertTrue(activeUidBindings.stream().anyMatch(item -> "UK-PRIMARY".equals(item.getUkeySerial()))); @Test
Assertions.assertTrue(activeUidBindings.stream().anyMatch(item -> "UK-BACKUP".equals(item.getUkeySerial()))); void shouldRejectSameUkeyBindingToDifferentSeat() {
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository();
InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository();
roles.save(role(RoleCode.SUPER_ADMIN, RoleAccountStatus.ACTIVE));
accounts.save(account(RoleCode.SUPER_ADMIN, 1, "super-admin-01", "HASH-OLD-1", "SALT-OLD-1"));
accounts.save(account(RoleCode.SUPER_ADMIN, 2, "super-admin-02", "HASH-OLD-2", "SALT-OLD-2"));
bindings.save(binding(RoleCode.SUPER_ADMIN, 1, "UK-PRIMARY", "PUB-PRIMARY", "SIG-PRIMARY"));
LmkService lmkService = Mockito.mock(LmkService.class);
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey("PUB-PRIMARY");
dto.setUkeySerial("UK-PRIMARY");
dto.setUid(2);
BizException exception = Assertions.assertThrows(BizException.class, () -> service(roles, accounts, bindings, lmkService)
.issueUkeyBindingSign(null, null, RoleCode.SUPER_ADMIN.getCode(), dto));
Assertions.assertEquals(ErrorCode.CONFLICT.getCode(), exception.getCode());
Assertions.assertEquals("UKey已绑定到其他角色席位", exception.getMessage());
Assertions.assertTrue(bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).isEmpty());
Mockito.verify(lmkService, Mockito.never()).buildBackupPacket(2);
Mockito.verify(lmkService, Mockito.never()).signIk(Mockito.anyString());
} }
@Test @Test
@@ -222,9 +334,11 @@ class AuthAdminServiceTest {
Assertions.assertEquals(1, response.getItems().get(0).getUid()); Assertions.assertEquals(1, response.getItems().get(0).getUid());
Assertions.assertEquals("超级管理员", response.getItems().get(0).getRoleName()); Assertions.assertEquals("超级管理员", response.getItems().get(0).getRoleName());
Assertions.assertEquals("super-admin-full-01", response.getItems().get(0).getUsername()); Assertions.assertEquals("super-admin-full-01", response.getItems().get(0).getUsername());
Assertions.assertEquals("超级管理员UKey席位一-主U盾", response.getItems().get(0).getDisplayName());
Assertions.assertEquals("正常", response.getItems().get(0).getStatusName()); Assertions.assertEquals("正常", response.getItems().get(0).getStatusName());
Assertions.assertEquals("UK-BACKUP", response.getItems().get(0).getUkeySerialNo()); Assertions.assertEquals("UK-PRIMARY", response.getItems().get(0).getUkeySerialNo());
Assertions.assertEquals("UK-PRIMARY", response.getItems().get(1).getUkeySerialNo()); Assertions.assertEquals("超级管理员UKey席位一-备份U盾", response.getItems().get(1).getDisplayName());
Assertions.assertEquals("UK-BACKUP", response.getItems().get(1).getUkeySerialNo());
var disabledResponse = service(roles, accounts, bindings, Mockito.mock(LmkService.class)) var disabledResponse = service(roles, accounts, bindings, Mockito.mock(LmkService.class))
.listUsers(null, "未启用", 1, 10); .listUsers(null, "未启用", 1, 10);
@@ -385,6 +499,13 @@ class AuthAdminServiceTest {
private static class InMemoryRoleUkeyBindingRepository implements RoleUkeyBindingRepository { private static class InMemoryRoleUkeyBindingRepository implements RoleUkeyBindingRepository {
private final List<RoleUkeyBindingEntity> store = new ArrayList<>(); private final List<RoleUkeyBindingEntity> store = new ArrayList<>();
@Override
public Optional<RoleUkeyBindingEntity> findById(Long id) {
return store.stream()
.filter(entity -> id.equals(entity.getId()))
.findFirst();
}
@Override @Override
public List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode) { public List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode) {
return store.stream() return store.stream()
@@ -400,6 +521,14 @@ class AuthAdminServiceTest {
.toList(); .toList();
} }
@Override
public List<RoleUkeyBindingEntity> findActiveByUkeyIdentity(String ukeySerial, String ukeyPubkey) {
return store.stream()
.filter(entity -> "ACTIVE".equals(entity.getStatus()))
.filter(entity -> ukeySerial.equals(entity.getUkeySerial()) || ukeyPubkey.equals(entity.getUkeyPubkey()))
.toList();
}
@Override @Override
public void save(RoleUkeyBindingEntity entity) { public void save(RoleUkeyBindingEntity entity) {
store.add(entity); store.add(entity);
@@ -410,5 +539,14 @@ class AuthAdminServiceTest {
store.removeIf(item -> item.getId().equals(entity.getId())); store.removeIf(item -> item.getId().equals(entity.getId()));
store.add(entity); store.add(entity);
} }
@Override
public void deleteAll() {
store.clear();
}
private List<RoleUkeyBindingEntity> all() {
return store;
}
} }
} }
@@ -0,0 +1,116 @@
package com.cisd.tms.modules.auth.service;
import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleAccountEntity;
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.repository.AuthFullAccountRepository;
import com.cisd.tms.modules.auth.repository.AuthSessionRepository;
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.modules.auth.service.impl.AuthSecurityResetServiceImpl;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
class AuthSecurityResetServiceTest {
private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-05-13T08:00:00Z"), ZoneOffset.UTC);
@Test
void shouldClearBindingsSessionsAndRestoreSeededRolesAndPasswords() {
RoleAccountRepository roleAccountRepository = Mockito.mock(RoleAccountRepository.class);
AuthFullAccountRepository fullAccountRepository = Mockito.mock(AuthFullAccountRepository.class);
RoleUkeyBindingRepository ukeyBindingRepository = Mockito.mock(RoleUkeyBindingRepository.class);
AuthSessionRepository authSessionRepository = Mockito.mock(AuthSessionRepository.class);
Mockito.when(fullAccountRepository.findAll()).thenReturn(List.of(
dirtyAccount(RoleCode.SUPER_ADMIN, 1),
dirtyAccount(RoleCode.SUPER_ADMIN, 2),
dirtyAccount(RoleCode.KEY_ADMIN, 1),
dirtyAccount(RoleCode.AUDIT_ADMIN, 1),
dirtyAccount(RoleCode.OPS_ADMIN, 1)
));
Mockito.when(roleAccountRepository.findAll()).thenReturn(List.of(
dirtyRole(RoleCode.SUPER_ADMIN),
dirtyRole(RoleCode.KEY_ADMIN),
dirtyRole(RoleCode.AUDIT_ADMIN),
dirtyRole(RoleCode.OPS_ADMIN)
));
new AuthSecurityResetServiceImpl(
roleAccountRepository,
fullAccountRepository,
ukeyBindingRepository,
authSessionRepository,
FIXED_CLOCK
).resetAfterMasterKeyInitialized();
Mockito.verify(ukeyBindingRepository).deleteAll();
Mockito.verify(authSessionRepository).deleteAll();
ArgumentCaptor<RoleAccountEntity> roleCaptor = ArgumentCaptor.forClass(RoleAccountEntity.class);
Mockito.verify(roleAccountRepository, Mockito.times(4)).update(roleCaptor.capture());
RoleAccountEntity superAdmin = roleCaptor.getAllValues().stream()
.filter(item -> RoleCode.SUPER_ADMIN.getCode().equals(item.getRoleCode()))
.findFirst()
.orElseThrow();
RoleAccountEntity auditAdmin = roleCaptor.getAllValues().stream()
.filter(item -> RoleCode.AUDIT_ADMIN.getCode().equals(item.getRoleCode()))
.findFirst()
.orElseThrow();
Assertions.assertEquals("超级管理员", superAdmin.getDisplayName());
Assertions.assertEquals(2, superAdmin.getRequiredUkeyCount());
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), superAdmin.getStatus());
Assertions.assertEquals(RoleAccountStatus.UNENABLED.name(), auditAdmin.getStatus());
ArgumentCaptor<AuthFullAccountEntity> accountCaptor = ArgumentCaptor.forClass(AuthFullAccountEntity.class);
Mockito.verify(fullAccountRepository, Mockito.times(5)).update(accountCaptor.capture());
AuthFullAccountEntity firstSuperAdmin = accountCaptor.getAllValues().stream()
.filter(item -> RoleCode.SUPER_ADMIN.getCode().equals(item.getRoleCode()) && item.getUid() == 1)
.findFirst()
.orElseThrow();
Assertions.assertEquals("super-admin-full-01", firstSuperAdmin.getAccountName());
Assertions.assertEquals("超级管理员UKey席位一", firstSuperAdmin.getDisplayName());
Assertions.assertEquals("init-super-admin-salt-20260325", firstSuperAdmin.getPasswordSalt());
Assertions.assertEquals("Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=", firstSuperAdmin.getPasswordHash());
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), firstSuperAdmin.getStatus());
Assertions.assertTrue(Boolean.TRUE.equals(firstSuperAdmin.getNeedChangePassword()));
Assertions.assertEquals(0, firstSuperAdmin.getFailedCount());
Assertions.assertNull(firstSuperAdmin.getLockedUntil());
Assertions.assertNull(firstSuperAdmin.getLastLoginAt());
Assertions.assertNull(firstSuperAdmin.getLastActiveAt());
Assertions.assertEquals(LocalDateTime.ofInstant(FIXED_CLOCK.instant(), ZoneOffset.UTC), firstSuperAdmin.getPasswordChangedAt());
}
private static RoleAccountEntity dirtyRole(RoleCode roleCode) {
RoleAccountEntity entity = new RoleAccountEntity();
entity.setRoleCode(roleCode.getCode());
entity.setDisplayName("DIRTY-" + roleCode.getCode());
entity.setRequiredUkeyCount(99);
entity.setStatus(RoleAccountStatus.LOCKED.name());
return entity;
}
private static AuthFullAccountEntity dirtyAccount(RoleCode roleCode, int uid) {
AuthFullAccountEntity entity = new AuthFullAccountEntity();
entity.setRoleCode(roleCode.getCode());
entity.setUid(uid);
entity.setAccountName("dirty");
entity.setDisplayName("dirty");
entity.setPasswordHash("dirty");
entity.setPasswordSalt("dirty");
entity.setStatus(RoleAccountStatus.LOCKED.name());
entity.setNeedChangePassword(Boolean.FALSE);
entity.setFailedCount(5);
entity.setLockedUntil(LocalDateTime.of(2026, 5, 13, 9, 0));
entity.setLastLoginAt(LocalDateTime.of(2026, 5, 13, 9, 0));
entity.setLastActiveAt(LocalDateTime.of(2026, 5, 13, 9, 0));
return entity;
}
}
@@ -493,11 +493,23 @@ class AuthServiceTest {
.filter(entity -> java.util.Objects.equals(authenticatedPrincipalsJson, entity.getAuthenticatedPrincipalsJson())) .filter(entity -> java.util.Objects.equals(authenticatedPrincipalsJson, entity.getAuthenticatedPrincipalsJson()))
.toList(); .toList();
} }
@Override
public void deleteAll() {
store.clear();
}
} }
private static class InMemoryRoleUkeyBindingRepository implements RoleUkeyBindingRepository { private static class InMemoryRoleUkeyBindingRepository implements RoleUkeyBindingRepository {
private final List<RoleUkeyBindingEntity> store = new ArrayList<>(); private final List<RoleUkeyBindingEntity> store = new ArrayList<>();
@Override
public Optional<RoleUkeyBindingEntity> findById(Long id) {
return store.stream()
.filter(entity -> id.equals(entity.getId()))
.findFirst();
}
@Override @Override
public List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode) { public List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode) {
return store.stream() return store.stream()
@@ -513,6 +525,14 @@ class AuthServiceTest {
.toList(); .toList();
} }
@Override
public List<RoleUkeyBindingEntity> findActiveByUkeyIdentity(String ukeySerial, String ukeyPubkey) {
return store.stream()
.filter(entity -> "ACTIVE".equals(entity.getStatus()))
.filter(entity -> ukeySerial.equals(entity.getUkeySerial()) || ukeyPubkey.equals(entity.getUkeyPubkey()))
.toList();
}
@Override @Override
public void save(RoleUkeyBindingEntity entity) { public void save(RoleUkeyBindingEntity entity) {
store.add(entity); store.add(entity);
@@ -522,5 +542,10 @@ class AuthServiceTest {
public void update(RoleUkeyBindingEntity entity) { public void update(RoleUkeyBindingEntity entity) {
save(entity); save(entity);
} }
@Override
public void deleteAll() {
store.clear();
}
} }
} }
@@ -16,6 +16,7 @@ import com.cisd.tms.integration.crypto.pcie.model.RecoverUserKeyRequest;
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
import com.cisd.tms.common.enums.ErrorCode; import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException; import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.auth.service.AuthSecurityResetService;
import com.cisd.tms.modules.mk.config.MasterKeyInitProperties; import com.cisd.tms.modules.mk.config.MasterKeyInitProperties;
import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket; import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket;
import com.cisd.tms.modules.mk.dto.MasterKeyPacketIkPart; import com.cisd.tms.modules.mk.dto.MasterKeyPacketIkPart;
@@ -57,6 +58,22 @@ class LmkServiceTest {
Assertions.assertArrayEquals("87654321".getBytes(StandardCharsets.UTF_8), newPinCaptor.getValue()); Assertions.assertArrayEquals("87654321".getBytes(StandardCharsets.UTF_8), newPinCaptor.getValue());
} }
@Test
void shouldResetAuthSecurityStateAfterMasterKeyInitializationSucceeds() {
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
AuthSecurityResetService authSecurityResetService = Mockito.mock(AuthSecurityResetService.class);
MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE=");
Mockito.when(pcieCryptoService.initializeMasterKey(Mockito.any(), Mockito.any())).thenReturn(new byte[] {0x01, 0x23});
LmkService service = new LmkServiceImpl(pcieCryptoService, properties, authSecurityResetService);
service.initMasterKey();
InOrder inOrder = Mockito.inOrder(pcieCryptoService, authSecurityResetService);
inOrder.verify(pcieCryptoService).initializeMasterKey(Mockito.any(), Mockito.any());
inOrder.verify(authSecurityResetService).resetAfterMasterKeyInitialized();
}
@Test @Test
void shouldPropagateInitMasterKeyFailure() { void shouldPropagateInitMasterKeyFailure() {
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);