角色和登录

This commit is contained in:
waner 2026-03-31 14:09:25 +08:00
parent b0dd28e60d
commit bd9ee429db
20 changed files with 946 additions and 746 deletions

View File

@ -0,0 +1,14 @@
package com.cisd.tms.modules.auth.config;
import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AuthSupportConfiguration {
@Bean
public Clock authClock() {
return Clock.systemUTC();
}
}

View File

@ -1,143 +1,15 @@
package com.cisd.tms.modules.auth.service;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.integration.crypto.pcie.service.MockPcieCryptoService;
import com.cisd.tms.modules.auth.entity.RoleAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
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.RoleAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.common.util.TraceIdUtil;
import com.cisd.tms.modules.mk.config.MasterKeyInitProperties;
import com.cisd.tms.modules.mk.dto.UKeySignDTO;
import com.cisd.tms.modules.mk.dto.UKeySignEntity;
import com.cisd.tms.modules.mk.dto.UKeySignResult;
import com.cisd.tms.modules.mk.service.LmkService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Supplier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AuthAdminService {
public interface AuthAdminService {
static final String DEFAULT_PASSWORD = "12345678";
void enableRole(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode);
private final RoleAccountRepository roleAccountRepository;
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
private final PasswordHasher passwordHasher;
private final LmkService lmkService;
private final ObjectMapper objectMapper;
private final Clock clock;
private final Supplier<String> saltSupplier;
void resetPassword(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode);
@Autowired
public AuthAdminService(
RoleAccountRepository roleAccountRepository,
RoleUkeyBindingRepository roleUkeyBindingRepository,
PasswordHasher passwordHasher,
LmkService lmkService,
ObjectMapper objectMapper
) {
this(
roleAccountRepository,
roleUkeyBindingRepository,
passwordHasher,
lmkService,
objectMapper,
Clock.systemUTC(),
() -> UUID.randomUUID().toString().replace("-", "")
);
}
AuthAdminService(
RoleAccountRepository roleAccountRepository,
RoleUkeyBindingRepository roleUkeyBindingRepository,
LmkService lmkService,
PasswordHasher passwordHasher,
Clock clock,
Supplier<String> saltSupplier
) {
this(
roleAccountRepository,
roleUkeyBindingRepository,
passwordHasher,
lmkService,
new ObjectMapper(),
clock,
saltSupplier
);
}
AuthAdminService(
RoleAccountRepository roleAccountRepository,
RoleUkeyBindingRepository roleUkeyBindingRepository,
PasswordHasher passwordHasher,
Clock clock,
Supplier<String> saltSupplier
) {
this(
roleAccountRepository,
roleUkeyBindingRepository,
new LmkService(new MockPcieCryptoService(), new MasterKeyInitProperties()),
passwordHasher,
clock,
saltSupplier
);
}
AuthAdminService(
RoleAccountRepository roleAccountRepository,
RoleUkeyBindingRepository roleUkeyBindingRepository,
PasswordHasher passwordHasher,
LmkService lmkService,
ObjectMapper objectMapper,
Clock clock,
Supplier<String> saltSupplier
) {
this.roleAccountRepository = roleAccountRepository;
this.roleUkeyBindingRepository = roleUkeyBindingRepository;
this.passwordHasher = passwordHasher;
this.lmkService = lmkService;
this.objectMapper = objectMapper;
this.clock = clock;
this.saltSupplier = saltSupplier;
}
public void enableRole(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
RoleAccountEntity target = loadRole(targetRoleCode);
target.setStatus(RoleAccountStatus.ACTIVE.name());
target.setNeedChangePassword(Boolean.TRUE);
target.setLockedUntil(null);
target.setFailedCount(0);
roleAccountRepository.update(target);
}
public void resetPassword(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
RoleAccountEntity target = loadRole(targetRoleCode);
String newSalt = saltSupplier.get();
target.setPasswordSalt(newSalt);
target.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt));
target.setStatus(RoleAccountStatus.ACTIVE.name());
target.setNeedChangePassword(Boolean.TRUE);
target.setFailedCount(0);
target.setLockedUntil(null);
roleAccountRepository.update(target);
}
public void bindUkey(
void bindUkey(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
@ -147,31 +19,9 @@ public class AuthAdminService {
String uid,
String rid,
String issuerSign
) {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
RoleCode targetRole = resolveRoleCode(targetRoleCode);
if (slotNo == null || slotNo < 1 || slotNo > targetRole.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "slotNo exceeds role ukey requirement");
}
);
RoleUkeyBindingEntity binding = roleUkeyBindingRepository
.findActiveByRoleCodeAndSlotNo(targetRoleCode, slotNo)
.orElseGet(RoleUkeyBindingEntity::new);
if (binding.getId() == null) {
binding.setId((long) Math.abs(Objects.hash(targetRoleCode, slotNo, ukeySerial, TraceIdUtil.newTraceId())));
binding.setRoleCode(targetRoleCode);
binding.setSlotNo(slotNo);
binding.setStatus("ACTIVE");
applyBinding(binding, ukeySerial, ukeyPubkey, uid, rid, issuerSign);
roleUkeyBindingRepository.save(binding);
return;
}
applyBinding(binding, ukeySerial, ukeyPubkey, uid, rid, issuerSign);
roleUkeyBindingRepository.update(binding);
}
public void bindIssuedUkey(
void bindIssuedUkey(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
@ -181,105 +31,12 @@ public class AuthAdminService {
String uid,
String rid,
String issuerSign
) {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
resolveRoleCode(targetRoleCode);
verifyIssuerSignature(targetRoleCode, ukeyPubkey, uid, rid, issuerSign);
bindUkey(operatorRoleCode, operatorAuthLevel, targetRoleCode, slotNo, ukeySerial, ukeyPubkey, uid, rid, issuerSign);
}
);
public UKeySignResult issueUkeyBindingSign(
UKeySignResult issueUkeyBindingSign(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
UKeySignDTO request
) {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
RoleCode targetRole = resolveRoleCode(targetRoleCode);
String authKeyPair = lmkService.exportIkPublicKeyHex();
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey(request.getPubKey());
// 新卡统一使用规范角色编码不再兼容旧卡角色字符串
dto.setRole(targetRole.getCode());
dto.setUid(request.getUid());
dto.setRid(request.getRid());
dto.setExtra(request.getExtra());
String signValue = lmkService.signIk(toIssuePayload(dto, authKeyPair));
com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket backupPacket = null;
// SUPER_ADMIN 发卡同时承担主密钥分量备份职责需要下发对应分量包
if (RoleCode.SUPER_ADMIN == targetRole) {
backupPacket = lmkService.buildBackupPacket(Integer.parseInt(request.getUid()));
}
return UKeySignResult.builder()
.sign(signValue)
.backupPacket(backupPacket)
.build();
}
private void applyBinding(
RoleUkeyBindingEntity binding,
String ukeySerial,
String ukeyPubkey,
String uid,
String rid,
String issuerSign
) {
binding.setUkeySerial(ukeySerial);
binding.setUkeyPubkey(ukeyPubkey);
binding.setUid(uid);
binding.setRid(rid);
binding.setIssuerSign(issuerSign);
binding.setStatus("ACTIVE");
binding.setBoundAt(now());
binding.setUnboundAt(null);
}
private void requireKeyAdminFull(String operatorRoleCode, String operatorAuthLevel) {
if (!RoleCode.KEY_ADMIN.getCode().equals(operatorRoleCode) || !AuthLevel.FULL.name().equals(operatorAuthLevel)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "operation requires KEY_ADMIN FULL session");
}
}
private RoleAccountEntity loadRole(String roleCode) {
resolveRoleCode(roleCode);
return roleAccountRepository.findByRoleCode(roleCode)
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role account not found"));
}
private RoleCode resolveRoleCode(String roleCode) {
return List.of(RoleCode.values()).stream()
.filter(item -> item.getCode().equals(roleCode))
.findFirst()
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "invalid roleCode"));
}
private LocalDateTime now() {
return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC);
}
private void verifyIssuerSignature(String targetRoleCode, String ukeyPubkey, String uid, String rid, String issuerSign) {
try {
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey(ukeyPubkey);
dto.setRole(resolveRoleCode(targetRoleCode).getCode());
dto.setUid(uid);
dto.setRid(rid);
String authKeyPair = lmkService.exportIkPublicKeyHex();
lmkService.verifyIk(toIssuePayload(dto, authKeyPair), issuerSign);
} catch (BizException ex) {
throw ex;
} catch (RuntimeException ex) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey issuer signature verification failed");
}
}
private String toIssuePayload(UKeySignDTO dto, String authKeyPair) {
try {
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair));
} catch (JsonProcessingException ex) {
throw new IllegalStateException("serialize ukey issue payload failed", ex);
}
}
);
}

