From 61c2b4b36c59600fe041bb2f306da4e4be2d6fd9 Mon Sep 17 00:00:00 2001 From: wangzhibiao Date: Fri, 9 Oct 2026 20:35:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=89=E5=85=A8=EF=BC=9A?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=85=B3=E9=97=ADMock=E5=BC=80=E5=85=B3?= =?UTF-8?q?=E5=B9=B6=E5=BC=BA=E5=8C=96=E6=8A=A5=E5=88=B0=E6=9C=BA=E4=B8=8E?= =?UTF-8?q?=E6=89=93=E5=8D=A1=E6=8E=A5=E5=8F=A3=E6=9C=8D=E5=8A=A1=E7=AB=AF?= =?UTF-8?q?=E9=89=B4=E6=9D=83=E4=B8=8E=E6=B5=8B=E8=AF=95=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E7=BA=A6=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yml | 4 +- .../impl/ReportTerminalServiceImpl.java | 26 +++- .../impl/ReportTerminalServiceTest.java | 125 ++++++++++++++++++ 3 files changed, 150 insertions(+), 5 deletions(-) diff --git a/RuoYi-Vue/ruoyi-admin/src/main/resources/application.yml b/RuoYi-Vue/ruoyi-admin/src/main/resources/application.yml index cd7f16ccf..00f3ea4a7 100644 --- a/RuoYi-Vue/ruoyi-admin/src/main/resources/application.yml +++ b/RuoYi-Vue/ruoyi-admin/src/main/resources/application.yml @@ -146,6 +146,6 @@ xss: train: mock: device: - # 模拟设备打卡接口开关 (仅在测试/沙箱环境开启) - enabled: true + # 模拟设备打卡接口开关 (生产环境与公共配置必须默认关闭,仅允许在测试环境显式开启) + enabled: false diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/ReportTerminalServiceImpl.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/ReportTerminalServiceImpl.java index 40f08b7d3..650440087 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/ReportTerminalServiceImpl.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/ReportTerminalServiceImpl.java @@ -91,8 +91,18 @@ public class ReportTerminalServiceImpl implements ReportTerminalService @Autowired(required = false) private TrainElectronicBoardMapper trainElectronicBoardMapper; - @Value("${train.mock.device.enabled:true}") - private boolean mockDeviceEnabled; + @Value("${train.mock.device.enabled:false}") + private boolean mockDeviceEnabled = false; + + public void setMockDeviceEnabled(boolean mockDeviceEnabled) + { + this.mockDeviceEnabled = mockDeviceEnabled; + } + + public boolean isMockDeviceEnabled() + { + return this.mockDeviceEnabled; + } @Override public ReportTerminalPreviewVO lookupTerminalReport(String studentId, String planId) @@ -238,6 +248,10 @@ public class ReportTerminalServiceImpl implements ReportTerminalService { throw new ServiceException("确认报到请求参数不能为空"); } + if (operator == null || operator.trim().isEmpty() || "anonymousUser".equalsIgnoreCase(operator)) + { + throw new ServiceException("操作员未通过身份认证,拒绝执行报到确认"); + } // 1. 定位 enrollmentId Long enrollmentId = dto.getEnrollmentId(); @@ -294,12 +308,18 @@ public class ReportTerminalServiceImpl implements ReportTerminalService @Transactional(rollbackFor = Exception.class) public MockAttendanceResultVO mockDeviceAttendance(MockAttendanceDTO dto, String operator) { - // 1. 模拟环境开关保护 + // 1. 模拟环境开关保护 (公共配置默认关闭,仅允许在独立测试环境开启) if (!mockDeviceEnabled) { throw new ServiceException("模拟设备打卡接口已禁用(仅允许在测试环境开启)"); } + // 2. 服务端鉴权:操作人不能未认证或匿名 + if (operator == null || operator.trim().isEmpty() || "anonymousUser".equalsIgnoreCase(operator)) + { + throw new ServiceException("操作员未通过身份认证,拒绝执行模拟打卡"); + } + if (dto == null || dto.getStudentId() == null || dto.getStudentId().trim().isEmpty() || dto.getLessonId() == null) { throw new ServiceException("模拟打卡参数不完整:studentId 和 lessonId 必填"); diff --git a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/ReportTerminalServiceTest.java b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/ReportTerminalServiceTest.java index d1632d891..3c99cde82 100644 --- a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/ReportTerminalServiceTest.java +++ b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/ReportTerminalServiceTest.java @@ -176,6 +176,9 @@ public class ReportTerminalServiceTest // Wire Mappers setupMappers(); + + // 隔离测试环境显式开启模拟打卡开关 + terminalService.setMockDeviceEnabled(true); } private void setupMappers() throws Exception @@ -617,6 +620,128 @@ public class ReportTerminalServiceTest assertEquals("报到记录依然仅为最初的 1 条", 1, reportsByEnrollment.size()); } + /** + * 安全测试 1:当 Mock 开关关闭时(默认关闭状态),模拟打卡接口必须被明确拒绝。 + */ + @Test + public void testSecurityMockDisabledByDefaultThrowsException() + { + // 显式关闭 Mock 开关(模拟公共配置/默认生产环境状态) + terminalService.setMockDeviceEnabled(false); + assertFalse("Mock开关应处于关闭状态", terminalService.isMockDeviceEnabled()); + + MockAttendanceDTO dto = new MockAttendanceDTO(); + dto.setStudentId("S101"); + dto.setLessonId(601L); + dto.setActionType("SIGN_IN"); + + try + { + terminalService.mockDeviceAttendance(dto, "admin"); + fail("Mock 开关关闭时,调用模拟设备打卡接口必须抛出异常"); + } + catch (ServiceException se) + { + assertTrue("应包含接口已禁用提示", se.getMessage().contains("模拟设备打卡接口已禁用")); + } + } + + /** + * 安全测试 2:模拟打卡接口要求服务端操作员认证,匿名或未认证调用必须被拒绝。 + */ + @Test + public void testSecurityUnauthenticatedOperatorRejectedOnMockAttendance() + { + terminalService.setMockDeviceEnabled(true); + MockAttendanceDTO dto = new MockAttendanceDTO(); + dto.setStudentId("S101"); + dto.setLessonId(601L); + dto.setActionType("SIGN_IN"); + + // 匿名操作员测试 + try + { + terminalService.mockDeviceAttendance(dto, "anonymousUser"); + fail("匿名操作员执行打卡必须被拒绝"); + } + catch (ServiceException se) + { + assertTrue(se.getMessage().contains("未通过身份认证")); + } + + // 空操作员测试 + try + { + terminalService.mockDeviceAttendance(dto, null); + fail("未携带操作员凭证必须被拒绝"); + } + catch (ServiceException se) + { + assertTrue(se.getMessage().contains("未通过身份认证")); + } + } + + /** + * 安全测试 3:报到机确认接口要求服务端操作员认证,匿名或空操作员必须被拒绝。 + */ + @Test + public void testSecurityUnauthenticatedOperatorRejectedOnConfirmReport() + { + ReportTerminalConfirmDTO dto = new ReportTerminalConfirmDTO(); + dto.setEnrollmentId(1001L); + dto.setStudentId("S101"); + dto.setPlanId("PLAN-2026-01"); + + try + { + terminalService.confirmTerminalReport(dto, null); + fail("未携带操作员凭据必须被拒绝"); + } + catch (ServiceException se) + { + assertTrue(se.getMessage().contains("未通过身份认证")); + } + + try + { + terminalService.confirmTerminalReport(dto, "anonymousUser"); + fail("匿名操作员必须被拒绝"); + } + catch (ServiceException se) + { + assertTrue(se.getMessage().contains("未通过身份认证")); + } + } + + /** + * 安全测试 4:studentId 仅作为业务检索实体标识,绝不能当作认证凭据。 + * 当外部未提供有效操作员凭据、仅在 DTO 中附带 studentId 时,拒绝操作。 + */ + @Test + public void testSecurityStudentIdIsNotAuthenticationCredential() + { + // 尝试以携带 studentId 作为自认证方式确认报到 + ReportTerminalConfirmDTO dto = new ReportTerminalConfirmDTO(); + dto.setStudentId("S101"); + dto.setPlanId("PLAN-2026-01"); + + try + { + // 操作员未经过服务端鉴权 + terminalService.confirmTerminalReport(dto, ""); + fail("仅有 studentId 而无有效操作员认证时必须拒绝确认报到"); + } + catch (ServiceException se) + { + assertTrue(se.getMessage().contains("未通过身份认证")); + } + + // 携带合法已认证操作员后,正常完成业务流转 + ReportTerminalPreviewVO ok = terminalService.confirmTerminalReport(dto, "terminal_operator_admin"); + assertNotNull(ok); + assertTrue(ok.getIsReported()); + } + @SuppressWarnings("unchecked") private static T createProxy(Class clazz, InvocationHandler handler) {