Ver Fonte

优化推送营服中心请求接口和返回数据的加解密逻辑

james há 4 dias atrás
pai
commit
643bd2fd74

+ 13 - 8
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/client/VppUnHttpExecutor.java

@@ -6,6 +6,8 @@ import com.usky.vpp.config.VppUnProperties;
 import com.usky.vpp.config.VppUnPropertiesRegistry;
 import com.usky.vpp.config.VppUnTenantContext;
 import com.usky.vpp.crypto.VppUnCryptoService;
+import com.usky.vpp.crypto.VppUnSmCryptoUtil;
+import com.usky.vpp.util.VppUnCipherPayloadHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -80,8 +82,9 @@ public class VppUnHttpExecutor {
             String requestBody = plainJson;
             boolean useCrypto = useCrypto(serviceName);
             if (useCrypto && cryptoService.isActive()) {
-                requestBody = cryptoService.encryptRequest(plainJson);
-                headers.set("X-Sign", cryptoService.signRequest(requestBody));
+                VppUnSmCryptoUtil.EncryptedPayload payload = cryptoService.prepareRequest(plainJson);
+                requestBody = VppUnCipherPayloadHelper.wrapCipherPayload(payload.getCipherBase64(), objectMapper);
+                headers.set("X-Sign", payload.getSignBase64());
             }
             if (withToken) {
                 headers.set("token", tokenHolder.getToken());
@@ -127,13 +130,15 @@ public class VppUnHttpExecutor {
             return "{}";
         }
         String trimmed = body.trim();
-        if (useCrypto && cryptoService.isActive() && !trimmed.startsWith("{")) {
-            if (!cryptoService.verifyResponse(trimmed, signHeader)) {
-                throw new BusinessException("UN 响应验签失败");
-            }
-            return cryptoService.decryptResponse(trimmed);
+        if (!useCrypto || !cryptoService.isActive()) {
+            return trimmed;
+        }
+        if (trimmed.startsWith("{")) {
+            log.warn("UN 响应为明文 JSON,跳过验签解密");
+            return trimmed;
         }
-        return trimmed;
+        String cipherBase64 = VppUnCipherPayloadHelper.extractCipherPayload(trimmed, objectMapper);
+        return cryptoService.parseResponse(cipherBase64, signHeader);
     }
 
     private static boolean useCrypto(String serviceName) {

+ 88 - 59
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/crypto/VppUnCryptoService.java

@@ -1,9 +1,6 @@
 package com.usky.vpp.crypto;
 
-import cn.hutool.core.codec.Base64;
-import cn.hutool.crypto.SmUtil;
-import cn.hutool.crypto.asymmetric.KeyType;
-import cn.hutool.crypto.asymmetric.SM2;
+import com.usky.common.core.exception.BusinessException;
 import com.usky.vpp.config.VppUnProperties;
 import com.usky.vpp.config.VppUnPropertiesRegistry;
 import com.usky.vpp.config.VppUnTenantContext;
@@ -13,10 +10,10 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 
-import java.nio.charset.StandardCharsets;
+import java.util.Base64;
 
 /**
- * 运管平台 UN/DN 国密加解密与签名(SM2/SM3withSM2)
+ * 运管平台 UN/DN 加解密门面(除 TokenRequest 外均启用)。
  */
 @Service
 public class VppUnCryptoService {
@@ -27,20 +24,49 @@ public class VppUnCryptoService {
     private VppUnPropertiesRegistry propertiesRegistry;
 
     public boolean isActive() {
-        VppUnProperties properties = currentProperties();
-        return isCryptoActive(properties);
+        return isCryptoActive(currentProperties());
     }
 
     public boolean isAnyActive() {
         return propertiesRegistry.hasCryptoEnabledTenant();
     }
 
+    /** DN 主动请求 UN:UN 公钥加密 + DN 私钥对密文字节签名。 */
+    public VppUnSmCryptoUtil.EncryptedPayload prepareRequest(String plainJson) {
+        VppUnProperties properties = requireCryptoProperties();
+        try {
+            return VppUnSmCryptoUtil.encryptAndSignRequest(
+                    plainJson, properties.getUnPublicKey(), properties.getDnPrivateKey());
+        } catch (Exception ex) {
+            throw new BusinessException("UN 请求加密签名失败: " + ex.getMessage());
+        }
+    }
+
+    /** DN 解析 UN 响应:UN 公钥验签 + DN 私钥解密。 */
+    public String parseResponse(String cipherBase64, String signBase64) {
+        if (!StringUtils.hasText(signBase64)) {
+            log.warn("UN 响应缺少 X-Sign");
+            throw new BusinessException("UN 响应验签失败");
+        }
+        VppUnProperties properties = requireCryptoProperties();
+        try {
+            return VppUnSmCryptoUtil.verifyAndDecryptResponse(
+                    cipherBase64, signBase64, properties.getUnPublicKey(), properties.getDnPrivateKey());
+        } catch (BusinessException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            if (ex.getMessage() != null && ex.getMessage().toLowerCase().contains("sign verify")) {
+                throw new BusinessException("UN 响应验签失败");
+            }
+            throw new BusinessException("UN 响应解密失败: " + ex.getMessage());
+        }
+    }
+
+    /** UN 被动调用 DN:验签后解密。 */
     public String decryptInboundWithAutoTenant(String cipherBase64, String signBase64) {
         Integer tenantId = VppUnTenantContext.getTenantId();
         if (tenantId != null) {
-            if (!verifyInbound(cipherBase64, signBase64)) {
-                throw new com.usky.common.core.exception.BusinessException("UN 请求验签失败");
-            }
+            assertInboundVerified(cipherBase64, signBase64);
             return decryptInbound(cipherBase64);
         }
         for (VppUnProperties properties : propertiesRegistry.getAll()) {
@@ -48,71 +74,74 @@ public class VppUnCryptoService {
                 continue;
             }
             VppUnTenantContext.setTenantId(properties.getTenantId());
-            if (verifyInbound(cipherBase64, signBase64)) {
-                return decryptInbound(cipherBase64);
+            try {
+                if (verifyInbound(cipherBase64, signBase64)) {
+                    return decryptInbound(cipherBase64);
+                }
+            } finally {
+                VppUnTenantContext.clear();
             }
-            VppUnTenantContext.clear();
         }
-        throw new com.usky.common.core.exception.BusinessException("UN 请求验签失败");
-    }
-
-    private VppUnProperties currentProperties() {
-        return propertiesRegistry.getCurrent();
-    }
-
-    private boolean isCryptoActive(VppUnProperties properties) {
-        return properties != null
-                && Boolean.TRUE.equals(properties.getCryptoEnabled())
-                && StringUtils.hasText(properties.getUnPublicKey())
-                && StringUtils.hasText(properties.getDnPrivateKey());
-    }
-
-    public String encryptRequest(String plainJson) {
-        VppUnProperties properties = currentProperties();
-        SM2 sm2 = SmUtil.sm2(null, properties.getUnPublicKey());
-        return sm2.encryptBase64(plainJson, KeyType.PublicKey);
+        throw new BusinessException("UN 请求验签失败");
     }
 
-    public String signRequest(String cipherBase64) {
+    /** UN→DN 请求验签(UN 公钥,对密文字节验签)。 */
+    public boolean verifyInbound(String cipherBase64, String signBase64) {
+        if (!StringUtils.hasText(signBase64)) {
+            log.warn("UN 请求缺少 X-Sign");
+            return false;
+        }
         VppUnProperties properties = currentProperties();
-        SM2 sm2 = SmUtil.sm2(properties.getDnPrivateKey(), null);
-        byte[] sign = sm2.sign(cipherBase64.getBytes(StandardCharsets.UTF_8));
-        return Base64.encode(sign);
+        if (!isCryptoActive(properties)) {
+            return true;
+        }
+        try {
+            byte[] cipherBytes = Base64.getDecoder().decode(cipherBase64.trim());
+            return VppUnSmCryptoUtil.verifyCipherBytes(
+                    cipherBytes, signBase64, properties.getUnPublicKey());
+        } catch (Exception ex) {
+            log.warn("UN 请求验签异常: {}", ex.getMessage());
+            return false;
+        }
     }
 
-    public String decryptResponse(String cipherBase64) {
-        VppUnProperties properties = currentProperties();
-        SM2 sm2 = SmUtil.sm2(properties.getDnPrivateKey(), properties.getDnPublicKey());
-        return sm2.decryptStr(cipherBase64, KeyType.PrivateKey);
+    /** UN→DN 请求解密(DN 私钥)。 */
+    public String decryptInbound(String cipherBase64) {
+        VppUnProperties properties = requireCryptoProperties();
+        try {
+            return VppUnSmCryptoUtil.decryptByDnPrivateKey(cipherBase64, properties.getDnPrivateKey());
+        } catch (Exception ex) {
+            throw new BusinessException("UN 请求解密失败: " + ex.getMessage());
+        }
     }
 
-    public boolean verifyResponse(String cipherBase64, String signBase64) {
-        return verifyInbound(cipherBase64, signBase64);
+    /** DN→UN 被动响应。 */
+    public VppUnSmCryptoUtil.EncryptedPayload prepareOutbound(String plainJson) {
+        return prepareRequest(plainJson);
     }
 
-    /** UN→DN 请求验签 */
-    public boolean verifyInbound(String cipherBase64, String signBase64) {
-        if (!StringUtils.hasText(signBase64)) {
-            log.warn("UN 请求缺少 X-Sign,跳过验签");
-            return true;
+    private void assertInboundVerified(String cipherBase64, String signBase64) {
+        if (!verifyInbound(cipherBase64, signBase64)) {
+            throw new BusinessException("UN 请求验签失败");
         }
-        VppUnProperties properties = currentProperties();
-        SM2 sm2 = SmUtil.sm2(null, properties.getUnPublicKey());
-        return sm2.verify(cipherBase64.getBytes(StandardCharsets.UTF_8), Base64.decode(signBase64));
     }
 
-    /** UN→DN 请求解密 */
-    public String decryptInbound(String cipherBase64) {
-        return decryptResponse(cipherBase64);
+    private VppUnProperties requireCryptoProperties() {
+        VppUnProperties properties = currentProperties();
+        if (!isCryptoActive(properties)) {
+            throw new BusinessException("运管平台国密加解密未启用或密钥未配置");
+        }
+        return properties;
     }
 
-    /** DN→UN 响应加密 */
-    public String encryptOutbound(String plainJson) {
-        return encryptRequest(plainJson);
+    private VppUnProperties currentProperties() {
+        return propertiesRegistry.getCurrent();
     }
 
-    /** DN→UN 响应签名 */
-    public String signOutbound(String cipherBase64) {
-        return signRequest(cipherBase64);
+    private boolean isCryptoActive(VppUnProperties properties) {
+        return properties != null
+                && Boolean.TRUE.equals(properties.getCryptoEnabled())
+                && StringUtils.hasText(properties.getUnPublicKey())
+                && StringUtils.hasText(properties.getDnPrivateKey());
     }
 }

+ 232 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/crypto/VppUnSmCryptoUtil.java

@@ -0,0 +1,232 @@
+package com.usky.vpp.crypto;
+
+import org.bouncycastle.crypto.engines.SM2Engine;
+import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
+import org.bouncycastle.crypto.params.ECPublicKeyParameters;
+import org.bouncycastle.crypto.params.ParametersWithRandom;
+import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import cn.hutool.crypto.SmUtil;
+import cn.hutool.crypto.asymmetric.KeyType;
+import cn.hutool.crypto.asymmetric.SM2;
+
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.Signature;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+
+import javax.crypto.Cipher;
+
+/**
+ * 运管平台 UN/DN 国密工具(SM2 加解密 + SM3withSM2 签名/验签)。
+ * <p>加解密优先使用 BC {@code Cipher("SM2")}(与联调 SM3Util 一致,C1C2C3);签名/验签针对密文字节。</p>
+ */
+public final class VppUnSmCryptoUtil {
+
+    private static final String EC = "EC";
+    private static final String BC = BouncyCastleProvider.PROVIDER_NAME;
+    private static final String SM2 = "SM2";
+    private static final String SIGNATURE_ALGORITHM = "SM3withSM2";
+
+    static {
+        if (Security.getProvider(BC) == null) {
+            Security.addProvider(new BouncyCastleProvider());
+        }
+    }
+
+    private VppUnSmCryptoUtil() {
+    }
+
+    public static final class EncryptedPayload {
+        private final String cipherBase64;
+        private final String signBase64;
+
+        public EncryptedPayload(String cipherBase64, String signBase64) {
+            this.cipherBase64 = cipherBase64;
+            this.signBase64 = signBase64;
+        }
+
+        public String getCipherBase64() {
+            return cipherBase64;
+        }
+
+        public String getSignBase64() {
+            return signBase64;
+        }
+    }
+
+    /** DN 请求 UN:UN 公钥加密 + DN 私钥对密文字节签名。 */
+    public static EncryptedPayload encryptAndSignRequest(String plainJson,
+                                                         String unPublicKeyBase64,
+                                                         String dnPrivateKeyBase64) throws Exception {
+        byte[] cipherBytes = encryptByPublicKey(plainJson, unPublicKeyBase64);
+        String cipherBase64 = Base64.getEncoder().encodeToString(cipherBytes);
+        String signBase64 = signCipherBytes(cipherBytes, dnPrivateKeyBase64);
+        return new EncryptedPayload(cipherBase64, signBase64);
+    }
+
+    /** DN 解析 UN 响应:UN 公钥验签密文字节 + DN 私钥解密。 */
+    public static String verifyAndDecryptResponse(String cipherBase64,
+                                                  String signBase64,
+                                                  String unPublicKeyBase64,
+                                                  String dnPrivateKeyBase64) throws Exception {
+        byte[] cipherBytes = decodeBase64(cipherBase64);
+        if (!verifyCipherBytes(cipherBytes, signBase64, unPublicKeyBase64)) {
+            throw new IllegalStateException("sign verify fail");
+        }
+        return new String(decryptByPrivateKey(cipherBytes, dnPrivateKeyBase64), StandardCharsets.UTF_8);
+    }
+
+    /** 仅 SM2 解密(已验签场景)。 */
+    public static String decryptByDnPrivateKey(String cipherBase64, String dnPrivateKeyBase64) throws Exception {
+        byte[] cipherBytes = decodeBase64(cipherBase64);
+        return new String(decryptByPrivateKey(cipherBytes, dnPrivateKeyBase64), StandardCharsets.UTF_8);
+    }
+
+    public static String signCipherBytes(byte[] cipherBytes, String privateKeyBase64) throws Exception {
+        return sign(cipherBytes, privateKeyBase64);
+    }
+
+    public static boolean verifyCipherBytes(byte[] cipherBytes, String signBase64, String publicKeyBase64)
+            throws Exception {
+        return verify(cipherBytes, publicKeyBase64, signBase64);
+    }
+
+    private static String sign(byte[] data, String privateKeyBase64) throws Exception {
+        try {
+            PrivateKey privateKey = loadPrivateKey(privateKeyBase64);
+            Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM, BC);
+            signature.initSign(privateKey);
+            signature.update(data);
+            return Base64.getEncoder().encodeToString(signature.sign());
+        } catch (Exception ex) {
+            SM2 sm2 = SmUtil.sm2(privateKeyBase64.trim(), null);
+            return Base64.getEncoder().encodeToString(sm2.sign(data));
+        }
+    }
+
+    private static boolean verify(byte[] data, String publicKeyBase64, String signBase64) throws Exception {
+        try {
+            PublicKey publicKey = loadPublicKey(publicKeyBase64);
+            Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM, BC);
+            signature.initVerify(publicKey);
+            signature.update(data);
+            return signature.verify(decodeBase64(signBase64));
+        } catch (Exception ex) {
+            SM2 sm2 = SmUtil.sm2(null, publicKeyBase64.trim());
+            return sm2.verify(data, decodeBase64(signBase64));
+        }
+    }
+
+    /** 与 SM3Util.encryptByPublicKey 一致:BC Cipher SM2。 */
+    private static byte[] encryptByPublicKey(String data, String publicKeyBase64) throws Exception {
+        Exception last = null;
+        try {
+            return encryptByPublicKeyCipher(data, publicKeyBase64);
+        } catch (Exception ex) {
+            last = ex;
+        }
+        try {
+            return encryptByPublicKeyEngine(data, publicKeyBase64, SM2Engine.Mode.C1C2C3);
+        } catch (Exception ex) {
+            last = ex;
+        }
+        try {
+            return encryptByPublicKeyEngine(data, publicKeyBase64, SM2Engine.Mode.C1C3C2);
+        } catch (Exception ex) {
+            last = ex;
+        }
+        try {
+            SM2 sm2 = SmUtil.sm2(null, publicKeyBase64.trim());
+            return sm2.encrypt(data.getBytes(StandardCharsets.UTF_8), KeyType.PublicKey);
+        } catch (Exception ex) {
+            last = ex;
+        }
+        throw last != null ? last : new IllegalStateException("SM2 加密失败");
+    }
+
+    /** 与 SM3Util.decryptByPrivateKey 一致,并兼容 C1C3C2 密文。 */
+    private static byte[] decryptByPrivateKey(byte[] cipherBytes, String privateKeyBase64) throws Exception {
+        Exception last = null;
+        try {
+            return decryptByPrivateKeyCipher(cipherBytes, privateKeyBase64);
+        } catch (Exception ex) {
+            last = ex;
+        }
+        try {
+            return decryptByPrivateKeyEngine(cipherBytes, privateKeyBase64, SM2Engine.Mode.C1C2C3);
+        } catch (Exception ex) {
+            last = ex;
+        }
+        try {
+            return decryptByPrivateKeyEngine(cipherBytes, privateKeyBase64, SM2Engine.Mode.C1C3C2);
+        } catch (Exception ex) {
+            last = ex;
+        }
+        try {
+            SM2 sm2 = SmUtil.sm2(privateKeyBase64.trim(), null);
+            return sm2.decrypt(cipherBytes, KeyType.PrivateKey);
+        } catch (Exception ex) {
+            last = ex;
+        }
+        throw last != null ? last : new IllegalStateException("SM2 解密失败");
+    }
+
+    private static byte[] encryptByPublicKeyCipher(String data, String publicKeyBase64) throws Exception {
+        PublicKey publicKey = loadPublicKey(publicKeyBase64);
+        Cipher cipher = Cipher.getInstance(SM2, BC);
+        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+        return cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
+    }
+
+    private static byte[] decryptByPrivateKeyCipher(byte[] cipherBytes, String privateKeyBase64) throws Exception {
+        PrivateKey privateKey = loadPrivateKey(privateKeyBase64);
+        Cipher cipher = Cipher.getInstance(SM2, BC);
+        cipher.init(Cipher.DECRYPT_MODE, privateKey);
+        return cipher.doFinal(cipherBytes);
+    }
+
+    private static byte[] encryptByPublicKeyEngine(String data, String publicKeyBase64, SM2Engine.Mode mode)
+            throws Exception {
+        PublicKey publicKey = loadPublicKey(publicKeyBase64);
+        ECPublicKeyParameters publicKeyParameters =
+                (ECPublicKeyParameters) ECUtil.generatePublicKeyParameter(publicKey);
+        SM2Engine engine = new SM2Engine(mode);
+        engine.init(true, new ParametersWithRandom(publicKeyParameters, new SecureRandom()));
+        byte[] input = data.getBytes(StandardCharsets.UTF_8);
+        return engine.processBlock(input, 0, input.length);
+    }
+
+    private static byte[] decryptByPrivateKeyEngine(byte[] cipherBytes, String privateKeyBase64, SM2Engine.Mode mode)
+            throws Exception {
+        PrivateKey privateKey = loadPrivateKey(privateKeyBase64);
+        ECPrivateKeyParameters privateKeyParameters =
+                (ECPrivateKeyParameters) ECUtil.generatePrivateKeyParameter(privateKey);
+        SM2Engine engine = new SM2Engine(mode);
+        engine.init(false, privateKeyParameters);
+        return engine.processBlock(cipherBytes, 0, cipherBytes.length);
+    }
+
+    private static PublicKey loadPublicKey(String publicKeyBase64) throws Exception {
+        byte[] keyBytes = decodeBase64(publicKeyBase64);
+        KeyFactory keyFactory = KeyFactory.getInstance(EC, BC);
+        return keyFactory.generatePublic(new X509EncodedKeySpec(keyBytes));
+    }
+
+    private static PrivateKey loadPrivateKey(String privateKeyBase64) throws Exception {
+        byte[] keyBytes = decodeBase64(privateKeyBase64);
+        KeyFactory keyFactory = KeyFactory.getInstance(EC, BC);
+        return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
+    }
+
+    private static byte[] decodeBase64(String value) {
+        return Base64.getDecoder().decode(value.trim());
+    }
+}

+ 45 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnCipherPayloadHelper.java

@@ -0,0 +1,45 @@
+package com.usky.vpp.util;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.common.core.exception.BusinessException;
+import org.springframework.util.StringUtils;
+
+/**
+ * 运管平台 UN/DN 国密 HTTP 载荷解析(application/json 下密文常为 JSON 字符串)。
+ */
+public final class VppUnCipherPayloadHelper {
+
+    private VppUnCipherPayloadHelper() {
+    }
+
+    /**
+     * 从 HTTP body 提取 Base64 密文。
+     * <ul>
+     *   <li>JSON 字符串:{@code "MIIB..."}</li>
+     *   <li>裸 Base64:{@code MIIB...}</li>
+     * </ul>
+     */
+    public static String extractCipherPayload(String body, ObjectMapper objectMapper) {
+        if (!StringUtils.hasText(body)) {
+            return "";
+        }
+        String trimmed = body.trim();
+        if (trimmed.startsWith("\"")) {
+            try {
+                return objectMapper.readValue(trimmed, String.class);
+            } catch (Exception ex) {
+                throw new BusinessException("密文 JSON 字符串解析失败: " + ex.getMessage());
+            }
+        }
+        return trimmed;
+    }
+
+    /** 将 Base64 密文包装为合法 JSON 字符串请求体。 */
+    public static String wrapCipherPayload(String cipherBase64, ObjectMapper objectMapper) {
+        try {
+            return objectMapper.writeValueAsString(cipherBase64);
+        } catch (Exception ex) {
+            throw new BusinessException("密文 JSON 包装失败: " + ex.getMessage());
+        }
+    }
+}

+ 3 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnMessageBuilder.java

@@ -219,7 +219,9 @@ public final class VppUnMessageBuilder {
         description.put("rID", rid);
         Map<String, Object> metricBody = new LinkedHashMap<>();
         metricBody.put("metricName", metric.getMetricName());
-        metricBody.put("multiplier", metric.getMultiplier());
+        if (StringUtils.hasText(metric.getMultiplier())) {
+            metricBody.put("multiplier", metric.getMultiplier());
+        }
         metricBody.put("symbol", metric.getSymbol());
         description.put("metric", metricBody);
         Map<String, Object> dataSource = new LinkedHashMap<>();

+ 5 - 5
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnRegisterMetricDefinitions.java

@@ -10,10 +10,10 @@ import java.util.List;
 public final class VppUnRegisterMetricDefinitions {
 
     private static final String MULTIPLIER_K = "k";
-    private static final String MULTIPLIER_NONE = "x-notApplicable";
 
     public static final class RegisterMetric {
         private final String metricName;
+        /** SI 前缀倍数,无倍数时为 null(JSON 中省略 multiplier)。 */
         private final String multiplier;
         private final String symbol;
 
@@ -46,14 +46,14 @@ public final class VppUnRegisterMetricDefinitions {
             new RegisterMetric("AP_PE", MULTIPLIER_K, "Wh"),
             new RegisterMetric("REGULATE_UP", MULTIPLIER_K, "W"),
             new RegisterMetric("REGULATE_DOWN", MULTIPLIER_K, "W"),
-            new RegisterMetric("RESPONSE_TIME", MULTIPLIER_NONE, "s"),
+            new RegisterMetric("RESPONSE_TIME", null, "s"),
             new RegisterMetric("CONTROL_RATE", MULTIPLIER_K, "W/min"),
-            new RegisterMetric("DURATION", MULTIPLIER_NONE, "min"),
+            new RegisterMetric("DURATION", null, "min"),
             new RegisterMetric("F_REGULATE_UP", MULTIPLIER_K, "W"),
             new RegisterMetric("F_REGULATE_DOWN", MULTIPLIER_K, "W"),
-            new RegisterMetric("F_RESPONSE_TIME", MULTIPLIER_NONE, "s"),
+            new RegisterMetric("F_RESPONSE_TIME", null, "s"),
             new RegisterMetric("F_CONTROL_RATE", MULTIPLIER_K, "W/min"),
-            new RegisterMetric("F_DURATION", MULTIPLIER_NONE, "min")
+            new RegisterMetric("F_DURATION", null, "min")
     ));
 
     private VppUnRegisterMetricDefinitions() {

+ 11 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/web/advice/VppUnDnCryptoRequestAdvice.java

@@ -2,12 +2,15 @@ package com.usky.vpp.web.advice;
 
 import com.usky.vpp.controller.un.UnDnController;
 import com.usky.vpp.crypto.VppUnCryptoService;
+import com.usky.vpp.util.VppUnCipherPayloadHelper;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.MethodParameter;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpInputMessage;
 import org.springframework.http.converter.HttpMessageConverter;
 import org.springframework.util.StreamUtils;
+import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.ControllerAdvice;
 import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;
 
@@ -26,6 +29,9 @@ public class VppUnDnCryptoRequestAdvice extends RequestBodyAdviceAdapter {
     @Autowired
     private VppUnCryptoService cryptoService;
 
+    @Autowired
+    private ObjectMapper objectMapper;
+
     @Override
     public boolean supports(MethodParameter methodParameter, Type targetType,
                             Class<? extends HttpMessageConverter<?>> converterType) {
@@ -47,7 +53,11 @@ public class VppUnDnCryptoRequestAdvice extends RequestBodyAdviceAdapter {
             return new DecodedHttpInputMessage(inputMessage.getHeaders(), bodyBytes);
         }
         String sign = inputMessage.getHeaders().getFirst("X-Sign");
-        String plain = cryptoService.decryptInboundWithAutoTenant(body, sign);
+        if (!StringUtils.hasText(sign)) {
+            throw new com.usky.common.core.exception.BusinessException("UN 请求缺少 X-Sign");
+        }
+        String cipherBase64 = VppUnCipherPayloadHelper.extractCipherPayload(body, objectMapper);
+        String plain = cryptoService.decryptInboundWithAutoTenant(cipherBase64, sign);
         return new DecodedHttpInputMessage(inputMessage.getHeaders(), plain.getBytes(StandardCharsets.UTF_8));
     }
 

+ 9 - 5
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/web/advice/VppUnDnCryptoResponseAdvice.java

@@ -3,6 +3,8 @@ package com.usky.vpp.web.advice;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.usky.vpp.controller.un.UnDnController;
 import com.usky.vpp.crypto.VppUnCryptoService;
+import com.usky.vpp.crypto.VppUnSmCryptoUtil;
+import com.usky.vpp.util.VppUnCipherPayloadHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.MethodParameter;
 import org.springframework.http.MediaType;
@@ -41,12 +43,14 @@ public class VppUnDnCryptoResponseAdvice implements ResponseBodyAdvice<Object> {
         }
         try {
             String plainJson = body instanceof String ? (String) body : objectMapper.writeValueAsString(body);
-            String cipher = cryptoService.encryptOutbound(plainJson);
-            response.getHeaders().set("X-Sign", cryptoService.signOutbound(cipher));
-            response.getHeaders().setContentType(MediaType.TEXT_PLAIN);
-            return cipher;
+            VppUnSmCryptoUtil.EncryptedPayload payload = cryptoService.prepareOutbound(plainJson);
+            response.getHeaders().set("X-Sign", payload.getSignBase64());
+            response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
+            return VppUnCipherPayloadHelper.wrapCipherPayload(payload.getCipherBase64(), objectMapper);
+        } catch (com.usky.common.core.exception.BusinessException ex) {
+            throw ex;
         } catch (Exception ex) {
-            return body;
+            throw new com.usky.common.core.exception.BusinessException("DN 响应加密失败: " + ex.getMessage());
         }
     }
 }