View File

@ -3,22 +3,10 @@ package com.cisd.tms.modules.auth.service;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.AuthMethod;
import com.cisd.tms.modules.auth.enums.RoleCode;
import org.springframework.stereotype.Service;
@Service
public class AuthPolicyService {
public interface AuthPolicyService {
/**
* 统一根据认证方式推导本次登录会话的认证等级
*/
public AuthLevel resolveAuthLevel(AuthMethod authMethod) {
return authMethod == AuthMethod.UKEY ? AuthLevel.FULL : AuthLevel.LIMITED;
}
AuthLevel resolveAuthLevel(AuthMethod authMethod);
/**
* 统一读取角色要求的 UKey 数量避免控制器和服务层散落硬编码
*/
public int requiredUkeyCount(RoleCode roleCode) {
return roleCode.getRequiredUkeyCount();
}
int requiredUkeyCount(RoleCode roleCode);
}

View File

@ -1,445 +1,31 @@
package com.cisd.tms.modules.auth.service;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.auth.dto.CaptchaResponse;
import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
import com.cisd.tms.modules.auth.dto.LoginRequest;
import com.cisd.tms.modules.auth.dto.LoginResponse;
import com.cisd.tms.modules.auth.dto.PasswordLoginRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginProof;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
import com.cisd.tms.modules.auth.entity.AuthSessionEntity;
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;
import com.cisd.tms.modules.auth.enums.AuthMethod;
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.repository.AuthUserRepository;
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.mk.dto.UKeySignDTO;
import com.cisd.tms.modules.mk.dto.UKeySignEntity;
import com.cisd.tms.modules.mk.enums.MasterKeyStatus;
import com.cisd.tms.modules.mk.service.LmkService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Base64;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AuthService {
public interface AuthService {
private static final int MAX_FAILED_ATTEMPTS = 5;
private static final int IDLE_TIMEOUT_MINUTES = 10;
LoginResponse login(LoginRequest request);
private final RoleAccountRepository roleAccountRepository;
private final AuthUserRepository authUserRepository;
private final AuthSessionRepository authSessionRepository;
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
private final PasswordHasher passwordHasher;
private final LmkService lmkService;
private final UkeyLoginRandomService ukeyLoginRandomService;
private final CompatUkeyVerifier compatUkeyVerifier;
private final CaptchaService captchaService;
private final ObjectMapper objectMapper;
private final Clock clock;
private final Supplier<String> tokenSupplier;
LoginResponse passwordLogin(PasswordLoginRequest request);
@Autowired
public AuthService(
RoleAccountRepository roleAccountRepository,
AuthUserRepository authUserRepository,
AuthSessionRepository authSessionRepository,
RoleUkeyBindingRepository roleUkeyBindingRepository,
PasswordHasher passwordHasher
) {
this(
roleAccountRepository,
authUserRepository,
authSessionRepository,
roleUkeyBindingRepository,
passwordHasher,
null,
null,
null,
null,
new ObjectMapper(),
Clock.systemUTC(),
() -> "tms-" + UUID.randomUUID()
);
}
UkeyLoginRandomResponse issueUkeyLoginRandoms(UkeyLoginRandomRequest request);
AuthService(
RoleAccountRepository roleAccountRepository,
AuthUserRepository authUserRepository,
AuthSessionRepository authSessionRepository,
RoleUkeyBindingRepository roleUkeyBindingRepository,
PasswordHasher passwordHasher,
Clock clock,
Supplier<String> tokenSupplier
) {
this(
roleAccountRepository,
authUserRepository,
authSessionRepository,
roleUkeyBindingRepository,
passwordHasher,
null,
null,
null,
null,
new ObjectMapper(),
clock,
tokenSupplier
);
}
LoginResponse ukeyLogin(UkeyLoginRequest request);
AuthService(
RoleAccountRepository roleAccountRepository,
AuthUserRepository authUserRepository,
AuthSessionRepository authSessionRepository,
RoleUkeyBindingRepository roleUkeyBindingRepository,
PasswordHasher passwordHasher,
LmkService lmkService,
UkeyLoginRandomService ukeyLoginRandomService,
CompatUkeyVerifier compatUkeyVerifier,
CaptchaService captchaService,
ObjectMapper objectMapper,
Clock clock,
Supplier<String> tokenSupplier
) {
this.roleAccountRepository = roleAccountRepository;
this.authUserRepository = authUserRepository;
this.authSessionRepository = authSessionRepository;
this.roleUkeyBindingRepository = roleUkeyBindingRepository;
this.passwordHasher = passwordHasher;
this.lmkService = lmkService;
this.ukeyLoginRandomService = ukeyLoginRandomService;
this.compatUkeyVerifier = compatUkeyVerifier;
this.captchaService = captchaService;
this.objectMapper = objectMapper;
this.clock = clock;
this.tokenSupplier = tokenSupplier;
}
CaptchaResponse issueCaptcha();
public LoginResponse login(LoginRequest request) {
RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(request.getRoleCode())
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account not found"));
CurrentUserResponse me(String username);
validateRoleStatus(roleAccount);
if (!passwordHasher.matches(request.getPassword(), roleAccount.getPasswordSalt(), roleAccount.getPasswordHash())) {
onPasswordFailed(roleAccount);
}
CurrentUserResponse me(String username, String sessionToken);
resetFailureState(roleAccount);
AuthMethod authMethod = resolveAuthMethod(request.getUkeySerials());
AuthLevel authLevel = resolveAuthLevel(roleAccount, request.getUkeySerials());
AuthSessionEntity session = buildSession(roleAccount.getRoleCode(), authMethod, authLevel);
authSessionRepository.save(session);
void logout(String sessionToken);
LoginResponse response = new LoginResponse();
response.setRoleCode(roleAccount.getRoleCode());
response.setAuthLevel(authLevel.name());
response.setToken(session.getSessionToken());
response.setExpiresAt(OffsetDateTime.of(session.getExpiresAt(), ZoneOffset.UTC).toString());
response.setNeedChangePassword(Boolean.TRUE.equals(roleAccount.getNeedChangePassword()));
return response;
}
/**
* 标准化口令登录入口当前先复用统一登录实现并签发 LIMITED 会话
*/
public LoginResponse passwordLogin(PasswordLoginRequest request) {
ensureMasterKeyReady();
requireCaptchaService().verify(request.getCaptchaId(), request.getCaptchaCode());
LoginRequest loginRequest = new LoginRequest();
loginRequest.setRoleCode(request.getRoleCode());
loginRequest.setPassword(request.getPassword());
return login(loginRequest);
}
/**
* 标准化 UKey 随机数申请入口当前先按角色要求生成占位随机数后续再接旧系统完整校验链路
*/
public UkeyLoginRandomResponse issueUkeyLoginRandoms(UkeyLoginRandomRequest request) {
RoleCode roleCode = RoleCode.valueOf(request.getRoleCode());
UkeyLoginRandomResponse response = new UkeyLoginRandomResponse();
response.setRoleCode(roleCode.getCode());
response.setRandoms(requireUkeyLoginRandomService().issue(roleCode.getCode(), roleCode.getRequiredUkeyCount()));
return response;
}
/**
* 标准化 UKey 登录入口当前先按证明数量映射到完整登录
*/
public LoginResponse ukeyLogin(UkeyLoginRequest request) {
ensureMasterKeyReady();
RoleCode roleCode = RoleCode.valueOf(request.getRoleCode());
List<RoleUkeyBindingEntity> activeBindings = roleUkeyBindingRepository.findActiveByRoleCode(roleCode.getCode());
validateUkeyCount(roleCode, activeBindings, request.getUkeyProofs());
Map<String, RoleUkeyBindingEntity> bindingsByPubKey = activeBindings.stream()
.collect(Collectors.toMap(RoleUkeyBindingEntity::getUkeyPubkey, item -> item, (left, right) -> left, java.util.LinkedHashMap::new));
Set<String> requestPubKeys = request.getUkeyProofs().stream()
.map(UkeyLoginProof::getPubKey)
.collect(Collectors.toSet());
if (requestPubKeys.size() != request.getUkeyProofs().size() || !bindingsByPubKey.keySet().containsAll(requestPubKeys)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role");
}
requireUkeyLoginRandomService().assertIssued(
roleCode.getCode(),
request.getUkeyProofs().stream().map(UkeyLoginProof::getServerRandom).toList()
);
String authKeyPair = requireLmkService().exportIkPublicKeyHex();
List<String> matchedSerials = new java.util.ArrayList<>();
for (UkeyLoginProof proof : request.getUkeyProofs()) {
RoleUkeyBindingEntity binding = bindingsByPubKey.get(proof.getPubKey());
requireCompatUkeyVerifier().verifyIssuedBinding(buildIssuePayload(request.getRoleCode(), proof, authKeyPair), proof.getIssueSignature());
requireCompatUkeyVerifier().verifyLoginSignature(proof.getPubKey(), proof.getLoginPayload(), proof.getLoginSignature());
matchedSerials.add(binding.getUkeySerial());
}
LoginRequest loginRequest = new LoginRequest();
loginRequest.setRoleCode(request.getRoleCode());
loginRequest.setPassword(request.getPassword());
loginRequest.setUkeySerials(matchedSerials);
return login(loginRequest);
}
/**
* 标准化验证码生成入口当前先返回可用于联调的占位图像内容
*/
public CaptchaResponse issueCaptcha() {
return requireCaptchaService().issueCaptcha();
}
public CurrentUserResponse me(String username) {
return me(username, null);
}
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) {
CurrentUserResponse response = new CurrentUserResponse();
response.setUsername(roleAccount.getRoleCode());
response.setDisplayName(roleAccount.getDisplayName());
response.setRole(roleAccount.getRoleCode());
AuthSessionEntity session = sessionToken == null ? null : authSessionRepository.findBySessionToken(sessionToken).orElse(null);
if (session != null) {
response.setAuthLevel(session.getAuthLevel());
}
response.setNeedChangePassword(Boolean.TRUE.equals(roleAccount.getNeedChangePassword()));
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");
return response;
}
response.setUsername(entity.getUsername());
response.setDisplayName(entity.getDisplayName());
response.setRole(entity.getRole());
return response;
}
public void logout(String sessionToken) {
AuthSessionEntity session = requireActiveSession(sessionToken);
session.setLogoutAt(now());
session.setExpiresAt(now());
authSessionRepository.update(session);
}
public void changePassword(String sessionToken, String currentPassword, String newPassword) {
AuthSessionEntity session = requireActiveSession(sessionToken);
RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(session.getRoleCode())
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account not found"));
if (!passwordHasher.matches(currentPassword, roleAccount.getPasswordSalt(), roleAccount.getPasswordHash())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "current password is incorrect");
}
String newSalt = UUID.randomUUID().toString().replace("-", "");
roleAccount.setPasswordSalt(newSalt);
roleAccount.setPasswordHash(passwordHasher.hash(newPassword, newSalt));
roleAccount.setNeedChangePassword(Boolean.FALSE);
roleAccount.setFailedCount(0);
roleAccount.setLockedUntil(null);
roleAccount.setStatus(RoleAccountStatus.ACTIVE.name());
roleAccountRepository.update(roleAccount);
}
private void validateRoleStatus(RoleAccountEntity roleAccount) {
if (RoleAccountStatus.UNENABLED.name().equals(roleAccount.getStatus())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role is not enabled");
}
if (RoleAccountStatus.LOCKED.name().equals(roleAccount.getStatus()) && isStillLocked(roleAccount)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account is locked");
}
}
private boolean isStillLocked(RoleAccountEntity roleAccount) {
return roleAccount.getLockedUntil() == null || roleAccount.getLockedUntil().isAfter(now());
}
private void onPasswordFailed(RoleAccountEntity roleAccount) {
int failedCount = roleAccount.getFailedCount() == null ? 0 : roleAccount.getFailedCount();
failedCount++;
roleAccount.setFailedCount(failedCount);
if (failedCount >= MAX_FAILED_ATTEMPTS) {
roleAccount.setStatus(RoleAccountStatus.LOCKED.name());
roleAccount.setLockedUntil(now().plusMinutes(IDLE_TIMEOUT_MINUTES));
roleAccountRepository.update(roleAccount);
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account is locked");
}
roleAccountRepository.update(roleAccount);
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "password is incorrect");
}
private void resetFailureState(RoleAccountEntity roleAccount) {
roleAccount.setFailedCount(0);
roleAccount.setLockedUntil(null);
roleAccount.setStatus(RoleAccountStatus.ACTIVE.name());
roleAccount.setLastLoginAt(now());
roleAccount.setLastActiveAt(now());
roleAccountRepository.update(roleAccount);
}
private AuthMethod resolveAuthMethod(List<String> ukeySerials) {
return (ukeySerials == null || ukeySerials.isEmpty()) ? AuthMethod.PASSWORD : AuthMethod.UKEY;
}
private void ensureMasterKeyReady() {
LmkService service = requireLmkService();
MasterKeyStatus.StatusDetail status = service.getMasterKeyStatus();
if (status == null || status.getCode() == MasterKeyStatus.ABNORMAL.getCode()) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "master key is not ready");
}
}
private AuthLevel resolveAuthLevel(RoleAccountEntity roleAccount, List<String> ukeySerials) {
if (ukeySerials == null || ukeySerials.isEmpty()) {
return AuthLevel.LIMITED;
}
Set<String> uniqueSerials = new LinkedHashSet<>(ukeySerials);
if (uniqueSerials.size() != roleAccount.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
List<RoleUkeyBindingEntity> bindings = roleUkeyBindingRepository.findActiveByRoleCode(roleAccount.getRoleCode());
if (bindings.size() != roleAccount.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
Set<String> boundSerials = bindings.stream()
.map(RoleUkeyBindingEntity::getUkeySerial)
.collect(java.util.stream.Collectors.toSet());
if (!boundSerials.containsAll(uniqueSerials)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey verification failed");
}
return AuthLevel.FULL;
}
private void validateUkeyCount(
RoleCode roleCode,
List<RoleUkeyBindingEntity> activeBindings,
List<UkeyLoginProof> proofs
) {
if (proofs == null || proofs.size() != roleCode.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
if (activeBindings.size() != roleCode.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
}
private String buildIssuePayload(String roleCode, UkeyLoginProof proof, String authKeyPair) {
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey(proof.getPubKey());
// 新卡统一使用规范角色编码参与签名和验签
dto.setRole(RoleCode.valueOf(roleCode).getCode());
dto.setUid(proof.getUid());
dto.setRid(proof.getRid());
try {
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair));
} catch (JsonProcessingException ex) {
throw new IllegalStateException("serialize ukey issue payload failed", ex);
}
}
private AuthSessionEntity buildSession(String roleCode, AuthMethod authMethod, AuthLevel authLevel) {
LocalDateTime issuedAt = now();
AuthSessionEntity entity = new AuthSessionEntity();
entity.setRoleCode(roleCode);
entity.setAuthMethod(authMethod.name());
entity.setAuthLevel(authLevel.name());
entity.setSessionToken(tokenSupplier.get());
entity.setIssuedAt(issuedAt);
entity.setLastActiveAt(issuedAt);
entity.setExpiresAt(issuedAt.plusMinutes(IDLE_TIMEOUT_MINUTES));
return entity;
}
private LocalDateTime now() {
return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC);
}
private AuthSessionEntity requireActiveSession(String sessionToken) {
AuthSessionEntity session = authSessionRepository.findBySessionToken(sessionToken)
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "session not found"));
if (session.getLogoutAt() != null || session.getExpiresAt() == null || !session.getExpiresAt().isAfter(now())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "session expired");
}
return session;
}
private LmkService requireLmkService() {
if (lmkService == null) {
throw new IllegalStateException("lmkService is required for this operation");
}
return lmkService;
}
private UkeyLoginRandomService requireUkeyLoginRandomService() {
if (ukeyLoginRandomService == null) {
throw new IllegalStateException("ukeyLoginRandomService is required for this operation");
}
return ukeyLoginRandomService;
}
private CompatUkeyVerifier requireCompatUkeyVerifier() {
if (compatUkeyVerifier == null) {
throw new IllegalStateException("compatUkeyVerifier is required for this operation");
}
return compatUkeyVerifier;
}
private CaptchaService requireCaptchaService() {
if (captchaService == null) {
throw new IllegalStateException("captchaService is required for this operation");
}
return captchaService;
}
void changePassword(String sessionToken, String currentPassword, String newPassword);
}

