Commit f1368d9e792a2035720d3b38d94ed5a1b96d5ef1
1 parent
2737ffa1
reactor:【IoT 物联网】重构 MQTT 认证和事件处理逻辑,符合 EMQX 官方规范
Showing
3 changed files
with
129 additions
and
138 deletions
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 |