修复安全:默认关闭Mock开关并强化报到机与打卡接口服务端鉴权与测试环境约束

This commit is contained in:
wangzhibiao committed 2026-10-09 20:35:12 +08:00
1 parent cecf11790e
commit 61c2b4b36c
3 files changed
+150 -5

No files matched your search

@@ -146,6 +146,6 @@ xss:
train:
mock:
device:
# 模拟设备打卡接口开关 (仅在测试/沙箱环境开启)
enabled: true
# 模拟设备打卡接口开关 (生产环境与公共配置必须默认关闭,仅允许在测试环境显式开启)
enabled: false
@@ -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 必填");
@@ -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> T createProxy(Class<T> clazz, InvocationHandler handler)
{