package com.sinata.appointment.service; import com.sinata.appointment.config.AdminLoginCryptoProperties; import com.sinata.appointment.constants.BizErrorCode; import com.sinata.appointment.exception.BizException; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Base64; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @Service @RequiredArgsConstructor public class AdminLoginCryptoService { private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding"; private static final String AES = "AES"; private static final String SEPARATOR = "|"; private final AdminLoginCryptoProperties properties; public String decryptLoginField(String encryptedValue, String fieldLabel) { if (!StringUtils.hasText(encryptedValue)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, fieldLabel + "不能为空"); } String plain = decryptAes(encryptedValue.trim()); int separatorIndex = plain.indexOf(SEPARATOR); if (separatorIndex <= 0 || separatorIndex >= plain.length() - 1) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "登录信息格式错误,请重试"); } long timestamp = parseTimestamp(plain.substring(0, separatorIndex)); validateTimestamp(timestamp); String value = plain.substring(separatorIndex + 1); if (!StringUtils.hasText(value)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, fieldLabel + "不能为空"); } return value.trim(); } private String decryptAes(String encryptedValue) { validateKeyAndIv(); try { Cipher cipher = Cipher.getInstance(TRANSFORMATION); SecretKeySpec keySpec = new SecretKeySpec(properties.getKey().getBytes(StandardCharsets.UTF_8), AES); IvParameterSpec ivSpec = new IvParameterSpec(properties.getIv().getBytes(StandardCharsets.UTF_8)); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedValue.getBytes(StandardCharsets.UTF_8))); return new String(decrypted, StandardCharsets.UTF_8); } catch (Exception ex) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "登录信息解密失败,请重试"); } } private long parseTimestamp(String raw) { try { return Long.parseLong(raw); } catch (NumberFormatException ex) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "登录信息时间戳无效,请重试"); } } private void validateTimestamp(long timestamp) { long now = Instant.now().toEpochMilli(); if (Math.abs(now - timestamp) > properties.getMaxSkewMillis()) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "登录信息已过期,请刷新后重试"); } } private void validateKeyAndIv() { if (!StringUtils.hasText(properties.getKey()) || !StringUtils.hasText(properties.getIv()) || properties.getKey().length() != 16 || properties.getIv().length() != 16) { int keyLength = properties.getKey() == null ? 0 : properties.getKey().length(); int ivLength = properties.getIv() == null ? 0 : properties.getIv().length(); throw new IllegalStateException( "管理后台登录AES配置无效,key/iv 必须为16位字符,当前 key=" + keyLength + " 位, iv=" + ivLength + " 位"); } } }