# EmqxExpand **Repository Path**: itwennet/EmqxExpand ## Basic Information - **Project Name**: EmqxExpand - **Description**: 实现emqx鉴权与登录,提供API管理emqx用户和规则。 - **Primary Language**: Java - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2025-05-28 - **Last Updated**: 2025-05-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 平台简介 - 项目基于ruoyi - 可以自行修改登陆和校验权限的逻辑 - 可以修改签名的方式 ## 安全性与使用建议 - 该系统不建议暴露到公网,建议仅在内部使用。 ## 技术栈 - 基于SpringBoot、Spring Security、JWT、MyBatis、Redis、Vue2.x、Element-UI等技术构建。 ## 主要功能 - emqx鉴权与登录 - 提供API管理emqx用户和规则 - 用户管理与角色管理 - 菜单管理与字典管理 - 参数管理与通知公告 - 操作日志与登录日志 - 在线用户管理 - 定时任务与服务监控 - 缓存监控 - 在线构建器与代码生成 - 系统接口与Swagger-UI文档 # 部署到Docker(测试环境) 请注意,以下命令仅适用于测试环境,并且不推荐在生产环境中挂载JAR文件作为卷: ```bash sudo docker run -d --name EmqxExpand \ --restart=always \ --ip 172.18.10.10 \ --network dockernet \ -p 9876:9876 \ -v /docker_data/EmqxExpand/emqx_expand-admin.jar:/usr/app/emqx_expand-admin.jar \ openjdk:8-jdk \ java -jar /usr/app/emqx_expand-admin.jar --spring.profiles.active=test ``` # 配置步骤 1. **安装Redis并修改连接信息** - 安装Redis服务。 - 在应用程序的配置文件中,修改Redis的连接信息,包括主机名、端口、密码等。 2. **安装MySQL并配置数据库** - 安装MySQL服务。 - 使用提供的SQL脚本(位于`sql`文件夹中,使用最新的sql文件)创建所需的数据库和表。 - 在应用程序的配置文件中,修改MySQL的连接信息,包括数据库URL、用户名、密码等。 3. **安装emqx** - 根据需求选择安装emqx的单机模式或集群模式。 - 确保emqx服务正常运行。 4. **创建密钥** ![屏幕截图 2025 02 05 152950](https://img.z4a.net/images/2025/02/10/-2025-02-05-152950.png) - 按照相关步骤生成或获取所需的密钥。 5. **保存密钥** ![屏幕截图 2025 02 05 153005](https://img.z4a.net/images/2025/02/10/-2025-02-05-153005.png) 6. **配置密钥权限** ![屏幕截图 2025 02 05 153703](https://img.z4a.net/images/2025/02/10/-2025-02-05-153703.png) 7. **配置emqx客户端认证** - 进入emqx后台管理界面。 - 配置客户端认证信息,具体URL为:`http://172.18.10.10:9876/auth/login` 实际地址自行更改 - 设置请求方式为POST。 - 添加请求头`appid`。 - 请求体如下: ```json { "username": "${username}", "password": "${password}", "clientid": "${clientid}" } ``` ![屏幕截图 2025 02 07 153518](https://img.z4a.net/images/2025/02/10/-2025-02-07-153518.png) ![屏幕截图 2025 02 07 155041](https://img.z4a.net/images/2025/02/10/-2025-02-07-155041.png) - 确保配置正确保存并生效。 8. **配置emqx客户端授权** - 和配置emqx客户端认证类似。 - 配置emqx客户端授权,具体URL为:`http://172.18.10.10:9876/auth/login` 实际地址自行更改 - 设置请求方式为POST。 - 添加请求头`appid`。 - 请求体如下: ```json { "username": "${username}", "clientid": "${clientid}", "peerhost": "${peerhost}", "mountpoint": "${mountpoint}", "action": "${action}", "topic": "${topic}" } ``` - 停用默认的File数据源。 - 确保配置正确保存并生效。 9. **系统参数** - 是否校验api签名时间,项目联调时可以关闭 - ![屏幕截图 2025 02 10 102221](https://img.z4a.net/images/2025/02/10/-2025-02-10-102221.png) - 是否允许emqx使用同一个ClientId重复登陆,直接创建的用户只有设置了允许ClientId重复登陆才能成功登陆,否则需要调用系统相关api指定了ClientId才能正常登陆(emqx本身不支持唯一ClientId) - ![屏幕截图 2025 02 10 103536](https://img.z4a.net/images/2025/02/10/-2025-02-10-103536.png) 10. **api调用** - 签名与验证签名 ```java import org.springframework.util.StringUtils; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.UUID; public class Signature { public static void getSecret() { //生成应用id和密钥提供给第三方使用,具体生成规则自己定 String appId = UUID.randomUUID().toString().replace("-", "").toLowerCase(); String appSecret = UUID.randomUUID().toString().replace("-", "").toUpperCase(); System.out.println("appId:" + appId); System.out.println("appSecret:" + appSecret); } /** * 签名 * @param secretKey 密钥 * @param url 请求url * @param data 请求体json数据 * @param timestampStr 时间戳 * @return 签名结果 */ public static String signWithHmacSha1(String secretKey,String url, String data,String timestampStr) { String dataToSign = (StringUtils.hasText(data)) ? data : ""; String origin = url+ "\n" + dataToSign +"\n" + timestampStr;; try { SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA1"); Mac mac = Mac.getInstance("HmacSHA1"); mac.init(signingKey); return Base64.getEncoder().encodeToString(mac.doFinal(origin.getBytes(StandardCharsets.UTF_8))); } catch (NoSuchAlgorithmException | InvalidKeyException e) { return null; } } /** * 验证签名 * @param secretKey 密钥 * @param url 请求url,不包括ip和端口 * @param data 请求体json数据 * @param timestampStr 时间戳 * @param signature 已经签名的数据 * @return 验证结果 */ public static boolean verify(String secretKey,String url, String data,String timestampStr, String signature) { String calculatedHmac = signWithHmacSha1(secretKey,url,data,timestampStr); return StringUtils.hasText(calculatedHmac) && calculatedHmac.equals(signature); } } ``` - api调用 ```java import com.alibaba.nacos.shaded.com.google.gson.JsonArray; import com.alibaba.nacos.shaded.com.google.gson.JsonObject; import com.fasterxml.jackson.databind.ObjectMapper; import icu.zlz.common.core.exception.ServiceException; import icu.zlz.common.core.utils.StringUtils; import icu.zlz.common.core.web.domain.AjaxResult; import icu.zlz.emqx.domain.MqttAcl; import icu.zlz.emqx.domain.MqttUserOnline; import icu.zlz.emqx.utils.Signature; import okhttp3.*; import org.springframework.stereotype.Service; import org.springframework.beans.factory.annotation.Value; import org.springframework.util.ObjectUtils; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.List; /** * @author zangsheng */ @Service public class MqttAuthService { @Value("${mqttAuth.url}") private String baseUrl; @Value("${mqttAuth.appId}") private String appId; @Value("${mqttAuth.appSecret}") private String appSecret; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private final OkHttpClient client = new OkHttpClient(); private static final String USER_API = "/api/emqx/user"; private static final String ACL_API = "/api/emqx/acl"; private static final String USER_ONLINE_API = "/api/emqx/userOnline"; private static final MediaType MEDIA_TYPE_JSON = MediaType.get("application/json; charset=utf-8"); private Request.Builder getBuilder(String url, String data) { String timestamp = String.valueOf(System.currentTimeMillis() / 1000); Request.Builder builder = new Request.Builder(); builder.url(baseUrl + url); builder.header("Content-Type", "application/json"); builder.header("appId", appId); builder.header("timestamp", timestamp); String signature = Signature.signWithHmacSha1(appSecret, url, data, timestamp); if (!StringUtils.hasText(signature)) { throw new ServiceException("签名失败"); } builder.header("signature", signature); System.out.println(baseUrl + url); System.out.println(appId); System.out.println(timestamp); System.out.println(signature); return builder; } private AjaxResult getResponse(Request.Builder builder) { try { try (Response response = client.newCall(builder.build()).execute()) { if (response.body() != null) { if (!response.isSuccessful()) { return AjaxResult.error(response.code(), "HTTP request failed with msg: " + response.body().toString()); } return OBJECT_MAPPER.readValue(response.body().string(), AjaxResult.class); } else { return AjaxResult.error("Response body is null"); } } } catch (IOException e) { return AjaxResult.error(e.getMessage()); } } public AjaxResult addUser(String username, String password, String userUid) { JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("username", username); jsonObject.addProperty("password", password); jsonObject.addProperty("userUid", userUid); Request.Builder builder = getBuilder(USER_API, jsonObject.toString()); builder.post(RequestBody.create(jsonObject.toString(), MEDIA_TYPE_JSON)); return getResponse(builder); } public AjaxResult editGroupPrefix(String oldName, String newName) { Request.Builder builder = getBuilder(ACL_API + "/topic/" + oldName + "/" + newName, null); builder.get(); return getResponse(builder); } } ``` - 使用自定义注解 @SignatureApi 标注为供第三方的调用的api,使用了此注解不会走系统的权限校验,会走SignInterceptor拦截器进行验签和权限校验 11. **功能完善中**