角色和登录
This commit is contained in:
parent
b0dd28e60d
commit
bd9ee429db
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,143 +1,15 @@
|
|||||||
package com.cisd.tms.modules.auth.service;
|
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.UKeySignDTO;
|
||||||
import com.cisd.tms.modules.mk.dto.UKeySignEntity;
|
|
||||||
import com.cisd.tms.modules.mk.dto.UKeySignResult;
|
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 interface AuthAdminService {
|
||||||
public class AuthAdminService {
|
|
||||||
|
|
||||||
static final String DEFAULT_PASSWORD = "12345678";
|
void enableRole(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode);
|
||||||
|
|
||||||
private final RoleAccountRepository roleAccountRepository;
|
void resetPassword(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode);
|
||||||
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;
|
|
||||||
|
|
||||||
@Autowired
|
void bindUkey(
|
||||||
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(
|
|
||||||
String operatorRoleCode,
|
String operatorRoleCode,
|
||||||
String operatorAuthLevel,
|
String operatorAuthLevel,
|
||||||
String targetRoleCode,
|
String targetRoleCode,
|
||||||
@ -147,31 +19,9 @@ public class AuthAdminService {
|
|||||||
String uid,
|
String uid,
|
||||||
String rid,
|
String rid,
|
||||||
String issuerSign
|
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
|
void bindIssuedUkey(
|
||||||
.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(
|
|
||||||
String operatorRoleCode,
|
String operatorRoleCode,
|
||||||
String operatorAuthLevel,
|
String operatorAuthLevel,
|
||||||
String targetRoleCode,
|
String targetRoleCode,
|
||||||
@ -181,105 +31,12 @@ public class AuthAdminService {
|
|||||||
String uid,
|
String uid,
|
||||||
String rid,
|
String rid,
|
||||||
String issuerSign
|
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 operatorRoleCode,
|
||||||
String operatorAuthLevel,
|
String operatorAuthLevel,
|
||||||
String targetRoleCode,
|
String targetRoleCode,
|
||||||
UKeySignDTO request
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.AuthLevel;
|
||||||
import com.cisd.tms.modules.auth.enums.AuthMethod;
|
import com.cisd.tms.modules.auth.enums.AuthMethod;
|
||||||
import com.cisd.tms.modules.auth.enums.RoleCode;
|
import com.cisd.tms.modules.auth.enums.RoleCode;
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
public interface AuthPolicyService {
|
||||||
public class AuthPolicyService {
|
|
||||||
|
|
||||||
/**
|
AuthLevel resolveAuthLevel(AuthMethod authMethod);
|
||||||
* 统一根据认证方式推导本次登录会话的认证等级。
|
|
||||||
*/
|
|
||||||
public AuthLevel resolveAuthLevel(AuthMethod authMethod) {
|
|
||||||
return authMethod == AuthMethod.UKEY ? AuthLevel.FULL : AuthLevel.LIMITED;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
int requiredUkeyCount(RoleCode roleCode);
|
||||||
* 统一读取角色要求的 UKey 数量,避免控制器和服务层散落硬编码。
|
|
||||||
*/
|
|
||||||
public int requiredUkeyCount(RoleCode roleCode) {
|
|
||||||
return roleCode.getRequiredUkeyCount();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,445 +1,31 @@
|
|||||||
package com.cisd.tms.modules.auth.service;
|
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.CaptchaResponse;
|
||||||
import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
|
import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
|
||||||
import com.cisd.tms.modules.auth.dto.LoginRequest;
|
import com.cisd.tms.modules.auth.dto.LoginRequest;
|
||||||
import com.cisd.tms.modules.auth.dto.LoginResponse;
|
import com.cisd.tms.modules.auth.dto.LoginResponse;
|
||||||
import com.cisd.tms.modules.auth.dto.PasswordLoginRequest;
|
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.UkeyLoginRandomRequest;
|
||||||
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
|
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
|
||||||
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
|
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 interface AuthService {
|
||||||
public class AuthService {
|
|
||||||
|
|
||||||
private static final int MAX_FAILED_ATTEMPTS = 5;
|
LoginResponse login(LoginRequest request);
|
||||||
private static final int IDLE_TIMEOUT_MINUTES = 10;
|
|
||||||
|
|
||||||
private final RoleAccountRepository roleAccountRepository;
|
LoginResponse passwordLogin(PasswordLoginRequest request);
|
||||||
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;
|
|
||||||
|
|
||||||
@Autowired
|
UkeyLoginRandomResponse issueUkeyLoginRandoms(UkeyLoginRandomRequest request);
|
||||||
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()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthService(
|
LoginResponse ukeyLogin(UkeyLoginRequest request);
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthService(
|
CaptchaResponse issueCaptcha();
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LoginResponse login(LoginRequest request) {
|
CurrentUserResponse me(String username);
|
||||||
RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(request.getRoleCode())
|
|
||||||
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account not found"));
|
|
||||||
|
|
||||||
validateRoleStatus(roleAccount);
|
CurrentUserResponse me(String username, String sessionToken);
|
||||||
if (!passwordHasher.matches(request.getPassword(), roleAccount.getPasswordSalt(), roleAccount.getPasswordHash())) {
|
|
||||||
onPasswordFailed(roleAccount);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetFailureState(roleAccount);
|
void logout(String sessionToken);
|
||||||
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();
|
void changePassword(String sessionToken, String currentPassword, String newPassword);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.cisd.tms.modules.auth.service;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface PasswordSaltGenerator {
|
||||||
|
|
||||||
|
String nextSalt();
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.cisd.tms.modules.auth.service;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface SessionTokenGenerator {
|
||||||
|
|
||||||
|
String nextToken();
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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.enums.ErrorCode;
|
||||||
import com.cisd.tms.common.exception.BizException;
|
import com.cisd.tms.common.exception.BizException;
|
||||||
@ -10,6 +10,8 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import com.cisd.tms.modules.auth.service.UkeyLoginRandomService;
|
||||||
import org.bouncycastle.util.encoders.Hex;
|
import org.bouncycastle.util.encoders.Hex;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -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.nio.charset.StandardCharsets;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
@ -6,6 +6,8 @@ import java.security.MessageDigest;
|
|||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import javax.crypto.SecretKeyFactory;
|
import javax.crypto.SecretKeyFactory;
|
||||||
import javax.crypto.spec.PBEKeySpec;
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
|
|
||||||
|
import com.cisd.tms.modules.auth.service.PasswordHasher;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@ -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.enums.ErrorCode;
|
||||||
import com.cisd.tms.common.exception.BizException;
|
import com.cisd.tms.common.exception.BizException;
|
||||||
import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds;
|
import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds;
|
||||||
import com.cisd.tms.integration.crypto.pcie.model.EccExternalVerifyRequest;
|
import com.cisd.tms.integration.crypto.pcie.model.EccExternalVerifyRequest;
|
||||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||||
|
import com.cisd.tms.modules.auth.service.CompatUkeyVerifier;
|
||||||
import com.cisd.tms.modules.mk.service.LmkService;
|
import com.cisd.tms.modules.mk.service.LmkService;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
@ -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("-", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ import org.springframework.core.io.ClassPathResource;
|
|||||||
import org.springframework.test.web.servlet.MockMvc;
|
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.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.content;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
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 登录\"")));
|
.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
|
@Test
|
||||||
void shouldCreateAuthRoleTablesAndSeedFixedRoleAccounts() {
|
void shouldCreateAuthRoleTablesAndSeedFixedRoleAccounts() {
|
||||||
ClassPathResource migration = new ClassPathResource("db/migration/V1__tms_schema_full.sql");
|
ClassPathResource migration = new ClassPathResource("db/migration/V1__tms_schema_full.sql");
|
||||||
|
|||||||
@ -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.enums.RoleCode;
|
||||||
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
|
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
|
||||||
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
|
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.UKeySignDTO;
|
||||||
import com.cisd.tms.modules.mk.dto.UKeySignResult;
|
import com.cisd.tms.modules.mk.dto.UKeySignResult;
|
||||||
import com.cisd.tms.modules.mk.service.LmkService;
|
import com.cisd.tms.modules.mk.service.LmkService;
|
||||||
@ -35,10 +36,9 @@ class AuthAdminServiceTest {
|
|||||||
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.UNENABLED, "OLD-HASH", "OLD-SALT", true));
|
roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.UNENABLED, "OLD-HASH", "OLD-SALT", true));
|
||||||
|
|
||||||
AuthAdminService service = new AuthAdminService(
|
AuthAdminService service = newAuthAdminService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
new FakePasswordHasher(),
|
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
new FixedSaltSupplier("salt-001")
|
new FixedSaltSupplier("salt-001")
|
||||||
);
|
);
|
||||||
@ -52,10 +52,9 @@ class AuthAdminServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRejectEnableRoleWhenOperatorIsNotKeyAdminFull() {
|
void shouldRejectEnableRoleWhenOperatorIsNotKeyAdminFull() {
|
||||||
AuthAdminService service = new AuthAdminService(
|
AuthAdminService service = newAuthAdminService(
|
||||||
new InMemoryRoleAccountRepository(),
|
new InMemoryRoleAccountRepository(),
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
new FakePasswordHasher(),
|
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
new FixedSaltSupplier("salt-001")
|
new FixedSaltSupplier("salt-001")
|
||||||
);
|
);
|
||||||
@ -76,10 +75,9 @@ class AuthAdminServiceTest {
|
|||||||
target.setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 30));
|
target.setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 30));
|
||||||
roleAccounts.save(target);
|
roleAccounts.save(target);
|
||||||
|
|
||||||
AuthAdminService service = new AuthAdminService(
|
AuthAdminService service = newAuthAdminService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
new FakePasswordHasher(),
|
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
new FixedSaltSupplier("salt-002")
|
new FixedSaltSupplier("salt-002")
|
||||||
);
|
);
|
||||||
@ -102,11 +100,12 @@ class AuthAdminServiceTest {
|
|||||||
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
|
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
|
||||||
org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
|
org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
|
||||||
|
|
||||||
AuthAdminService service = new AuthAdminService(
|
AuthAdminService service = new AuthAdminServiceImpl(
|
||||||
new InMemoryRoleAccountRepository(),
|
new InMemoryRoleAccountRepository(),
|
||||||
bindings,
|
bindings,
|
||||||
lmkService,
|
|
||||||
new FakePasswordHasher(),
|
new FakePasswordHasher(),
|
||||||
|
lmkService,
|
||||||
|
new com.fasterxml.jackson.databind.ObjectMapper(),
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
new FixedSaltSupplier("salt-001")
|
new FixedSaltSupplier("salt-001")
|
||||||
);
|
);
|
||||||
@ -139,11 +138,12 @@ class AuthAdminServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldRejectBindingWhenSlotExceedsRoleRequirement() {
|
void shouldRejectBindingWhenSlotExceedsRoleRequirement() {
|
||||||
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
|
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
|
||||||
AuthAdminService service = new AuthAdminService(
|
AuthAdminService service = new AuthAdminServiceImpl(
|
||||||
new InMemoryRoleAccountRepository(),
|
new InMemoryRoleAccountRepository(),
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
lmkService,
|
|
||||||
new FakePasswordHasher(),
|
new FakePasswordHasher(),
|
||||||
|
lmkService,
|
||||||
|
new com.fasterxml.jackson.databind.ObjectMapper(),
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
new FixedSaltSupplier("salt-001")
|
new FixedSaltSupplier("salt-001")
|
||||||
);
|
);
|
||||||
@ -175,11 +175,12 @@ class AuthAdminServiceTest {
|
|||||||
packet.setComponentIndex(1);
|
packet.setComponentIndex(1);
|
||||||
org.mockito.Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet);
|
org.mockito.Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet);
|
||||||
|
|
||||||
AuthAdminService service = new AuthAdminService(
|
AuthAdminService service = new AuthAdminServiceImpl(
|
||||||
new InMemoryRoleAccountRepository(),
|
new InMemoryRoleAccountRepository(),
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
lmkService,
|
|
||||||
new FakePasswordHasher(),
|
new FakePasswordHasher(),
|
||||||
|
lmkService,
|
||||||
|
new com.fasterxml.jackson.databind.ObjectMapper(),
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
new FixedSaltSupplier("salt-001")
|
new FixedSaltSupplier("salt-001")
|
||||||
);
|
);
|
||||||
@ -188,7 +189,7 @@ class AuthAdminServiceTest {
|
|||||||
dto.setRole("ignored");
|
dto.setRole("ignored");
|
||||||
dto.setUid("1");
|
dto.setUid("1");
|
||||||
dto.setRid("RID-001");
|
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);
|
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.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
|
||||||
org.mockito.Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-002");
|
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 InMemoryRoleAccountRepository(),
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
lmkService,
|
|
||||||
new FakePasswordHasher(),
|
new FakePasswordHasher(),
|
||||||
|
lmkService,
|
||||||
|
new com.fasterxml.jackson.databind.ObjectMapper(),
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
new FixedSaltSupplier("salt-001")
|
new FixedSaltSupplier("salt-001")
|
||||||
);
|
);
|
||||||
@ -220,7 +222,7 @@ class AuthAdminServiceTest {
|
|||||||
dto.setRole("ignored");
|
dto.setRole("ignored");
|
||||||
dto.setUid("1");
|
dto.setUid("1");
|
||||||
dto.setRid("RID-002");
|
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);
|
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 final Deque<String> salts;
|
||||||
|
|
||||||
private FixedSaltSupplier(String... salts) {
|
private FixedSaltSupplier(String... salts) {
|
||||||
@ -288,7 +290,7 @@ class AuthAdminServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String get() {
|
public String nextSalt() {
|
||||||
return Optional.ofNullable(salts.pollFirst()).orElse("salt-fallback");
|
return Optional.ofNullable(salts.pollFirst()).orElse("salt-fallback");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -343,4 +345,21 @@ class AuthAdminServiceTest {
|
|||||||
list.add(entity);
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.cisd.tms.modules.auth.service;
|
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.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@ -32,13 +33,13 @@ class AuthDomainModelTest {
|
|||||||
void shouldResolveExpectedAuthLevelByAuthMethod() {
|
void shouldResolveExpectedAuthLevelByAuthMethod() {
|
||||||
Assertions.assertEquals(
|
Assertions.assertEquals(
|
||||||
com.cisd.tms.modules.auth.enums.AuthLevel.LIMITED,
|
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
|
com.cisd.tms.modules.auth.enums.AuthMethod.PASSWORD
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
Assertions.assertEquals(
|
Assertions.assertEquals(
|
||||||
com.cisd.tms.modules.auth.enums.AuthLevel.FULL,
|
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
|
com.cisd.tms.modules.auth.enums.AuthMethod.UKEY
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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.AuthSessionRepository;
|
||||||
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
|
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
|
||||||
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
|
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.enums.MasterKeyStatus;
|
||||||
import com.cisd.tms.modules.mk.service.LmkService;
|
import com.cisd.tms.modules.mk.service.LmkService;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
@ -48,12 +50,10 @@ class AuthServiceTest {
|
|||||||
InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository();
|
InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository();
|
||||||
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 0, true));
|
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 0, true));
|
||||||
|
|
||||||
AuthService service = new AuthService(
|
AuthService service = newAuthService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryLegacyAuthUserRepository(),
|
|
||||||
sessions,
|
sessions,
|
||||||
ukeyBindings,
|
ukeyBindings,
|
||||||
new FakePasswordHasher(),
|
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
() -> "token-limited-001"
|
() -> "token-limited-001"
|
||||||
);
|
);
|
||||||
@ -86,12 +86,10 @@ class AuthServiceTest {
|
|||||||
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1"));
|
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1"));
|
||||||
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 2, "UK-2"));
|
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 2, "UK-2"));
|
||||||
|
|
||||||
AuthService service = new AuthService(
|
AuthService service = newAuthService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryLegacyAuthUserRepository(),
|
|
||||||
sessions,
|
sessions,
|
||||||
ukeyBindings,
|
ukeyBindings,
|
||||||
new FakePasswordHasher(),
|
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
() -> "token-full-001"
|
() -> "token-full-001"
|
||||||
);
|
);
|
||||||
@ -115,12 +113,10 @@ class AuthServiceTest {
|
|||||||
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
roleAccounts.save(role(RoleCode.OPS_ADMIN, RoleAccountStatus.UNENABLED, "HASH:12345678:SALT-O", "SALT-O", 0, true, null));
|
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,
|
roleAccounts,
|
||||||
new InMemoryLegacyAuthUserRepository(),
|
|
||||||
new InMemoryAuthSessionRepository(),
|
new InMemoryAuthSessionRepository(),
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
new FakePasswordHasher(),
|
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
() -> "unused"
|
() -> "unused"
|
||||||
);
|
);
|
||||||
@ -140,12 +136,10 @@ class AuthServiceTest {
|
|||||||
RoleAccountEntity role = activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 4, false);
|
RoleAccountEntity role = activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 4, false);
|
||||||
roleAccounts.save(role);
|
roleAccounts.save(role);
|
||||||
|
|
||||||
AuthService service = new AuthService(
|
AuthService service = newAuthService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryLegacyAuthUserRepository(),
|
|
||||||
new InMemoryAuthSessionRepository(),
|
new InMemoryAuthSessionRepository(),
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
new FakePasswordHasher(),
|
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
() -> "unused"
|
() -> "unused"
|
||||||
);
|
);
|
||||||
@ -172,12 +166,10 @@ class AuthServiceTest {
|
|||||||
ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 2, "UK-2"));
|
ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 2, "UK-2"));
|
||||||
ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 3, "UK-3"));
|
ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 3, "UK-3"));
|
||||||
|
|
||||||
AuthService service = new AuthService(
|
AuthService service = newAuthService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryLegacyAuthUserRepository(),
|
|
||||||
new InMemoryAuthSessionRepository(),
|
new InMemoryAuthSessionRepository(),
|
||||||
ukeyBindings,
|
ukeyBindings,
|
||||||
new FakePasswordHasher(),
|
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
() -> "unused"
|
() -> "unused"
|
||||||
);
|
);
|
||||||
@ -199,12 +191,10 @@ class AuthServiceTest {
|
|||||||
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 0, false));
|
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()));
|
sessions.save(session("token-me-001", RoleCode.AUDIT_ADMIN.getCode(), AuthLevel.LIMITED.name()));
|
||||||
|
|
||||||
AuthService service = new AuthService(
|
AuthService service = newAuthService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryLegacyAuthUserRepository(),
|
|
||||||
sessions,
|
sessions,
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
new FakePasswordHasher(),
|
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
() -> "unused"
|
() -> "unused"
|
||||||
);
|
);
|
||||||
@ -225,12 +215,10 @@ class AuthServiceTest {
|
|||||||
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "HASH:12345678:SALT-K", "SALT-K", 0, true));
|
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()));
|
sessions.save(session("token-change-001", RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name()));
|
||||||
|
|
||||||
AuthService service = new AuthService(
|
AuthService service = newAuthService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryLegacyAuthUserRepository(),
|
|
||||||
sessions,
|
sessions,
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
new FakePasswordHasher(),
|
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
() -> "unused"
|
() -> "unused"
|
||||||
);
|
);
|
||||||
@ -250,12 +238,10 @@ class AuthServiceTest {
|
|||||||
roleAccounts.save(activeRole(RoleCode.OPS_ADMIN, "HASH:12345678:SALT-O", "SALT-O", 0, false));
|
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()));
|
sessions.save(session("token-logout-001", RoleCode.OPS_ADMIN.getCode(), AuthLevel.LIMITED.name()));
|
||||||
|
|
||||||
AuthService service = new AuthService(
|
AuthService service = newAuthService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryLegacyAuthUserRepository(),
|
|
||||||
sessions,
|
sessions,
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
new FakePasswordHasher(),
|
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
() -> "unused"
|
() -> "unused"
|
||||||
);
|
);
|
||||||
@ -272,7 +258,7 @@ class AuthServiceTest {
|
|||||||
UkeyLoginRandomService randomService = org.mockito.Mockito.mock(UkeyLoginRandomService.class);
|
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"));
|
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 InMemoryRoleAccountRepository(),
|
||||||
new InMemoryLegacyAuthUserRepository(),
|
new InMemoryLegacyAuthUserRepository(),
|
||||||
new InMemoryAuthSessionRepository(),
|
new InMemoryAuthSessionRepository(),
|
||||||
@ -284,7 +270,9 @@ class AuthServiceTest {
|
|||||||
new InMemoryCaptchaService(),
|
new InMemoryCaptchaService(),
|
||||||
new ObjectMapper(),
|
new ObjectMapper(),
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
() -> "unused"
|
() -> "unused",
|
||||||
|
() -> "salt-unused",
|
||||||
|
new AuthPolicyServiceImpl()
|
||||||
);
|
);
|
||||||
|
|
||||||
UkeyLoginRandomRequest request = new UkeyLoginRandomRequest();
|
UkeyLoginRandomRequest request = new UkeyLoginRandomRequest();
|
||||||
@ -305,7 +293,7 @@ class AuthServiceTest {
|
|||||||
InMemoryCaptchaService captchaService = new InMemoryCaptchaService();
|
InMemoryCaptchaService captchaService = new InMemoryCaptchaService();
|
||||||
String captchaId = captchaService.issue("ABCD").getCaptchaId();
|
String captchaId = captchaService.issue("ABCD").getCaptchaId();
|
||||||
|
|
||||||
AuthService service = new AuthService(
|
AuthService service = new AuthServiceImpl(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryLegacyAuthUserRepository(),
|
new InMemoryLegacyAuthUserRepository(),
|
||||||
new InMemoryAuthSessionRepository(),
|
new InMemoryAuthSessionRepository(),
|
||||||
@ -317,7 +305,9 @@ class AuthServiceTest {
|
|||||||
captchaService,
|
captchaService,
|
||||||
new ObjectMapper(),
|
new ObjectMapper(),
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
() -> "unused"
|
() -> "unused",
|
||||||
|
() -> "salt-unused",
|
||||||
|
new AuthPolicyServiceImpl()
|
||||||
);
|
);
|
||||||
|
|
||||||
PasswordLoginRequest request = new PasswordLoginRequest();
|
PasswordLoginRequest request = new PasswordLoginRequest();
|
||||||
@ -339,7 +329,7 @@ class AuthServiceTest {
|
|||||||
org.mockito.Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001"));
|
org.mockito.Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001"));
|
||||||
InMemoryCaptchaService captchaService = new InMemoryCaptchaService();
|
InMemoryCaptchaService captchaService = new InMemoryCaptchaService();
|
||||||
|
|
||||||
AuthService service = new AuthService(
|
AuthService service = new AuthServiceImpl(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryLegacyAuthUserRepository(),
|
new InMemoryLegacyAuthUserRepository(),
|
||||||
new InMemoryAuthSessionRepository(),
|
new InMemoryAuthSessionRepository(),
|
||||||
@ -351,7 +341,9 @@ class AuthServiceTest {
|
|||||||
captchaService,
|
captchaService,
|
||||||
new ObjectMapper(),
|
new ObjectMapper(),
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
() -> "token-password-002"
|
() -> "token-password-002",
|
||||||
|
() -> "salt-password-002",
|
||||||
|
new AuthPolicyServiceImpl()
|
||||||
);
|
);
|
||||||
|
|
||||||
CaptchaResponse captcha = service.issueCaptcha();
|
CaptchaResponse captcha = service.issueCaptcha();
|
||||||
@ -384,7 +376,7 @@ class AuthServiceTest {
|
|||||||
org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
|
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"));
|
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,
|
roleAccounts,
|
||||||
new InMemoryLegacyAuthUserRepository(),
|
new InMemoryLegacyAuthUserRepository(),
|
||||||
sessions,
|
sessions,
|
||||||
@ -396,7 +388,9 @@ class AuthServiceTest {
|
|||||||
new InMemoryCaptchaService(),
|
new InMemoryCaptchaService(),
|
||||||
new ObjectMapper(),
|
new ObjectMapper(),
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
() -> "token-full-ukey-002"
|
() -> "token-full-ukey-002",
|
||||||
|
() -> "salt-full-ukey-002",
|
||||||
|
new AuthPolicyServiceImpl()
|
||||||
);
|
);
|
||||||
|
|
||||||
UkeyLoginRandomRequest randomRequest = new UkeyLoginRandomRequest();
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user