fix:角色授权修改

This commit is contained in:
waner
2026-04-25 17:38:57 +08:00
parent 7bc364f367
commit 291c7cfc8a
28 changed files with 520 additions and 2240 deletions
@@ -58,18 +58,18 @@ public class AuthAdminController {
return ApiResponse.success();
}
@PostMapping("/roles/{roleCode}/full-accounts/{uid}/change-password")
@Operation(summary = "管理员修改 FULL 账户口令", description = "管理员为目标角色指定 UKey 席位账号设置新口令。")
@PostMapping("/roles/{roleCode}/accounts/{uid}/change-password")
@Operation(summary = "管理员修改角色席位口令", description = "管理员为目标角色指定席位账号设置新口令。")
@ReplayProtected
// @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "管理员修改 FULL 账户口令")
public ApiResponse<Void> changeFullAccountPassword(
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "管理员修改角色席位口令")
public ApiResponse<Void> changeAccountPassword(
@PathVariable("roleCode") String roleCode,
@PathVariable("uid") Integer uid,
@Valid @RequestBody AdminChangePasswordRequest request,
HttpServletRequest httpRequest
) {
authAdminService.changeFullAccountPassword(
authAdminService.changeAccountPassword(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL),
roleCode,
@@ -80,28 +80,6 @@ public class AuthAdminController {
return ApiResponse.success();
}
@PostMapping("/roles/{roleCode}/limited-accounts/{username}/change-password")
@Operation(summary = "管理员修改 LIMITED 账户口令", description = "管理员为目标角色指定独立用户账号设置新口令。")
@ReplayProtected
// @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "管理员修改 LIMITED 账户口令")
public ApiResponse<Void> changeLimitedAccountPassword(
@PathVariable("roleCode") String roleCode,
@PathVariable("username") String username,
@Valid @RequestBody AdminChangePasswordRequest request,
HttpServletRequest httpRequest
) {
authAdminService.changeLimitedAccountPassword(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL),
roleCode,
username,
request.getOldPassword(),
request.getNewPassword()
);
return ApiResponse.success();
}
@PostMapping("/super-admin/ukeys/issue-sign")
@Operation(summary = "签发并绑定超级管理员 UKey", description = "系统初始化后登记超级管理员 UKey,不要求已有登录态。")
public ApiResponse<UKeySignResult> issueSuperAdminUkeyBindingSign(
@@ -65,16 +65,16 @@ public class AuthController {
return ApiResponse.success();
}
@PostMapping("/full-accounts/{uid}/change-password")
@Operation(summary = "修改当前 FULL 账户口令", description = "基于当前 FULL 会话校验并更新指定席位账户口令。")
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "修改当前 FULL 账户口令")
@PostMapping("/accounts/{uid}/change-password")
@Operation(summary = "修改当前登录席位口令", description = "基于当前会话校验并更新指定角色席位口令。")
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "修改当前登录席位口令")
@ReplayProtected
public ApiResponse<Void> changeFullAccountPassword(
public ApiResponse<Void> changeAccountPassword(
@PathVariable("uid") Integer uid,
@Valid @RequestBody ChangePasswordRequest request,
HttpServletRequest httpRequest
) {
authService.changeFullAccountPassword(
authService.changeAccountPassword(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN),
uid,
request.getCurrentPassword(),
@@ -82,22 +82,4 @@ public class AuthController {
);
return ApiResponse.success();
}
@PostMapping("/limited-accounts/{username}/change-password")
@Operation(summary = "修改当前 LIMITED 账户口令", description = "基于当前 LIMITED 会话校验并更新指定受限账户口令。")
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "修改当前 LIMITED 账户口令")
@ReplayProtected
public ApiResponse<Void> changeLimitedAccountPassword(
@PathVariable("username") String username,
@Valid @RequestBody ChangePasswordRequest request,
HttpServletRequest httpRequest
) {
authService.changeLimitedAccountPassword(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN),
username,
request.getCurrentPassword(),
request.getNewPassword()
);
return ApiResponse.success();
}
}
@@ -13,9 +13,16 @@ public class AuthSessionPrincipal {
return principal;
}
public static AuthSessionPrincipal limited(String username) {
public static AuthSessionPrincipal full(Integer uid, String username) {
AuthSessionPrincipal principal = full(uid);
principal.setUsername(username);
return principal;
}
public static AuthSessionPrincipal limited(Integer uid, String username) {
AuthSessionPrincipal principal = new AuthSessionPrincipal();
principal.setType("LIMITED");
principal.setUid(uid);
principal.setUsername(username);
return principal;
}
@@ -2,24 +2,25 @@ package com.cisd.tms.modules.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@Schema(description = "角色口令校验账号")
public class PasswordLoginAccountRequest {
@NotBlank(message = "username is required")
@Schema(description = "账号名", example = "super-admin-01")
private String username;
@NotNull(message = "uid is required")
@Schema(description = "角色固定席位编号", example = "1")
private Integer uid;
@NotBlank(message = "password is required")
@Schema(description = "账号口令", example = "12345678")
private String password;
public String getUsername() {
return username;
public Integer getUid() {
return uid;
}
public void setUsername(String username) {
this.username = username;
public void setUid(Integer uid) {
this.uid = uid;
}
public String getPassword() {
@@ -1,118 +0,0 @@
package com.cisd.tms.modules.auth.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
import java.time.LocalDateTime;
@TableName("tms_auth_user_account")
public class AuthUserAccountEntity extends BaseEntity {
private String username;
private String displayName;
private String roleCode;
private String passwordHash;
private String passwordSalt;
private String status;
private Boolean needChangePassword;
private LocalDateTime passwordChangedAt;
private Integer failedCount;
private LocalDateTime lockedUntil;
private LocalDateTime lastLoginAt;
private LocalDateTime lastActiveAt;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getRoleCode() {
return roleCode;
}
public void setRoleCode(String roleCode) {
this.roleCode = roleCode;
}
public String getPasswordHash() {
return passwordHash;
}
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}
public String getPasswordSalt() {
return passwordSalt;
}
public void setPasswordSalt(String passwordSalt) {
this.passwordSalt = passwordSalt;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Boolean getNeedChangePassword() {
return needChangePassword;
}
public void setNeedChangePassword(Boolean needChangePassword) {
this.needChangePassword = needChangePassword;
}
public LocalDateTime getPasswordChangedAt() {
return passwordChangedAt;
}
public void setPasswordChangedAt(LocalDateTime passwordChangedAt) {
this.passwordChangedAt = passwordChangedAt;
}
public Integer getFailedCount() {
return failedCount;
}
public void setFailedCount(Integer failedCount) {
this.failedCount = failedCount;
}
public LocalDateTime getLockedUntil() {
return lockedUntil;
}
public void setLockedUntil(LocalDateTime lockedUntil) {
this.lockedUntil = lockedUntil;
}
public LocalDateTime getLastLoginAt() {
return lastLoginAt;
}
public void setLastLoginAt(LocalDateTime lastLoginAt) {
this.lastLoginAt = lastLoginAt;
}
public LocalDateTime getLastActiveAt() {
return lastActiveAt;
}
public void setLastActiveAt(LocalDateTime lastActiveAt) {
this.lastActiveAt = lastActiveAt;
}
}
@@ -1,36 +0,0 @@
package com.cisd.tms.modules.auth.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
@TableName("tms_auth_user")
public class AuthUserEntity extends BaseEntity {
private String username;
private String displayName;
private String role;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}
@@ -1,7 +1,7 @@
package com.cisd.tms.modules.auth.enums;
public enum RoleCode {
SUPER_ADMIN("SUPER_ADMIN", "超级管理员", 2),
SUPER_ADMIN("SUPER_ADMIN", "系统管理员", 2),
KEY_ADMIN("KEY_ADMIN", "密钥管理员", 1),
AUDIT_ADMIN("AUDIT_ADMIN", "审计管理员", 1),
OPS_ADMIN("OPS_ADMIN", "运维管理员", 1);
@@ -1,15 +0,0 @@
package com.cisd.tms.modules.auth.mapper;
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface AuthUserAccountMapper extends BaseMapperX<AuthUserAccountEntity> {
AuthUserAccountEntity selectByUsername(@Param("username") String username);
List<AuthUserAccountEntity> selectByRoleCode(@Param("roleCode") String roleCode);
}
@@ -1,12 +0,0 @@
package com.cisd.tms.modules.auth.mapper;
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
import com.cisd.tms.modules.auth.entity.AuthUserEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface AuthUserMapper extends BaseMapperX<AuthUserEntity> {
AuthUserEntity selectByUsername(@Param("username") String username);
}
@@ -1,16 +0,0 @@
package com.cisd.tms.modules.auth.repository;
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
import java.util.List;
import java.util.Optional;
public interface AuthUserAccountRepository {
Optional<AuthUserAccountEntity> findByUsername(String username);
List<AuthUserAccountEntity> findByRoleCode(String roleCode);
void save(AuthUserAccountEntity entity);
void update(AuthUserAccountEntity entity);
}
@@ -1,9 +0,0 @@
package com.cisd.tms.modules.auth.repository;
import com.cisd.tms.modules.auth.entity.AuthUserEntity;
import java.util.Optional;
public interface AuthUserRepository {
Optional<AuthUserEntity> findByUsername(String username);
}
@@ -1,38 +0,0 @@
package com.cisd.tms.modules.auth.repository.impl;
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
import com.cisd.tms.modules.auth.mapper.AuthUserAccountMapper;
import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
@Repository
public class AuthUserAccountRepositoryImpl implements AuthUserAccountRepository {
private final AuthUserAccountMapper authUserAccountMapper;
public AuthUserAccountRepositoryImpl(AuthUserAccountMapper authUserAccountMapper) {
this.authUserAccountMapper = authUserAccountMapper;
}
@Override
public Optional<AuthUserAccountEntity> findByUsername(String username) {
return Optional.ofNullable(authUserAccountMapper.selectByUsername(username));
}
@Override
public List<AuthUserAccountEntity> findByRoleCode(String roleCode) {
return authUserAccountMapper.selectByRoleCode(roleCode);
}
@Override
public void save(AuthUserAccountEntity entity) {
authUserAccountMapper.insert(entity);
}
@Override
public void update(AuthUserAccountEntity entity) {
authUserAccountMapper.updateById(entity);
}
}
@@ -1,22 +0,0 @@
package com.cisd.tms.modules.auth.repository.impl;
import com.cisd.tms.modules.auth.entity.AuthUserEntity;
import com.cisd.tms.modules.auth.mapper.AuthUserMapper;
import com.cisd.tms.modules.auth.repository.AuthUserRepository;
import java.util.Optional;
import org.springframework.stereotype.Repository;
@Repository
public class AuthUserRepositoryImpl implements AuthUserRepository {
private final AuthUserMapper authUserMapper;
public AuthUserRepositoryImpl(AuthUserMapper authUserMapper) {
this.authUserMapper = authUserMapper;
}
@Override
public Optional<AuthUserEntity> findByUsername(String username) {
return Optional.ofNullable(authUserMapper.selectByUsername(username));
}
}
@@ -9,7 +9,7 @@ public interface AuthAdminService {
void resetPassword(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode);
void changeFullAccountPassword(
void changeAccountPassword(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
@@ -18,15 +18,6 @@ public interface AuthAdminService {
String newPassword
);
void changeLimitedAccountPassword(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
String username,
String oldPassword,
String newPassword
);
UKeySignResult issueUkeyBindingSign(
String operatorRoleCode,
String operatorAuthLevel,
@@ -27,7 +27,5 @@ public interface AuthService {
void logout(String sessionToken);
void changeFullAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword);
void changeLimitedAccountPassword(String sessionToken, String username, String currentPassword, String newPassword);
void changeAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword);
}
@@ -4,13 +4,11 @@ import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.common.util.TraceIdUtil;
import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity;
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
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.AuthUserAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.modules.auth.service.AuthAdminService;
@@ -40,7 +38,6 @@ public class AuthAdminServiceImpl implements AuthAdminService {
private final RoleAccountRepository roleAccountRepository;
private final AuthFullAccountRepository authFullAccountRepository;
private final AuthUserAccountRepository authUserAccountRepository;
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
private final PasswordHasher passwordHasher;
private final LmkService lmkService;
@@ -68,19 +65,6 @@ public class AuthAdminServiceImpl implements AuthAdminService {
authFullAccountRepository.update(account);
}
List<AuthUserAccountEntity> accounts = authUserAccountRepository.findByRoleCode(targetRoleCode);
if (accounts.isEmpty()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role user accounts not found");
}
for (AuthUserAccountEntity account : accounts) {
account.setStatus(RoleAccountStatus.ACTIVE.name());
account.setNeedChangePassword(Boolean.TRUE);
account.setFailedCount(0);
account.setLockedUntil(null);
account.setLastActiveAt(null);
account.setLastLoginAt(null);
authUserAccountRepository.update(account);
}
}
@Override
@@ -108,27 +92,10 @@ public class AuthAdminServiceImpl implements AuthAdminService {
authFullAccountRepository.update(account);
}
List<AuthUserAccountEntity> accounts = authUserAccountRepository.findByRoleCode(targetRoleCode);
if (accounts.isEmpty()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role user accounts not found");
}
for (AuthUserAccountEntity account : accounts) {
String newSalt = passwordSaltGenerator.nextSalt();
account.setPasswordSalt(newSalt);
account.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt));
account.setStatus(RoleAccountStatus.ACTIVE.name());
account.setNeedChangePassword(Boolean.TRUE);
account.setPasswordChangedAt(current);
account.setFailedCount(0);
account.setLockedUntil(null);
account.setLastActiveAt(null);
account.setLastLoginAt(null);
authUserAccountRepository.update(account);
}
}
@Override
public void changeFullAccountPassword(
public void changeAccountPassword(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
@@ -151,29 +118,6 @@ public class AuthAdminServiceImpl implements AuthAdminService {
authFullAccountRepository.update(account);
}
@Override
public void changeLimitedAccountPassword(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
String username,
String oldPassword,
String newPassword
) {
PasswordComplexityValidator.validate(newPassword);
loadRole(targetRoleCode);
AuthUserAccountEntity account = authUserAccountRepository.findByUsername(username)
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role user account not found"));
if (!targetRoleCode.equals(account.getRoleCode())) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role user account not found");
}
if (!passwordHasher.matches(oldPassword, account.getPasswordSalt(), account.getPasswordHash())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "old password is incorrect");
}
applyAdminPasswordChange(account, newPassword);
authUserAccountRepository.update(account);
}
private void bindUkey(
String targetRoleCode,
Integer uid,
@@ -277,19 +221,6 @@ public class AuthAdminServiceImpl implements AuthAdminService {
}
}
private void applyAdminPasswordChange(AuthUserAccountEntity account, String newPassword) {
String newSalt = passwordSaltGenerator.nextSalt();
account.setPasswordSalt(newSalt);
account.setPasswordHash(passwordHasher.hash(newPassword, newSalt));
account.setNeedChangePassword(Boolean.TRUE);
account.setPasswordChangedAt(now());
account.setFailedCount(0);
account.setLockedUntil(null);
if (RoleAccountStatus.LOCKED.name().equals(account.getStatus())) {
account.setStatus(RoleAccountStatus.ACTIVE.name());
}
}
private RoleAccountEntity loadRole(String roleCode) {
resolveRoleCode(roleCode);
return roleAccountRepository.findByRoleCode(roleCode)
@@ -16,8 +16,6 @@ import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity;
import com.cisd.tms.modules.auth.entity.AuthSessionEntity;
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
import com.cisd.tms.modules.auth.entity.AuthUserEntity;
import com.cisd.tms.modules.auth.entity.RoleAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
import com.cisd.tms.modules.auth.enums.AuthLevel;
@@ -26,8 +24,6 @@ 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.AuthUserAccountRepository;
import com.cisd.tms.modules.auth.repository.AuthUserRepository;
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.modules.auth.service.AuthPolicyService;
@@ -69,8 +65,6 @@ public class AuthServiceImpl implements AuthService {
private static final int PASSWORD_EXPIRE_DAYS = 30;
private final RoleAccountRepository roleAccountRepository;
private final AuthUserRepository authUserRepository;
private final AuthUserAccountRepository authUserAccountRepository;
private final AuthFullAccountRepository authFullAccountRepository;
private final AuthSessionRepository authSessionRepository;
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
@@ -92,19 +86,17 @@ public class AuthServiceImpl implements AuthService {
validateRoleStatus(roleAccount);
AuthMethod authMethod = resolveAuthMethod(request.getUkeySerials());
List<AuthUserAccountEntity> validatedAccounts = new ArrayList<>();
List<AuthFullAccountEntity> validatedFullAccounts = new ArrayList<>();
List<AuthFullAccountEntity> validatedAccounts;
if (AuthMethod.PASSWORD == authMethod) {
validatedAccounts = validateRoleAccounts(roleAccount, request.getAccounts());
resetValidatedAccounts(validatedAccounts);
} else {
validatedFullAccounts = validateFullAccounts(roleAccount, request.getFullAccounts());
resetValidatedFullAccounts(validatedFullAccounts);
validatedAccounts = validateFullAccounts(roleAccount, request.getFullAccounts());
}
resetValidatedAccounts(validatedAccounts);
AuthLevel authLevel = resolveAuthLevel(roleAccount, request.getUkeySerials());
List<AuthSessionPrincipal> authenticatedPrincipals = AuthMethod.PASSWORD == authMethod
? normalizeAuthenticatedUsers(validatedAccounts)
: normalizeAuthenticatedFullAccounts(validatedFullAccounts);
? normalizeAuthenticatedAccounts(validatedAccounts, AuthLevel.LIMITED)
: normalizeAuthenticatedAccounts(validatedAccounts, AuthLevel.FULL);
expireConcurrentSessions(roleAccount.getRoleCode(), authLevel, authenticatedPrincipals);
AuthSessionEntity session = buildSession(
roleAccount.getRoleCode(),
@@ -120,9 +112,7 @@ public class AuthServiceImpl implements AuthService {
response.setToken(session.getSessionToken());
response.setExpiresAt(OffsetDateTime.of(session.getExpiresAt(), ZoneOffset.UTC).toString());
response.setNeedChangePassword(
AuthMethod.PASSWORD == authMethod
? needsPasswordChangeForLimitedAccounts(validatedAccounts)
: needsPasswordChangeForFullAccounts(validatedFullAccounts)
needsPasswordChangeForAccounts(validatedAccounts)
);
return response;
}
@@ -189,8 +179,8 @@ public class AuthServiceImpl implements AuthService {
@Override
public CurrentUserResponse me(String username, String sessionToken) {
String normalized = username == null || username.isBlank() ? "admin" : username;
RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(normalized).orElse(null);
if (roleAccount != null) {
RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(normalized)
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account not found"));
CurrentUserResponse response = new CurrentUserResponse();
response.setUsername(roleAccount.getRoleCode());
response.setDisplayName(roleAccount.getDisplayName());
@@ -205,23 +195,6 @@ public class AuthServiceImpl implements AuthService {
return response;
}
AuthUserEntity entity = authUserRepository.findByUsername(normalized).orElse(null);
CurrentUserResponse response = new CurrentUserResponse();
if (entity == null) {
response.setUsername(normalized);
response.setDisplayName("TMS Administrator");
response.setRole("ADMIN");
response.setNeedChangePassword(Boolean.FALSE);
return response;
}
response.setUsername(entity.getUsername());
response.setDisplayName(entity.getDisplayName());
response.setRole(entity.getRole());
response.setNeedChangePassword(Boolean.FALSE);
return response;
}
@Override
public void logout(String sessionToken) {
AuthSessionEntity session = requireActiveSession(sessionToken);
@@ -231,17 +204,14 @@ public class AuthServiceImpl implements AuthService {
}
@Override
public void changeFullAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword) {
public void changeAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword) {
PasswordComplexityValidator.validate(newPassword);
AuthSessionEntity session = requireActiveSession(sessionToken);
if (!AuthLevel.FULL.name().equals(session.getAuthLevel())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full session is required");
}
if (uid == null || !containsAuthenticatedFullUid(session, uid)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full account is not authenticated in current session");
if (uid == null || !containsAuthenticatedUid(session, uid)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "account is not authenticated in current session");
}
AuthFullAccountEntity fullAccount = authFullAccountRepository.findByRoleCodeAndUid(session.getRoleCode(), uid)
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full account not found"));
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "account not found"));
validateFullAccountStatus(fullAccount);
if (!passwordHasher.matches(currentPassword, fullAccount.getPasswordSalt(), fullAccount.getPasswordHash())) {
onFullPasswordFailed(fullAccount);
@@ -265,49 +235,13 @@ public class AuthServiceImpl implements AuthService {
authSessionRepository.update(session);
}
@Override
public void changeLimitedAccountPassword(String sessionToken, String username, String currentPassword, String newPassword) {
PasswordComplexityValidator.validate(newPassword);
AuthSessionEntity session = requireActiveSession(sessionToken);
if (!AuthLevel.LIMITED.name().equals(session.getAuthLevel())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "limited session is required");
}
if (username == null || username.isBlank() || !containsAuthenticatedLimitedUsername(session, username)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "limited account is not authenticated in current session");
}
AuthUserAccountEntity userAccount = authUserAccountRepository.findByUsername(username)
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account not found"));
validateUserAccountRole(session.getRoleCode(), userAccount);
validateUserAccountStatus(userAccount);
if (!passwordHasher.matches(currentPassword, userAccount.getPasswordSalt(), userAccount.getPasswordHash())) {
onPasswordFailed(userAccount);
}
String salt = passwordSaltGenerator.nextSalt();
LocalDateTime current = now();
userAccount.setPasswordSalt(salt);
userAccount.setPasswordHash(passwordHasher.hash(newPassword, salt));
userAccount.setFailedCount(0);
userAccount.setLockedUntil(null);
userAccount.setStatus(RoleAccountStatus.ACTIVE.name());
userAccount.setNeedChangePassword(Boolean.FALSE);
userAccount.setPasswordChangedAt(current);
userAccount.setLastLoginAt(current);
userAccount.setLastActiveAt(current);
authUserAccountRepository.update(userAccount);
session.setLastActiveAt(current);
session.setExpiresAt(current.plusMinutes(IDLE_TIMEOUT_MINUTES));
authSessionRepository.update(session);
}
private void validateRoleStatus(RoleAccountEntity roleAccount) {
if (!RoleAccountStatus.ACTIVE.name().equals(roleAccount.getStatus())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role is not enabled");
}
}
private List<AuthUserAccountEntity> validateRoleAccounts(
private List<AuthFullAccountEntity> validateRoleAccounts(
RoleAccountEntity roleAccount,
List<PasswordLoginAccountRequest> accounts
) {
@@ -319,54 +253,25 @@ public class AuthServiceImpl implements AuthService {
);
}
Set<String> uniqueUsernames = new LinkedHashSet<>();
List<AuthUserAccountEntity> validatedAccounts = new ArrayList<>();
Set<Integer> uniqueUids = new LinkedHashSet<>();
List<AuthFullAccountEntity> validatedAccounts = new ArrayList<>();
for (PasswordLoginAccountRequest account : accounts) {
if (!uniqueUsernames.add(account.getUsername())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "duplicate account username submitted");
Integer uid = account.getUid();
if (uid == null || !uniqueUids.add(uid)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "duplicate account uid submitted");
}
AuthUserAccountEntity userAccount = authUserAccountRepository.findByUsername(account.getUsername())
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account not found"));
validateUserAccountRole(roleAccount.getRoleCode(), userAccount);
validateUserAccountStatus(userAccount);
if (!passwordHasher.matches(account.getPassword(), userAccount.getPasswordSalt(), userAccount.getPasswordHash())) {
onPasswordFailed(userAccount);
AuthFullAccountEntity roleAccountSeat = authFullAccountRepository
.findByRoleCodeAndUid(roleAccount.getRoleCode(), uid)
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "account not found"));
validateFullAccountStatus(roleAccountSeat);
if (!passwordHasher.matches(account.getPassword(), roleAccountSeat.getPasswordSalt(), roleAccountSeat.getPasswordHash())) {
onFullPasswordFailed(roleAccountSeat);
}
validatedAccounts.add(userAccount);
validatedAccounts.add(roleAccountSeat);
}
return validatedAccounts;
}
private void validateUserAccountRole(String expectedRoleCode, AuthUserAccountEntity userAccount) {
if (!expectedRoleCode.equals(userAccount.getRoleCode())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account does not belong to target role");
}
}
private void validateUserAccountStatus(AuthUserAccountEntity userAccount) {
if (RoleAccountStatus.UNENABLED.name().equals(userAccount.getStatus())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account is not enabled");
}
if (RoleAccountStatus.LOCKED.name().equals(userAccount.getStatus())
&& (userAccount.getLockedUntil() == null || userAccount.getLockedUntil().isAfter(now()))) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account is locked");
}
}
private void onPasswordFailed(AuthUserAccountEntity userAccount) {
int failedCount = userAccount.getFailedCount() == null ? 0 : userAccount.getFailedCount();
failedCount++;
userAccount.setFailedCount(failedCount);
if (failedCount >= MAX_FAILED_ATTEMPTS) {
userAccount.setStatus(RoleAccountStatus.LOCKED.name());
userAccount.setLockedUntil(now().plusMinutes(IDLE_TIMEOUT_MINUTES));
authUserAccountRepository.update(userAccount);
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account is locked");
}
authUserAccountRepository.update(userAccount);
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "password is incorrect");
}
private void validateFullAccountStatus(AuthFullAccountEntity fullAccount) {
if (RoleAccountStatus.UNENABLED.name().equals(fullAccount.getStatus())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full account is not enabled");
@@ -391,19 +296,7 @@ public class AuthServiceImpl implements AuthService {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "password is incorrect");
}
private void resetValidatedAccounts(List<AuthUserAccountEntity> validatedAccounts) {
LocalDateTime current = now();
for (AuthUserAccountEntity account : validatedAccounts) {
account.setFailedCount(0);
account.setLockedUntil(null);
account.setStatus(RoleAccountStatus.ACTIVE.name());
account.setLastLoginAt(current);
account.setLastActiveAt(current);
authUserAccountRepository.update(account);
}
}
private void resetValidatedFullAccounts(List<AuthFullAccountEntity> validatedAccounts) {
private void resetValidatedAccounts(List<AuthFullAccountEntity> validatedAccounts) {
LocalDateTime current = now();
for (AuthFullAccountEntity account : validatedAccounts) {
account.setFailedCount(0);
@@ -587,25 +480,18 @@ public class AuthServiceImpl implements AuthService {
}
}
private List<AuthSessionPrincipal> normalizeAuthenticatedUsers(List<AuthUserAccountEntity> validatedAccounts) {
private List<AuthSessionPrincipal> normalizeAuthenticatedAccounts(
List<AuthFullAccountEntity> validatedAccounts,
AuthLevel authLevel
) {
if (validatedAccounts == null || validatedAccounts.isEmpty()) {
return null;
}
return validatedAccounts.stream()
.map(AuthUserAccountEntity::getUsername)
.sorted()
.map(AuthSessionPrincipal::limited)
.toList();
}
private List<AuthSessionPrincipal> normalizeAuthenticatedFullAccounts(List<AuthFullAccountEntity> validatedAccounts) {
if (validatedAccounts == null || validatedAccounts.isEmpty()) {
return null;
}
return validatedAccounts.stream()
.map(AuthFullAccountEntity::getUid)
.sorted()
.map(AuthSessionPrincipal::full)
.sorted(java.util.Comparator.comparing(AuthFullAccountEntity::getUid))
.map(account -> AuthLevel.FULL == authLevel
? AuthSessionPrincipal.full(account.getUid(), account.getAccountName())
: AuthSessionPrincipal.limited(account.getUid(), account.getAccountName()))
.toList();
}
@@ -636,7 +522,11 @@ public class AuthServiceImpl implements AuthService {
if (value.startsWith("FULL:")) {
principals.add(AuthSessionPrincipal.full(Integer.parseInt(value.substring("FULL:".length()))));
} else {
principals.add(AuthSessionPrincipal.limited(value));
AuthFullAccountEntity account = authFullAccountRepository.findByRoleCode(session.getRoleCode()).stream()
.filter(item -> value.equals(item.getAccountName()))
.findFirst()
.orElse(null);
principals.add(AuthSessionPrincipal.limited(account == null ? null : account.getUid(), value));
}
}
return principals;
@@ -650,25 +540,16 @@ public class AuthServiceImpl implements AuthService {
}
}
private boolean containsAuthenticatedFullUid(AuthSessionEntity session, Integer uid) {
private boolean containsAuthenticatedUid(AuthSessionEntity session, Integer uid) {
return authenticatedPrincipals(session).stream()
.anyMatch(principal -> "FULL".equals(principal.getType()) && uid.equals(principal.getUid()));
}
private boolean containsAuthenticatedLimitedUsername(AuthSessionEntity session, String username) {
return authenticatedPrincipals(session).stream()
.anyMatch(principal -> "LIMITED".equals(principal.getType()) && username.equals(principal.getUsername()));
.anyMatch(principal -> uid.equals(principal.getUid()));
}
private LocalDateTime now() {
return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC);
}
private boolean needsPasswordChangeForLimitedAccounts(List<AuthUserAccountEntity> accounts) {
return accounts != null && accounts.stream().anyMatch(this::needsPasswordChange);
}
private boolean needsPasswordChangeForFullAccounts(List<AuthFullAccountEntity> accounts) {
private boolean needsPasswordChangeForAccounts(List<AuthFullAccountEntity> accounts) {
return accounts != null && accounts.stream().anyMatch(this::needsPasswordChange);
}
@@ -680,18 +561,10 @@ public class AuthServiceImpl implements AuthService {
if (principals.isEmpty()) {
return false;
}
if (AuthLevel.FULL.name().equals(session.getAuthLevel())) {
List<AuthFullAccountEntity> accounts = authFullAccountRepository.findByRoleCode(session.getRoleCode());
return accounts.stream()
.filter(account -> principals.stream()
.anyMatch(principal -> "FULL".equals(principal.getType()) && account.getUid().equals(principal.getUid())))
.anyMatch(this::needsPasswordChange);
}
List<AuthUserAccountEntity> accounts = authUserAccountRepository.findByRoleCode(session.getRoleCode());
return accounts.stream()
.filter(account -> principals.stream()
.anyMatch(principal -> "LIMITED".equals(principal.getType())
&& account.getUsername().equals(principal.getUsername())))
.anyMatch(principal -> account.getUid().equals(principal.getUid())))
.anyMatch(this::needsPasswordChange);
}
@@ -700,11 +573,6 @@ public class AuthServiceImpl implements AuthService {
|| isPasswordExpired(account.getPasswordChangedAt());
}
private boolean needsPasswordChange(AuthUserAccountEntity account) {
return Boolean.TRUE.equals(account.getNeedChangePassword())
|| isPasswordExpired(account.getPasswordChangedAt());
}
private boolean isPasswordExpired(LocalDateTime passwordChangedAt) {
return passwordChangedAt == null || !passwordChangedAt.isAfter(now().minusDays(PASSWORD_EXPIRE_DAYS));
}
@@ -37,6 +37,9 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier {
private static final String PROVIDER = "BC";
private static final String SIGNATURE_ALGORITHM = "SM3withSM2";
private static final String SM2_CURVE = "sm2p256v1";
private static final int GM0018_ECCREF_PUBLIC_KEY_LENGTH = 132;
private static final int GM0018_ECCREF_COMPONENT_LENGTH = 64;
private static final int SM2_COORDINATE_LENGTH = 32;
static {
if (Security.getProvider(PROVIDER) == null) {
@@ -65,6 +68,9 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier {
public void verifyLoginSignature(String pubKey, String loginSignData, String loginSign) {
try {
PublicKey publicKey = parsePublicKey(pubKey);
if (loginSignData == null || loginSignData.isBlank()) {
throw new IllegalArgumentException("login sign data is required");
}
byte[] signatureBytes = decodeSignature(loginSign);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM, PROVIDER);
signature.initVerify(publicKey);
@@ -96,7 +102,7 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier {
try {
return KeyFactory.getInstance("EC", PROVIDER).generatePublic(new X509EncodedKeySpec(decoded));
} catch (Exception ignored) {
return parseUncompressedPointPublicKey(decoded);
return parseUncompressedPointPublicKey(toPointBytes(decoded));
}
}
@@ -110,12 +116,40 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier {
}
}
private PublicKey parseUncompressedPointPublicKey(byte[] keyBytes) throws Exception {
byte[] pointBytes = keyBytes;
private byte[] toPointBytes(byte[] keyBytes) {
if (keyBytes.length == GM0018_ECCREF_PUBLIC_KEY_LENGTH) {
return gm0018EccRefToPointBytes(keyBytes);
}
return keyBytes;
}
private byte[] gm0018EccRefToPointBytes(byte[] keyBytes) {
int bitsLittleEndian = (keyBytes[0] & 0xff)
| ((keyBytes[1] & 0xff) << 8)
| ((keyBytes[2] & 0xff) << 16)
| ((keyBytes[3] & 0xff) << 24);
int bitsBigEndian = ((keyBytes[0] & 0xff) << 24)
| ((keyBytes[1] & 0xff) << 16)
| ((keyBytes[2] & 0xff) << 8)
| (keyBytes[3] & 0xff);
if (bitsLittleEndian != 256 && bitsBigEndian != 256) {
throw new IllegalArgumentException("login public key is invalid");
}
byte[] pointBytes = new byte[65];
pointBytes[0] = 0x04;
System.arraycopy(keyBytes, 4 + GM0018_ECCREF_COMPONENT_LENGTH - SM2_COORDINATE_LENGTH,
pointBytes, 1, SM2_COORDINATE_LENGTH);
System.arraycopy(keyBytes, 4 + GM0018_ECCREF_COMPONENT_LENGTH * 2 - SM2_COORDINATE_LENGTH,
pointBytes, 1 + SM2_COORDINATE_LENGTH, SM2_COORDINATE_LENGTH);
return pointBytes;
}
private PublicKey parseUncompressedPointPublicKey(byte[] pointBytes) throws Exception {
if (pointBytes.length == 64) {
byte[] rawPointBytes = pointBytes;
pointBytes = new byte[65];
pointBytes[0] = 0x04;
System.arraycopy(keyBytes, 0, pointBytes, 1, keyBytes.length);
System.arraycopy(rawPointBytes, 0, pointBytes, 1, rawPointBytes.length);
}
if (pointBytes.length != 65 || pointBytes[0] != 0x04) {
throw new IllegalArgumentException("login public key is invalid");
@@ -129,16 +163,31 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier {
}
private byte[] decodeSignature(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("login signature is required");
}
byte[] decoded = decodeHexOrBase64(normalize(value), "login signature is invalid");
if (isDerSignature(decoded)) {
return decoded;
}
if (decoded.length == GM0018_ECCREF_COMPONENT_LENGTH * 2) {
return rawRsToDer(gm0018SignatureToRawRs(decoded));
}
if (decoded.length == 64) {
return rawRsToDer(decoded);
}
throw new IllegalArgumentException("login signature is invalid");
}
private byte[] gm0018SignatureToRawRs(byte[] signatureBytes) {
byte[] rawSignature = new byte[64];
System.arraycopy(signatureBytes, GM0018_ECCREF_COMPONENT_LENGTH - SM2_COORDINATE_LENGTH,
rawSignature, 0, SM2_COORDINATE_LENGTH);
System.arraycopy(signatureBytes, GM0018_ECCREF_COMPONENT_LENGTH * 2 - SM2_COORDINATE_LENGTH,
rawSignature, SM2_COORDINATE_LENGTH, SM2_COORDINATE_LENGTH);
return rawSignature;
}
private boolean isDerSignature(byte[] signatureBytes) {
try (ASN1InputStream inputStream = new ASN1InputStream(signatureBytes)) {
ASN1Primitive primitive = inputStream.readObject();
@@ -142,7 +142,7 @@ public class OperationAuditAspect {
return "";
}
return request.getAccounts().stream()
.map(account -> account.getUsername() == null ? "" : account.getUsername().trim())
.map(account -> account.getUid() == null ? "" : "uid=" + account.getUid())
.filter(value -> !value.isBlank())
.collect(Collectors.joining(","));
}
@@ -139,16 +139,6 @@ CREATE TABLE IF NOT EXISTS tms_resource_task_step (
-- 认证与设备基础表
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_auth_user (
id BIGINT PRIMARY KEY,
username VARCHAR(64) NOT NULL,
display_name VARCHAR(128) NOT NULL,
role VARCHAR(64) NOT NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_auth_user_username (username)
);
CREATE TABLE IF NOT EXISTS tms_device_node (
id BIGINT PRIMARY KEY,
node_id VARCHAR(64) NOT NULL,
@@ -267,26 +257,6 @@ CREATE TABLE IF NOT EXISTS tms_role_ukey_binding (
KEY idx_tms_role_ukey_binding_role_status (role_code, status)
);
CREATE TABLE IF NOT EXISTS tms_auth_user_account (
id BIGINT PRIMARY KEY,
username VARCHAR(64) NOT NULL,
display_name VARCHAR(128) NOT NULL,
role_code VARCHAR(64) NOT NULL,
password_hash VARCHAR(256) NOT NULL,
password_salt VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
need_change_password TINYINT(1) NOT NULL DEFAULT 1,
password_changed_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
failed_count INT NOT NULL DEFAULT 0,
locked_until DATETIME(3) NULL,
last_login_at DATETIME(3) NULL,
last_active_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_auth_user_account_username (username),
KEY idx_tms_auth_user_account_role_code (role_code)
);
CREATE TABLE IF NOT EXISTS tms_auth_full_account (
id BIGINT PRIMARY KEY,
role_code VARCHAR(64) NOT NULL,
@@ -439,110 +409,6 @@ VALUES
CURRENT_TIMESTAMP(3)
);
INSERT IGNORE INTO tms_auth_user_account (
id,
username,
display_name,
role_code,
password_hash,
password_salt,
status,
need_change_password,
password_changed_at,
failed_count,
locked_until,
last_login_at,
last_active_at,
create_time,
update_time
)
VALUES
(
12001,
'super-admin-01',
'超级管理员账号一',
'SUPER_ADMIN',
'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=',
'init-super-admin-salt-20260325',
'ACTIVE',
1,
CURRENT_TIMESTAMP(3),
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
12002,
'super-admin-02',
'超级管理员账号二',
'SUPER_ADMIN',
'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=',
'init-super-admin-salt-20260325',
'ACTIVE',
1,
CURRENT_TIMESTAMP(3),
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
12003,
'key-admin-01',
'密钥管理员账号',
'KEY_ADMIN',
'/xqmDy3A/q9X5p7XWznIPSBabQ6bxTLAPgtHQ6Be33c=',
'init-key-admin-salt-20260325',
'ACTIVE',
1,
CURRENT_TIMESTAMP(3),
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
12004,
'audit-admin-01',
'审计管理员账号',
'AUDIT_ADMIN',
'j/J1LKyUOzVEJfbbHe2dwKBKNdabRk502olk/LWIwhg=',
'init-audit-admin-salt-20260325',
'ACTIVE',
1,
CURRENT_TIMESTAMP(3),
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
12005,
'ops-admin-01',
'运维管理员账号',
'OPS_ADMIN',
'2e9F4aCgsDb+AuD1LrBEqEg8yjy3ODo27UHtx06vj/A=',
'init-ops-admin-salt-20260325',
'ACTIVE',
1,
CURRENT_TIMESTAMP(3),
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
);
INSERT IGNORE INTO tms_auth_full_account (
id,
role_code,
@@ -1,66 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cisd.tms.modules.auth.mapper.AuthUserAccountMapper">
<resultMap id="AuthUserAccountResultMap" type="com.cisd.tms.modules.auth.entity.AuthUserAccountEntity">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="displayName" column="display_name"/>
<result property="roleCode" column="role_code"/>
<result property="passwordHash" column="password_hash"/>
<result property="passwordSalt" column="password_salt"/>
<result property="status" column="status"/>
<result property="needChangePassword" column="need_change_password"/>
<result property="passwordChangedAt" column="password_changed_at"/>
<result property="failedCount" column="failed_count"/>
<result property="lockedUntil" column="locked_until"/>
<result property="lastLoginAt" column="last_login_at"/>
<result property="lastActiveAt" column="last_active_at"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<select id="selectByUsername" parameterType="string" resultMap="AuthUserAccountResultMap">
SELECT id,
username,
display_name,
role_code,
password_hash,
password_salt,
status,
need_change_password,
password_changed_at,
failed_count,
locked_until,
last_login_at,
last_active_at,
create_time,
update_time
FROM tms_auth_user_account
WHERE username = #{username}
LIMIT 1
</select>
<select id="selectByRoleCode" parameterType="string" resultMap="AuthUserAccountResultMap">
SELECT id,
username,
display_name,
role_code,
password_hash,
password_salt,
status,
need_change_password,
password_changed_at,
failed_count,
locked_until,
last_login_at,
last_active_at,
create_time,
update_time
FROM tms_auth_user_account
WHERE role_code = #{roleCode}
ORDER BY username ASC
</select>
</mapper>
@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cisd.tms.modules.auth.mapper.AuthUserMapper">
<resultMap id="AuthUserResultMap" type="com.cisd.tms.modules.auth.entity.AuthUserEntity">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="displayName" column="display_name"/>
<result property="role" column="role"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<select id="selectByUsername" parameterType="string" resultMap="AuthUserResultMap">
SELECT id, username, display_name, role, create_time, update_time
FROM tms_auth_user
WHERE username = #{username}
LIMIT 1
</select>
</mapper>
@@ -48,7 +48,7 @@ class AuthControllerTest {
"roleCode": "AUDIT_ADMIN",
"accounts": [
{
"username": "audit-admin-01",
"uid": 1,
"password": "12345678"
}
],
@@ -199,7 +199,7 @@ class AuthControllerTest {
}
@Test
void shouldChangeFullAccountPasswordThroughCurrentSessionEndpoint() throws Exception {
void shouldChangeAccountPasswordThroughCurrentSessionEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
@@ -208,7 +208,7 @@ class AuthControllerTest {
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/full-accounts/1/change-password")
mockMvc.perform(post("/api/v1/auth/accounts/1/change-password")
.requestAttr(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN, "token-change-001")
.contentType(MediaType.APPLICATION_JSON)
.content("""
@@ -220,32 +220,7 @@ class AuthControllerTest {
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"success\":true")));
Mockito.verify(authService).changeFullAccountPassword("token-change-001", 1, "12345678", "Abc1234!");
}
@Test
void shouldChangeLimitedAccountPasswordThroughCurrentSessionEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/limited-accounts/audit-admin-01/change-password")
.requestAttr(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN, "token-change-limited-001")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"oldPassword": "12345678",
"newPassword": "Abc1234!"
}
"""))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"success\":true")));
Mockito.verify(authService).changeLimitedAccountPassword("token-change-limited-001", "audit-admin-01", "12345678", "Abc1234!");
Mockito.verify(authService).changeAccountPassword("token-change-001", 1, "12345678", "Abc1234!");
}
@Test
@@ -268,7 +243,7 @@ class AuthControllerTest {
}
@Test
void shouldChangeFullAccountPasswordThroughAdminEndpoint() throws Exception {
void shouldChangeAccountPasswordThroughAdminEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
@@ -277,7 +252,7 @@ class AuthControllerTest {
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/roles/KEY_ADMIN/full-accounts/1/change-password")
mockMvc.perform(post("/api/v1/auth/roles/KEY_ADMIN/accounts/1/change-password")
.requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "SUPER_ADMIN")
.requestAttr(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL")
.contentType(MediaType.APPLICATION_JSON)
@@ -290,33 +265,7 @@ class AuthControllerTest {
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"success\":true")));
Mockito.verify(authAdminService).changeFullAccountPassword("SUPER_ADMIN", "FULL", "KEY_ADMIN", 1, "12345678", "Abc1234!");
}
@Test
void shouldChangeLimitedAccountPasswordThroughAdminEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/roles/AUDIT_ADMIN/limited-accounts/audit-admin-01/change-password")
.requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "SUPER_ADMIN")
.requestAttr(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"oldPassword": "12345678",
"newPassword": "Abc1234!"
}
"""))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"success\":true")));
Mockito.verify(authAdminService).changeLimitedAccountPassword("SUPER_ADMIN", "FULL", "AUDIT_ADMIN", "audit-admin-01", "12345678", "Abc1234!");
Mockito.verify(authAdminService).changeAccountPassword("SUPER_ADMIN", "FULL", "KEY_ADMIN", 1, "12345678", "Abc1234!");
}
@Test
@@ -1,6 +1,7 @@
package com.cisd.tms.modules.auth.service;
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
@@ -8,21 +9,20 @@ import com.cisd.tms.modules.auth.enums.AuthLevel;
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.AuthUserAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.modules.auth.service.impl.AuthAdminServiceImpl;
import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket;
import com.cisd.tms.modules.mk.dto.UKeySignDTO;
import com.cisd.tms.modules.mk.dto.UKeySignResult;
import com.cisd.tms.modules.mk.service.LmkService;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -33,412 +33,148 @@ import org.mockito.Mockito;
class AuthAdminServiceTest {
private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-23T03:00:00Z"), ZoneOffset.UTC);
private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-23T02:00:00Z"), ZoneOffset.UTC);
@Test
void shouldEnableRoleWhenOperatorIsKeyAdminWithFullSession() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository();
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.UNENABLED));
fullAccounts.save(fullAccount(RoleCode.AUDIT_ADMIN, 1, "audit-admin-full-01", "OLD-FULL-HASH-1", "OLD-FULL-SALT-1"));
userAccounts.save(user("audit-admin-01", RoleCode.AUDIT_ADMIN, "OLD-HASH-1", "OLD-SALT-1"));
fullAccounts.findByRoleCodeAndUid(RoleCode.AUDIT_ADMIN.getCode(), 1).orElseThrow().setStatus(RoleAccountStatus.UNENABLED.name());
userAccounts.findByUsername("audit-admin-01").orElseThrow().setStatus(RoleAccountStatus.UNENABLED.name());
void shouldEnableRoleAndUnifiedSeatAccounts() {
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository();
RoleAccountEntity role = role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.UNENABLED);
roles.save(role);
AuthFullAccountEntity account = account(RoleCode.AUDIT_ADMIN, 1, "audit-admin-01", "HASH-OLD", "SALT-OLD");
account.setStatus(RoleAccountStatus.UNENABLED.name());
account.setFailedCount(3);
account.setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 0));
accounts.save(account);
AuthAdminService service = newAuthAdminService(
roleAccounts,
fullAccounts,
userAccounts,
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
);
service(roles, accounts, new InMemoryRoleUkeyBindingRepository(), Mockito.mock(LmkService.class))
.enableRole(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.AUDIT_ADMIN.getCode());
service.enableRole(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.AUDIT_ADMIN.getCode());
RoleAccountEntity updated = roleAccounts.findByRoleCode(RoleCode.AUDIT_ADMIN.getCode()).orElseThrow();
AuthFullAccountEntity fullAccount = fullAccounts.findByRoleCodeAndUid(RoleCode.AUDIT_ADMIN.getCode(), 1).orElseThrow();
AuthUserAccountEntity userAccount = userAccounts.findByUsername("audit-admin-01").orElseThrow();
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), updated.getStatus());
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), fullAccount.getStatus());
Assertions.assertTrue(Boolean.TRUE.equals(fullAccount.getNeedChangePassword()));
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), userAccount.getStatus());
Assertions.assertTrue(Boolean.TRUE.equals(userAccount.getNeedChangePassword()));
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), roles.findByRoleCode(RoleCode.AUDIT_ADMIN.getCode()).orElseThrow().getStatus());
AuthFullAccountEntity changed = accounts.findByRoleCodeAndUid(RoleCode.AUDIT_ADMIN.getCode(), 1).orElseThrow();
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), changed.getStatus());
Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword()));
Assertions.assertEquals(0, changed.getFailedCount());
Assertions.assertNull(changed.getLockedUntil());
}
@Test
void shouldResetPasswordForAllAccountsUnderRole() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository();
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
roleAccounts.save(role(RoleCode.SUPER_ADMIN, RoleAccountStatus.ACTIVE));
fullAccounts.save(fullAccount(RoleCode.SUPER_ADMIN, 1, "super-admin-full-01", "OLD-FULL-HASH-1", "OLD-FULL-SALT-1"));
fullAccounts.save(fullAccount(RoleCode.SUPER_ADMIN, 2, "super-admin-full-02", "OLD-FULL-HASH-2", "OLD-FULL-SALT-2"));
userAccounts.save(user("super-admin-01", RoleCode.SUPER_ADMIN, "OLD-HASH-1", "OLD-SALT-1"));
userAccounts.save(user("super-admin-02", RoleCode.SUPER_ADMIN, "OLD-HASH-2", "OLD-SALT-2"));
fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow().setFailedCount(2);
fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow().setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 20));
fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow().setFailedCount(5);
fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow().setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 25));
userAccounts.findByUsername("super-admin-01").orElseThrow().setFailedCount(3);
userAccounts.findByUsername("super-admin-01").orElseThrow().setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 30));
userAccounts.findByUsername("super-admin-02").orElseThrow().setFailedCount(5);
userAccounts.findByUsername("super-admin-02").orElseThrow().setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 40));
void shouldResetOnlyUnifiedSeatPasswords() {
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository();
roles.save(role(RoleCode.SUPER_ADMIN, RoleAccountStatus.ACTIVE));
accounts.save(account(RoleCode.SUPER_ADMIN, 1, "super-admin-01", "OLD-HASH-1", "OLD-SALT-1"));
accounts.save(account(RoleCode.SUPER_ADMIN, 2, "super-admin-02", "OLD-HASH-2", "OLD-SALT-2"));
AuthAdminService service = newAuthAdminService(
roleAccounts,
fullAccounts,
userAccounts,
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-full-001", "salt-full-002", "salt-002", "salt-003")
);
service(roles, accounts, new InMemoryRoleUkeyBindingRepository(), Mockito.mock(LmkService.class))
.resetPassword(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode());
service.resetPassword(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode());
RoleAccountEntity role = roleAccounts.findByRoleCode(RoleCode.SUPER_ADMIN.getCode()).orElseThrow();
AuthFullAccountEntity fullFirst = fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow();
AuthFullAccountEntity fullSecond = fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow();
AuthUserAccountEntity first = userAccounts.findByUsername("super-admin-01").orElseThrow();
AuthUserAccountEntity second = userAccounts.findByUsername("super-admin-02").orElseThrow();
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), role.getStatus());
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), fullFirst.getStatus());
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), fullSecond.getStatus());
Assertions.assertTrue(Boolean.TRUE.equals(fullFirst.getNeedChangePassword()));
Assertions.assertTrue(Boolean.TRUE.equals(fullSecond.getNeedChangePassword()));
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), fullFirst.getPasswordChangedAt());
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), fullSecond.getPasswordChangedAt());
Assertions.assertEquals(0, fullFirst.getFailedCount());
Assertions.assertEquals(0, fullSecond.getFailedCount());
Assertions.assertNull(fullFirst.getLockedUntil());
Assertions.assertNull(fullSecond.getLockedUntil());
Assertions.assertEquals("salt-full-001", fullFirst.getPasswordSalt());
Assertions.assertEquals("HASH:12345678:salt-full-001", fullFirst.getPasswordHash());
Assertions.assertEquals("salt-full-002", fullSecond.getPasswordSalt());
Assertions.assertEquals("HASH:12345678:salt-full-002", fullSecond.getPasswordHash());
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), first.getStatus());
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), second.getStatus());
AuthFullAccountEntity first = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow();
AuthFullAccountEntity second = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow();
Assertions.assertEquals("salt-reset-1", first.getPasswordSalt());
Assertions.assertEquals("HASH:12345678:salt-reset-1", first.getPasswordHash());
Assertions.assertEquals("salt-reset-2", second.getPasswordSalt());
Assertions.assertEquals("HASH:12345678:salt-reset-2", second.getPasswordHash());
Assertions.assertTrue(Boolean.TRUE.equals(first.getNeedChangePassword()));
Assertions.assertTrue(Boolean.TRUE.equals(second.getNeedChangePassword()));
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), first.getPasswordChangedAt());
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), second.getPasswordChangedAt());
Assertions.assertEquals(0, first.getFailedCount());
Assertions.assertEquals(0, second.getFailedCount());
Assertions.assertNull(first.getLockedUntil());
Assertions.assertNull(second.getLockedUntil());
Assertions.assertEquals("salt-002", first.getPasswordSalt());
Assertions.assertEquals("HASH:12345678:salt-002", first.getPasswordHash());
Assertions.assertEquals("salt-003", second.getPasswordSalt());
Assertions.assertEquals("HASH:12345678:salt-003", second.getPasswordHash());
}
@Test
void shouldChangeTargetFullAccountPasswordByAdmin() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository();
roleAccounts.save(role(RoleCode.KEY_ADMIN, RoleAccountStatus.ACTIVE));
fullAccounts.save(fullAccount(RoleCode.KEY_ADMIN, 1, "key-admin-full-01", "HASH:12345678:OLD-FULL-SALT", "OLD-FULL-SALT"));
AuthFullAccountEntity account = fullAccounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow();
account.setStatus(RoleAccountStatus.LOCKED.name());
account.setFailedCount(5);
account.setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 50));
void shouldChangeUnifiedSeatPassword() {
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository();
roles.save(role(RoleCode.KEY_ADMIN, RoleAccountStatus.ACTIVE));
accounts.save(account(RoleCode.KEY_ADMIN, 1, "key-admin-01", "HASH:Old1234!:SALT-OLD", "SALT-OLD"));
AuthAdminService service = newAuthAdminService(
roleAccounts,
fullAccounts,
new InMemoryAuthUserAccountRepository(),
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-full-admin")
);
service.changeFullAccountPassword(
service(roles, accounts, new InMemoryRoleUkeyBindingRepository(), Mockito.mock(LmkService.class))
.changeAccountPassword(
RoleCode.SUPER_ADMIN.getCode(),
AuthLevel.FULL.name(),
RoleCode.KEY_ADMIN.getCode(),
1,
"12345678",
"Abc1234!"
"Old1234!",
"New1234!"
);
AuthFullAccountEntity changed = fullAccounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow();
Assertions.assertEquals("salt-full-admin", changed.getPasswordSalt());
Assertions.assertEquals("HASH:Abc1234!:salt-full-admin", changed.getPasswordHash());
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), changed.getStatus());
AuthFullAccountEntity changed = accounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow();
Assertions.assertEquals("salt-reset-1", changed.getPasswordSalt());
Assertions.assertEquals("HASH:New1234!:salt-reset-1", changed.getPasswordHash());
Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword()));
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), changed.getPasswordChangedAt());
Assertions.assertEquals(0, changed.getFailedCount());
Assertions.assertNull(changed.getLockedUntil());
}
@Test
void shouldChangeTargetLimitedAccountPasswordByAdmin() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.ACTIVE));
userAccounts.save(user("audit-admin-01", RoleCode.AUDIT_ADMIN, "HASH:12345678:OLD-SALT", "OLD-SALT"));
AuthUserAccountEntity account = userAccounts.findByUsername("audit-admin-01").orElseThrow();
account.setStatus(RoleAccountStatus.LOCKED.name());
account.setFailedCount(5);
account.setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 55));
AuthAdminService service = newAuthAdminService(
roleAccounts,
new InMemoryAuthFullAccountRepository(),
userAccounts,
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-limited-admin")
);
service.changeLimitedAccountPassword(
RoleCode.SUPER_ADMIN.getCode(),
AuthLevel.FULL.name(),
RoleCode.AUDIT_ADMIN.getCode(),
"audit-admin-01",
"12345678",
"Abc1234!"
);
AuthUserAccountEntity changed = userAccounts.findByUsername("audit-admin-01").orElseThrow();
Assertions.assertEquals("salt-limited-admin", changed.getPasswordSalt());
Assertions.assertEquals("HASH:Abc1234!:salt-limited-admin", changed.getPasswordHash());
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), changed.getStatus());
Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword()));
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), changed.getPasswordChangedAt());
Assertions.assertEquals(0, changed.getFailedCount());
Assertions.assertNull(changed.getLockedUntil());
}
@Test
void shouldRejectAdminPasswordChangeWhenOldPasswordIsWrong() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.ACTIVE));
userAccounts.save(user("audit-admin-01", RoleCode.AUDIT_ADMIN, "HASH:12345678:OLD-SALT", "OLD-SALT"));
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository();
roles.save(role(RoleCode.KEY_ADMIN, RoleAccountStatus.ACTIVE));
accounts.save(account(RoleCode.KEY_ADMIN, 1, "key-admin-01", "HASH:Old1234!:SALT-OLD", "SALT-OLD"));
AuthAdminService service = newAuthAdminService(
roleAccounts,
new InMemoryAuthFullAccountRepository(),
userAccounts,
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-unused")
);
com.cisd.tms.common.exception.BizException exception = Assertions.assertThrows(
com.cisd.tms.common.exception.BizException.class,
() -> service.changeLimitedAccountPassword(
RoleCode.SUPER_ADMIN.getCode(),
AuthLevel.FULL.name(),
RoleCode.AUDIT_ADMIN.getCode(),
"audit-admin-01",
"bad-password",
"Abc1234!"
)
);
Assertions.assertEquals("old password is incorrect", exception.getMessage());
AuthUserAccountEntity unchanged = userAccounts.findByUsername("audit-admin-01").orElseThrow();
Assertions.assertEquals("OLD-SALT", unchanged.getPasswordSalt());
Assertions.assertEquals("HASH:12345678:OLD-SALT", unchanged.getPasswordHash());
}
@Test
void shouldRejectAdminPasswordChangeWhenNewPasswordIsTooSimple() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.ACTIVE));
userAccounts.save(user("audit-admin-01", RoleCode.AUDIT_ADMIN, "HASH:12345678:OLD-SALT", "OLD-SALT"));
AuthAdminService service = newAuthAdminService(
roleAccounts,
new InMemoryAuthFullAccountRepository(),
userAccounts,
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-unused")
);
com.cisd.tms.common.exception.BizException exception = Assertions.assertThrows(
com.cisd.tms.common.exception.BizException.class,
() -> service.changeLimitedAccountPassword(
RoleCode.SUPER_ADMIN.getCode(),
AuthLevel.FULL.name(),
RoleCode.AUDIT_ADMIN.getCode(),
"audit-admin-01",
"12345678",
"87654321"
)
);
Assertions.assertEquals("密码复杂度不符合要求:需由8到16位大小写字母、数字、符号组成,且三类均需包含", exception.getMessage());
}
@Test
void shouldUpdateActiveBindingWhenIssueSignUsesSameSerial() {
InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository();
bindings.save(binding(RoleCode.SUPER_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.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("SIG-NEW");
com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket packet = new com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket();
packet.setPacketIndex(1);
Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet);
AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(),
fullAccountRepository(binding(RoleCode.SUPER_ADMIN, 1, "UK-OLD", "PUB-OLD", "SIG-OLD")),
new InMemoryAuthUserAccountRepository(),
bindings,
new FakePasswordHasher(),
lmkService,
new com.fasterxml.jackson.databind.ObjectMapper(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
);
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey("PUB-NEW");
dto.setUkeySerial("UK-OLD");
dto.setUid(1);
service.issueUkeyBindingSign(
null,
null,
RoleCode.SUPER_ADMIN.getCode(),
dto
);
RoleUkeyBindingEntity updated = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).stream()
.filter(binding -> "UK-OLD".equals(binding.getUkeySerial()))
.findFirst()
.orElseThrow();
Assertions.assertEquals("UK-OLD", updated.getUkeySerial());
Assertions.assertEquals("PUB-NEW", updated.getUkeyPubkey());
Assertions.assertEquals("SIG-NEW", updated.getIssuerSign());
}
@Test
void shouldIssueBindingSignWithBackupPacketForSuperAdmin() {
LmkService lmkService = Mockito.mock(LmkService.class);
Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-001");
com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket packet = new com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket();
packet.setPacketIndex(1);
Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet);
AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(),
fullAccountRepository(binding(RoleCode.SUPER_ADMIN, 1, "UK-001", "PUB-OLD", "SIG-OLD")),
new InMemoryAuthUserAccountRepository(),
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
lmkService,
new com.fasterxml.jackson.databind.ObjectMapper(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
);
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey("PUB-001");
dto.setUkeySerial("UK-001");
dto.setUid(1);
UKeySignResult result = service.issueUkeyBindingSign(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto);
Assertions.assertEquals("ISSUE-SIGN-001", result.getSign());
Assertions.assertNotNull(result.getBackupPacket());
Assertions.assertEquals(1, result.getBackupPacket().getPacketIndex());
}
@Test
void shouldIssueAndBindNormalRoleUkeyInOneStep() {
InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository();
LmkService lmkService = Mockito.mock(LmkService.class);
Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-KEY-001");
AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(),
fullAccountRepository(binding(RoleCode.KEY_ADMIN, 1, "UK-KEY-001", "PUB-OLD", "SIG-OLD")),
new InMemoryAuthUserAccountRepository(),
bindings,
new FakePasswordHasher(),
lmkService,
new com.fasterxml.jackson.databind.ObjectMapper(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
);
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey("PUB-KEY-001");
dto.setUkeySerial("UK-KEY-001");
dto.setUid(1);
UKeySignResult result = service.issueUkeyBindingSign(
BizException exception = Assertions.assertThrows(BizException.class,
() -> service(roles, accounts, new InMemoryRoleUkeyBindingRepository(), Mockito.mock(LmkService.class))
.changeAccountPassword(
RoleCode.SUPER_ADMIN.getCode(),
AuthLevel.FULL.name(),
RoleCode.KEY_ADMIN.getCode(),
dto
);
1,
"bad-password",
"New1234!"
));
Assertions.assertEquals("ISSUE-SIGN-KEY-001", result.getSign());
Assertions.assertNull(result.getBackupPacket());
RoleUkeyBindingEntity binding = bindings.findActiveByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).stream()
.filter(item -> "UK-KEY-001".equals(item.getUkeySerial()))
.findFirst()
.orElseThrow();
Assertions.assertEquals("PUB-KEY-001", binding.getUkeyPubkey());
Assertions.assertEquals("ISSUE-SIGN-KEY-001", binding.getIssuerSign());
Assertions.assertEquals(ErrorCode.UNAUTHORIZED.getCode(), exception.getCode());
Assertions.assertEquals("old password is incorrect", exception.getMessage());
}
@Test
void shouldIssueSuperAdminBackupUkeyWithoutReplacingExistingBindingForSameUid() {
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"));
bindings.save(binding(RoleCode.SUPER_ADMIN, 1, "UK-PRIMARY", "PUB-PRIMARY", "SIG-PRIMARY"));
LmkService lmkService = Mockito.mock(LmkService.class);
Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-BACKUP");
com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket packet = new com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket();
Mockito.when(lmkService.signIk(Mockito.anyString())).thenReturn("ISSUE-SIGN-BACKUP");
MasterKeyBackupPacket packet = new MasterKeyBackupPacket();
packet.setPacketIndex(1);
Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet);
AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(),
fullAccountRepository(binding(RoleCode.SUPER_ADMIN, 1, "UK-PRIMARY", "PUB-PRIMARY", "SIG-PRIMARY")),
new InMemoryAuthUserAccountRepository(),
bindings,
new FakePasswordHasher(),
lmkService,
new com.fasterxml.jackson.databind.ObjectMapper(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
);
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey("PUB-BACKUP");
dto.setUkeySerial("UK-BACKUP");
dto.setUid(1);
UKeySignResult result = service.issueUkeyBindingSign(
RoleCode.KEY_ADMIN.getCode(),
AuthLevel.FULL.name(),
RoleCode.SUPER_ADMIN.getCode(),
dto
);
UKeySignResult result = service(roles, accounts, bindings, lmkService)
.issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto);
Assertions.assertEquals("ISSUE-SIGN-BACKUP", 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(binding -> "UK-PRIMARY".equals(binding.getUkeySerial())
&& "PUB-PRIMARY".equals(binding.getUkeyPubkey())));
Assertions.assertTrue(activeUidBindings.stream().anyMatch(binding -> "UK-BACKUP".equals(binding.getUkeySerial())
&& "PUB-BACKUP".equals(binding.getUkeyPubkey())
&& "ISSUE-SIGN-BACKUP".equals(binding.getIssuerSign())));
Assertions.assertTrue(activeUidBindings.stream().anyMatch(item -> "UK-PRIMARY".equals(item.getUkeySerial())));
Assertions.assertTrue(activeUidBindings.stream().anyMatch(item -> "UK-BACKUP".equals(item.getUkeySerial())));
}
private static AuthAdminService service(
InMemoryRoleAccountRepository roles,
InMemoryAuthFullAccountRepository accounts,
InMemoryRoleUkeyBindingRepository bindings,
LmkService lmkService
) {
return new AuthAdminServiceImpl(
roles,
accounts,
bindings,
new FakePasswordHasher(),
lmkService,
new ObjectMapper(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-reset-1", "salt-reset-2", "salt-reset-3")
);
}
private static RoleAccountEntity role(RoleCode roleCode, RoleAccountStatus status) {
RoleAccountEntity entity = new RoleAccountEntity();
entity.setId((long) roleCode.ordinal() + 100);
entity.setId((long) roleCode.ordinal() + 1);
entity.setRoleCode(roleCode.getCode());
entity.setDisplayName(roleCode.getDisplayName());
entity.setRequiredUkeyCount(roleCode.getRequiredUkeyCount());
@@ -446,22 +182,7 @@ class AuthAdminServiceTest {
return entity;
}
private static AuthUserAccountEntity user(String username, RoleCode roleCode, String passwordHash, String passwordSalt) {
AuthUserAccountEntity entity = new AuthUserAccountEntity();
entity.setId((long) (username.hashCode() & Integer.MAX_VALUE));
entity.setUsername(username);
entity.setDisplayName(username);
entity.setRoleCode(roleCode.getCode());
entity.setPasswordHash(passwordHash);
entity.setPasswordSalt(passwordSalt);
entity.setStatus(RoleAccountStatus.ACTIVE.name());
entity.setNeedChangePassword(Boolean.FALSE);
entity.setPasswordChangedAt(LocalDateTime.of(2026, 3, 23, 2, 0));
entity.setFailedCount(0);
return entity;
}
private static AuthFullAccountEntity fullAccount(RoleCode roleCode, int uid, String accountName, String passwordHash, String passwordSalt) {
private static AuthFullAccountEntity account(RoleCode roleCode, int uid, String accountName, String passwordHash, String passwordSalt) {
AuthFullAccountEntity entity = new AuthFullAccountEntity();
entity.setId((long) (accountName.hashCode() & Integer.MAX_VALUE));
entity.setRoleCode(roleCode.getCode());
@@ -472,32 +193,24 @@ class AuthAdminServiceTest {
entity.setPasswordSalt(passwordSalt);
entity.setStatus(RoleAccountStatus.ACTIVE.name());
entity.setNeedChangePassword(Boolean.FALSE);
entity.setPasswordChangedAt(LocalDateTime.of(2026, 3, 23, 2, 0));
entity.setPasswordChangedAt(LocalDateTime.of(2026, 3, 23, 1, 0));
entity.setFailedCount(0);
return entity;
}
private static RoleUkeyBindingEntity binding(
RoleCode roleCode,
int uid,
String serial,
String pubkey,
String issuerSign
) {
private static RoleUkeyBindingEntity binding(RoleCode roleCode, int uid, String serial, String pubKey, String sign) {
RoleUkeyBindingEntity entity = new RoleUkeyBindingEntity();
entity.setId((long) uid);
entity.setId((long) Math.abs((roleCode.getCode() + uid + serial).hashCode()));
entity.setRoleCode(roleCode.getCode());
entity.setUid(uid);
entity.setUkeySerial(serial);
entity.setUkeyPubkey(pubkey);
entity.setIssuerSign(issuerSign);
entity.setUkeyPubkey(pubKey);
entity.setIssuerSign(sign);
entity.setStatus("ACTIVE");
entity.setBoundAt(LocalDateTime.of(2026, 3, 22, 8, 0));
return entity;
}
private static class FakePasswordHasher implements PasswordHasher {
@Override
public String hash(String password, String salt) {
return "HASH:" + password + ":" + salt;
@@ -510,15 +223,19 @@ class AuthAdminServiceTest {
}
private static class FixedSaltSupplier implements PasswordSaltGenerator {
private final Deque<String> salts;
private final List<String> salts;
private int index = 0;
private FixedSaltSupplier(String... salts) {
this.salts = new ArrayDeque<>(List.of(salts));
this.salts = List.of(salts);
}
@Override
public String nextSalt() {
return Optional.ofNullable(salts.pollFirst()).orElse("salt-fallback");
if (index >= salts.size()) {
return salts.get(salts.size() - 1);
}
return salts.get(index++);
}
}
@@ -541,33 +258,6 @@ class AuthAdminServiceTest {
}
}
private static class InMemoryAuthUserAccountRepository implements AuthUserAccountRepository {
private final Map<String, AuthUserAccountEntity> store = new ConcurrentHashMap<>();
@Override
public Optional<AuthUserAccountEntity> findByUsername(String username) {
return Optional.ofNullable(store.get(username));
}
@Override
public List<AuthUserAccountEntity> findByRoleCode(String roleCode) {
return store.values().stream()
.filter(entity -> roleCode.equals(entity.getRoleCode()))
.sorted(Comparator.comparing(AuthUserAccountEntity::getUsername))
.toList();
}
@Override
public void save(AuthUserAccountEntity entity) {
store.put(entity.getUsername(), entity);
}
@Override
public void update(AuthUserAccountEntity entity) {
store.put(entity.getUsername(), entity);
}
}
private static class InMemoryAuthFullAccountRepository implements AuthFullAccountRepository {
private final Map<String, AuthFullAccountEntity> store = new ConcurrentHashMap<>();
@@ -591,75 +281,37 @@ class AuthAdminServiceTest {
@Override
public void update(AuthFullAccountEntity entity) {
store.put(entity.getRoleCode() + "#" + entity.getUid(), entity);
save(entity);
}
}
private static class InMemoryRoleUkeyBindingRepository implements RoleUkeyBindingRepository {
private final Map<String, List<RoleUkeyBindingEntity>> store = new ConcurrentHashMap<>();
private final List<RoleUkeyBindingEntity> store = new ArrayList<>();
@Override
public List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
return store.getOrDefault(roleCode, List.of()).stream()
.filter(entity -> uid.equals(entity.getUid()))
public List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode) {
return store.stream()
.filter(entity -> roleCode.equals(entity.getRoleCode()))
.filter(entity -> "ACTIVE".equals(entity.getStatus()))
.sorted(Comparator.comparing(RoleUkeyBindingEntity::getBoundAt).reversed())
.toList();
}
@Override
public List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode) {
return store.getOrDefault(roleCode, List.of()).stream()
.filter(entity -> "ACTIVE".equals(entity.getStatus()))
.sorted(Comparator.comparing(RoleUkeyBindingEntity::getUid))
public List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
return findActiveByRoleCode(roleCode).stream()
.filter(entity -> uid.equals(entity.getUid()))
.toList();
}
@Override
public void save(RoleUkeyBindingEntity entity) {
store.computeIfAbsent(entity.getRoleCode(), ignored -> new ArrayList<>()).add(entity);
store.add(entity);
}
@Override
public void update(RoleUkeyBindingEntity entity) {
List<RoleUkeyBindingEntity> list = store.computeIfAbsent(entity.getRoleCode(), ignored -> new ArrayList<>());
list.removeIf(item -> item.getId().equals(entity.getId()));
list.add(entity);
store.removeIf(item -> item.getId().equals(entity.getId()));
store.add(entity);
}
}
private static AuthAdminService newAuthAdminService(
InMemoryRoleAccountRepository roleAccounts,
InMemoryAuthFullAccountRepository fullAccounts,
InMemoryAuthUserAccountRepository userAccounts,
InMemoryRoleUkeyBindingRepository bindings,
Clock clock,
PasswordSaltGenerator saltGenerator
) {
return new AuthAdminServiceImpl(
roleAccounts,
fullAccounts,
userAccounts,
bindings,
new FakePasswordHasher(),
Mockito.mock(LmkService.class),
new com.fasterxml.jackson.databind.ObjectMapper(),
clock,
saltGenerator
);
}
private static InMemoryAuthFullAccountRepository fullAccountRepository(RoleUkeyBindingEntity... bindings) {
InMemoryAuthFullAccountRepository repository = new InMemoryAuthFullAccountRepository();
for (RoleUkeyBindingEntity binding : bindings) {
repository.save(fullAccount(
RoleCode.valueOf(binding.getRoleCode()),
binding.getUid(),
binding.getRoleCode().toLowerCase() + "-full-" + binding.getUid(),
"HASH:12345678:SALT",
"SALT"
));
}
return repository;
}
}
File diff suppressed because it is too large Load Diff
@@ -49,6 +49,55 @@ class PcieCompatUkeyVerifierTest {
verifier().verifyLoginSignature(rawPointHex, payload, rawSignatureHex);
}
@Test
void shouldVerifyLoginSignatureWithGm0018EccRefPublicKeyBase64() throws Exception {
KeyPair keyPair = generateSm2KeyPair();
String payload = "LOGIN-DATA-GM0018";
String eccRefPublicKeyBase64 = Base64.getEncoder().encodeToString(gm0018EccRefPublicKey((ECPublicKey) keyPair.getPublic()));
String signatureBase64 = Base64.getEncoder().encodeToString(gm0018Signature(signDer(payload, keyPair)));
verifier().verifyLoginSignature(eccRefPublicKeyBase64, payload, signatureBase64);
}
@Test
void shouldParseProvidedGm0018EccRefPublicKeyBase64() {
String pubKey = "AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA09yf/T4UEoEnHOFslOQWh+oArfbK+RWK0jfZ5yOfPDQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADOo3fWGXgwtVeodmyfNHPZwz5SnqjMSCzVKtnc322mm";
String rawZeroSignatureHex = "00".repeat(64);
BizException exception = Assertions.assertThrows(
BizException.class,
() -> verifier().verifyLoginSignature(pubKey, "LOGIN-DATA", rawZeroSignatureHex)
);
Assertions.assertEquals("login signature verification failed", exception.getMessage());
}
@Test
void shouldRejectProvidedGm0018LoginFactorsWhenSignatureDoesNotMatchPayload() {
String pubKey = "AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA09yf/T4UEoEnHOFslOQWh+oArfbK+RWK0jfZ5yOfPDQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADOo3fWGXgwtVeodmyfNHPZwz5SnqjMSCzVKtnc322mm";
String payload = "{\"ra\":\"5893784655680600\",\"rb\":\"2005fe7a2df071c378f3198de5eb2138\",\"role\":\"SUPER_ADMIN\",\"uid\":\"2\",\"rid\":\"SUPER_ADMIN\",\"block\":\"58937846556806002005fe7a2df071c378f3198de5eb2138SUPER_ADMIN\"}";
String signature = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC19q5+THL9c3za11LSV3onnloEogld9DOkkAJodr9aUQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnvomyeYPgfTcGnqXYZwHez6rkwNyhtXtlFpVLFg9SUk=";
BizException exception = Assertions.assertThrows(
BizException.class,
() -> verifier().verifyLoginSignature(pubKey, payload, signature)
);
Assertions.assertEquals("login signature verification failed", exception.getMessage());
}
@Test
void shouldReportMissingLoginSignatureAfterProvidedGm0018PublicKeyIsParsed() {
String pubKey = "AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA09yf/T4UEoEnHOFslOQWh+oArfbK+RWK0jfZ5yOfPDQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADOo3fWGXgwtVeodmyfNHPZwz5SnqjMSCzVKtnc322mm";
BizException exception = Assertions.assertThrows(
BizException.class,
() -> verifier().verifyLoginSignature(pubKey, "LOGIN-DATA", null)
);
Assertions.assertEquals("login signature is required", exception.getMessage());
}
@Test
void shouldRejectWhenLoginSignatureDoesNotMatch() throws Exception {
KeyPair keyPair = generateSm2KeyPair();
@@ -91,6 +140,27 @@ class PcieCompatUkeyVerifierTest {
return result;
}
private byte[] gm0018EccRefPublicKey(ECPublicKey publicKey) {
byte[] x = toFixed32(publicKey.getW().getAffineX().toByteArray());
byte[] y = toFixed32(publicKey.getW().getAffineY().toByteArray());
byte[] result = new byte[132];
result[0] = 0x00;
result[1] = 0x01;
result[2] = 0x00;
result[3] = 0x00;
System.arraycopy(x, 0, result, 4 + 32, 32);
System.arraycopy(y, 0, result, 4 + 64 + 32, 32);
return result;
}
private byte[] gm0018Signature(byte[] derSignature) throws Exception {
byte[] rawSignature = derToRawRs(derSignature);
byte[] result = new byte[128];
System.arraycopy(rawSignature, 0, result, 32, 32);
System.arraycopy(rawSignature, 32, result, 96, 32);
return result;
}
private byte[] derToRawRs(byte[] derSignature) throws Exception {
org.bouncycastle.asn1.ASN1Sequence sequence =
(org.bouncycastle.asn1.ASN1Sequence) org.bouncycastle.asn1.ASN1Primitive.fromByteArray(derSignature);
@@ -42,7 +42,7 @@ class OperationAuditAspectLoginTest {
PasswordLoginRequest request = new PasswordLoginRequest();
request.setRoleCode("AUDIT_ADMIN");
PasswordLoginAccountRequest account = new PasswordLoginAccountRequest();
account.setUsername("audit-admin-01");
account.setUid(1);
request.setAccounts(List.of(account));
LoginResponse response = new LoginResponse();
response.setRoleCode("AUDIT_ADMIN");
@@ -59,7 +59,7 @@ class OperationAuditAspectLoginTest {
Assertions.assertEquals(OperatorRoleCode.AUDIT_ADMIN, command.getOperatorRoleCode());
Assertions.assertEquals(AuthLevel.LIMITED, command.getOperatorAuthLevel());
Assertions.assertEquals(OperationResult.SUCCESS, command.getOperationResult());
Assertions.assertEquals("口令登录 [role=AUDIT_ADMIN, principal=audit-admin-01]", command.getSummary());
Assertions.assertEquals("口令登录 [role=AUDIT_ADMIN, principal=uid=1]", command.getSummary());
Assertions.assertEquals("10.0.0.8", command.getRemoteIp());
}
@@ -35,10 +35,8 @@ class ReplayProtectedEndpointsTest {
@Test
void shouldProtectSensitiveControllersAndMethods() throws Exception {
Assertions.assertNotNull(annotation(AuthController.class, "changeFullAccountPassword",
Assertions.assertNotNull(annotation(AuthController.class, "changeAccountPassword",
Integer.class, com.cisd.tms.modules.auth.dto.ChangePasswordRequest.class, jakarta.servlet.http.HttpServletRequest.class));
Assertions.assertNotNull(annotation(AuthController.class, "changeLimitedAccountPassword",
String.class, com.cisd.tms.modules.auth.dto.ChangePasswordRequest.class, jakarta.servlet.http.HttpServletRequest.class));
Assertions.assertNotNull(annotation(DeviceController.class, "restart", jakarta.servlet.http.HttpServletRequest.class));
Assertions.assertNull(AnnotatedElementUtils.findMergedAnnotation(AuthAdminController.class, ReplayProtected.class));