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) {