fix(iot): jt808消息应答机制调整-每条消息都需应答
This commit is contained in:
@@ -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)
|
||||
* <p>
|
||||
* JT808 心跳消息体为空,仅用于保活,平台必须回复 0x8001 通用应答
|
||||
*/
|
||||
private Map<String, Object> parseHeartbeatAsState() {
|
||||
private Map<String, Object> parseHeartbeat(Jt808DataPack dataPack) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("state", "online");
|
||||
result.put("timestamp", System.currentTimeMillis());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -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 协议处理器
|
||||
* <p>
|
||||
* 实现 JT808 部标协议的完整认证流程:
|
||||
* 1. 终端注册(0x0100):设备首次连接时注册,平台返回鉴权码
|
||||
* 1. 终端注册(0x0100):设备首次连接时注册,平台生成并返回随机鉴权码
|
||||
* 2. 终端鉴权(0x0102):设备使用鉴权码进行鉴权,鉴权成功后可发送业务消息
|
||||
* <p>
|
||||
* 认证策略(基于设备配置的 authType):
|
||||
* - SECRET(一机一密):使用设备的 deviceSecret 作为鉴权码
|
||||
* - PRODUCT_SECRET(一型一密):使用产品的 productSecret 作为鉴权码
|
||||
* - NONE(免鉴权):使用终端手机号作为鉴权码(兼容模式)
|
||||
* 鉴权码机制:
|
||||
* - 注册时生成随机 token(32位UUID大写形式)
|
||||
* - 鉴权码永久有效,缓存在 Redis 中
|
||||
* - 鉴权成功后保留鉴权码,设备断线重连时可重复使用
|
||||
* - 基于连接的信任:鉴权成功后,该 TCP 连接上的后续消息无需再鉴权
|
||||
* <p>
|
||||
* 设计说明:
|
||||
* - 基于 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)
|
||||
|
||||
/**
|
||||
* 鉴权码缓存过期时间(分钟)
|
||||
* 鉴权码缓存过期时间(天)
|
||||
* <p>
|
||||
* 设备在注册后需要在此时间内完成鉴权,否则需要重新注册。
|
||||
* Redis 会自动清理过期的鉴权码,无需手动管理。
|
||||
* 设置较长的过期时间,支持设备断线重连时重复使用鉴权码。
|
||||
*/
|
||||
private static final int AUTH_TOKEN_EXPIRE_DAYS = 30;
|
||||
|
||||
/**
|
||||
* 鉴权码缓存 Key 前缀
|
||||
* <p>
|
||||
* 鉴权码在注册时生成并缓存到 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 {
|
||||
/**
|
||||
* 生成鉴权码
|
||||
* <p>
|
||||
* 使用系统标准认证 API 生成鉴权码,保持与标准协议认证的一致性。
|
||||
* 鉴权码的生成策略由系统统一管理,支持:
|
||||
* - SECRET(一机一密):使用设备的 deviceSecret
|
||||
* - PRODUCT_SECRET(一型一密):使用产品的 productSecret
|
||||
* - NONE(免鉴权):使用终端手机号
|
||||
* <p>
|
||||
* 注意:此方法基于 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;
|
||||
|
||||
Reference in New Issue
Block a user