View File

@ -0,0 +1,7 @@
package com.cisd.tms.modules.auth.service;
@FunctionalInterface
public interface PasswordSaltGenerator {
String nextSalt();
}

View File

@ -0,0 +1,7 @@
package com.cisd.tms.modules.auth.service;
@FunctionalInterface
public interface SessionTokenGenerator {
String nextToken();
}

View File

@ -0,0 +1,215 @@
package com.cisd.tms.modules.auth.service.impl;
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.RoleAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
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.RoleAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.modules.auth.service.AuthAdminService;
import com.cisd.tms.modules.auth.service.PasswordHasher;
import com.cisd.tms.modules.auth.service.PasswordSaltGenerator;
import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket;
import com.cisd.tms.modules.mk.dto.UKeySignDTO;
import com.cisd.tms.modules.mk.dto.UKeySignEntity;
import com.cisd.tms.modules.mk.dto.UKeySignResult;
import com.cisd.tms.modules.mk.service.LmkService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Objects;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@AllArgsConstructor
public class AuthAdminServiceImpl implements AuthAdminService {
static final String DEFAULT_PASSWORD = "12345678";
private final RoleAccountRepository roleAccountRepository;
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
private final PasswordHasher passwordHasher;
private final LmkService lmkService;
private final ObjectMapper objectMapper;
private final Clock clock;
private final PasswordSaltGenerator passwordSaltGenerator;
@Override
public void enableRole(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
RoleAccountEntity target = loadRole(targetRoleCode);
target.setStatus(RoleAccountStatus.ACTIVE.name());
target.setNeedChangePassword(Boolean.TRUE);
target.setLockedUntil(null);
target.setFailedCount(0);
roleAccountRepository.update(target);
}
@Override
public void resetPassword(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
RoleAccountEntity target = loadRole(targetRoleCode);
String newSalt = passwordSaltGenerator.nextSalt();
target.setPasswordSalt(newSalt);
target.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt));
target.setStatus(RoleAccountStatus.ACTIVE.name());
target.setNeedChangePassword(Boolean.TRUE);
target.setFailedCount(0);
target.setLockedUntil(null);
roleAccountRepository.update(target);
}
@Override
public void bindUkey(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
Integer slotNo,
String ukeySerial,
String ukeyPubkey,
String uid,
String rid,
String issuerSign
) {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
RoleCode targetRole = resolveRoleCode(targetRoleCode);
if (slotNo == null || slotNo < 1 || slotNo > targetRole.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "slotNo exceeds role ukey requirement");
}
RoleUkeyBindingEntity binding = roleUkeyBindingRepository
.findActiveByRoleCodeAndSlotNo(targetRoleCode, slotNo)
.orElseGet(RoleUkeyBindingEntity::new);
if (binding.getId() == null) {
binding.setId((long) Math.abs(Objects.hash(targetRoleCode, slotNo, ukeySerial, TraceIdUtil.newTraceId())));
binding.setRoleCode(targetRoleCode);
binding.setSlotNo(slotNo);
binding.setStatus("ACTIVE");
applyBinding(binding, ukeySerial, ukeyPubkey, uid, rid, issuerSign);
roleUkeyBindingRepository.save(binding);
return;
}
applyBinding(binding, ukeySerial, ukeyPubkey, uid, rid, issuerSign);
roleUkeyBindingRepository.update(binding);
}
@Override
public void bindIssuedUkey(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
Integer slotNo,
String ukeySerial,
String ukeyPubkey,
String uid,
String rid,
String issuerSign
) {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
resolveRoleCode(targetRoleCode);
verifyIssuerSignature(targetRoleCode, ukeyPubkey, uid, rid, issuerSign);
bindUkey(operatorRoleCode, operatorAuthLevel, targetRoleCode, slotNo, ukeySerial, ukeyPubkey, uid, rid, issuerSign);
}
@Override
public UKeySignResult issueUkeyBindingSign(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
UKeySignDTO request
) {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
RoleCode targetRole = resolveRoleCode(targetRoleCode);
String authKeyPair = lmkService.exportIkPublicKeyHex();
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey(request.getPubKey());
dto.setRole(targetRole.getCode());
dto.setUid(request.getUid());
dto.setRid(request.getRid());
String signValue = lmkService.signIk(toIssuePayload(dto, authKeyPair));
MasterKeyBackupPacket backupPacket = null;
if (RoleCode.SUPER_ADMIN == targetRole) {
backupPacket = lmkService.buildBackupPacket(Integer.parseInt(request.getUid()));
}
return UKeySignResult.builder()
.sign(signValue)
.backupPacket(backupPacket)
.build();
}
private void applyBinding(
RoleUkeyBindingEntity binding,
String ukeySerial,
String ukeyPubkey,
String uid,
String rid,
String issuerSign
) {
binding.setUkeySerial(ukeySerial);
binding.setUkeyPubkey(ukeyPubkey);
binding.setUid(uid);
binding.setRid(rid);
binding.setIssuerSign(issuerSign);
binding.setStatus("ACTIVE");
binding.setBoundAt(now());
binding.setUnboundAt(null);
}
private void requireKeyAdminFull(String operatorRoleCode, String operatorAuthLevel) {
if (!RoleCode.KEY_ADMIN.getCode().equals(operatorRoleCode) || !AuthLevel.FULL.name().equals(operatorAuthLevel)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "operation requires KEY_ADMIN FULL session");
}
}
private RoleAccountEntity loadRole(String roleCode) {
resolveRoleCode(roleCode);
return roleAccountRepository.findByRoleCode(roleCode)
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role account not found"));
}
private RoleCode resolveRoleCode(String roleCode) {
return List.of(RoleCode.values()).stream()
.filter(item -> item.getCode().equals(roleCode))
.findFirst()
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "invalid roleCode"));
}
private LocalDateTime now() {
return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC);
}
private void verifyIssuerSignature(String targetRoleCode, String ukeyPubkey, String uid, String rid, String issuerSign) {
try {
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey(ukeyPubkey);
dto.setRole(resolveRoleCode(targetRoleCode).getCode());
dto.setUid(uid);
dto.setRid(rid);
String authKeyPair = lmkService.exportIkPublicKeyHex();
lmkService.verifyIk(toIssuePayload(dto, authKeyPair), issuerSign);
} catch (BizException ex) {
throw ex;
} catch (RuntimeException ex) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey issuer signature verification failed");
}
}
private String toIssuePayload(UKeySignDTO dto, String authKeyPair) {
try {
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair));
} catch (JsonProcessingException ex) {
throw new IllegalStateException("serialize ukey issue payload failed", ex);
}
}
}

