chenxiaofei 1 mês atrás
pai
commit
2e576b0a94

+ 40 - 1
iot-platform-manager/src/main/java/com/platform/api/controller/XpCloudPrintController.java

@@ -2,15 +2,21 @@ package com.platform.api.controller;
 
 import com.platform.api.request.XpPrintImageReqVo;
 import com.platform.api.request.XpPrintReceiptReqVo;
+import com.platform.config.XpCloudProperties;
+import com.platform.exception.IotException;
 import com.platform.result.BaseResult;
 import com.platform.service.XpCloudPrintService;
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -19,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
  *
  * @author assistant
  */
+@Slf4j
 @Validated
 @RestController
 @RequiredArgsConstructor
@@ -26,8 +33,15 @@ import org.springframework.web.bind.annotation.RestController;
 @RequestMapping("/kwsPrinter/xp")
 public class XpCloudPrintController {
 
+    /**
+     * 小票打印鉴权请求头名(与请求体打印机字段 {@code sn} 区分开)
+     */
+    private static final String HEADER_PRINT_RECEIPT_AUTH_SN = "X-Print-Receipt-Auth-Sn";
+
     private final XpCloudPrintService xpCloudPrintService;
 
+    private final XpCloudProperties xpCloudProperties;
+
     /**
      * 图片打印接口
      *
@@ -45,12 +59,37 @@ public class XpCloudPrintController {
      *
      * <p>content示例:\n&lt;CB&gt;四川顺采建筑材料有限公司...&lt;/BOLD&gt;</p>
      *
+     * @param printReceiptAuthSn 请求头 {@code X-Print-Receipt-Auth-Sn},须与配置的 {@code xp.dev.print-receipt-authorized-sn} 一致方可调用
      * @param reqVo 小票订单请求参数(包含公共参数与业务参数)
      * @return 打印任务号
      */
     @PostMapping("/printReceipt")
     @Operation(summary = "调用芯烨SDK print 接口打印小票订单")
-    public BaseResult<String> printReceipt(@Valid @RequestBody XpPrintReceiptReqVo reqVo) {
+    public BaseResult<String> printReceipt(
+            @Parameter(
+                    name = HEADER_PRINT_RECEIPT_AUTH_SN,
+                    description = "小票打印接口鉴权,须与服务端 xp.dev.print-receipt-authorized-sn 一致",
+                    in = ParameterIn.HEADER)
+            @RequestHeader(value = HEADER_PRINT_RECEIPT_AUTH_SN, required = false)
+            String printReceiptAuthSn,
+            @Valid @RequestBody XpPrintReceiptReqVo reqVo) {
+        assertPrintReceiptAuthorized(printReceiptAuthSn);
         return BaseResult.success(xpCloudPrintService.printReceipt(reqVo));
     }
+
+    /**
+     * 简单鉴权:当配置了允许的凭证时,请求头 {@link #HEADER_PRINT_RECEIPT_AUTH_SN} 的值必须与其一致。
+     *
+     * @param headerSn 请求头 {@code X-Print-Receipt-Auth-Sn} 的取值
+     */
+    private void assertPrintReceiptAuthorized(String headerSn) {
+        if (!xpCloudProperties.isPrintReceiptAuthorizationConfigured()) {
+            log.debug("未配置 xp.dev.print-receipt-authorized-sn,跳过小票打印请求头 {} 鉴权", HEADER_PRINT_RECEIPT_AUTH_SN);
+            return;
+        }
+        if (xpCloudProperties.isUnauthorizedForPrintReceipt(headerSn)) {
+            log.warn("小票打印鉴权拒绝:请求头 {} 与配置不一致(已屏蔽具体值)", HEADER_PRINT_RECEIPT_AUTH_SN);
+            throw new IotException("无权限调用小票打印接口");
+        }
+    }
 }

+ 32 - 2
iot-platform-manager/src/main/java/com/platform/config/XpCloudProperties.java

@@ -51,13 +51,20 @@ public class XpCloudProperties {
      */
     private Integer receiptMaxSuccessPrintCount;
 
+    /**
+     * 小票打印接口({@code printReceipt})鉴权期望值,与<strong>请求头</strong> {@code X-Print-Receipt-Auth-Sn} 比对。
+     * <p>非空时:客户端须在请求头携带 {@code X-Print-Receipt-Auth-Sn},且取值与本字段(去首尾空格后)完全一致才放行;为空时不做该校验(便于过渡期)。</p>
+     * <p>配置项:{@code xp.dev.print-receipt-authorized-sn}</p>
+     */
+    private String printReceiptAuthorizedSn;
+
     /**
      * 启动时输出脱敏后的配置,便于排查环境问题
      */
     @PostConstruct
     public void logConfigOnStartup() {
-        log.info("芯烨云SDK配置加载完成, domain={}, receiptPrintUrl={}, receiptMaxSuccessPrintCount={}, debug={}, user={}, userKey={}",
-                domain, receiptPrintUrl, resolvedReceiptMaxSuccessPrintCount(), debug, desensitize(user), desensitize(userKey));
+        log.info("芯烨云SDK配置加载完成, domain={}, receiptPrintUrl={}, receiptMaxSuccessPrintCount={}, printReceiptAuthorizedSn={}, debug={}, user={}, userKey={}",
+                domain, receiptPrintUrl, resolvedReceiptMaxSuccessPrintCount(), desensitize(printReceiptAuthorizedSn), debug, desensitize(user), desensitize(userKey));
         if (StringUtils.isAnyBlank(user, userKey)) {
             log.warn("检测到芯烨云配置未完整设置,请配置 XP_USER 与 XP_USER_KEY 环境变量");
         }
@@ -99,4 +106,27 @@ public class XpCloudProperties {
         }
         return v;
     }
+
+    /**
+     * 是否已配置小票打印 sn 鉴权(去首尾空格后非空)。
+     *
+     * @return true 表示需按 {@link #isUnauthorizedForPrintReceipt(String)} 校验请求头 {@code X-Print-Receipt-Auth-Sn}
+     */
+    public boolean isPrintReceiptAuthorizationConfigured() {
+        return StringUtils.isNotBlank(StringUtils.trimToEmpty(printReceiptAuthorizedSn));
+    }
+
+    /**
+     * 小票打印 sn 是否与配置不一致。
+     *
+     * @param requestSn 请求头 {@code X-Print-Receipt-Auth-Sn} 的取值
+     * @return 未配置鉴权打印机编号时为 false(不拦截);已配置但与请求不一致时为 true(应拦截)
+     */
+    public boolean isUnauthorizedForPrintReceipt(String requestSn) {
+        String configuredSn = StringUtils.trimToEmpty(printReceiptAuthorizedSn);
+        if (StringUtils.isBlank(configuredSn)) {
+            return false;
+        }
+        return !configuredSn.equals(StringUtils.trimToEmpty(requestSn));
+    }
 }

+ 35 - 0
iot-platform-manager/src/test/java/com/platform/config/XpCloudPropertiesPrintReceiptAuthTest.java

@@ -0,0 +1,35 @@
+package com.platform.config;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+/**
+ * 小票打印请求头 {@code X-Print-Receipt-Auth-Sn} 与配置比对逻辑单测(不依赖 Mockito)
+ */
+class XpCloudPropertiesPrintReceiptAuthTest {
+
+    @Test
+    @DisplayName("未配置或仅空白时:不启用鉴权,且不应判为未授权")
+    void shouldNotEnforceWhenNotConfigured() {
+        XpCloudProperties p = new XpCloudProperties();
+        p.setPrintReceiptAuthorizedSn(null);
+        Assertions.assertFalse(p.isPrintReceiptAuthorizationConfigured());
+        Assertions.assertFalse(p.isUnauthorizedForPrintReceipt("ANY"));
+
+        p.setPrintReceiptAuthorizedSn("   ");
+        Assertions.assertFalse(p.isPrintReceiptAuthorizationConfigured());
+        Assertions.assertFalse(p.isUnauthorizedForPrintReceipt("ANY"));
+    }
+
+    @Test
+    @DisplayName("已配置时:sn 必须一致(去首尾空格后比较)")
+    void shouldEnforceExactMatchAfterTrim() {
+        XpCloudProperties p = new XpCloudProperties();
+        p.setPrintReceiptAuthorizedSn("  XP_DEVICE_1  ");
+
+        Assertions.assertTrue(p.isPrintReceiptAuthorizationConfigured());
+        Assertions.assertFalse(p.isUnauthorizedForPrintReceipt("XP_DEVICE_1"));
+        Assertions.assertTrue(p.isUnauthorizedForPrintReceipt("OTHER"));
+    }
+}