目录
一、核验核心目标与维度
1. 核心目标
2. 核验维度
二、核验技术方案
1. 前置准备
2. 数据库扩展设计
(1)用户签名特征表(user_sign_feature)
3. 核心核验流程(四层校验)
流程总览
(1)第一层:设备源校验(快速拦截非法设备)
(2)第二层:数据合法性校验(拦截伪造数据)
(3)第三层:摘要防篡改校验(核心防篡改)
(4)第四层:笔迹特征比对(防代签)
4. 核验入口整合(核心 Service)
5. 核验接口(Controller)
三、汉王 ESP560 专属适配要点
1. 特征提取适配
2. 离线核验支持
3. 阈值配置化
四、测试与异常处理
1. 核验测试用例
2. 异常处理
五、可视化与审计
1. 核验结果展示
2. 核验日志审计
六、核心扩展
1. 批量核验
2. 司法级核验
笔迹核验是考核签名项目的核心风控环节,针对汉王 ESP560 的 2048 级压感特性,需实现「数据合法性校验→摘要防篡改→笔迹特征比对→设备源校验」四层核验逻辑,确保签名为本人真实操作、数据未被篡改。以下是完整的笔迹核验实施方案:
一、核验核心目标与维度
1. 核心目标
- 防篡改:校验签名原始数据(坐标 / 压力 / 时间戳)是否被修改;
- 防代签:对比签名笔迹特征(压感变化、书写轨迹)与历史数据一致性;
- 防伪造:校验数据是否来自合法的汉王 ESP560 设备。
2. 核验维度
| 核验维度 | 核验内容 | 核心依据 |
|---|---|---|
| 数据合法性 | 压感值范围(0-2048)、坐标范围(0-800/0-480)、时间戳连续性 | ESP560 硬件参数(2048 级压感、800×480 分辨率) |
| 摘要防篡改 | 对比当前签名数据 SM3 摘要与存储的原始摘要 | 哈希不可逆特性,数据篡改则摘要不一致 |
| 笔迹特征比对 | 书写速度、压感变化趋势、笔画拐点、书写轨迹相似度 | 汉王 ESP560 SDK 笔迹特征提取接口 |
| 设备源校验 | 校验签名设备 ID 是否在白名单内、设备驱动版本是否合规 | 设备注册台账、汉王官方驱动版本 |
二、核验技术方案
1. 前置准备
- 汉王 SDK 集成:引入汉王 ESP560 的笔迹特征比对 SDK(
hanvon-feature-sdk.jar),支持提取笔迹特征值、计算相似度; - 历史特征库:在 MySQL 中建立
user_sign_feature表,存储员工历史签名的特征值(加密存储); - 设备白名单:在 Redis 中维护 ESP560 设备 ID 白名单(
hanvon:esp560:whitelist),仅允许合法设备签名。
2. 数据库扩展设计
(1)用户签名特征表(user_sign_feature)
java
运行
@Entity @Table(name = "user_sign_feature") public class UserSignFeature { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long userId; // 员工ID private String featureValue; // 笔迹特征值(SM4加密) private String deviceId; // 签名设备ID private LocalDateTime createTime; // 特征录入时间 // getter/setter }3. 核心核验流程(四层校验)
流程总览
plaintext
前端提交签名数据 → 第一层:设备源校验 → 第二层:数据合法性校验 → 第三层:摘要防篡改校验 → 第四层:笔迹特征比对 → 返回核验结果(1)第一层:设备源校验(快速拦截非法设备)
java
运行
/** * 设备源校验:校验设备ID是否在白名单、驱动版本是否合规 * @param deviceId ESP560设备ID * @param driverVersion 设备驱动版本 * @return 校验结果 */ public boolean checkDeviceValid(String deviceId, String driverVersion) { // 1. 从Redis获取设备白名单 Set<String> whiteList = redisTemplate.opsForSet().members("hanvon:esp560:whitelist"); if (whiteList == null || !whiteList.contains(deviceId)) { log.error("设备ID{}不在白名单内", deviceId); return false; } // 2. 校验驱动版本(汉王ESP560最低驱动版本:V2.1.0) if (VersionUtil.compare(driverVersion, "V2.1.0") < 0) { log.error("设备{}驱动版本过低,当前{},要求≥V2.1.0", deviceId, driverVersion); return false; } return true; }(2)第二层:数据合法性校验(拦截伪造数据)
java
运行
/** * 数据合法性校验:校验压感、坐标、时间戳 * @param signRawData 签名原始数据(JSON字符串) * @return 校验结果 */ public boolean checkDataValid(String signRawData) { try { JSONObject signObj = JSONUtil.parseObj(signRawData); JSONArray points = signObj.getJSONArray("points"); if (points.isEmpty()) { log.error("签名数据为空"); return false; } // 记录上一个点的时间戳,校验连续性 long lastTime = 0; for (int i = 0; i < points.size(); i++) { JSONObject point = points.getJSONObject(i); // 1. 压感值校验(0-2048) int pressure = point.getInt("pressure"); if (pressure < 0 || pressure > 2048) { log.error("压感值{}超出ESP560范围(0-2048)", pressure); return false; } // 2. 坐标校验(800×480) int x = point.getInt("x"); int y = point.getInt("y"); if (x < 0 || x > 800 || y < 0 || y > 480) { log.error("坐标({},{})超出ESP560范围(800×480)", x, y); return false; } // 3. 时间戳校验(递增、无跳变) long time = point.getLong("time"); if (i > 0 && time < lastTime) { log.error("时间戳不连续,上一个{},当前{}", lastTime, time); return false; } // 4. 时间间隔校验(书写间隔≤1000ms,防止伪造) if (i > 0 && (time - lastTime) > 1000) { log.error("书写间隔过长({}ms),疑似伪造数据", time - lastTime); return false; } lastTime = time; } return true; } catch (Exception e) { log.error("数据合法性校验失败", e); return false; } }(3)第三层:摘要防篡改校验(核心防篡改)
java
运行
/** * 摘要防篡改校验:对比SM3摘要 * @param examNo 考核单号 * @param signRawData 待核验签名数据 * @return 校验结果 */ public boolean checkHashValid(String examNo, String signRawData) { // 1. 获取存储的原始摘要 String storedHash = redisTemplate.opsForValue().get("sign:hash:" + examNo); if (storedHash == null) { ExamSignRecord record = signRecordRepository.findByExamNo(examNo) .orElseThrow(() -> new RuntimeException("签名记录不存在")); storedHash = record.getSignHash(); // 缓存摘要,提升后续核验效率 redisTemplate.opsForValue().set("sign:hash:" + examNo, storedHash, 7, TimeUnit.DAYS); } // 2. 计算当前数据的SM3摘要 String currentHash = smCryptoUtil.sm3Digest(signRawData.getBytes(StandardCharsets.UTF_8)); // 3. 对比摘要 boolean valid = storedHash.equals(currentHash); if (!valid) { log.error("摘要不一致,存储{},当前{}", storedHash, currentHash); } return valid; }(4)第四层:笔迹特征比对(防代签)
java
运行
/** * 笔迹特征比对:提取特征值,对比与历史签名的相似度 * @param userId 员工ID * @param signRawData 待核验签名数据 * @return 校验结果(相似度≥80%则通过) */ public boolean checkFeatureMatch(Long userId, String signRawData) { // 1. 获取用户历史签名特征值 List<UserSignFeature> historyFeatures = userSignFeatureRepository.findByUserId(userId); if (historyFeatures.isEmpty()) { log.warn("用户{}无历史签名特征,跳过特征比对", userId); return true; // 首次签名跳过,仅记录特征 } // 2. 提取当前签名的特征值(调用汉王SDK) HanvonFeatureExtractor extractor = new HanvonFeatureExtractor(); String currentFeature = extractor.extract(signRawData); // 提取特征值 if (StrUtil.isBlank(currentFeature)) { log.error("提取当前签名特征失败"); return false; } // 3. 对比与历史特征的相似度(调用汉王SDK) HanvonFeatureComparator comparator = new HanvonFeatureComparator(); boolean match = false; for (UserSignFeature history : historyFeatures) { // 解密历史特征值 String historyFeature = new String(smCryptoUtil.sm4Decrypt(history.getFeatureValue()), StandardCharsets.UTF_8); // 计算相似度(0-100) int similarity = comparator.compare(currentFeature, historyFeature); log.info("用户{}笔迹相似度:{}%", userId, similarity); if (similarity >= 80) { // 阈值可配置 match = true; break; } } // 4. 首次核验通过后,更新最新特征(滚动更新) if (match) { UserSignFeature newFeature = new UserSignFeature(); newFeature.setUserId(userId); newFeature.setFeatureValue(smCryptoUtil.sm4Encrypt(currentFeature.getBytes(StandardCharsets.UTF_8))); newFeature.setDeviceId(extractDeviceId(signRawData)); newFeature.setCreateTime(LocalDateTime.now()); userSignFeatureRepository.save(newFeature); } return match; }4. 核验入口整合(核心 Service)
java
运行
@Service public class SignVerifyService { @Autowired private RedisTemplate<String, String> redisTemplate; @Autowired private SmCryptoUtil smCryptoUtil; @Autowired private ExamSignRecordRepository signRecordRepository; @Autowired private UserSignFeatureRepository userSignFeatureRepository; /** * 全流程笔迹核验 * @param examNo 考核单号 * @param userId 员工ID * @param signRawData 待核验签名数据 * @param deviceId 设备ID * @param driverVersion 驱动版本 * @return 核验结果+失败原因 */ public VerifyResult verifySign(String examNo, Long userId, String signRawData, String deviceId, String driverVersion) { VerifyResult result = new VerifyResult(); // 第一层:设备源校验 if (!checkDeviceValid(deviceId, driverVersion)) { result.setSuccess(false); result.setReason("非法设备/驱动版本过低"); return result; } // 第二层:数据合法性校验 if (!checkDataValid(signRawData)) { result.setSuccess(false); result.setReason("签名数据非法(压感/坐标/时间戳异常)"); return result; } // 第三层:摘要防篡改校验 if (!checkHashValid(examNo, signRawData)) { result.setSuccess(false); result.setReason("签名数据已篡改(摘要不一致)"); return result; } // 第四层:笔迹特征比对 if (!checkFeatureMatch(userId, signRawData)) { result.setSuccess(false); result.setReason("笔迹特征不匹配(疑似代签)"); return result; } // 所有校验通过 result.setSuccess(true); result.setReason("核验通过"); // 更新验签状态 signRecordRepository.updateVerifyStatus(examNo, "核验通过"); return result; } // 内部校验方法(前文已定义:checkDeviceValid/checkDataValid/checkHashValid/checkFeatureMatch) } // 核验结果封装 @Data public class VerifyResult { private boolean success; private String reason; private int similarity; // 笔迹相似度(可选) }5. 核验接口(Controller)
java
运行
@RestController @RequestMapping("/api/exam/sign/verify") public class SignVerifyController { @Autowired private SignVerifyService signVerifyService; @PostMapping("/hanvon") public Result<VerifyResult> verifyHanvonSign( @RequestParam String examNo, @RequestParam Long userId, @RequestBody String signRawData, @RequestParam String deviceId, @RequestParam String driverVersion) { VerifyResult result = signVerifyService.verifySign(examNo, userId, signRawData, deviceId, driverVersion); return Result.success(result); } }三、汉王 ESP560 专属适配要点
1. 特征提取适配
- 汉王 ESP560 的 2048 级压感是核心特征,提取特征时需启用「压感权重优先」模式:
java
运行
extractor.setConfig("pressureWeight", "0.7"); // 压感权重70%,坐标权重30% - 适配 ESP560 的 800×480 分辨率,特征提取时需归一化坐标(避免不同设备分辨率影响)。
2. 离线核验支持
- 若 ESP560 设备离线,可先缓存核验请求,联网后自动触发核验;
- 离线状态下仅执行前三层校验(设备 / 数据 / 摘要),第四层特征比对待联网后完成。
3. 阈值配置化
- 将笔迹相似度阈值(80%)、时间间隔阈值(1000ms)配置在
application.yml,支持动态调整:yaml
hanvon: sign: similarity-threshold: 80 time-interval-threshold: 1000
四、测试与异常处理
1. 核验测试用例
| 测试场景 | 测试步骤 | 预期结果 |
|---|---|---|
| 合法签名核验 | 本人在 ESP560 签名,提交核验 | 四层校验通过,返回 “核验通过” |
| 篡改压感数据 | 修改签名数据中的压感值(如改为 3000),提交核验 | 第二层校验失败,原因 “压感值超出范围” |
| 篡改坐标数据 | 修改坐标为 900(超出 800),提交核验 | 第二层校验失败,原因 “坐标超出范围” |
| 代签核验 | 他人模仿签名,提交核验 | 第四层校验失败,原因 “笔迹特征不匹配” |
| 非法设备签名 | 使用非白名单 ESP560 设备签名,提交核验 | 第一层校验失败,原因 “非法设备” |
2. 异常处理
- 特征提取失败:记录日志,降级为仅前三层校验,人工复核;
- 历史特征为空:首次签名跳过特征比对,仅记录特征值,二次签名开始校验;
- 设备白名单更新:提供后台接口,支持动态添加 / 删除设备 ID,无需重启服务。
五、可视化与审计
1. 核验结果展示
- 前端展示核验结果(通过 / 失败)、失败原因、笔迹相似度;
- 支持查看签名轨迹对比图(当前签名 vs 历史签名)。
2. 核验日志审计
- 记录每一次核验的「考核单号、用户 ID、设备 ID、核验结果、失败原因、时间」;
- 支持按时间 / 用户 / 设备筛选核验日志,导出审计报表。
六、核心扩展
1. 批量核验
- 支持按考核批次批量核验签名,生成核验报告(通过率、失败原因分布);
- 批量核验采用异步任务(@Async),避免阻塞主线程。
2. 司法级核验
- 对接汉王司法级笔迹鉴定接口,核验结果可生成司法鉴定报告;
- 签名数据同步至区块链存证平台,核验结果上链,不可篡改。
该方案基于汉王 ESP560 的硬件特性,实现了从 “设备源头” 到 “笔迹特征” 的全维度核验,既满足考核场景的风控需求,又符合电子签名法的合规要求。可直接集成到 SpringBoot 考核签名项目中,如需调整核验维度(如增加人脸核验联动),可按需扩展。