View File

@ -0,0 +1,29 @@
package com.cisd.tms.modules.auth.service.impl;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.AuthMethod;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.service.AuthPolicyService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@AllArgsConstructor
public class AuthPolicyServiceImpl implements AuthPolicyService {
/**
* 统一根据认证方式推导本次登录会话的认证等级
*/
@Override
public AuthLevel resolveAuthLevel(AuthMethod authMethod) {
return authMethod == AuthMethod.UKEY ? AuthLevel.FULL : AuthLevel.LIMITED;
}
/**
* 统一读取角色要求的 UKey 数量避免控制器和服务层散落硬编码
*/
@Override
public int requiredUkeyCount(RoleCode roleCode) {
return roleCode.getRequiredUkeyCount();
}
}

View File

@ -0,0 +1,336 @@
package com.cisd.tms.modules.auth.service.impl;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.auth.dto.CaptchaResponse;
import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
import com.cisd.tms.modules.auth.dto.LoginRequest;
import com.cisd.tms.modules.auth.dto.LoginResponse;
import com.cisd.tms.modules.auth.dto.PasswordLoginRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginProof;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
import com.cisd.tms.modules.auth.entity.AuthSessionEntity;
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;
import com.cisd.tms.modules.auth.enums.AuthMethod;
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.repository.AuthSessionRepository;
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.*;
import com.cisd.tms.modules.mk.dto.UKeySignDTO;
import com.cisd.tms.modules.mk.dto.UKeySignEntity;
import com.cisd.tms.modules.mk.enums.MasterKeyStatus;
import com.cisd.tms.modules.mk.service.LmkService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@AllArgsConstructor
public class AuthServiceImpl implements AuthService {
private static final int MAX_FAILED_ATTEMPTS = 5;
private static final int IDLE_TIMEOUT_MINUTES = 10;
private final RoleAccountRepository roleAccountRepository;
private final AuthUserRepository authUserRepository;
private final AuthSessionRepository authSessionRepository;
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
private final PasswordHasher passwordHasher;
private final LmkService lmkService;
private final UkeyLoginRandomService ukeyLoginRandomService;
private final CompatUkeyVerifier compatUkeyVerifier;
private final CaptchaService captchaService;
private final ObjectMapper objectMapper;
private final Clock clock;
private final SessionTokenGenerator sessionTokenGenerator;
private final PasswordSaltGenerator passwordSaltGenerator;
private final AuthPolicyService authPolicyService;
@Override
public LoginResponse login(LoginRequest request) {
RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(request.getRoleCode())
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account not found"));
validateRoleStatus(roleAccount);
if (!passwordHasher.matches(request.getPassword(), roleAccount.getPasswordSalt(), roleAccount.getPasswordHash())) {
onPasswordFailed(roleAccount);
}
resetFailureState(roleAccount);
AuthMethod authMethod = resolveAuthMethod(request.getUkeySerials());
AuthLevel authLevel = resolveAuthLevel(roleAccount, request.getUkeySerials());
AuthSessionEntity session = buildSession(roleAccount.getRoleCode(), authMethod, authLevel);
authSessionRepository.save(session);
LoginResponse response = new LoginResponse();
response.setRoleCode(roleAccount.getRoleCode());
response.setAuthLevel(authLevel.name());
response.setToken(session.getSessionToken());
response.setExpiresAt(OffsetDateTime.of(session.getExpiresAt(), ZoneOffset.UTC).toString());
response.setNeedChangePassword(Boolean.TRUE.equals(roleAccount.getNeedChangePassword()));
return response;
}
@Override
public LoginResponse passwordLogin(PasswordLoginRequest request) {
ensureMasterKeyReady();
captchaService.verify(request.getCaptchaId(), request.getCaptchaCode());
LoginRequest loginRequest = new LoginRequest();
loginRequest.setRoleCode(request.getRoleCode());
loginRequest.setPassword(request.getPassword());
return login(loginRequest);
}
@Override
public UkeyLoginRandomResponse issueUkeyLoginRandoms(UkeyLoginRandomRequest request) {
RoleCode roleCode = RoleCode.valueOf(request.getRoleCode());
UkeyLoginRandomResponse response = new UkeyLoginRandomResponse();
response.setRoleCode(roleCode.getCode());
response.setRandoms(ukeyLoginRandomService.issue(roleCode.getCode(), authPolicyService.requiredUkeyCount(roleCode)));
return response;
}
@Override
public LoginResponse ukeyLogin(UkeyLoginRequest request) {
ensureMasterKeyReady();
RoleCode roleCode = RoleCode.valueOf(request.getRoleCode());
List<RoleUkeyBindingEntity> activeBindings = roleUkeyBindingRepository.findActiveByRoleCode(roleCode.getCode());
validateUkeyCount(roleCode, activeBindings, request.getUkeyProofs());
Map<String, RoleUkeyBindingEntity> bindingsByPubKey = activeBindings.stream()
.collect(Collectors.toMap(RoleUkeyBindingEntity::getUkeyPubkey, item -> item, (left, right) -> left, java.util.LinkedHashMap::new));
Set<String> requestPubKeys = request.getUkeyProofs().stream()
.map(UkeyLoginProof::getPubKey)
.collect(Collectors.toSet());
if (requestPubKeys.size() != request.getUkeyProofs().size() || !bindingsByPubKey.keySet().containsAll(requestPubKeys)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role");
}
ukeyLoginRandomService.assertIssued(
roleCode.getCode(),
request.getUkeyProofs().stream().map(UkeyLoginProof::getServerRandom).toList()
);
String authKeyPair = lmkService.exportIkPublicKeyHex();
List<String> matchedSerials = new java.util.ArrayList<>();
for (UkeyLoginProof proof : request.getUkeyProofs()) {
RoleUkeyBindingEntity binding = bindingsByPubKey.get(proof.getPubKey());
compatUkeyVerifier.verifyIssuedBinding(buildIssuePayload(request.getRoleCode(), proof, authKeyPair), proof.getIssueSignature());
compatUkeyVerifier.verifyLoginSignature(proof.getPubKey(), proof.getLoginPayload(), proof.getLoginSignature());
matchedSerials.add(binding.getUkeySerial());
}
LoginRequest loginRequest = new LoginRequest();
loginRequest.setRoleCode(request.getRoleCode());
loginRequest.setPassword(request.getPassword());
loginRequest.setUkeySerials(matchedSerials);
return login(loginRequest);
}
@Override
public CaptchaResponse issueCaptcha() {
return captchaService.issueCaptcha();
}
@Override
public CurrentUserResponse me(String username) {
return me(username, null);
}
@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) {
CurrentUserResponse response = new CurrentUserResponse();
response.setUsername(roleAccount.getRoleCode());
response.setDisplayName(roleAccount.getDisplayName());
response.setRole(roleAccount.getRoleCode());
AuthSessionEntity session = sessionToken == null ? null : authSessionRepository.findBySessionToken(sessionToken).orElse(null);
if (session != null) {
response.setAuthLevel(session.getAuthLevel());
}
response.setNeedChangePassword(Boolean.TRUE.equals(roleAccount.getNeedChangePassword()));
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");
return response;
}
response.setUsername(entity.getUsername());
response.setDisplayName(entity.getDisplayName());
response.setRole(entity.getRole());
return response;
}
@Override
public void logout(String sessionToken) {
AuthSessionEntity session = requireActiveSession(sessionToken);
session.setLogoutAt(now());
session.setExpiresAt(now());
authSessionRepository.update(session);
}
@Override
public void changePassword(String sessionToken, String currentPassword, String newPassword) {
AuthSessionEntity session = requireActiveSession(sessionToken);
RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(session.getRoleCode())
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account not found"));
if (!passwordHasher.matches(currentPassword, roleAccount.getPasswordSalt(), roleAccount.getPasswordHash())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "current password is incorrect");
}
String newSalt = passwordSaltGenerator.nextSalt();
roleAccount.setPasswordSalt(newSalt);
roleAccount.setPasswordHash(passwordHasher.hash(newPassword, newSalt));
roleAccount.setNeedChangePassword(Boolean.FALSE);
roleAccount.setFailedCount(0);
roleAccount.setLockedUntil(null);
roleAccount.setStatus(RoleAccountStatus.ACTIVE.name());
roleAccountRepository.update(roleAccount);
}
private void validateRoleStatus(RoleAccountEntity roleAccount) {
if (RoleAccountStatus.UNENABLED.name().equals(roleAccount.getStatus())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role is not enabled");
}
if (RoleAccountStatus.LOCKED.name().equals(roleAccount.getStatus()) && isStillLocked(roleAccount)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account is locked");
}
}
private boolean isStillLocked(RoleAccountEntity roleAccount) {
return roleAccount.getLockedUntil() == null || roleAccount.getLockedUntil().isAfter(now());
}
private void onPasswordFailed(RoleAccountEntity roleAccount) {
int failedCount = roleAccount.getFailedCount() == null ? 0 : roleAccount.getFailedCount();
failedCount++;
roleAccount.setFailedCount(failedCount);
if (failedCount >= MAX_FAILED_ATTEMPTS) {
roleAccount.setStatus(RoleAccountStatus.LOCKED.name());
roleAccount.setLockedUntil(now().plusMinutes(IDLE_TIMEOUT_MINUTES));
roleAccountRepository.update(roleAccount);
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account is locked");
}
roleAccountRepository.update(roleAccount);
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "password is incorrect");
}
private void resetFailureState(RoleAccountEntity roleAccount) {
roleAccount.setFailedCount(0);
roleAccount.setLockedUntil(null);
roleAccount.setStatus(RoleAccountStatus.ACTIVE.name());
roleAccount.setLastLoginAt(now());
roleAccount.setLastActiveAt(now());
roleAccountRepository.update(roleAccount);
}
private void ensureMasterKeyReady() {
MasterKeyStatus.StatusDetail status = lmkService.getMasterKeyStatus();
if (status == null || status.getCode() == MasterKeyStatus.ABNORMAL.getCode()) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "master key is not ready");
}
}
private AuthMethod resolveAuthMethod(List<String> ukeySerials) {
return (ukeySerials == null || ukeySerials.isEmpty()) ? AuthMethod.PASSWORD : AuthMethod.UKEY;
}
private AuthLevel resolveAuthLevel(RoleAccountEntity roleAccount, List<String> ukeySerials) {
if (ukeySerials == null || ukeySerials.isEmpty()) {
return authPolicyService.resolveAuthLevel(AuthMethod.PASSWORD);
}
Set<String> uniqueSerials = new LinkedHashSet<>(ukeySerials);
if (uniqueSerials.size() != roleAccount.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
List<RoleUkeyBindingEntity> bindings = roleUkeyBindingRepository.findActiveByRoleCode(roleAccount.getRoleCode());
if (bindings.size() != roleAccount.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
Set<String> boundSerials = bindings.stream()
.map(RoleUkeyBindingEntity::getUkeySerial)
.collect(java.util.stream.Collectors.toSet());
if (!boundSerials.containsAll(uniqueSerials)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey verification failed");
}
return authPolicyService.resolveAuthLevel(AuthMethod.UKEY);
}
private void validateUkeyCount(
RoleCode roleCode,
List<RoleUkeyBindingEntity> activeBindings,
List<UkeyLoginProof> proofs
) {
if (proofs == null || proofs.size() != authPolicyService.requiredUkeyCount(roleCode)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
if (activeBindings.size() != authPolicyService.requiredUkeyCount(roleCode)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
}
private String buildIssuePayload(String roleCode, UkeyLoginProof proof, String authKeyPair) {
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey(proof.getPubKey());
dto.setRole(RoleCode.valueOf(roleCode).getCode());
dto.setUid(proof.getUid());
dto.setRid(proof.getRid());
try {
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair));
} catch (JsonProcessingException ex) {
throw new IllegalStateException("serialize ukey issue payload failed", ex);
}
}
private AuthSessionEntity buildSession(String roleCode, AuthMethod authMethod, AuthLevel authLevel) {
LocalDateTime issuedAt = now();
AuthSessionEntity entity = new AuthSessionEntity();
entity.setRoleCode(roleCode);
entity.setAuthMethod(authMethod.name());
entity.setAuthLevel(authLevel.name());
entity.setSessionToken(sessionTokenGenerator.nextToken());
entity.setIssuedAt(issuedAt);
entity.setLastActiveAt(issuedAt);
entity.setExpiresAt(issuedAt.plusMinutes(IDLE_TIMEOUT_MINUTES));
return entity;
}
private LocalDateTime now() {
return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC);
}
private AuthSessionEntity requireActiveSession(String sessionToken) {
AuthSessionEntity session = authSessionRepository.findBySessionToken(sessionToken)
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "session not found"));
if (session.getLogoutAt() != null || session.getExpiresAt() == null || !session.getExpiresAt().isAfter(now())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "session expired");
}
return session;
}
}

