Commit f1368d9e792a2035720d3b38d94ed5a1b96d5ef1

Authored by haohao
1 parent 2737ffa1

reactor:【IoT 物联网】重构 MQTT 认证和事件处理逻辑,符合 EMQX 官方规范

yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java
@@ -125,9 +125,8 @@ public class IotMqttUpstreamProtocol { @@ -125,9 +125,8 @@ public class IotMqttUpstreamProtocol {
125 125
126 // 创建认证处理器 126 // 创建认证处理器
127 IotMqttHttpAuthHandler authHandler = new IotMqttHttpAuthHandler(this); 127 IotMqttHttpAuthHandler authHandler = new IotMqttHttpAuthHandler(this);
128 - router.post(IotMqttTopicUtils.MQTT_AUTH_AUTHENTICATE_PATH).handler(authHandler::authenticate);  
129 - router.post(IotMqttTopicUtils.MQTT_AUTH_CONNECTED_PATH).handler(authHandler::connected);  
130 - router.post(IotMqttTopicUtils.MQTT_AUTH_DISCONNECTED_PATH).handler(authHandler::disconnected); 128 + router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(authHandler::handleAuth);
  129 + router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(authHandler::handleEvent);
131 130
132 // 启动 HTTP 服务器 131 // 启动 HTTP 服务器
133 int authPort = emqxProperties.getHttpAuthPort(); 132 int authPort = emqxProperties.getHttpAuthPort();
@@ -338,8 +337,8 @@ public class IotMqttUpstreamProtocol { @@ -338,8 +337,8 @@ public class IotMqttUpstreamProtocol {
338 int qos = emqxProperties.getMqttQos(); 337 int qos = emqxProperties.getMqttQos();
339 log.info("[subscribeToTopics][开始订阅主题, 共 {} 个, QoS: {}]", topicList.size(), qos); 338 log.info("[subscribeToTopics][开始订阅主题, 共 {} 个, QoS: {}]", topicList.size(), qos);
340 339
341 - int[] successCount = {0}; // 使用数组以便在 lambda 中修改  
342 - int[] failCount = {0}; 340 + int[] successCount = { 0 }; // 使用数组以便在 lambda 中修改
  341 + int[] failCount = { 0 };
343 342
344 for (String topic : topicList) { 343 for (String topic : topicList) {
345 mqttClient.subscribe(topic, qos, subscribeResult -> { 344 mqttClient.subscribe(topic, qos, subscribeResult -> {
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttHttpAuthHandler.java
@@ -27,29 +27,34 @@ import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVIC @@ -27,29 +27,34 @@ import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVIC
27 public class IotMqttHttpAuthHandler { 27 public class IotMqttHttpAuthHandler {
28 28
29 /** 29 /**
30 - * 认证成功状态码 30 + * HTTP 成功状态码(EMQX 要求固定使用 200)
31 */ 31 */
32 private static final int SUCCESS_STATUS_CODE = 200; 32 private static final int SUCCESS_STATUS_CODE = 200;
33 33
34 /** 34 /**
35 - * 参数错误状态码 35 + * 认证允许结果
36 */ 36 */
37 - private static final int BAD_REQUEST_STATUS_CODE = 400; 37 + private static final String RESULT_ALLOW = "allow";
38 38
39 /** 39 /**
40 - * 认证失败状态码 40 + * 认证拒绝结果
41 */ 41 */
42 - private static final int UNAUTHORIZED_STATUS_CODE = 401; 42 + private static final String RESULT_DENY = "deny";
43 43
44 /** 44 /**
45 - * 服务器错误状态码 45 + * 认证忽略结果
46 */ 46 */
47 - private static final int INTERNAL_ERROR_STATUS_CODE = 500; 47 + private static final String RESULT_IGNORE = "ignore";
48 48
49 /** 49 /**
50 - * MQTT 协议实例,用于获取服务器ID 50 + * EMQX 事件类型常量
51 */ 51 */
  52 + private static final String EVENT_CLIENT_CONNECTED = "client.connected";
  53 + private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected";
  54 +
52 private final IotMqttUpstreamProtocol protocol; 55 private final IotMqttUpstreamProtocol protocol;
  56 + private final IotDeviceMessageService deviceMessageService;
  57 + private final IotDeviceCommonApi deviceApi;
53 58
54 /** 59 /**
55 * 构造器 60 * 构造器
@@ -58,12 +63,14 @@ public class IotMqttHttpAuthHandler { @@ -58,12 +63,14 @@ public class IotMqttHttpAuthHandler {
58 */ 63 */
59 public IotMqttHttpAuthHandler(IotMqttUpstreamProtocol protocol) { 64 public IotMqttHttpAuthHandler(IotMqttUpstreamProtocol protocol) {
60 this.protocol = protocol; 65 this.protocol = protocol;
  66 + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
  67 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
61 } 68 }
62 69
63 /** 70 /**
64 * EMQX 认证接口 71 * EMQX 认证接口
65 */ 72 */
66 - public void authenticate(RoutingContext context) { 73 + public void handleAuth(RoutingContext context) {
67 try { 74 try {
68 // 解析请求体 75 // 解析请求体
69 JsonObject body = parseRequestBody(context); 76 JsonObject body = parseRequestBody(context);
@@ -71,35 +78,41 @@ public class IotMqttHttpAuthHandler { @@ -71,35 +78,41 @@ public class IotMqttHttpAuthHandler {
71 return; 78 return;
72 } 79 }
73 80
74 - String clientid = body.getString("clientid"); 81 + String clientId = body.getString("clientid");
75 String username = body.getString("username"); 82 String username = body.getString("username");
76 String password = body.getString("password"); 83 String password = body.getString("password");
77 84
78 - log.debug("[authenticate][EMQX 设备认证, clientId: {}, username: {}]", clientid, username); 85 + log.debug("[handleAuth][设备认证请求: clientId={}, username={}]", clientId, username);
79 86
80 // 参数校验 87 // 参数校验
81 - if (!validateAuthParams(context, clientid, username, password)) { 88 + if (StrUtil.hasEmpty(clientId, username, password)) {
  89 + log.info("[handleAuth][认证参数不完整: clientId={}, username={}]", clientId, username);
  90 + sendAuthResponse(context, RESULT_DENY, false, "认证参数不完整");
82 return; 91 return;
83 } 92 }
84 93
85 // 执行设备认证 94 // 执行设备认证
86 - if (!performDeviceAuth(context, clientid, username, password)) {  
87 - return; 95 + boolean authResult = performDeviceAuth(clientId, username, password);
  96 + if (authResult) {
  97 + log.info("[handleAuth][设备认证成功: {}]", username);
  98 + sendAuthResponse(context, RESULT_ALLOW, false, null);
  99 + } else {
  100 + log.info("[handleAuth][设备认证失败: {}]", username);
  101 + sendAuthResponse(context, RESULT_DENY, false, DEVICE_AUTH_FAIL.getMsg());
88 } 102 }
89 103
90 - log.debug("[authenticate][设备认证成功, clientId: {}, username: {}]", clientid, username);  
91 - sendSuccessResponse(context, "认证成功");  
92 -  
93 } catch (Exception e) { 104 } catch (Exception e) {
94 - log.error("[authenticate][设备认证异常, 详细信息: {}]", e.getMessage(), e);  
95 - sendErrorResponse(context, INTERNAL_ERROR_STATUS_CODE, "认证服务异常"); 105 + log.error("[handleAuth][设备认证异常]", e);
  106 + sendAuthResponse(context, RESULT_IGNORE, false, "认证服务异常");
96 } 107 }
97 } 108 }
98 109
99 /** 110 /**
100 - * EMQX 客户端连接事件钩子 111 + * EMQX 统一事件处理接口
  112 + * 根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件
  113 + * 支持的事件类型:client.connected、client.disconnected 等
101 */ 114 */
102 - public void connected(RoutingContext context) { 115 + public void handleEvent(RoutingContext context) {
103 try { 116 try {
104 // 解析请求体 117 // 解析请求体
105 JsonObject body = parseRequestBody(context); 118 JsonObject body = parseRequestBody(context);
@@ -107,48 +120,52 @@ public class IotMqttHttpAuthHandler { @@ -107,48 +120,52 @@ public class IotMqttHttpAuthHandler {
107 return; 120 return;
108 } 121 }
109 122
110 - String clientid = body.getString("clientid"); 123 + String event = body.getString("event");
111 String username = body.getString("username"); 124 String username = body.getString("username");
112 - Long timestamp = body.getLong("timestamp");  
113 125
114 - log.debug("[connected][设备连接, clientId: {}, username: {}, timestamp: {}]",  
115 - clientid, username, timestamp); 126 + log.debug("[handleEvent][收到事件: {} - {}]", event, username);
  127 +
  128 + // 根据事件类型进行分发处理
  129 + switch (event) {
  130 + case EVENT_CLIENT_CONNECTED:
  131 + handleClientConnected(body);
  132 + break;
  133 + case EVENT_CLIENT_DISCONNECTED:
  134 + handleClientDisconnected(body);
  135 + break;
  136 + default:
  137 + log.debug("[handleEvent][忽略事件: {}]", event);
  138 + break;
  139 + }
116 140
117 - handleDeviceStateChange(username, true, "设备连接");  
118 - sendSuccessResponse(context, "处理成功"); 141 + // EMQX Webhook 只需要200状态码,无需响应体
  142 + context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
119 143
120 } catch (Exception e) { 144 } catch (Exception e) {
121 - log.error("[connected][处理设备连接事件失败, 详细信息: {}]", e.getMessage(), e);  
122 - sendErrorResponse(context, INTERNAL_ERROR_STATUS_CODE, "处理失败"); 145 + log.error("[handleEvent][事件处理失败]", e);
  146 + // 即使处理失败,也返回200避免EMQX重试
  147 + context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
123 } 148 }
124 } 149 }
125 150
126 /** 151 /**
127 - * EMQX 客户端断开连接事件钩子 152 + * 处理客户端连接事件
128 */ 153 */
129 - public void disconnected(RoutingContext context) {  
130 - try {  
131 - // 解析请求体  
132 - JsonObject body = parseRequestBody(context);  
133 - if (body == null) {  
134 - return;  
135 - }  
136 -  
137 - String clientid = body.getString("clientid");  
138 - String username = body.getString("username");  
139 - String reason = body.getString("reason");  
140 - Long timestamp = body.getLong("timestamp");  
141 -  
142 - log.debug("[disconnected][设备断开连接, clientId: {}, username: {}, reason: {}, timestamp: {}]",  
143 - clientid, username, reason, timestamp); 154 + private void handleClientConnected(JsonObject body) {
  155 + String username = body.getString("username");
  156 + log.info("[handleClientConnected][设备上线: {}]", username);
  157 + handleDeviceStateChange(username, true);
  158 + }
144 159
145 - handleDeviceStateChange(username, false, "设备断开连接,原因:" + reason);  
146 - sendSuccessResponse(context, "处理成功"); 160 + /**
  161 + * 处理客户端断开连接事件
  162 + */
  163 + private void handleClientDisconnected(JsonObject body) {
  164 + String username = body.getString("username");
  165 + String reason = body.getString("reason");
147 166
148 - } catch (Exception e) {  
149 - log.error("[disconnected][处理设备断开连接事件失败, 详细信息: {}]", e.getMessage(), e);  
150 - sendErrorResponse(context, INTERNAL_ERROR_STATUS_CODE, "处理失败");  
151 - } 167 + log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason);
  168 + handleDeviceStateChange(username, false);
152 } 169 }
153 170
154 /** 171 /**
@@ -161,142 +178,113 @@ public class IotMqttHttpAuthHandler { @@ -161,142 +178,113 @@ public class IotMqttHttpAuthHandler {
161 try { 178 try {
162 JsonObject body = context.body().asJsonObject(); 179 JsonObject body = context.body().asJsonObject();
163 if (body == null) { 180 if (body == null) {
164 - log.warn("[parseRequestBody][请求体为空]");  
165 - sendErrorResponse(context, BAD_REQUEST_STATUS_CODE, "请求体不能为空"); 181 + log.info("[parseRequestBody][请求体为空]");
  182 + sendAuthResponse(context, RESULT_IGNORE, false, "请求体不能为空");
166 return null; 183 return null;
167 } 184 }
168 return body; 185 return body;
169 } catch (Exception e) { 186 } catch (Exception e) {
170 log.error("[parseRequestBody][解析请求体失败]", e); 187 log.error("[parseRequestBody][解析请求体失败]", e);
171 - sendErrorResponse(context, BAD_REQUEST_STATUS_CODE, "请求体格式错误"); 188 + sendAuthResponse(context, RESULT_IGNORE, false, "请求体格式错误");
172 return null; 189 return null;
173 } 190 }
174 } 191 }
175 192
176 /** 193 /**
177 - * 验证认证参数  
178 - *  
179 - * @param context 路由上下文  
180 - * @param clientid 客户端ID  
181 - * @param username 用户名  
182 - * @param password 密码  
183 - * @return 验证是否通过  
184 - */  
185 - private boolean validateAuthParams(RoutingContext context, String clientid, String username, String password) {  
186 - if (StrUtil.hasEmpty(clientid, username, password)) {  
187 - log.warn("[validateAuthParams][认证参数不完整, clientId: {}, username: {}, password: {}]",  
188 - clientid, username, StrUtil.isNotEmpty(password) ? "***" : "空");  
189 - sendErrorResponse(context, BAD_REQUEST_STATUS_CODE, "认证参数不完整");  
190 - return false;  
191 - }  
192 - return true;  
193 - }  
194 -  
195 - /**  
196 * 执行设备认证 194 * 执行设备认证
197 * 195 *
198 - * @param context 路由上下文  
199 - * @param clientid 客户端ID 196 + * @param clientId 客户端ID
200 * @param username 用户名 197 * @param username 用户名
201 * @param password 密码 198 * @param password 密码
202 * @return 认证是否成功 199 * @return 认证是否成功
203 */ 200 */
204 - private boolean performDeviceAuth(RoutingContext context, String clientid, String username, String password) { 201 + private boolean performDeviceAuth(String clientId, String username, String password) {
205 try { 202 try {
206 - IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);  
207 CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() 203 CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
208 - .setClientId(clientid) 204 + .setClientId(clientId)
209 .setUsername(username) 205 .setUsername(username)
210 .setPassword(password)); 206 .setPassword(password));
211 207
212 result.checkError(); 208 result.checkError();
213 - if (!BooleanUtil.isTrue(result.getData())) {  
214 - log.warn("[performDeviceAuth][设备认证失败, clientId: {}, username: {}]", clientid, username);  
215 - sendErrorResponse(context, UNAUTHORIZED_STATUS_CODE, DEVICE_AUTH_FAIL.getMsg());  
216 - return false;  
217 - }  
218 - return true; 209 + return BooleanUtil.isTrue(result.getData());
219 } catch (Exception e) { 210 } catch (Exception e) {
220 - log.error("[performDeviceAuth][设备认证异常, clientId: {}, username: {}]", clientid, username, e);  
221 - sendErrorResponse(context, INTERNAL_ERROR_STATUS_CODE, "认证服务异常");  
222 - return false; 211 + log.error("[performDeviceAuth][认证接口调用失败: {}]", username, e);
  212 + throw e;
223 } 213 }
224 } 214 }
225 215
226 /** 216 /**
227 * 处理设备状态变化 217 * 处理设备状态变化
228 * 218 *
229 - * @param username 用户名  
230 - * @param online 是否在线  
231 - * @param actionDesc 操作描述 219 + * @param username 用户名
  220 + * @param online 是否在线
232 */ 221 */
233 - private void handleDeviceStateChange(String username, boolean online, String actionDesc) { 222 + private void handleDeviceStateChange(String username, boolean online) {
234 if (StrUtil.isEmpty(username) || "undefined".equals(username)) { 223 if (StrUtil.isEmpty(username) || "undefined".equals(username)) {
235 - log.warn("[handleDeviceStateChange][用户名为空或'undefined', username: {}, action: {}]",  
236 - username, actionDesc);  
237 return; 224 return;
238 } 225 }
239 226
240 // 解析设备信息 227 // 解析设备信息
241 IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username); 228 IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username);
242 if (deviceInfo == null) { 229 if (deviceInfo == null) {
243 - log.warn("[handleDeviceStateChange][无法从 username({}) 解析设备信息, action: {}]",  
244 - username, actionDesc); 230 + log.debug("[handleDeviceStateChange][跳过非设备连接: {}]", username);
245 return; 231 return;
246 } 232 }
247 233
248 try { 234 try {
249 - // 从协议实例获取服务器 ID  
250 String serverId = protocol.getServerId(); 235 String serverId = protocol.getServerId();
251 if (StrUtil.isEmpty(serverId)) { 236 if (StrUtil.isEmpty(serverId)) {
252 - log.error("[handleDeviceStateChange][获取服务器ID失败, username: {}, action: {}]",  
253 - username, actionDesc); 237 + log.error("[handleDeviceStateChange][获取服务器ID失败]");
254 return; 238 return;
255 } 239 }
256 240
257 // 构建设备状态消息 241 // 构建设备状态消息
258 - IotDeviceMessageService deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);  
259 - IotDeviceMessage message;  
260 - if (online) {  
261 - message = IotDeviceMessage.buildStateOnline();  
262 - log.debug("[handleDeviceStateChange][发送设备上线消息, username: {}, serverId: {}]",  
263 - username, serverId);  
264 - } else {  
265 - message = IotDeviceMessage.buildStateOffline();  
266 - log.debug("[handleDeviceStateChange][发送设备下线消息, username: {}, serverId: {}]",  
267 - username, serverId);  
268 - } 242 + IotDeviceMessage message = online ? IotDeviceMessage.buildStateOnline()
  243 + : IotDeviceMessage.buildStateOffline();
269 244
270 // 发送消息到消息总线 245 // 发送消息到消息总线
271 deviceMessageService.sendDeviceMessage(message, 246 deviceMessageService.sendDeviceMessage(message,
272 deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); 247 deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId);
273 248
274 - log.info("[handleDeviceStateChange][{}处理成功, productKey: {}, deviceName: {}, serverId: {}]",  
275 - actionDesc, deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); 249 + log.info("[handleDeviceStateChange][设备状态更新: {}/{} -> {}]",
  250 + deviceInfo.getProductKey(), deviceInfo.getDeviceName(),
  251 + online ? "在线" : "离线");
276 252
277 } catch (Exception e) { 253 } catch (Exception e) {
278 - log.error("[handleDeviceStateChange][发送设备状态消息失败, username: {}, online: {}, action: {}]",  
279 - username, online, actionDesc, e); 254 + log.error("[handleDeviceStateChange][发送设备状态消息失败: {}]", username, e);
280 } 255 }
281 } 256 }
282 257
283 /** 258 /**
284 - * 发送成功响应 259 + * 发送 EMQX 认证响应
  260 + * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应
  261 + *
  262 + * @param context 路由上下文
  263 + * @param result 认证结果:allow、deny、ignore
  264 + * @param isSuperuser 是否超级用户
  265 + * @param message 日志消息(仅用于日志记录,不返回给EMQX)
285 */ 266 */
286 - private void sendSuccessResponse(RoutingContext context, String message) { 267 + private void sendAuthResponse(RoutingContext context, String result, boolean isSuperuser, String message) {
  268 + // 构建符合 EMQX 官方规范的响应
  269 + JsonObject response = new JsonObject()
  270 + .put("result", result)
  271 + .put("is_superuser", isSuperuser);
  272 +
  273 + // 可以根据业务需求添加客户端属性
  274 + // response.put("client_attrs", new JsonObject().put("role", "device"));
  275 +
  276 + // 可以添加认证过期时间(可选)
  277 + // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600);
  278 +
  279 + // 记录详细的响应日志(message仅用于日志,不返回给EMQX)
  280 + if (StrUtil.isNotEmpty(message)) {
  281 + log.debug("[sendAuthResponse][响应详情: result={}, message={}]", result, message);
  282 + }
  283 +
287 context.response() 284 context.response()
288 .setStatusCode(SUCCESS_STATUS_CODE) 285 .setStatusCode(SUCCESS_STATUS_CODE)
289 - .putHeader("Content-Type", "text/plain; charset=utf-8")  
290 - .end(message); 286 + .putHeader("Content-Type", "application/json; charset=utf-8")
  287 + .end(response.encode());
291 } 288 }
292 289
293 - /**  
294 - * 发送错误响应  
295 - */  
296 - private void sendErrorResponse(RoutingContext context, int statusCode, String message) {  
297 - context.response()  
298 - .setStatusCode(statusCode)  
299 - .putHeader("Content-Type", "text/plain; charset=utf-8")  
300 - .end(message);  
301 - }  
302 } 290 }
303 \ No newline at end of file 291 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java
@@ -21,22 +21,26 @@ public final class IotMqttTopicUtils { @@ -21,22 +21,26 @@ public final class IotMqttTopicUtils {
21 */ 21 */
22 private static final String SERVICE_TOPIC_PREFIX = "/thing/"; 22 private static final String SERVICE_TOPIC_PREFIX = "/thing/";
23 23
24 - // ========== MQTT 认证路径常量 ========== 24 + // ========== MQTT HTTP 接口路径常量 ==========
25 25
26 /** 26 /**
27 - * MQTT 认证路径 27 + * MQTT 认证接口路径
  28 + * 对应 EMQX HTTP 认证插件的认证请求接口
28 */ 29 */
29 - public static final String MQTT_AUTH_AUTHENTICATE_PATH = "/mqtt/auth/authenticate"; 30 + public static final String MQTT_AUTH_PATH = "/mqtt/auth";
30 31
31 /** 32 /**
32 - * MQTT 连接事件路径 33 + * MQTT 统一事件处理接口路径
  34 + * 对应 EMQX Webhook 的统一事件处理接口,支持所有客户端事件
  35 + * 包括:client.connected、client.disconnected、message.publish 等
33 */ 36 */
34 - public static final String MQTT_AUTH_CONNECTED_PATH = "/mqtt/auth/connected"; 37 + public static final String MQTT_EVENT_PATH = "/mqtt/event";
35 38
36 /** 39 /**
37 - * MQTT 断开事件路径 40 + * MQTT 授权接口路径(预留)
  41 + * 对应 EMQX HTTP 授权插件的授权检查接口
38 */ 42 */
39 - public static final String MQTT_AUTH_DISCONNECTED_PATH = "/mqtt/auth/disconnected"; 43 + public static final String MQTT_AUTHZ_PATH = "/mqtt/authz";
40 44
41 // ========== 工具方法 ========== 45 // ========== 工具方法 ==========
42 46