From ed127c3b2932772f758c42a890d6f820e29492d9 Mon Sep 17 00:00:00 2001 From: lzh Date: Wed, 21 Jan 2026 14:48:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(iot):=20jt808=E6=B6=88=E6=81=AF=E5=BA=94?= =?UTF-8?q?=E7=AD=94=E6=9C=BA=E5=88=B6=E8=B0=83=E6=95=B4-=E6=AF=8F?= =?UTF-8?q?=E6=9D=A1=E6=B6=88=E6=81=AF=E9=83=BD=E9=9C=80=E5=BA=94=E7=AD=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jt808/IotJt808DeviceMessageCodec.java | 17 ++-- .../tcp/handler/Jt808ProtocolHandler.java | 95 +++++++++---------- 2 files changed, 52 insertions(+), 60 deletions(-) diff --git a/viewsh-module-iot/viewsh-module-iot-gateway/src/main/java/com/viewsh/module/iot/gateway/codec/jt808/IotJt808DeviceMessageCodec.java b/viewsh-module-iot/viewsh-module-iot-gateway/src/main/java/com/viewsh/module/iot/gateway/codec/jt808/IotJt808DeviceMessageCodec.java index 31bf9db1..568c912d 100644 --- a/viewsh-module-iot/viewsh-module-iot-gateway/src/main/java/com/viewsh/module/iot/gateway/codec/jt808/IotJt808DeviceMessageCodec.java +++ b/viewsh-module-iot/viewsh-module-iot-gateway/src/main/java/com/viewsh/module/iot/gateway/codec/jt808/IotJt808DeviceMessageCodec.java @@ -137,7 +137,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { * 根据 JT808 消息ID获取物模型标准方法名 * * 映射关系: - * - 0x0002 心跳 -> thing.state.update(设备状态更新) + * - 0x0002 心跳 -> jt808.terminal.heartbeat(JT808 专用,不映射到物模型) * - 0x0200 位置上报 -> thing.property.post(属性上报) * - 0x0704 批量位置上报 -> thing.property.post(属性上报) * - 0x0006 按键事件 -> thing.event.post(事件上报) @@ -147,8 +147,8 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { */ private String getStandardMethodName(int msgId) { return switch (msgId) { - // 设备状态类 - case 0x0002 -> IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(); // 心跳 -> 状态更新 + // JT808 协议专用消息(不映射到物模型,避免被 REPLY_DISABLED 影响) + case 0x0002 -> "jt808.terminal.heartbeat"; // 心跳 -> JT808 专用方法 // 属性上报类 case 0x0200 -> IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(); // 位置信息汇报 -> 属性上报 @@ -179,8 +179,8 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { // thing.property.post - 返回 properties case 0x0200, 0x0704 -> parseLocationInfoAsProperties(dataPack); - // thing.state.update - 返回 state 信息 - case 0x0002 -> parseHeartbeatAsState(); + // jt808.terminal.heartbeat - 心跳消息 + case 0x0002 -> parseHeartbeat(dataPack); // thing.event.post - 返回 event 信息 case 0x0006 -> parseButtonEventAsEvent(dataPack); @@ -193,11 +193,12 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { } /** - * 解析心跳为状态信息(thing.state.update) + * 解析心跳消息(jt808.terminal.heartbeat) + *

+ * JT808 心跳消息体为空,仅用于保活,平台必须回复 0x8001 通用应答 */ - private Map parseHeartbeatAsState() { + private Map parseHeartbeat(Jt808DataPack dataPack) { Map result = new HashMap<>(); - result.put("state", "online"); result.put("timestamp", System.currentTimeMillis()); return result; } diff --git a/viewsh-module-iot/viewsh-module-iot-gateway/src/main/java/com/viewsh/module/iot/gateway/protocol/tcp/handler/Jt808ProtocolHandler.java b/viewsh-module-iot/viewsh-module-iot-gateway/src/main/java/com/viewsh/module/iot/gateway/protocol/tcp/handler/Jt808ProtocolHandler.java index c1da7396..5380ad44 100644 --- a/viewsh-module-iot/viewsh-module-iot-gateway/src/main/java/com/viewsh/module/iot/gateway/protocol/tcp/handler/Jt808ProtocolHandler.java +++ b/viewsh-module-iot/viewsh-module-iot-gateway/src/main/java/com/viewsh/module/iot/gateway/protocol/tcp/handler/Jt808ProtocolHandler.java @@ -1,6 +1,7 @@ package com.viewsh.module.iot.gateway.protocol.tcp.handler; import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import com.viewsh.framework.common.pojo.CommonResult; @@ -25,19 +26,18 @@ import java.util.concurrent.TimeUnit; * JT808 协议处理器 *

* 实现 JT808 部标协议的完整认证流程: - * 1. 终端注册(0x0100):设备首次连接时注册,平台返回鉴权码 + * 1. 终端注册(0x0100):设备首次连接时注册,平台生成并返回随机鉴权码 * 2. 终端鉴权(0x0102):设备使用鉴权码进行鉴权,鉴权成功后可发送业务消息 *

- * 认证策略(基于设备配置的 authType): - * - SECRET(一机一密):使用设备的 deviceSecret 作为鉴权码 - * - PRODUCT_SECRET(一型一密):使用产品的 productSecret 作为鉴权码 - * - NONE(免鉴权):使用终端手机号作为鉴权码(兼容模式) + * 鉴权码机制: + * - 注册时生成随机 token(32位UUID大写形式) + * - 鉴权码永久有效,缓存在 Redis 中 + * - 鉴权成功后保留鉴权码,设备断线重连时可重复使用 + * - 基于连接的信任:鉴权成功后,该 TCP 连接上的后续消息无需再鉴权 *

* 设计说明: - * - 基于 demo 项目(com/iot/transport/jt808)的实现逻辑 - * - 注册和鉴权分两步完成,符合 JT808 标准流程 - * - 鉴权码在注册阶段生成并缓存,鉴权阶段验证后清除 - * - 支持设备级和产品级认证类型配置(设备级优先) + * - 符合 JT808 标准流程:注册 → 获取鉴权码 → 鉴权 → 发送业务数据 + * - 鉴权码是"进门密码",进门一次即可,断线重连时需重新验证 * * @author lzh */ @@ -54,12 +54,18 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler { private static final String METHOD_AUTH = "jt808.terminal.auth"; // 终端鉴权 (0x0102) /** - * 鉴权码缓存过期时间(分钟) + * 鉴权码缓存过期时间(天) *

- * 设备在注册后需要在此时间内完成鉴权,否则需要重新注册。 - * Redis 会自动清理过期的鉴权码,无需手动管理。 + * 设置较长的过期时间,支持设备断线重连时重复使用鉴权码。 + */ + private static final int AUTH_TOKEN_EXPIRE_DAYS = 30; + + /** + * 鉴权码缓存 Key 前缀 + *

+ * 鉴权码在注册时生成并缓存到 Redis,过期时间30天。 + * 鉴权成功后保留鉴权码,设备断线重连时可重复使用。 */ - private static final int AUTH_TOKEN_EXPIRE_MINUTES = 5; private final String JT808_AUTH_TOKEN = "iot:jt808_auth_token:%s"; /** * Redis 模板(用于鉴权码缓存) @@ -88,7 +94,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler { @Override public AuthResult handleAuthentication(String clientId, IotDeviceMessage message, - String codecType, NetSocket socket) { + String codecType, NetSocket socket) { String method = message.getMethod(); // 路由到不同的认证处理方法 @@ -104,8 +110,8 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler { @Override public void handleBusinessMessage(String clientId, IotDeviceMessage message, - String codecType, NetSocket socket, - String productKey, String deviceName, String serverId) { + String codecType, NetSocket socket, + String productKey, String deviceName, String serverId) { try { // 1. 发送消息到消息总线 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); @@ -117,7 +123,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler { // 根据 JT808 协议规范,平台需要对终端上报的业务消息进行应答,包括: // - 0x0200 位置信息汇报 // - 0x0002 终端心跳 - // - 0x0704 批量位置上报 + // - 0x0704 批量位置补报上报 // - 等其他业务消息 if (!IotDeviceMessageMethodEnum.isReplyDisabled(message.getMethod())) { // 提取终端手机号、流水号和原始消息ID @@ -145,7 +151,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler { @Override public void sendResponse(NetSocket socket, IotDeviceMessage originalMessage, - boolean success, String message, String codecType) { + boolean success, String message, String codecType) { // JT808 协议的响应由具体的处理方法发送(注册应答、通用应答等) // 此方法不需要实现,保留空实现 log.debug("[sendResponse][JT808 协议响应由具体方法发送,跳过通用响应]"); @@ -172,7 +178,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler { * @return 认证结果(PENDING 表示注册成功但需要继续鉴权) */ private AuthResult handleRegister(String clientId, IotDeviceMessage message, - String codecType, NetSocket socket) { + String codecType, NetSocket socket) { try { // 1. 提取终端手机号 String terminalPhone = extractTerminalPhone(message); @@ -195,13 +201,13 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler { // 3. 生成鉴权码(根据设备的认证类型) String authToken = generateAuthToken(terminalPhone, device); - // 4. 缓存鉴权码到 Redis(设置过期时间) + // 4. 缓存鉴权码到 Redis(30天过期) String redisKey = String.format(JT808_AUTH_TOKEN, terminalPhone); stringRedisTemplate.opsForValue().set(redisKey, authToken, - AUTH_TOKEN_EXPIRE_MINUTES, TimeUnit.MINUTES); + AUTH_TOKEN_EXPIRE_DAYS, TimeUnit.DAYS); - log.debug("[handleRegister][鉴权码已缓存到 Redis,终端手机号: {}, 过期时间: {} 分钟]", - terminalPhone, AUTH_TOKEN_EXPIRE_MINUTES); + log.debug("[handleRegister][鉴权码已缓存到 Redis,终端手机号: {}, 过期时间: {} 天]", + terminalPhone, AUTH_TOKEN_EXPIRE_DAYS); // 5. 发送注册应答(成功,result_code=0) sendRegisterResp(socket, terminalPhone, flowId, (byte) 0, authToken, codecType, message.getRequestId()); @@ -244,7 +250,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler { * @return 认证结果(SUCCESS 表示鉴权成功,可以发送业务消息) */ private AuthResult handleAuth(String clientId, IotDeviceMessage message, - String codecType, NetSocket socket) { + String codecType, NetSocket socket) { try { // 1. 提取终端手机号和鉴权码 String terminalPhone = extractTerminalPhone(message); @@ -292,8 +298,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler { // 4. 发送通用应答(成功,result_code=0) sendCommonResp(socket, terminalPhone, flowId, 0x0102, (byte) 0, codecType, message.getRequestId()); - // 5. 从 Redis 删除鉴权码 - stringRedisTemplate.delete(redisKey); + // 注意:鉴权码保留在 Redis 中,设备断线重连时可重复使用 log.info("[handleAuth][JT808 鉴权成功,终端手机号: {}, 设备ID: {}]", terminalPhone, device.getId()); @@ -320,41 +325,27 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler { /** * 生成鉴权码 *

- * 使用系统标准认证 API 生成鉴权码,保持与标准协议认证的一致性。 - * 鉴权码的生成策略由系统统一管理,支持: - * - SECRET(一机一密):使用设备的 deviceSecret - * - PRODUCT_SECRET(一型一密):使用产品的 productSecret - * - NONE(免鉴权):使用终端手机号 - *

- * 注意:此方法基于 demo 的实现(RegisterHandler:48),demo 使用手机号作为鉴权码 + * 生成随机 token 作为鉴权码,缓存在 Redis 中(过期时间:AUTH_TOKEN_EXPIRE_DAYS)。 + * 终端使用此 token 进行鉴权,鉴权成功后清除缓存。 * * @param terminalPhone 终端手机号 * @param device 设备信息 - * @return 鉴权码 + * @return 随机鉴权码(32位十六进制字符串) */ private String generateAuthToken(String terminalPhone, IotDeviceRespDTO device) { try { - // 构建认证参数(username 格式:productKey.deviceName) - // String username = IotDeviceAuthUtils.buildUsername(device.getProductKey(), - // device.getDeviceName()); + // 生成 32 位随机十六进制字符串作为鉴权码 + String authToken = IdUtil.simpleUUID().toUpperCase(); - // 调用系统标准认证 API 获取认证信息 - // 注意:这里只是为了获取密钥信息,不是真正的认证 - // 实际的鉴权码应该是设备的密钥(deviceSecret 或 productSecret) - // 但由于当前 API 不返回密钥,我们使用终端手机号作为鉴权码(与 demo 保持一致) + log.debug("[generateAuthToken][生成鉴权码,终端手机号: {}, 设备: {}, 鉴权码: {}]", + terminalPhone, device.getDeviceName(), authToken); - log.debug("[generateAuthToken][生成鉴权码,终端手机号: {}, 设备: {}, 认证类型: {}]", - terminalPhone, device.getDeviceName(), - StrUtil.isNotBlank(device.getAuthType()) ? device.getAuthType() : device.getProductAuthType()); - - // 使用终端手机号作为鉴权码(与 demo 保持一致) - // TODO: 后续可以根据 authType 从系统获取真实的密钥 - return terminalPhone; + return authToken; } catch (Exception e) { log.error("[generateAuthToken][生成鉴权码异常,终端手机号: {}]", terminalPhone, e); - // 异常时使用终端手机号兜底 - return terminalPhone; + // 异常时使用时间戳兜底 + return Long.toHexString(System.currentTimeMillis()).toUpperCase(); } } @@ -377,7 +368,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler { * @param requestId 请求ID */ private void sendRegisterResp(NetSocket socket, String phone, int replyFlowId, byte replyCode, - String authToken, String codecType, String requestId) { + String authToken, String codecType, String requestId) { try { // 生成平台流水号 int flowId = (int) (System.currentTimeMillis() % 65535) + 1; @@ -427,7 +418,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler { * @param requestId 请求ID */ private void sendCommonResp(NetSocket socket, String phone, int replyFlowId, int replyId, - byte replyCode, String codecType, String requestId) { + byte replyCode, String codecType, String requestId) { try { // 生成平台流水号 int flowId = (int) (System.currentTimeMillis() % 65535) + 1;