View File

@ -0,0 +1,131 @@
package com.cisd.tms.modules.auth.service.impl;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.auth.dto.CaptchaResponse;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import javax.imageio.ImageIO;
import com.cisd.tms.modules.auth.service.CaptchaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class InMemoryCaptchaService implements CaptchaService {
private static final long EXPIRE_MINUTES = 10;
private static final int CAPTCHA_LENGTH = 4;
private static final int IMAGE_WIDTH = 130;
private static final int IMAGE_HEIGHT = 40;
private static final char[] CAPTCHA_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789".toCharArray();
private final Clock clock;
private final Map<String, CaptchaEntry> store = new ConcurrentHashMap<>();
@Autowired
public InMemoryCaptchaService() {
this(Clock.systemUTC());
}
InMemoryCaptchaService(Clock clock) {
this.clock = clock;
}
@Override
public CaptchaResponse issueCaptcha() {
String captchaCode = randomCaptchaCode();
String captchaId = "captcha-" + UUID.randomUUID();
store.put(captchaId, new CaptchaEntry(captchaCode, clock.instant().plus(EXPIRE_MINUTES, ChronoUnit.MINUTES)));
CaptchaResponse response = new CaptchaResponse();
response.setCaptchaId(captchaId);
// response.setImageBase64(Base64.getEncoder().encodeToString("ABCD".getBytes(StandardCharsets.UTF_8)));
response.setImageBase64(renderCaptchaImageBase64(captchaCode));
return response;
}
@Override
public void verify(String captchaId, String captchaCode) {
CaptchaEntry entry = store.remove(captchaId);
if (entry == null || entry.expiresAt().isBefore(clock.instant())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "captcha verification failed");
}
if (captchaCode == null || !entry.code().equalsIgnoreCase(captchaCode.trim())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "captcha verification failed");
}
}
private record CaptchaEntry(String code, Instant expiresAt) {
}
private String randomCaptchaCode() {
StringBuilder builder = new StringBuilder(CAPTCHA_LENGTH);
for (int i = 0; i < CAPTCHA_LENGTH; i++) {
builder.append(CAPTCHA_CHARS[ThreadLocalRandom.current().nextInt(CAPTCHA_CHARS.length)]);
}
return builder.toString();
}
private String renderCaptchaImageBase64(String captchaCode) {
try {
BufferedImage image = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = image.createGraphics();
try {
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
// 适度加干扰线避免返回纯文本图片
graphics.setStroke(new BasicStroke(1.2f));
for (int i = 0; i < 6; i++) {
graphics.setColor(randomColor(80, 180));
graphics.drawLine(
ThreadLocalRandom.current().nextInt(IMAGE_WIDTH),
ThreadLocalRandom.current().nextInt(IMAGE_HEIGHT),
ThreadLocalRandom.current().nextInt(IMAGE_WIDTH),
ThreadLocalRandom.current().nextInt(IMAGE_HEIGHT)
);
}
graphics.setFont(new Font("SansSerif", Font.BOLD, 28));
for (int i = 0; i < captchaCode.length(); i++) {
graphics.setColor(randomColor(30, 140));
graphics.drawString(String.valueOf(captchaCode.charAt(i)), 18 + i * 24, 30 + ThreadLocalRandom.current().nextInt(4));
}
} finally {
graphics.dispose();
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "png", outputStream);
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
} catch (Exception ex) {
throw new IllegalStateException("generate captcha image failed", ex);
}
}
private Color randomColor(int min, int max) {
int bound = max - min;
return new Color(
min + ThreadLocalRandom.current().nextInt(bound),
min + ThreadLocalRandom.current().nextInt(bound),
min + ThreadLocalRandom.current().nextInt(bound)
);
}
}

