角色和登录
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;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.MockPcieCryptoService;
|
||||
import com.cisd.tms.modules.auth.entity.RoleAccountEntity;
|
||||
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
|
||||
import com.cisd.tms.modules.auth.enums.AuthLevel;
|
||||
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
|
||||
import com.cisd.tms.modules.auth.enums.RoleCode;
|
||||
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
|
||||
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
|
||||
import com.cisd.tms.common.util.TraceIdUtil;
|
||||
import com.cisd.tms.modules.mk.config.MasterKeyInitProperties;
|
||||
import com.cisd.tms.modules.mk.dto.UKeySignDTO;
|
||||
import com.cisd.tms.modules.mk.dto.UKeySignEntity;
|
||||
import com.cisd.tms.modules.mk.dto.UKeySignResult;
|
||||
import com.cisd.tms.modules.mk.service.LmkService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class AuthAdminService {
|
||||
public interface AuthAdminService {
|
||||
|
||||
static final String DEFAULT_PASSWORD = "12345678";
|
||||
void enableRole(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode);
|
||||
|
||||
private final RoleAccountRepository roleAccountRepository;
|
||||
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
|
||||
private final PasswordHasher passwordHasher;
|
||||
private final LmkService lmkService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final Clock clock;
|
||||
private final Supplier<String> saltSupplier;
|
||||
void resetPassword(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode);
|
||||
|
||||
@Autowired
|
||||
public AuthAdminService(
|
||||
RoleAccountRepository roleAccountRepository,
|
||||
RoleUkeyBindingRepository roleUkeyBindingRepository,
|
||||
PasswordHasher passwordHasher,
|
||||
LmkService lmkService,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this(
|
||||
roleAccountRepository,
|
||||
roleUkeyBindingRepository,
|
||||
passwordHasher,
|
||||
lmkService,
|
||||
objectMapper,
|
||||
Clock.systemUTC(),
|
||||
() -> UUID.randomUUID().toString().replace("-", "")
|
||||
);
|
||||
}
|
||||
|
||||
AuthAdminService(
|
||||
RoleAccountRepository roleAccountRepository,
|
||||
RoleUkeyBindingRepository roleUkeyBindingRepository,
|
||||
LmkService lmkService,
|
||||
PasswordHasher passwordHasher,
|
||||
Clock clock,
|
||||
Supplier<String> saltSupplier
|
||||
) {
|
||||
this(
|
||||
roleAccountRepository,
|
||||
roleUkeyBindingRepository,
|
||||
passwordHasher,
|
||||
lmkService,
|
||||
new ObjectMapper(),
|
||||
clock,
|
||||
saltSupplier
|
||||
);
|
||||
}
|
||||
|
||||
AuthAdminService(
|
||||
RoleAccountRepository roleAccountRepository,
|
||||
RoleUkeyBindingRepository roleUkeyBindingRepository,
|
||||
PasswordHasher passwordHasher,
|
||||
Clock clock,
|
||||
Supplier<String> saltSupplier
|
||||
) {
|
||||
this(
|
||||
roleAccountRepository,
|
||||
roleUkeyBindingRepository,
|
||||
new LmkService(new MockPcieCryptoService(), new MasterKeyInitProperties()),
|
||||
passwordHasher,
|
||||
clock,
|
||||
saltSupplier
|
||||
);
|
||||
}
|
||||
|
||||
AuthAdminService(
|
||||
RoleAccountRepository roleAccountRepository,
|
||||
RoleUkeyBindingRepository roleUkeyBindingRepository,
|
||||
PasswordHasher passwordHasher,
|
||||
LmkService lmkService,
|
||||
ObjectMapper objectMapper,
|
||||
Clock clock,
|
||||
Supplier<String> saltSupplier
|
||||
) {
|
||||
this.roleAccountRepository = roleAccountRepository;
|
||||
this.roleUkeyBindingRepository = roleUkeyBindingRepository;
|
||||
this.passwordHasher = passwordHasher;
|
||||
this.lmkService = lmkService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.clock = clock;
|
||||
this.saltSupplier = saltSupplier;
|
||||
}
|
||||
|
||||
public void enableRole(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) {
|
||||
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
|
||||
RoleAccountEntity target = loadRole(targetRoleCode);
|
||||
target.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||
target.setNeedChangePassword(Boolean.TRUE);
|
||||
target.setLockedUntil(null);
|
||||
target.setFailedCount(0);
|
||||
roleAccountRepository.update(target);
|
||||
}
|
||||
|
||||
public void resetPassword(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) {
|
||||
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
|
||||
RoleAccountEntity target = loadRole(targetRoleCode);
|
||||
String newSalt = saltSupplier.get();
|
||||
target.setPasswordSalt(newSalt);
|
||||
target.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt));
|
||||
target.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||
target.setNeedChangePassword(Boolean.TRUE);
|
||||
target.setFailedCount(0);
|
||||
target.setLockedUntil(null);
|
||||
roleAccountRepository.update(target);
|
||||
}
|
||||
|
||||
public void bindUkey(
|
||||
void bindUkey(
|
||||
String operatorRoleCode,
|
||||
String operatorAuthLevel,
|
||||
String targetRoleCode,
|
||||
@ -147,31 +19,9 @@ public class AuthAdminService {
|
||||
String uid,
|
||||
String rid,
|
||||
String issuerSign
|
||||
) {
|
||||
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
|
||||
RoleCode targetRole = resolveRoleCode(targetRoleCode);
|
||||
if (slotNo == null || slotNo < 1 || slotNo > targetRole.getRequiredUkeyCount()) {
|
||||
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "slotNo exceeds role ukey requirement");
|
||||
}
|
||||
);
|
||||
|
||||
RoleUkeyBindingEntity binding = roleUkeyBindingRepository
|
||||
.findActiveByRoleCodeAndSlotNo(targetRoleCode, slotNo)
|
||||
.orElseGet(RoleUkeyBindingEntity::new);
|
||||
if (binding.getId() == null) {
|
||||
binding.setId((long) Math.abs(Objects.hash(targetRoleCode, slotNo, ukeySerial, TraceIdUtil.newTraceId())));
|
||||
binding.setRoleCode(targetRoleCode);
|
||||
binding.setSlotNo(slotNo);
|
||||
binding.setStatus("ACTIVE");
|
||||
applyBinding(binding, ukeySerial, ukeyPubkey, uid, rid, issuerSign);
|
||||
roleUkeyBindingRepository.save(binding);
|
||||
return;
|
||||
}
|
||||
|
||||
applyBinding(binding, ukeySerial, ukeyPubkey, uid, rid, issuerSign);
|
||||
roleUkeyBindingRepository.update(binding);
|
||||
}
|
||||
|
||||
public void bindIssuedUkey(
|
||||
void bindIssuedUkey(
|
||||
String operatorRoleCode,
|
||||
String operatorAuthLevel,
|
||||
String targetRoleCode,
|
||||
@ -181,105 +31,12 @@ public class AuthAdminService {
|
||||
String uid,
|
||||
String rid,
|
||||
String issuerSign
|
||||
) {
|
||||
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
|
||||
resolveRoleCode(targetRoleCode);
|
||||
verifyIssuerSignature(targetRoleCode, ukeyPubkey, uid, rid, issuerSign);
|
||||
bindUkey(operatorRoleCode, operatorAuthLevel, targetRoleCode, slotNo, ukeySerial, ukeyPubkey, uid, rid, issuerSign);
|
||||
}
|
||||
);
|
||||
|
||||
public UKeySignResult issueUkeyBindingSign(
|
||||
UKeySignResult issueUkeyBindingSign(
|
||||
String operatorRoleCode,
|
||||
String operatorAuthLevel,
|
||||
String targetRoleCode,
|
||||
UKeySignDTO request
|
||||
) {
|
||||
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
|
||||
RoleCode targetRole = resolveRoleCode(targetRoleCode);
|
||||
String authKeyPair = lmkService.exportIkPublicKeyHex();
|
||||
|
||||
UKeySignDTO dto = new UKeySignDTO();
|
||||
dto.setPubKey(request.getPubKey());
|
||||
// 新卡统一使用规范角色编码,不再兼容旧卡角色字符串。
|
||||
dto.setRole(targetRole.getCode());
|
||||
dto.setUid(request.getUid());
|
||||
dto.setRid(request.getRid());
|
||||
dto.setExtra(request.getExtra());
|
||||
|
||||
String signValue = lmkService.signIk(toIssuePayload(dto, authKeyPair));
|
||||
com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket backupPacket = null;
|
||||
// SUPER_ADMIN 发卡同时承担主密钥分量备份职责,需要下发对应分量包。
|
||||
if (RoleCode.SUPER_ADMIN == targetRole) {
|
||||
backupPacket = lmkService.buildBackupPacket(Integer.parseInt(request.getUid()));
|
||||
}
|
||||
return UKeySignResult.builder()
|
||||
.sign(signValue)
|
||||
.backupPacket(backupPacket)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void applyBinding(
|
||||
RoleUkeyBindingEntity binding,
|
||||
String ukeySerial,
|
||||
String ukeyPubkey,
|
||||
String uid,
|
||||
String rid,
|
||||
String issuerSign
|
||||
) {
|
||||
binding.setUkeySerial(ukeySerial);
|
||||
binding.setUkeyPubkey(ukeyPubkey);
|
||||
binding.setUid(uid);
|
||||
binding.setRid(rid);
|
||||
binding.setIssuerSign(issuerSign);
|
||||
binding.setStatus("ACTIVE");
|
||||
binding.setBoundAt(now());
|
||||
binding.setUnboundAt(null);
|
||||
}
|
||||
|
||||
private void requireKeyAdminFull(String operatorRoleCode, String operatorAuthLevel) {
|
||||
if (!RoleCode.KEY_ADMIN.getCode().equals(operatorRoleCode) || !AuthLevel.FULL.name().equals(operatorAuthLevel)) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "operation requires KEY_ADMIN FULL session");
|
||||
}
|
||||
}
|
||||
|
||||
private RoleAccountEntity loadRole(String roleCode) {
|
||||
resolveRoleCode(roleCode);
|
||||
return roleAccountRepository.findByRoleCode(roleCode)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role account not found"));
|
||||
}
|
||||
|
||||
private RoleCode resolveRoleCode(String roleCode) {
|
||||
return List.of(RoleCode.values()).stream()
|
||||
.filter(item -> item.getCode().equals(roleCode))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "invalid roleCode"));
|
||||
}
|
||||
|
||||
private LocalDateTime now() {
|
||||
return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
private void verifyIssuerSignature(String targetRoleCode, String ukeyPubkey, String uid, String rid, String issuerSign) {
|
||||
try {
|
||||
UKeySignDTO dto = new UKeySignDTO();
|
||||
dto.setPubKey(ukeyPubkey);
|
||||
dto.setRole(resolveRoleCode(targetRoleCode).getCode());
|
||||
dto.setUid(uid);
|
||||
dto.setRid(rid);
|
||||
String authKeyPair = lmkService.exportIkPublicKeyHex();
|
||||
lmkService.verifyIk(toIssuePayload(dto, authKeyPair), issuerSign);
|
||||
} catch (BizException ex) {
|
||||
throw ex;
|
||||
} catch (RuntimeException ex) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey issuer signature verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
private String toIssuePayload(UKeySignDTO dto, String authKeyPair) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair));
|
||||
} catch (JsonProcessingException ex) {
|
||||
throw new IllegalStateException("serialize ukey issue payload failed", ex);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,22 +3,10 @@ package com.cisd.tms.modules.auth.service;
|
||||
import com.cisd.tms.modules.auth.enums.AuthLevel;
|
||||
import com.cisd.tms.modules.auth.enums.AuthMethod;
|
||||
import com.cisd.tms.modules.auth.enums.RoleCode;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class AuthPolicyService {
|
||||
public interface AuthPolicyService {
|
||||
|
||||
/**
|
||||
* 统一根据认证方式推导本次登录会话的认证等级。
|
||||
*/
|
||||
public AuthLevel resolveAuthLevel(AuthMethod authMethod) {
|
||||
return authMethod == AuthMethod.UKEY ? AuthLevel.FULL : AuthLevel.LIMITED;
|
||||
}
|
||||
AuthLevel resolveAuthLevel(AuthMethod authMethod);
|
||||
|
||||
/**
|
||||
* 统一读取角色要求的 UKey 数量,避免控制器和服务层散落硬编码。
|
||||
*/
|
||||
public int requiredUkeyCount(RoleCode roleCode) {
|
||||
return roleCode.getRequiredUkeyCount();
|
||||
}
|
||||
int requiredUkeyCount(RoleCode roleCode);
|
||||
}
|
||||
|
||||
@ -1,445 +1,31 @@
|
||||
package com.cisd.tms.modules.auth.service;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.auth.dto.CaptchaResponse;
|
||||
import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
|
||||
import com.cisd.tms.modules.auth.dto.LoginRequest;
|
||||
import com.cisd.tms.modules.auth.dto.LoginResponse;
|
||||
import com.cisd.tms.modules.auth.dto.PasswordLoginRequest;
|
||||
import com.cisd.tms.modules.auth.dto.UkeyLoginProof;
|
||||
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomRequest;
|
||||
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
|
||||
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
|
||||
import com.cisd.tms.modules.auth.entity.AuthSessionEntity;
|
||||
import com.cisd.tms.modules.auth.entity.AuthUserEntity;
|
||||
import com.cisd.tms.modules.auth.entity.RoleAccountEntity;
|
||||
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
|
||||
import com.cisd.tms.modules.auth.enums.AuthLevel;
|
||||
import com.cisd.tms.modules.auth.enums.AuthMethod;
|
||||
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
|
||||
import com.cisd.tms.modules.auth.enums.RoleCode;
|
||||
import com.cisd.tms.modules.auth.repository.AuthUserRepository;
|
||||
import com.cisd.tms.modules.auth.repository.AuthSessionRepository;
|
||||
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
|
||||
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
|
||||
import com.cisd.tms.modules.mk.dto.UKeySignDTO;
|
||||
import com.cisd.tms.modules.mk.dto.UKeySignEntity;
|
||||
import com.cisd.tms.modules.mk.enums.MasterKeyStatus;
|
||||
import com.cisd.tms.modules.mk.service.LmkService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.Base64;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class AuthService {
|
||||
public interface AuthService {
|
||||
|
||||
private static final int MAX_FAILED_ATTEMPTS = 5;
|
||||
private static final int IDLE_TIMEOUT_MINUTES = 10;
|
||||
LoginResponse login(LoginRequest request);
|
||||
|
||||
private final RoleAccountRepository roleAccountRepository;
|
||||
private final AuthUserRepository authUserRepository;
|
||||
private final AuthSessionRepository authSessionRepository;
|
||||
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
|
||||
private final PasswordHasher passwordHasher;
|
||||
private final LmkService lmkService;
|
||||
private final UkeyLoginRandomService ukeyLoginRandomService;
|
||||
private final CompatUkeyVerifier compatUkeyVerifier;
|
||||
private final CaptchaService captchaService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final Clock clock;
|
||||
private final Supplier<String> tokenSupplier;
|
||||
LoginResponse passwordLogin(PasswordLoginRequest request);
|
||||
|
||||
@Autowired
|
||||
public AuthService(
|
||||
RoleAccountRepository roleAccountRepository,
|
||||
AuthUserRepository authUserRepository,
|
||||
AuthSessionRepository authSessionRepository,
|
||||
RoleUkeyBindingRepository roleUkeyBindingRepository,
|
||||
PasswordHasher passwordHasher
|
||||
) {
|
||||
this(
|
||||
roleAccountRepository,
|
||||
authUserRepository,
|
||||
authSessionRepository,
|
||||
roleUkeyBindingRepository,
|
||||
passwordHasher,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new ObjectMapper(),
|
||||
Clock.systemUTC(),
|
||||
() -> "tms-" + UUID.randomUUID()
|
||||
);
|
||||
}
|
||||
UkeyLoginRandomResponse issueUkeyLoginRandoms(UkeyLoginRandomRequest request);
|
||||
|
||||
AuthService(
|
||||
RoleAccountRepository roleAccountRepository,
|
||||
AuthUserRepository authUserRepository,
|
||||
AuthSessionRepository authSessionRepository,
|
||||
RoleUkeyBindingRepository roleUkeyBindingRepository,
|
||||
PasswordHasher passwordHasher,
|
||||
Clock clock,
|
||||
Supplier<String> tokenSupplier
|
||||
) {
|
||||
this(
|
||||
roleAccountRepository,
|
||||
authUserRepository,
|
||||
authSessionRepository,
|
||||
roleUkeyBindingRepository,
|
||||
passwordHasher,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new ObjectMapper(),
|
||||
clock,
|
||||
tokenSupplier
|
||||
);
|
||||
}
|
||||
LoginResponse ukeyLogin(UkeyLoginRequest request);
|
||||
|
||||
AuthService(
|
||||
RoleAccountRepository roleAccountRepository,
|
||||
AuthUserRepository authUserRepository,
|
||||
AuthSessionRepository authSessionRepository,
|
||||
RoleUkeyBindingRepository roleUkeyBindingRepository,
|
||||
PasswordHasher passwordHasher,
|
||||
LmkService lmkService,
|
||||
UkeyLoginRandomService ukeyLoginRandomService,
|
||||
CompatUkeyVerifier compatUkeyVerifier,
|
||||
CaptchaService captchaService,
|
||||
ObjectMapper objectMapper,
|
||||
Clock clock,
|
||||
Supplier<String> tokenSupplier
|
||||
) {
|
||||
this.roleAccountRepository = roleAccountRepository;
|
||||
this.authUserRepository = authUserRepository;
|
||||
this.authSessionRepository = authSessionRepository;
|
||||
this.roleUkeyBindingRepository = roleUkeyBindingRepository;
|
||||
this.passwordHasher = passwordHasher;
|
||||
this.lmkService = lmkService;
|
||||
this.ukeyLoginRandomService = ukeyLoginRandomService;
|
||||
this.compatUkeyVerifier = compatUkeyVerifier;
|
||||
this.captchaService = captchaService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.clock = clock;
|
||||
this.tokenSupplier = tokenSupplier;
|
||||
}
|
||||
CaptchaResponse issueCaptcha();
|
||||
|
||||
public LoginResponse login(LoginRequest request) {
|
||||
RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(request.getRoleCode())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account not found"));
|
||||
CurrentUserResponse me(String username);
|
||||
|
||||
validateRoleStatus(roleAccount);
|
||||
if (!passwordHasher.matches(request.getPassword(), roleAccount.getPasswordSalt(), roleAccount.getPasswordHash())) {
|
||||
onPasswordFailed(roleAccount);
|
||||
}
|
||||
CurrentUserResponse me(String username, String sessionToken);
|
||||
|
||||
resetFailureState(roleAccount);
|
||||
AuthMethod authMethod = resolveAuthMethod(request.getUkeySerials());
|
||||
AuthLevel authLevel = resolveAuthLevel(roleAccount, request.getUkeySerials());
|
||||
AuthSessionEntity session = buildSession(roleAccount.getRoleCode(), authMethod, authLevel);
|
||||
authSessionRepository.save(session);
|
||||
void logout(String sessionToken);
|
||||
|
||||
LoginResponse response = new LoginResponse();
|
||||
response.setRoleCode(roleAccount.getRoleCode());
|
||||
response.setAuthLevel(authLevel.name());
|
||||
response.setToken(session.getSessionToken());
|
||||
response.setExpiresAt(OffsetDateTime.of(session.getExpiresAt(), ZoneOffset.UTC).toString());
|
||||
response.setNeedChangePassword(Boolean.TRUE.equals(roleAccount.getNeedChangePassword()));
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化口令登录入口,当前先复用统一登录实现并签发 LIMITED 会话。
|
||||
*/
|
||||
public LoginResponse passwordLogin(PasswordLoginRequest request) {
|
||||
ensureMasterKeyReady();
|
||||
requireCaptchaService().verify(request.getCaptchaId(), request.getCaptchaCode());
|
||||
LoginRequest loginRequest = new LoginRequest();
|
||||
loginRequest.setRoleCode(request.getRoleCode());
|
||||
loginRequest.setPassword(request.getPassword());
|
||||
return login(loginRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化 UKey 随机数申请入口。当前先按角色要求生成占位随机数,后续再接旧系统完整校验链路。
|
||||
*/
|
||||
public UkeyLoginRandomResponse issueUkeyLoginRandoms(UkeyLoginRandomRequest request) {
|
||||
RoleCode roleCode = RoleCode.valueOf(request.getRoleCode());
|
||||
UkeyLoginRandomResponse response = new UkeyLoginRandomResponse();
|
||||
response.setRoleCode(roleCode.getCode());
|
||||
response.setRandoms(requireUkeyLoginRandomService().issue(roleCode.getCode(), roleCode.getRequiredUkeyCount()));
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化 UKey 登录入口,当前先按证明数量映射到完整登录。
|
||||
*/
|
||||
public LoginResponse ukeyLogin(UkeyLoginRequest request) {
|
||||
ensureMasterKeyReady();
|
||||
RoleCode roleCode = RoleCode.valueOf(request.getRoleCode());
|
||||
List<RoleUkeyBindingEntity> activeBindings = roleUkeyBindingRepository.findActiveByRoleCode(roleCode.getCode());
|
||||
validateUkeyCount(roleCode, activeBindings, request.getUkeyProofs());
|
||||
Map<String, RoleUkeyBindingEntity> bindingsByPubKey = activeBindings.stream()
|
||||
.collect(Collectors.toMap(RoleUkeyBindingEntity::getUkeyPubkey, item -> item, (left, right) -> left, java.util.LinkedHashMap::new));
|
||||
Set<String> requestPubKeys = request.getUkeyProofs().stream()
|
||||
.map(UkeyLoginProof::getPubKey)
|
||||
.collect(Collectors.toSet());
|
||||
if (requestPubKeys.size() != request.getUkeyProofs().size() || !bindingsByPubKey.keySet().containsAll(requestPubKeys)) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role");
|
||||
}
|
||||
requireUkeyLoginRandomService().assertIssued(
|
||||
roleCode.getCode(),
|
||||
request.getUkeyProofs().stream().map(UkeyLoginProof::getServerRandom).toList()
|
||||
);
|
||||
String authKeyPair = requireLmkService().exportIkPublicKeyHex();
|
||||
List<String> matchedSerials = new java.util.ArrayList<>();
|
||||
for (UkeyLoginProof proof : request.getUkeyProofs()) {
|
||||
RoleUkeyBindingEntity binding = bindingsByPubKey.get(proof.getPubKey());
|
||||
requireCompatUkeyVerifier().verifyIssuedBinding(buildIssuePayload(request.getRoleCode(), proof, authKeyPair), proof.getIssueSignature());
|
||||
requireCompatUkeyVerifier().verifyLoginSignature(proof.getPubKey(), proof.getLoginPayload(), proof.getLoginSignature());
|
||||
matchedSerials.add(binding.getUkeySerial());
|
||||
}
|
||||
LoginRequest loginRequest = new LoginRequest();
|
||||
loginRequest.setRoleCode(request.getRoleCode());
|
||||
loginRequest.setPassword(request.getPassword());
|
||||
loginRequest.setUkeySerials(matchedSerials);
|
||||
return login(loginRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化验证码生成入口。当前先返回可用于联调的占位图像内容。
|
||||
*/
|
||||
public CaptchaResponse issueCaptcha() {
|
||||
return requireCaptchaService().issueCaptcha();
|
||||
}
|
||||
|
||||
public CurrentUserResponse me(String username) {
|
||||
return me(username, null);
|
||||
}
|
||||
|
||||
public CurrentUserResponse me(String username, String sessionToken) {
|
||||
String normalized = username == null || username.isBlank() ? "admin" : username;
|
||||
RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(normalized).orElse(null);
|
||||
if (roleAccount != null) {
|
||||
CurrentUserResponse response = new CurrentUserResponse();
|
||||
response.setUsername(roleAccount.getRoleCode());
|
||||
response.setDisplayName(roleAccount.getDisplayName());
|
||||
response.setRole(roleAccount.getRoleCode());
|
||||
AuthSessionEntity session = sessionToken == null ? null : authSessionRepository.findBySessionToken(sessionToken).orElse(null);
|
||||
if (session != null) {
|
||||
response.setAuthLevel(session.getAuthLevel());
|
||||
}
|
||||
response.setNeedChangePassword(Boolean.TRUE.equals(roleAccount.getNeedChangePassword()));
|
||||
return response;
|
||||
}
|
||||
|
||||
AuthUserEntity entity = authUserRepository.findByUsername(normalized).orElse(null);
|
||||
|
||||
CurrentUserResponse response = new CurrentUserResponse();
|
||||
if (entity == null) {
|
||||
response.setUsername(normalized);
|
||||
response.setDisplayName("TMS Administrator");
|
||||
response.setRole("ADMIN");
|
||||
return response;
|
||||
}
|
||||
|
||||
response.setUsername(entity.getUsername());
|
||||
response.setDisplayName(entity.getDisplayName());
|
||||
response.setRole(entity.getRole());
|
||||
return response;
|
||||
}
|
||||
|
||||
public void logout(String sessionToken) {
|
||||
AuthSessionEntity session = requireActiveSession(sessionToken);
|
||||
session.setLogoutAt(now());
|
||||
session.setExpiresAt(now());
|
||||
authSessionRepository.update(session);
|
||||
}
|
||||
|
||||
public void changePassword(String sessionToken, String currentPassword, String newPassword) {
|
||||
AuthSessionEntity session = requireActiveSession(sessionToken);
|
||||
RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(session.getRoleCode())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account not found"));
|
||||
if (!passwordHasher.matches(currentPassword, roleAccount.getPasswordSalt(), roleAccount.getPasswordHash())) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "current password is incorrect");
|
||||
}
|
||||
|
||||
String newSalt = UUID.randomUUID().toString().replace("-", "");
|
||||
roleAccount.setPasswordSalt(newSalt);
|
||||
roleAccount.setPasswordHash(passwordHasher.hash(newPassword, newSalt));
|
||||
roleAccount.setNeedChangePassword(Boolean.FALSE);
|
||||
roleAccount.setFailedCount(0);
|
||||
roleAccount.setLockedUntil(null);
|
||||
roleAccount.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||
roleAccountRepository.update(roleAccount);
|
||||
}
|
||||
|
||||
private void validateRoleStatus(RoleAccountEntity roleAccount) {
|
||||
if (RoleAccountStatus.UNENABLED.name().equals(roleAccount.getStatus())) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role is not enabled");
|
||||
}
|
||||
if (RoleAccountStatus.LOCKED.name().equals(roleAccount.getStatus()) && isStillLocked(roleAccount)) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account is locked");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isStillLocked(RoleAccountEntity roleAccount) {
|
||||
return roleAccount.getLockedUntil() == null || roleAccount.getLockedUntil().isAfter(now());
|
||||
}
|
||||
|
||||
private void onPasswordFailed(RoleAccountEntity roleAccount) {
|
||||
int failedCount = roleAccount.getFailedCount() == null ? 0 : roleAccount.getFailedCount();
|
||||
failedCount++;
|
||||
roleAccount.setFailedCount(failedCount);
|
||||
if (failedCount >= MAX_FAILED_ATTEMPTS) {
|
||||
roleAccount.setStatus(RoleAccountStatus.LOCKED.name());
|
||||
roleAccount.setLockedUntil(now().plusMinutes(IDLE_TIMEOUT_MINUTES));
|
||||
roleAccountRepository.update(roleAccount);
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account is locked");
|
||||
}
|
||||
roleAccountRepository.update(roleAccount);
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "password is incorrect");
|
||||
}
|
||||
|
||||
private void resetFailureState(RoleAccountEntity roleAccount) {
|
||||
roleAccount.setFailedCount(0);
|
||||
roleAccount.setLockedUntil(null);
|
||||
roleAccount.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||
roleAccount.setLastLoginAt(now());
|
||||
roleAccount.setLastActiveAt(now());
|
||||
roleAccountRepository.update(roleAccount);
|
||||
}
|
||||
|
||||
private AuthMethod resolveAuthMethod(List<String> ukeySerials) {
|
||||
return (ukeySerials == null || ukeySerials.isEmpty()) ? AuthMethod.PASSWORD : AuthMethod.UKEY;
|
||||
}
|
||||
|
||||
private void ensureMasterKeyReady() {
|
||||
LmkService service = requireLmkService();
|
||||
MasterKeyStatus.StatusDetail status = service.getMasterKeyStatus();
|
||||
if (status == null || status.getCode() == MasterKeyStatus.ABNORMAL.getCode()) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "master key is not ready");
|
||||
}
|
||||
}
|
||||
|
||||
private AuthLevel resolveAuthLevel(RoleAccountEntity roleAccount, List<String> ukeySerials) {
|
||||
if (ukeySerials == null || ukeySerials.isEmpty()) {
|
||||
return AuthLevel.LIMITED;
|
||||
}
|
||||
|
||||
Set<String> uniqueSerials = new LinkedHashSet<>(ukeySerials);
|
||||
if (uniqueSerials.size() != roleAccount.getRequiredUkeyCount()) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
|
||||
}
|
||||
|
||||
List<RoleUkeyBindingEntity> bindings = roleUkeyBindingRepository.findActiveByRoleCode(roleAccount.getRoleCode());
|
||||
if (bindings.size() != roleAccount.getRequiredUkeyCount()) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
|
||||
}
|
||||
|
||||
Set<String> boundSerials = bindings.stream()
|
||||
.map(RoleUkeyBindingEntity::getUkeySerial)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
if (!boundSerials.containsAll(uniqueSerials)) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey verification failed");
|
||||
}
|
||||
return AuthLevel.FULL;
|
||||
}
|
||||
|
||||
private void validateUkeyCount(
|
||||
RoleCode roleCode,
|
||||
List<RoleUkeyBindingEntity> activeBindings,
|
||||
List<UkeyLoginProof> proofs
|
||||
) {
|
||||
if (proofs == null || proofs.size() != roleCode.getRequiredUkeyCount()) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
|
||||
}
|
||||
if (activeBindings.size() != roleCode.getRequiredUkeyCount()) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
|
||||
}
|
||||
}
|
||||
|
||||
private String buildIssuePayload(String roleCode, UkeyLoginProof proof, String authKeyPair) {
|
||||
UKeySignDTO dto = new UKeySignDTO();
|
||||
dto.setPubKey(proof.getPubKey());
|
||||
// 新卡统一使用规范角色编码参与签名和验签。
|
||||
dto.setRole(RoleCode.valueOf(roleCode).getCode());
|
||||
dto.setUid(proof.getUid());
|
||||
dto.setRid(proof.getRid());
|
||||
try {
|
||||
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair));
|
||||
} catch (JsonProcessingException ex) {
|
||||
throw new IllegalStateException("serialize ukey issue payload failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private AuthSessionEntity buildSession(String roleCode, AuthMethod authMethod, AuthLevel authLevel) {
|
||||
LocalDateTime issuedAt = now();
|
||||
AuthSessionEntity entity = new AuthSessionEntity();
|
||||
entity.setRoleCode(roleCode);
|
||||
entity.setAuthMethod(authMethod.name());
|
||||
entity.setAuthLevel(authLevel.name());
|
||||
entity.setSessionToken(tokenSupplier.get());
|
||||
entity.setIssuedAt(issuedAt);
|
||||
entity.setLastActiveAt(issuedAt);
|
||||
entity.setExpiresAt(issuedAt.plusMinutes(IDLE_TIMEOUT_MINUTES));
|
||||
return entity;
|
||||
}
|
||||
|
||||
private LocalDateTime now() {
|
||||
return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
private AuthSessionEntity requireActiveSession(String sessionToken) {
|
||||
AuthSessionEntity session = authSessionRepository.findBySessionToken(sessionToken)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "session not found"));
|
||||
if (session.getLogoutAt() != null || session.getExpiresAt() == null || !session.getExpiresAt().isAfter(now())) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "session expired");
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
private LmkService requireLmkService() {
|
||||
if (lmkService == null) {
|
||||
throw new IllegalStateException("lmkService is required for this operation");
|
||||
}
|
||||
return lmkService;
|
||||
}
|
||||
|
||||
private UkeyLoginRandomService requireUkeyLoginRandomService() {
|
||||
if (ukeyLoginRandomService == null) {
|
||||
throw new IllegalStateException("ukeyLoginRandomService is required for this operation");
|
||||
}
|
||||
return ukeyLoginRandomService;
|
||||
}
|
||||
|
||||
private CompatUkeyVerifier requireCompatUkeyVerifier() {
|
||||
if (compatUkeyVerifier == null) {
|
||||
throw new IllegalStateException("compatUkeyVerifier is required for this operation");
|
||||
}
|
||||
return compatUkeyVerifier;
|
||||
}
|
||||
|
||||
private CaptchaService requireCaptchaService() {
|
||||
if (captchaService == null) {
|
||||
throw new IllegalStateException("captchaService is required for this operation");
|
||||
}
|
||||
return captchaService;
|
||||
}
|
||||
void changePassword(String sessionToken, String currentPassword, String newPassword);
|
||||
}
|
||||
|
||||
@ -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.exception.BizException;
|
||||
@ -10,6 +10,8 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.cisd.tms.modules.auth.service.UkeyLoginRandomService;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -1,4 +1,4 @@
|
||||
package com.cisd.tms.modules.auth.service;
|
||||
package com.cisd.tms.modules.auth.service.impl;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
@ -6,6 +6,8 @@ import java.security.MessageDigest;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
|
||||
import com.cisd.tms.modules.auth.service.PasswordHasher;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ -1,10 +1,11 @@
|
||||
package com.cisd.tms.modules.auth.service;
|
||||
package com.cisd.tms.modules.auth.service.impl;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccExternalVerifyRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.auth.service.CompatUkeyVerifier;
|
||||
import com.cisd.tms.modules.mk.service.LmkService;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
@ -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 static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@ -42,6 +43,14 @@ class TmsApplicationTests {
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"summary\":\"UKey 登录\"")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIssueCaptchaThroughAuthEndpoint() throws Exception {
|
||||
mockMvc.perform(post("/api/v1/auth/captcha"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"captchaId\"")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"imageBase64\"")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateAuthRoleTablesAndSeedFixedRoleAccounts() {
|
||||
ClassPathResource migration = new ClassPathResource("db/migration/V1__tms_schema_full.sql");
|
||||
|
||||
@ -8,6 +8,7 @@ import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
|
||||
import com.cisd.tms.modules.auth.enums.RoleCode;
|
||||
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
|
||||
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
|
||||
import com.cisd.tms.modules.auth.service.impl.AuthAdminServiceImpl;
|
||||
import com.cisd.tms.modules.mk.dto.UKeySignDTO;
|
||||
import com.cisd.tms.modules.mk.dto.UKeySignResult;
|
||||
import com.cisd.tms.modules.mk.service.LmkService;
|
||||
@ -35,10 +36,9 @@ class AuthAdminServiceTest {
|
||||
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||
roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.UNENABLED, "OLD-HASH", "OLD-SALT", true));
|
||||
|
||||
AuthAdminService service = new AuthAdminService(
|
||||
AuthAdminService service = newAuthAdminService(
|
||||
roleAccounts,
|
||||
new InMemoryRoleUkeyBindingRepository(),
|
||||
new FakePasswordHasher(),
|
||||
FIXED_CLOCK,
|
||||
new FixedSaltSupplier("salt-001")
|
||||
);
|
||||
@ -52,10 +52,9 @@ class AuthAdminServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldRejectEnableRoleWhenOperatorIsNotKeyAdminFull() {
|
||||
AuthAdminService service = new AuthAdminService(
|
||||
AuthAdminService service = newAuthAdminService(
|
||||
new InMemoryRoleAccountRepository(),
|
||||
new InMemoryRoleUkeyBindingRepository(),
|
||||
new FakePasswordHasher(),
|
||||
FIXED_CLOCK,
|
||||
new FixedSaltSupplier("salt-001")
|
||||
);
|
||||
@ -76,10 +75,9 @@ class AuthAdminServiceTest {
|
||||
target.setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 30));
|
||||
roleAccounts.save(target);
|
||||
|
||||
AuthAdminService service = new AuthAdminService(
|
||||
AuthAdminService service = newAuthAdminService(
|
||||
roleAccounts,
|
||||
new InMemoryRoleUkeyBindingRepository(),
|
||||
new FakePasswordHasher(),
|
||||
FIXED_CLOCK,
|
||||
new FixedSaltSupplier("salt-002")
|
||||
);
|
||||
@ -102,11 +100,12 @@ class AuthAdminServiceTest {
|
||||
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
|
||||
org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
|
||||
|
||||
AuthAdminService service = new AuthAdminService(
|
||||
AuthAdminService service = new AuthAdminServiceImpl(
|
||||
new InMemoryRoleAccountRepository(),
|
||||
bindings,
|
||||
lmkService,
|
||||
new FakePasswordHasher(),
|
||||
lmkService,
|
||||
new com.fasterxml.jackson.databind.ObjectMapper(),
|
||||
FIXED_CLOCK,
|
||||
new FixedSaltSupplier("salt-001")
|
||||
);
|
||||
@ -139,11 +138,12 @@ class AuthAdminServiceTest {
|
||||
@Test
|
||||
void shouldRejectBindingWhenSlotExceedsRoleRequirement() {
|
||||
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
|
||||
AuthAdminService service = new AuthAdminService(
|
||||
AuthAdminService service = new AuthAdminServiceImpl(
|
||||
new InMemoryRoleAccountRepository(),
|
||||
new InMemoryRoleUkeyBindingRepository(),
|
||||
lmkService,
|
||||
new FakePasswordHasher(),
|
||||
lmkService,
|
||||
new com.fasterxml.jackson.databind.ObjectMapper(),
|
||||
FIXED_CLOCK,
|
||||
new FixedSaltSupplier("salt-001")
|
||||
);
|
||||
@ -175,11 +175,12 @@ class AuthAdminServiceTest {
|
||||
packet.setComponentIndex(1);
|
||||
org.mockito.Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet);
|
||||
|
||||
AuthAdminService service = new AuthAdminService(
|
||||
AuthAdminService service = new AuthAdminServiceImpl(
|
||||
new InMemoryRoleAccountRepository(),
|
||||
new InMemoryRoleUkeyBindingRepository(),
|
||||
lmkService,
|
||||
new FakePasswordHasher(),
|
||||
lmkService,
|
||||
new com.fasterxml.jackson.databind.ObjectMapper(),
|
||||
FIXED_CLOCK,
|
||||
new FixedSaltSupplier("salt-001")
|
||||
);
|
||||
@ -188,7 +189,7 @@ class AuthAdminServiceTest {
|
||||
dto.setRole("ignored");
|
||||
dto.setUid("1");
|
||||
dto.setRid("RID-001");
|
||||
dto.setExtra("EXTRA-001");
|
||||
// dto.setExtra("EXTRA-001");
|
||||
|
||||
UKeySignResult result = service.issueUkeyBindingSign(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto);
|
||||
|
||||
@ -207,11 +208,12 @@ class AuthAdminServiceTest {
|
||||
org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
|
||||
org.mockito.Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-002");
|
||||
|
||||
AuthAdminService service = new AuthAdminService(
|
||||
AuthAdminService service = new AuthAdminServiceImpl(
|
||||
new InMemoryRoleAccountRepository(),
|
||||
new InMemoryRoleUkeyBindingRepository(),
|
||||
lmkService,
|
||||
new FakePasswordHasher(),
|
||||
lmkService,
|
||||
new com.fasterxml.jackson.databind.ObjectMapper(),
|
||||
FIXED_CLOCK,
|
||||
new FixedSaltSupplier("salt-001")
|
||||
);
|
||||
@ -220,7 +222,7 @@ class AuthAdminServiceTest {
|
||||
dto.setRole("ignored");
|
||||
dto.setUid("1");
|
||||
dto.setRid("RID-002");
|
||||
dto.setExtra("EXTRA-KEEP");
|
||||
// dto.setExtra("EXTRA-KEEP");
|
||||
|
||||
UKeySignResult result = service.issueUkeyBindingSign(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.AUDIT_ADMIN.getCode(), dto);
|
||||
|
||||
@ -280,7 +282,7 @@ class AuthAdminServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static class FixedSaltSupplier implements java.util.function.Supplier<String> {
|
||||
private static class FixedSaltSupplier implements PasswordSaltGenerator {
|
||||
private final Deque<String> salts;
|
||||
|
||||
private FixedSaltSupplier(String... salts) {
|
||||
@ -288,7 +290,7 @@ class AuthAdminServiceTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get() {
|
||||
public String nextSalt() {
|
||||
return Optional.ofNullable(salts.pollFirst()).orElse("salt-fallback");
|
||||
}
|
||||
}
|
||||
@ -343,4 +345,21 @@ class AuthAdminServiceTest {
|
||||
list.add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private static AuthAdminService newAuthAdminService(
|
||||
InMemoryRoleAccountRepository roleAccounts,
|
||||
InMemoryRoleUkeyBindingRepository bindings,
|
||||
Clock clock,
|
||||
PasswordSaltGenerator saltGenerator
|
||||
) {
|
||||
return new AuthAdminServiceImpl(
|
||||
roleAccounts,
|
||||
bindings,
|
||||
new FakePasswordHasher(),
|
||||
org.mockito.Mockito.mock(LmkService.class),
|
||||
new com.fasterxml.jackson.databind.ObjectMapper(),
|
||||
clock,
|
||||
saltGenerator
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.cisd.tms.modules.auth.service;
|
||||
|
||||
import com.cisd.tms.modules.auth.service.impl.AuthPolicyServiceImpl;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@ -32,13 +33,13 @@ class AuthDomainModelTest {
|
||||
void shouldResolveExpectedAuthLevelByAuthMethod() {
|
||||
Assertions.assertEquals(
|
||||
com.cisd.tms.modules.auth.enums.AuthLevel.LIMITED,
|
||||
new com.cisd.tms.modules.auth.service.AuthPolicyService().resolveAuthLevel(
|
||||
new AuthPolicyServiceImpl().resolveAuthLevel(
|
||||
com.cisd.tms.modules.auth.enums.AuthMethod.PASSWORD
|
||||
)
|
||||
);
|
||||
Assertions.assertEquals(
|
||||
com.cisd.tms.modules.auth.enums.AuthLevel.FULL,
|
||||
new com.cisd.tms.modules.auth.service.AuthPolicyService().resolveAuthLevel(
|
||||
new AuthPolicyServiceImpl().resolveAuthLevel(
|
||||
com.cisd.tms.modules.auth.enums.AuthMethod.UKEY
|
||||
)
|
||||
);
|
||||
|
||||
@ -21,6 +21,8 @@ import com.cisd.tms.modules.auth.repository.AuthUserRepository;
|
||||
import com.cisd.tms.modules.auth.repository.AuthSessionRepository;
|
||||
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
|
||||
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
|
||||
import com.cisd.tms.modules.auth.service.impl.AuthPolicyServiceImpl;
|
||||
import com.cisd.tms.modules.auth.service.impl.AuthServiceImpl;
|
||||
import com.cisd.tms.modules.mk.enums.MasterKeyStatus;
|
||||
import com.cisd.tms.modules.mk.service.LmkService;
|
||||
import java.time.Clock;
|
||||
@ -48,12 +50,10 @@ class AuthServiceTest {
|
||||
InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository();
|
||||
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 0, true));
|
||||
|
||||
AuthService service = new AuthService(
|
||||
AuthService service = newAuthService(
|
||||
roleAccounts,
|
||||
new InMemoryLegacyAuthUserRepository(),
|
||||
sessions,
|
||||
ukeyBindings,
|
||||
new FakePasswordHasher(),
|
||||
FIXED_CLOCK,
|
||||
() -> "token-limited-001"
|
||||
);
|
||||
@ -86,12 +86,10 @@ class AuthServiceTest {
|
||||
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1"));
|
||||
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 2, "UK-2"));
|
||||
|
||||
AuthService service = new AuthService(
|
||||
AuthService service = newAuthService(
|
||||
roleAccounts,
|
||||
new InMemoryLegacyAuthUserRepository(),
|
||||
sessions,
|
||||
ukeyBindings,
|
||||
new FakePasswordHasher(),
|
||||
FIXED_CLOCK,
|
||||
() -> "token-full-001"
|
||||
);
|
||||
@ -115,12 +113,10 @@ class AuthServiceTest {
|
||||
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||
roleAccounts.save(role(RoleCode.OPS_ADMIN, RoleAccountStatus.UNENABLED, "HASH:12345678:SALT-O", "SALT-O", 0, true, null));
|
||||
|
||||
AuthService service = new AuthService(
|
||||
AuthService service = newAuthService(
|
||||
roleAccounts,
|
||||
new InMemoryLegacyAuthUserRepository(),
|
||||
new InMemoryAuthSessionRepository(),
|
||||
new InMemoryRoleUkeyBindingRepository(),
|
||||
new FakePasswordHasher(),
|
||||
FIXED_CLOCK,
|
||||
() -> "unused"
|
||||
);
|
||||
@ -140,12 +136,10 @@ class AuthServiceTest {
|
||||
RoleAccountEntity role = activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 4, false);
|
||||
roleAccounts.save(role);
|
||||
|
||||
AuthService service = new AuthService(
|
||||
AuthService service = newAuthService(
|
||||
roleAccounts,
|
||||
new InMemoryLegacyAuthUserRepository(),
|
||||
new InMemoryAuthSessionRepository(),
|
||||
new InMemoryRoleUkeyBindingRepository(),
|
||||
new FakePasswordHasher(),
|
||||
FIXED_CLOCK,
|
||||
() -> "unused"
|
||||
);
|
||||
@ -172,12 +166,10 @@ class AuthServiceTest {
|
||||
ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 2, "UK-2"));
|
||||
ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 3, "UK-3"));
|
||||
|
||||
AuthService service = new AuthService(
|
||||
AuthService service = newAuthService(
|
||||
roleAccounts,
|
||||
new InMemoryLegacyAuthUserRepository(),
|
||||
new InMemoryAuthSessionRepository(),
|
||||
ukeyBindings,
|
||||
new FakePasswordHasher(),
|
||||
FIXED_CLOCK,
|
||||
() -> "unused"
|
||||
);
|
||||
@ -199,12 +191,10 @@ class AuthServiceTest {
|
||||
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 0, false));
|
||||
sessions.save(session("token-me-001", RoleCode.AUDIT_ADMIN.getCode(), AuthLevel.LIMITED.name()));
|
||||
|
||||
AuthService service = new AuthService(
|
||||
AuthService service = newAuthService(
|
||||
roleAccounts,
|
||||
new InMemoryLegacyAuthUserRepository(),
|
||||
sessions,
|
||||
new InMemoryRoleUkeyBindingRepository(),
|
||||
new FakePasswordHasher(),
|
||||
FIXED_CLOCK,
|
||||
() -> "unused"
|
||||
);
|
||||
@ -225,12 +215,10 @@ class AuthServiceTest {
|
||||
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "HASH:12345678:SALT-K", "SALT-K", 0, true));
|
||||
sessions.save(session("token-change-001", RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name()));
|
||||
|
||||
AuthService service = new AuthService(
|
||||
AuthService service = newAuthService(
|
||||
roleAccounts,
|
||||
new InMemoryLegacyAuthUserRepository(),
|
||||
sessions,
|
||||
new InMemoryRoleUkeyBindingRepository(),
|
||||
new FakePasswordHasher(),
|
||||
FIXED_CLOCK,
|
||||
() -> "unused"
|
||||
);
|
||||
@ -250,12 +238,10 @@ class AuthServiceTest {
|
||||
roleAccounts.save(activeRole(RoleCode.OPS_ADMIN, "HASH:12345678:SALT-O", "SALT-O", 0, false));
|
||||
sessions.save(session("token-logout-001", RoleCode.OPS_ADMIN.getCode(), AuthLevel.LIMITED.name()));
|
||||
|
||||
AuthService service = new AuthService(
|
||||
AuthService service = newAuthService(
|
||||
roleAccounts,
|
||||
new InMemoryLegacyAuthUserRepository(),
|
||||
sessions,
|
||||
new InMemoryRoleUkeyBindingRepository(),
|
||||
new FakePasswordHasher(),
|
||||
FIXED_CLOCK,
|
||||
() -> "unused"
|
||||
);
|
||||
@ -272,7 +258,7 @@ class AuthServiceTest {
|
||||
UkeyLoginRandomService randomService = org.mockito.Mockito.mock(UkeyLoginRandomService.class);
|
||||
org.mockito.Mockito.when(randomService.issue(RoleCode.SUPER_ADMIN.getCode(), 3)).thenReturn(List.of("RB-1", "RB-2", "RB-3"));
|
||||
|
||||
AuthService service = new AuthService(
|
||||
AuthService service = new AuthServiceImpl(
|
||||
new InMemoryRoleAccountRepository(),
|
||||
new InMemoryLegacyAuthUserRepository(),
|
||||
new InMemoryAuthSessionRepository(),
|
||||
@ -284,7 +270,9 @@ class AuthServiceTest {
|
||||
new InMemoryCaptchaService(),
|
||||
new ObjectMapper(),
|
||||
FIXED_CLOCK,
|
||||
() -> "unused"
|
||||
() -> "unused",
|
||||
() -> "salt-unused",
|
||||
new AuthPolicyServiceImpl()
|
||||
);
|
||||
|
||||
UkeyLoginRandomRequest request = new UkeyLoginRandomRequest();
|
||||
@ -305,7 +293,7 @@ class AuthServiceTest {
|
||||
InMemoryCaptchaService captchaService = new InMemoryCaptchaService();
|
||||
String captchaId = captchaService.issue("ABCD").getCaptchaId();
|
||||
|
||||
AuthService service = new AuthService(
|
||||
AuthService service = new AuthServiceImpl(
|
||||
roleAccounts,
|
||||
new InMemoryLegacyAuthUserRepository(),
|
||||
new InMemoryAuthSessionRepository(),
|
||||
@ -317,7 +305,9 @@ class AuthServiceTest {
|
||||
captchaService,
|
||||
new ObjectMapper(),
|
||||
FIXED_CLOCK,
|
||||
() -> "unused"
|
||||
() -> "unused",
|
||||
() -> "salt-unused",
|
||||
new AuthPolicyServiceImpl()
|
||||
);
|
||||
|
||||
PasswordLoginRequest request = new PasswordLoginRequest();
|
||||
@ -339,7 +329,7 @@ class AuthServiceTest {
|
||||
org.mockito.Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001"));
|
||||
InMemoryCaptchaService captchaService = new InMemoryCaptchaService();
|
||||
|
||||
AuthService service = new AuthService(
|
||||
AuthService service = new AuthServiceImpl(
|
||||
roleAccounts,
|
||||
new InMemoryLegacyAuthUserRepository(),
|
||||
new InMemoryAuthSessionRepository(),
|
||||
@ -351,7 +341,9 @@ class AuthServiceTest {
|
||||
captchaService,
|
||||
new ObjectMapper(),
|
||||
FIXED_CLOCK,
|
||||
() -> "token-password-002"
|
||||
() -> "token-password-002",
|
||||
() -> "salt-password-002",
|
||||
new AuthPolicyServiceImpl()
|
||||
);
|
||||
|
||||
CaptchaResponse captcha = service.issueCaptcha();
|
||||
@ -384,7 +376,7 @@ class AuthServiceTest {
|
||||
org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
|
||||
org.mockito.Mockito.when(randomService.issue(RoleCode.KEY_ADMIN.getCode(), 2)).thenReturn(List.of("RB-1", "RB-2"));
|
||||
|
||||
AuthService service = new AuthService(
|
||||
AuthService service = new AuthServiceImpl(
|
||||
roleAccounts,
|
||||
new InMemoryLegacyAuthUserRepository(),
|
||||
sessions,
|
||||
@ -396,7 +388,9 @@ class AuthServiceTest {
|
||||
new InMemoryCaptchaService(),
|
||||
new ObjectMapper(),
|
||||
FIXED_CLOCK,
|
||||
() -> "token-full-ukey-002"
|
||||
() -> "token-full-ukey-002",
|
||||
() -> "salt-full-ukey-002",
|
||||
new AuthPolicyServiceImpl()
|
||||
);
|
||||
|
||||
UkeyLoginRandomRequest randomRequest = new UkeyLoginRandomRequest();
|
||||
@ -638,4 +632,29 @@ class AuthServiceTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static AuthService newAuthService(
|
||||
InMemoryRoleAccountRepository roleAccounts,
|
||||
InMemoryAuthSessionRepository sessions,
|
||||
InMemoryRoleUkeyBindingRepository ukeyBindings,
|
||||
Clock clock,
|
||||
SessionTokenGenerator tokenGenerator
|
||||
) {
|
||||
return new AuthServiceImpl(
|
||||
roleAccounts,
|
||||
new InMemoryLegacyAuthUserRepository(),
|
||||
sessions,
|
||||
ukeyBindings,
|
||||
new FakePasswordHasher(),
|
||||
org.mockito.Mockito.mock(LmkService.class),
|
||||
org.mockito.Mockito.mock(UkeyLoginRandomService.class),
|
||||
org.mockito.Mockito.mock(CompatUkeyVerifier.class),
|
||||
new InMemoryCaptchaService(),
|
||||
new ObjectMapper(),
|
||||
clock,
|
||||
tokenGenerator,
|
||||
() -> "salt-test",
|
||||
new AuthPolicyServiceImpl()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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