View File

@ -1,4 +1,4 @@
package com.cisd.tms.modules.auth.service;
package com.cisd.tms.modules.auth.service.impl;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
@ -10,6 +10,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.cisd.tms.modules.auth.service.UkeyLoginRandomService;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

View File

@ -1,4 +1,4 @@
package com.cisd.tms.modules.auth.service;
package com.cisd.tms.modules.auth.service.impl;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
@ -6,6 +6,8 @@ import java.security.MessageDigest;
import java.util.Base64;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import com.cisd.tms.modules.auth.service.PasswordHasher;
import org.springframework.stereotype.Component;
@Component

View File

@ -1,10 +1,11 @@
package com.cisd.tms.modules.auth.service;
package com.cisd.tms.modules.auth.service.impl;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds;
import com.cisd.tms.integration.crypto.pcie.model.EccExternalVerifyRequest;
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
import com.cisd.tms.modules.auth.service.CompatUkeyVerifier;
import com.cisd.tms.modules.mk.service.LmkService;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

View File

@ -0,0 +1,15 @@
package com.cisd.tms.modules.auth.service.impl;
import java.util.UUID;
import com.cisd.tms.modules.auth.service.PasswordSaltGenerator;
import org.springframework.stereotype.Service;
@Service
public class UuidPasswordSaltGenerator implements PasswordSaltGenerator {
@Override
public String nextSalt() {
return UUID.randomUUID().toString().replace("-", "");
}
}

View File

@ -0,0 +1,15 @@
package com.cisd.tms.modules.auth.service.impl;
import java.util.UUID;
import com.cisd.tms.modules.auth.service.SessionTokenGenerator;
import org.springframework.stereotype.Service;
@Service
public class UuidSessionTokenGenerator implements SessionTokenGenerator {
@Override
public String nextToken() {
return "tms-" + UUID.randomUUID();
}
}

View File

@ -9,6 +9,7 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -42,6 +43,14 @@ class TmsApplicationTests {
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"summary\":\"UKey 登录\"")));
}
@Test
void shouldIssueCaptchaThroughAuthEndpoint() throws Exception {
mockMvc.perform(post("/api/v1/auth/captcha"))
.andExpect(status().isOk())
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"captchaId\"")))
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"imageBase64\"")));
}
@Test
void shouldCreateAuthRoleTablesAndSeedFixedRoleAccounts() {
ClassPathResource migration = new ClassPathResource("db/migration/V1__tms_schema_full.sql");

View File

@ -8,6 +8,7 @@ import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
import com.cisd.tms.modules.auth.enums.RoleCode;
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.UKeySignDTO;
import com.cisd.tms.modules.mk.dto.UKeySignResult;
import com.cisd.tms.modules.mk.service.LmkService;
@ -35,10 +36,9 @@ class AuthAdminServiceTest {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.UNENABLED, "OLD-HASH", "OLD-SALT", true));
AuthAdminService service = new AuthAdminService(
AuthAdminService service = newAuthAdminService(
roleAccounts,
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
);
@ -52,10 +52,9 @@ class AuthAdminServiceTest {
@Test
void shouldRejectEnableRoleWhenOperatorIsNotKeyAdminFull() {
AuthAdminService service = new AuthAdminService(
AuthAdminService service = newAuthAdminService(
new InMemoryRoleAccountRepository(),
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
);
@ -76,10 +75,9 @@ class AuthAdminServiceTest {
target.setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 30));
roleAccounts.save(target);
AuthAdminService service = new AuthAdminService(
AuthAdminService service = newAuthAdminService(
roleAccounts,
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-002")
);
@ -102,11 +100,12 @@ class AuthAdminServiceTest {
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
AuthAdminService service = new AuthAdminService(
AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(),
bindings,
lmkService,
new FakePasswordHasher(),
lmkService,
new com.fasterxml.jackson.databind.ObjectMapper(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
);
@ -139,11 +138,12 @@ class AuthAdminServiceTest {
@Test
void shouldRejectBindingWhenSlotExceedsRoleRequirement() {
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
AuthAdminService service = new AuthAdminService(
AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(),
new InMemoryRoleUkeyBindingRepository(),
lmkService,
new FakePasswordHasher(),
lmkService,
new com.fasterxml.jackson.databind.ObjectMapper(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
);
@ -175,11 +175,12 @@ class AuthAdminServiceTest {
packet.setComponentIndex(1);
org.mockito.Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet);
AuthAdminService service = new AuthAdminService(
AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(),
new InMemoryRoleUkeyBindingRepository(),
lmkService,
new FakePasswordHasher(),
lmkService,
new com.fasterxml.jackson.databind.ObjectMapper(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
);
@ -188,7 +189,7 @@ class AuthAdminServiceTest {
dto.setRole("ignored");
dto.setUid("1");
dto.setRid("RID-001");
dto.setExtra("EXTRA-001");
// dto.setExtra("EXTRA-001");
UKeySignResult result = service.issueUkeyBindingSign(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto);
@ -207,11 +208,12 @@ class AuthAdminServiceTest {
org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
org.mockito.Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-002");
AuthAdminService service = new AuthAdminService(
AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(),
new InMemoryRoleUkeyBindingRepository(),
lmkService,
new FakePasswordHasher(),
lmkService,
new com.fasterxml.jackson.databind.ObjectMapper(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
);
@ -220,7 +222,7 @@ class AuthAdminServiceTest {
dto.setRole("ignored");
dto.setUid("1");
dto.setRid("RID-002");
dto.setExtra("EXTRA-KEEP");
// dto.setExtra("EXTRA-KEEP");
UKeySignResult result = service.issueUkeyBindingSign(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.AUDIT_ADMIN.getCode(), dto);
@ -280,7 +282,7 @@ class AuthAdminServiceTest {
}
}
private static class FixedSaltSupplier implements java.util.function.Supplier<String> {
private static class FixedSaltSupplier implements PasswordSaltGenerator {
private final Deque<String> salts;
private FixedSaltSupplier(String... salts) {
@ -288,7 +290,7 @@ class AuthAdminServiceTest {
}
@Override
public String get() {
public String nextSalt() {
return Optional.ofNullable(salts.pollFirst()).orElse("salt-fallback");
}
}
@ -343,4 +345,21 @@ class AuthAdminServiceTest {
list.add(entity);
}
}
private static AuthAdminService newAuthAdminService(
InMemoryRoleAccountRepository roleAccounts,
InMemoryRoleUkeyBindingRepository bindings,
Clock clock,
PasswordSaltGenerator saltGenerator
) {
return new AuthAdminServiceImpl(
roleAccounts,
bindings,
new FakePasswordHasher(),
org.mockito.Mockito.mock(LmkService.class),
new com.fasterxml.jackson.databind.ObjectMapper(),
clock,
saltGenerator
);
}
}

View File

@ -1,5 +1,6 @@
package com.cisd.tms.modules.auth.service;
import com.cisd.tms.modules.auth.service.impl.AuthPolicyServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@ -32,13 +33,13 @@ class AuthDomainModelTest {
void shouldResolveExpectedAuthLevelByAuthMethod() {
Assertions.assertEquals(
com.cisd.tms.modules.auth.enums.AuthLevel.LIMITED,
new com.cisd.tms.modules.auth.service.AuthPolicyService().resolveAuthLevel(
new AuthPolicyServiceImpl().resolveAuthLevel(
com.cisd.tms.modules.auth.enums.AuthMethod.PASSWORD
)
);
Assertions.assertEquals(
com.cisd.tms.modules.auth.enums.AuthLevel.FULL,
new com.cisd.tms.modules.auth.service.AuthPolicyService().resolveAuthLevel(
new AuthPolicyServiceImpl().resolveAuthLevel(
com.cisd.tms.modules.auth.enums.AuthMethod.UKEY
)
);

View File

@ -21,6 +21,8 @@ import com.cisd.tms.modules.auth.repository.AuthUserRepository;
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.AuthPolicyServiceImpl;
import com.cisd.tms.modules.auth.service.impl.AuthServiceImpl;
import com.cisd.tms.modules.mk.enums.MasterKeyStatus;
import com.cisd.tms.modules.mk.service.LmkService;
import java.time.Clock;
@ -48,12 +50,10 @@ class AuthServiceTest {
InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository();
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 0, true));
AuthService service = new AuthService(
AuthService service = newAuthService(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
sessions,
ukeyBindings,
new FakePasswordHasher(),
FIXED_CLOCK,
() -> "token-limited-001"
);
@ -86,12 +86,10 @@ class AuthServiceTest {
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1"));
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 2, "UK-2"));
AuthService service = new AuthService(
AuthService service = newAuthService(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
sessions,
ukeyBindings,
new FakePasswordHasher(),
FIXED_CLOCK,
() -> "token-full-001"
);
@ -115,12 +113,10 @@ class AuthServiceTest {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
roleAccounts.save(role(RoleCode.OPS_ADMIN, RoleAccountStatus.UNENABLED, "HASH:12345678:SALT-O", "SALT-O", 0, true, null));
AuthService service = new AuthService(
AuthService service = newAuthService(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
new InMemoryAuthSessionRepository(),
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
FIXED_CLOCK,
() -> "unused"
);
@ -140,12 +136,10 @@ class AuthServiceTest {
RoleAccountEntity role = activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 4, false);
roleAccounts.save(role);
AuthService service = new AuthService(
AuthService service = newAuthService(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
new InMemoryAuthSessionRepository(),
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
FIXED_CLOCK,
() -> "unused"
);
@ -172,12 +166,10 @@ class AuthServiceTest {
ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 2, "UK-2"));
ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 3, "UK-3"));
AuthService service = new AuthService(
AuthService service = newAuthService(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
new InMemoryAuthSessionRepository(),
ukeyBindings,
new FakePasswordHasher(),
FIXED_CLOCK,
() -> "unused"
);
@ -199,12 +191,10 @@ class AuthServiceTest {
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 0, false));
sessions.save(session("token-me-001", RoleCode.AUDIT_ADMIN.getCode(), AuthLevel.LIMITED.name()));
AuthService service = new AuthService(
AuthService service = newAuthService(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
sessions,
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
FIXED_CLOCK,
() -> "unused"
);
@ -225,12 +215,10 @@ class AuthServiceTest {
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "HASH:12345678:SALT-K", "SALT-K", 0, true));
sessions.save(session("token-change-001", RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name()));
AuthService service = new AuthService(
AuthService service = newAuthService(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
sessions,
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
FIXED_CLOCK,
() -> "unused"
);
@ -250,12 +238,10 @@ class AuthServiceTest {
roleAccounts.save(activeRole(RoleCode.OPS_ADMIN, "HASH:12345678:SALT-O", "SALT-O", 0, false));
sessions.save(session("token-logout-001", RoleCode.OPS_ADMIN.getCode(), AuthLevel.LIMITED.name()));
AuthService service = new AuthService(
AuthService service = newAuthService(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
sessions,
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
FIXED_CLOCK,
() -> "unused"
);
@ -272,7 +258,7 @@ class AuthServiceTest {
UkeyLoginRandomService randomService = org.mockito.Mockito.mock(UkeyLoginRandomService.class);
org.mockito.Mockito.when(randomService.issue(RoleCode.SUPER_ADMIN.getCode(), 3)).thenReturn(List.of("RB-1", "RB-2", "RB-3"));
AuthService service = new AuthService(
AuthService service = new AuthServiceImpl(
new InMemoryRoleAccountRepository(),
new InMemoryLegacyAuthUserRepository(),
new InMemoryAuthSessionRepository(),
@ -284,7 +270,9 @@ class AuthServiceTest {
new InMemoryCaptchaService(),
new ObjectMapper(),
FIXED_CLOCK,
() -> "unused"
() -> "unused",
() -> "salt-unused",
new AuthPolicyServiceImpl()
);
UkeyLoginRandomRequest request = new UkeyLoginRandomRequest();
@ -305,7 +293,7 @@ class AuthServiceTest {
InMemoryCaptchaService captchaService = new InMemoryCaptchaService();
String captchaId = captchaService.issue("ABCD").getCaptchaId();
AuthService service = new AuthService(
AuthService service = new AuthServiceImpl(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
new InMemoryAuthSessionRepository(),
@ -317,7 +305,9 @@ class AuthServiceTest {
captchaService,
new ObjectMapper(),
FIXED_CLOCK,
() -> "unused"
() -> "unused",
() -> "salt-unused",
new AuthPolicyServiceImpl()
);
PasswordLoginRequest request = new PasswordLoginRequest();
@ -339,7 +329,7 @@ class AuthServiceTest {
org.mockito.Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001"));
InMemoryCaptchaService captchaService = new InMemoryCaptchaService();
AuthService service = new AuthService(
AuthService service = new AuthServiceImpl(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
new InMemoryAuthSessionRepository(),
@ -351,7 +341,9 @@ class AuthServiceTest {
captchaService,
new ObjectMapper(),
FIXED_CLOCK,
() -> "token-password-002"
() -> "token-password-002",
() -> "salt-password-002",
new AuthPolicyServiceImpl()
);
CaptchaResponse captcha = service.issueCaptcha();
@ -384,7 +376,7 @@ class AuthServiceTest {
org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
org.mockito.Mockito.when(randomService.issue(RoleCode.KEY_ADMIN.getCode(), 2)).thenReturn(List.of("RB-1", "RB-2"));
AuthService service = new AuthService(
AuthService service = new AuthServiceImpl(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
sessions,
@ -396,7 +388,9 @@ class AuthServiceTest {
new InMemoryCaptchaService(),
new ObjectMapper(),
FIXED_CLOCK,
() -> "token-full-ukey-002"
() -> "token-full-ukey-002",
() -> "salt-full-ukey-002",
new AuthPolicyServiceImpl()
);
UkeyLoginRandomRequest randomRequest = new UkeyLoginRandomRequest();
@ -638,4 +632,29 @@ class AuthServiceTest {
}
}
}
private static AuthService newAuthService(
InMemoryRoleAccountRepository roleAccounts,
InMemoryAuthSessionRepository sessions,
InMemoryRoleUkeyBindingRepository ukeyBindings,
Clock clock,
SessionTokenGenerator tokenGenerator
) {
return new AuthServiceImpl(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
sessions,
ukeyBindings,
new FakePasswordHasher(),
org.mockito.Mockito.mock(LmkService.class),
org.mockito.Mockito.mock(UkeyLoginRandomService.class),
org.mockito.Mockito.mock(CompatUkeyVerifier.class),
new InMemoryCaptchaService(),
new ObjectMapper(),
clock,
tokenGenerator,
() -> "salt-test",
new AuthPolicyServiceImpl()
);
}
}

View File

@ -0,0 +1,47 @@
package com.cisd.tms.modules.auth.service.impl;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.auth.dto.CaptchaResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.Base64;
import javax.imageio.ImageIO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class InMemoryCaptchaServiceTest {
private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-31T02:00:00Z"), ZoneOffset.UTC);
@Test
void shouldReturnDecodableCaptchaImageInsteadOfPlaceholderText() throws Exception {
InMemoryCaptchaService service = new InMemoryCaptchaService(FIXED_CLOCK);
CaptchaResponse response = service.issueCaptcha();
Assertions.assertNotNull(response.getCaptchaId());
Assertions.assertNotNull(response.getImageBase64());
Assertions.assertNotEquals(
Base64.getEncoder().encodeToString("ABCD".getBytes()),
response.getImageBase64()
);
byte[] bytes = Base64.getDecoder().decode(response.getImageBase64());
BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes));
Assertions.assertNotNull(image);
Assertions.assertTrue(image.getWidth() > 0);
Assertions.assertTrue(image.getHeight() > 0);
}
@Test
void shouldRejectWrongCaptchaCode() {
InMemoryCaptchaService service = new InMemoryCaptchaService(FIXED_CLOCK);
CaptchaResponse response = service.issueCaptcha();
Assertions.assertThrows(BizException.class, () -> service.verify(response.getCaptchaId(), "WRONG"));
}
}