From 4b9ed97f8f0389eeb93478247927980c945b3ac5 Mon Sep 17 00:00:00 2001 From: Crazylychee <110229037+Crazylychee@users.noreply.github.com> Date: Sat, 5 Jul 2025 20:51:42 +0800 Subject: [PATCH] [ISSUE #332] Add configuration options for login information, supporting ACL or file storage (#333) --- frontend-new/src/api/remoteApi/remoteApi.js | 156 +++++++++--------- .../dashboard/controller/AclController.java | 20 +-- .../apache/rocketmq/dashboard/model/User.java | 4 +- .../model/request/UserInfoParam.java | 2 + .../service/impl/AbstractFileStore.java | 4 +- .../service/impl/LoginServiceImpl.java | 24 +-- .../service/strategy/AclUserStrategy.java | 67 ++++++++ .../service/strategy/FileUserStrategy.java | 119 +++++++++++++ .../service/strategy/UserContext.java | 54 ++++++ .../service/strategy/UserStrategy.java | 24 +++ src/main/resources/application.yml | 6 + 11 files changed, 370 insertions(+), 110 deletions(-) create mode 100644 src/main/java/org/apache/rocketmq/dashboard/service/strategy/AclUserStrategy.java create mode 100644 src/main/java/org/apache/rocketmq/dashboard/service/strategy/FileUserStrategy.java create mode 100644 src/main/java/org/apache/rocketmq/dashboard/service/strategy/UserContext.java create mode 100644 src/main/java/org/apache/rocketmq/dashboard/service/strategy/UserStrategy.java diff --git a/frontend-new/src/api/remoteApi/remoteApi.js b/frontend-new/src/api/remoteApi/remoteApi.js index 048ede9..94cec36 100644 --- a/frontend-new/src/api/remoteApi/remoteApi.js +++ b/frontend-new/src/api/remoteApi/remoteApi.js @@ -49,7 +49,7 @@ const remoteApi = { if (_redirectHandler) { _redirectHandler(); // 如果设置了重定向处理函数,则调用它 } - return { __isRedirectHandled: true }; + return {__isRedirectHandled: true}; } return response; @@ -77,24 +77,24 @@ const remoteApi = { listUsers: async (brokerAddress) => { const params = new URLSearchParams(); if (brokerAddress) params.append('brokerAddress', brokerAddress); - const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/listUsers?${params.toString()}`)); + const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/acls.query?${params.toString()}`)); return await response.json(); }, createUser: async (brokerAddress, userInfo) => { - const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/createUser'), { + const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/createUser.do'), { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ brokerAddress, userInfo }) + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({brokerAddress, userInfo}) }); return await response.json(); // 返回字符串消息 }, updateUser: async (brokerAddress, userInfo) => { - const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/updateUser'), { + const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/updateUser.do'), { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ brokerAddress, userInfo }) + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({brokerAddress, userInfo}) }); return await response.json(); }, @@ -103,7 +103,7 @@ const remoteApi = { const params = new URLSearchParams(); if (brokerAddress) params.append('brokerAddress', brokerAddress); params.append('username', username); - const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/deleteUser?${params.toString()}`), { + const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/deleteUser.do?${params.toString()}`), { method: 'DELETE' }); return await response.json(); @@ -114,24 +114,24 @@ const remoteApi = { const params = new URLSearchParams(); if (brokerAddress) params.append('brokerAddress', brokerAddress); if (searchParam) params.append('searchParam', searchParam); - const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/listAcls?${params.toString()}`)); + const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/acls.query?${params.toString()}`)); return await response.json(); }, createAcl: async (brokerAddress, subject, policies) => { - const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/createAcl'), { + const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/createAcl.do'), { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ brokerAddress, subject, policies }) + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({brokerAddress, subject, policies}) }); return await response.json(); }, updateAcl: async (brokerAddress, subject, policies) => { - const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/updateAcl'), { + const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/updateAcl.do'), { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ brokerAddress, subject, policies }) + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({brokerAddress, subject, policies}) }); return await response.json(); }, @@ -141,7 +141,7 @@ const remoteApi = { if (brokerAddress) params.append('brokerAddress', brokerAddress); params.append('subject', subject); if (resource) params.append('resource', resource); - const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/deleteAcl?${params.toString()}`), { + const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/deleteAcl.do?${params.toString()}`), { method: 'DELETE' }); return await response.json(); @@ -159,7 +159,7 @@ const remoteApi = { return data } catch (error) { console.error("Error querying message by ID:", error); - callback({ status: 1, errMsg: "Failed to query message by ID" }); + callback({status: 1, errMsg: "Failed to query message by ID"}); } }, @@ -197,7 +197,7 @@ const remoteApi = { return data; } catch (error) { console.error("Error querying DLQ messages by consumer group:", error); - return { status: 1, errMsg: "Failed to query DLQ messages by consumer group" }; + return {status: 1, errMsg: "Failed to query DLQ messages by consumer group"}; } }, resendDlqMessage: async (msgId, consumerGroup, topic) => { @@ -217,7 +217,7 @@ const remoteApi = { return data; } catch (error) { console.error("Error resending DLQ message:", error); - return { status: 1, errMsg: "Failed to resend DLQ message" }; + return {status: 1, errMsg: "Failed to resend DLQ message"}; } }, exportDlqMessage: async (msgId, consumerGroup) => { @@ -236,7 +236,7 @@ const remoteApi = { if (!newWindow) { // 浏览器可能会阻止弹窗,需要用户允许 - return { status: 1, errMsg: "Failed to open new window. Please allow pop-ups for this site." }; + return {status: 1, errMsg: "Failed to open new window. Please allow pop-ups for this site."}; } // 2. 将 JSON 数据格式化后写入新窗口 @@ -247,10 +247,10 @@ const remoteApi = { newWindow.document.write(''); newWindow.document.close(); // 关闭文档流,确保内容显示 - return { status: 0, msg: "导出请求成功,内容已在新页面显示" }; + return {status: 0, msg: "导出请求成功,内容已在新页面显示"}; } catch (error) { console.error("Error exporting DLQ message:", error); - return { status: 1, errMsg: "Failed to export DLQ message: " + error.message }; + return {status: 1, errMsg: "Failed to export DLQ message: " + error.message}; } }, @@ -267,7 +267,7 @@ const remoteApi = { return data; } catch (error) { console.error("Error batch resending DLQ messages:", error); - return { status: 1, errMsg: "Failed to batch resend DLQ messages" }; + return {status: 1, errMsg: "Failed to batch resend DLQ messages"}; } }, @@ -372,7 +372,7 @@ const remoteApi = { callback(data); } catch (error) { console.error("Error fetching producer connection list:", error); - callback({ status: 1, errMsg: "Failed to fetch producer connection list" }); // Simulate error response + callback({status: 1, errMsg: "Failed to fetch producer connection list"}); // Simulate error response } }, @@ -383,7 +383,7 @@ const remoteApi = { return data; } catch (error) { console.error("Error fetching consumer group list:", error); - return { status: 1, errMsg: "Failed to fetch consumer group list" }; + return {status: 1, errMsg: "Failed to fetch consumer group list"}; } }, @@ -394,7 +394,7 @@ const remoteApi = { return data; } catch (error) { console.error(`Error refreshing consumer group ${consumerGroup}:`, error); - return { status: 1, errMsg: `Failed to refresh consumer group ${consumerGroup}` }; + return {status: 1, errMsg: `Failed to refresh consumer group ${consumerGroup}`}; } }, @@ -405,7 +405,7 @@ const remoteApi = { return data; } catch (error) { console.error("Error refreshing all consumer groups:", error); - return { status: 1, errMsg: "Failed to refresh all consumer groups" }; + return {status: 1, errMsg: "Failed to refresh all consumer groups"}; } }, @@ -416,7 +416,7 @@ const remoteApi = { return data; } catch (error) { console.error(`Error fetching monitor config for ${consumeGroupName}:`, error); - return { status: 1, errMsg: `Failed to fetch monitor config for ${consumeGroupName}` }; + return {status: 1, errMsg: `Failed to fetch monitor config for ${consumeGroupName}`}; } }, @@ -428,13 +428,13 @@ const remoteApi = { headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ consumeGroupName, minCount, maxDiffTotal }) + body: JSON.stringify({consumeGroupName, minCount, maxDiffTotal}) }); const data = await response.json(); return data; } catch (error) { console.error("Error creating or updating consumer monitor:", error); - return { status: 1, errMsg: "Failed to create or update consumer monitor" }; + return {status: 1, errMsg: "Failed to create or update consumer monitor"}; } }, @@ -445,7 +445,7 @@ const remoteApi = { return data; } catch (error) { console.error(`Error fetching broker name list for ${consumerGroup}:`, error); - return { status: 1, errMsg: `Failed to fetch broker name list for ${consumerGroup}` }; + return {status: 1, errMsg: `Failed to fetch broker name list for ${consumerGroup}`}; } }, @@ -456,13 +456,13 @@ const remoteApi = { headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ groupName, brokerNameList }) + body: JSON.stringify({groupName, brokerNameList}) }); const data = await response.json(); return data; } catch (error) { console.error(`Error deleting consumer group ${groupName}:`, error); - return { status: 1, errMsg: `Failed to delete consumer group ${groupName}` }; + return {status: 1, errMsg: `Failed to delete consumer group ${groupName}`}; } }, @@ -473,7 +473,7 @@ const remoteApi = { return data; } catch (error) { console.error(`Error fetching consumer config for ${consumerGroup}:`, error); - return { status: 1, errMsg: `Failed to fetch consumer config for ${consumerGroup}` }; + return {status: 1, errMsg: `Failed to fetch consumer config for ${consumerGroup}`}; } }, @@ -490,7 +490,7 @@ const remoteApi = { return data; } catch (error) { console.error("Error creating or updating consumer:", error); - return { status: 1, errMsg: "Failed to create or update consumer" }; + return {status: 1, errMsg: "Failed to create or update consumer"}; } }, @@ -501,7 +501,7 @@ const remoteApi = { return data; } catch (error) { console.error(`Error fetching topics for consumer group ${consumerGroup}:`, error); - return { status: 1, errMsg: `Failed to fetch topics for consumer group ${consumerGroup}` }; + return {status: 1, errMsg: `Failed to fetch topics for consumer group ${consumerGroup}`}; } }, @@ -512,7 +512,7 @@ const remoteApi = { return data; } catch (error) { console.error(`Error fetching consumer connections for ${consumerGroup}:`, error); - return { status: 1, errMsg: `Failed to fetch consumer connections for ${consumerGroup}` }; + return {status: 1, errMsg: `Failed to fetch consumer connections for ${consumerGroup}`}; } }, @@ -523,7 +523,7 @@ const remoteApi = { return data; } catch (error) { console.error(`Error fetching running info for client ${clientId} in group ${consumerGroup}:`, error); - return { status: 1, errMsg: `Failed to fetch running info for client ${clientId} in group ${consumerGroup}` }; + return {status: 1, errMsg: `Failed to fetch running info for client ${clientId} in group ${consumerGroup}`}; } }, queryTopicList: async () => { @@ -532,7 +532,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error fetching topic list:", error); - return { status: 1, errMsg: "Failed to fetch topic list" }; + return {status: 1, errMsg: "Failed to fetch topic list"}; } }, @@ -549,7 +549,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error deleting topic:", error); - return { status: 1, errMsg: "Failed to delete topic" }; + return {status: 1, errMsg: "Failed to delete topic"}; } }, @@ -559,7 +559,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error fetching topic stats:", error); - return { status: 1, errMsg: "Failed to fetch topic stats" }; + return {status: 1, errMsg: "Failed to fetch topic stats"}; } }, @@ -569,7 +569,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error fetching topic route:", error); - return { status: 1, errMsg: "Failed to fetch topic route" }; + return {status: 1, errMsg: "Failed to fetch topic route"}; } }, @@ -579,7 +579,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error fetching topic consumers:", error); - return { status: 1, errMsg: "Failed to fetch topic consumers" }; + return {status: 1, errMsg: "Failed to fetch topic consumers"}; } }, @@ -589,7 +589,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error fetching consumer groups:", error); - return { status: 1, errMsg: "Failed to fetch consumer groups" }; + return {status: 1, errMsg: "Failed to fetch consumer groups"}; } }, @@ -599,7 +599,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error fetching topic config:", error); - return { status: 1, errMsg: "Failed to fetch topic config" }; + return {status: 1, errMsg: "Failed to fetch topic config"}; } }, @@ -609,7 +609,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error fetching cluster list:", error); - return { status: 1, errMsg: "Failed to fetch cluster list" }; + return {status: 1, errMsg: "Failed to fetch cluster list"}; } }, @@ -625,7 +625,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error creating/updating topic:", error); - return { status: 1, errMsg: "Failed to create/update topic" }; + return {status: 1, errMsg: "Failed to create/update topic"}; } }, @@ -641,7 +641,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error resetting consumer offset:", error); - return { status: 1, errMsg: "Failed to reset consumer offset" }; + return {status: 1, errMsg: "Failed to reset consumer offset"}; } }, @@ -657,7 +657,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error skipping message accumulate:", error); - return { status: 1, errMsg: "Failed to skip message accumulate" }; + return {status: 1, errMsg: "Failed to skip message accumulate"}; } }, @@ -673,7 +673,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error sending topic message:", error); - return { status: 1, errMsg: "Failed to send topic message" }; + return {status: 1, errMsg: "Failed to send topic message"}; } }, @@ -684,12 +684,12 @@ const remoteApi = { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ brokerName, topic }) + body: JSON.stringify({brokerName, topic}) }); return await response.json(); } catch (error) { console.error("Error deleting topic by broker:", error); - return { status: 1, errMsg: "Failed to delete topic by broker" }; + return {status: 1, errMsg: "Failed to delete topic by broker"}; } }, @@ -700,7 +700,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error fetching ops home page data:", error); - return { status: 1, errMsg: "Failed to fetch ops home page data" }; + return {status: 1, errMsg: "Failed to fetch ops home page data"}; } }, @@ -712,7 +712,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error updating NameServer address:", error); - return { status: 1, errMsg: "Failed to update NameServer address" }; + return {status: 1, errMsg: "Failed to update NameServer address"}; } }, @@ -724,7 +724,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error adding NameServer address:", error); - return { status: 1, errMsg: "Failed to add NameServer address" }; + return {status: 1, errMsg: "Failed to add NameServer address"}; } }, @@ -736,7 +736,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error updating VIP Channel status:", error); - return { status: 1, errMsg: "Failed to update VIP Channel status" }; + return {status: 1, errMsg: "Failed to update VIP Channel status"}; } }, @@ -748,7 +748,7 @@ const remoteApi = { return await response.json(); } catch (error) { console.error("Error updating TLS status:", error); - return { status: 1, errMsg: "Failed to update TLS status" }; + return {status: 1, errMsg: "Failed to update TLS status"}; } }, @@ -759,7 +759,7 @@ const remoteApi = { callback(data); } catch (error) { console.error("Error fetching cluster list:", error); - callback({ status: 1, errMsg: "Failed to fetch cluster list" }); + callback({status: 1, errMsg: "Failed to fetch cluster list"}); } }, @@ -767,16 +767,16 @@ const remoteApi = { try { const url = new URL(remoteApi.buildUrl('/dashboard/broker.query')); url.searchParams.append('date', date); - const response = await remoteApi._fetch(url.toString(), { signal: AbortSignal.timeout(15000) }); // 15s timeout + const response = await remoteApi._fetch(url.toString(), {signal: AbortSignal.timeout(15000)}); // 15s timeout const data = await response.json(); callback(data); } catch (error) { if (error.name === 'TimeoutError') { console.error("Broker history data request timed out:", error); - callback({ status: 1, errMsg: "Request timed out for broker history data" }); + callback({status: 1, errMsg: "Request timed out for broker history data"}); } else { console.error("Error fetching broker history data:", error); - callback({ status: 1, errMsg: "Failed to fetch broker history data" }); + callback({status: 1, errMsg: "Failed to fetch broker history data"}); } } }, @@ -786,32 +786,32 @@ const remoteApi = { const url = new URL(remoteApi.buildUrl('/dashboard/topic.query')); url.searchParams.append('date', date); url.searchParams.append('topicName', topicName); - const response = await remoteApi._fetch(url.toString(), { signal: AbortSignal.timeout(15000) }); // 15s timeout + const response = await remoteApi._fetch(url.toString(), {signal: AbortSignal.timeout(15000)}); // 15s timeout const data = await response.json(); callback(data); } catch (error) { if (error.name === 'TimeoutError') { console.error("Topic history data request timed out:", error); - callback({ status: 1, errMsg: "Request timed out for topic history data" }); + callback({status: 1, errMsg: "Request timed out for topic history data"}); } else { console.error("Error fetching topic history data:", error); - callback({ status: 1, errMsg: "Failed to fetch topic history data" }); + callback({status: 1, errMsg: "Failed to fetch topic history data"}); } } }, queryTopicCurrentData: async (callback) => { try { - const response = await remoteApi._fetch(remoteApi.buildUrl('/dashboard/topicCurrent.query'), { signal: AbortSignal.timeout(15000) }); // 15s timeout + const response = await remoteApi._fetch(remoteApi.buildUrl('/dashboard/topicCurrent.query'), {signal: AbortSignal.timeout(15000)}); // 15s timeout const data = await response.json(); callback(data); } catch (error) { if (error.name === 'TimeoutError') { console.error("Topic current data request timed out:", error); - callback({ status: 1, errMsg: "Request timed out for topic current data" }); + callback({status: 1, errMsg: "Request timed out for topic current data"}); } else { console.error("Error fetching topic current data:", error); - callback({ status: 1, errMsg: "Failed to fetch topic current data" }); + callback({status: 1, errMsg: "Failed to fetch topic current data"}); } } }, @@ -825,7 +825,7 @@ const remoteApi = { callback(data); } catch (error) { console.error("Error fetching broker config:", error); - callback({ status: 1, errMsg: "Failed to fetch broker config" }); + callback({status: 1, errMsg: "Failed to fetch broker config"}); } }, @@ -839,7 +839,7 @@ const remoteApi = { callback(data); } catch (error) { console.error("Error fetching proxy home page:", error); - callback({ status: 1, errMsg: "Failed to fetch proxy home page" }); + callback({status: 1, errMsg: "Failed to fetch proxy home page"}); } }, @@ -850,14 +850,14 @@ const remoteApi = { try { const response = await remoteApi._fetch(remoteApi.buildUrl("/proxy/addProxyAddr.do"), { method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ newProxyAddr }).toString() + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: new URLSearchParams({newProxyAddr}).toString() }); const data = await response.json(); callback(data); } catch (error) { console.error("Error adding proxy address:", error); - callback({ status: 1, errMsg: "Failed to add proxy address" }); + callback({status: 1, errMsg: "Failed to add proxy address"}); } }, login: async (username, password) => { @@ -882,19 +882,19 @@ const remoteApi = { return data; } catch (error) { console.error("Error logging in:", error); - return { status: 1, errMsg: "Failed to log in" }; + return {status: 1, errMsg: "Failed to log in"}; } }, logout: async () => { try { - const response = await remoteApi._fetch(remoteApi.buildUrl("/login/logout.do"),{ + const response = await remoteApi._fetch(remoteApi.buildUrl("/login/logout.do"), { method: 'POST' }); return await response.json() - }catch (error) { + } catch (error) { console.error("Error logging out:", error); - return { status: 1, errMsg: "Failed to log out" }; + return {status: 1, errMsg: "Failed to log out"}; } } }; @@ -939,4 +939,4 @@ const tools = { } }; -export { remoteApi, tools }; +export {remoteApi, tools}; diff --git a/src/main/java/org/apache/rocketmq/dashboard/controller/AclController.java b/src/main/java/org/apache/rocketmq/dashboard/controller/AclController.java index 6e22958..787667a 100644 --- a/src/main/java/org/apache/rocketmq/dashboard/controller/AclController.java +++ b/src/main/java/org/apache/rocketmq/dashboard/controller/AclController.java @@ -23,6 +23,7 @@ import org.apache.rocketmq.dashboard.model.request.UserUpdateRequest; import org.apache.rocketmq.dashboard.service.impl.AclServiceImpl; import org.apache.rocketmq.remoting.protocol.body.UserInfo; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -31,24 +32,23 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.RestController; import java.util.List; -@RestController +@Controller @RequestMapping("/acl") public class AclController { @Autowired private AclServiceImpl aclService; - @GetMapping("/listUsers") + @GetMapping("/users.query") @ResponseBody public List listUsers(@RequestParam(required = false) String brokerAddress) { return aclService.listUsers(brokerAddress); } - @GetMapping("/listAcls") + @GetMapping("/acls.query") @ResponseBody public Object listAcls( @RequestParam(required = false) String brokerAddress, @@ -56,34 +56,34 @@ public class AclController { return aclService.listAcls(brokerAddress, searchParam); } - @PostMapping("/createAcl") + @PostMapping("/createAcl.do") @ResponseBody public Object createAcl(@RequestBody PolicyRequest request) { aclService.createAcl(request); return true; } - @DeleteMapping("/deleteUser") + @DeleteMapping("/deleteUser.do") @ResponseBody public Object deleteUser(@RequestParam(required = false) String brokerAddress, @RequestParam String username) { aclService.deleteUser(brokerAddress, username); return true; } - @RequestMapping(value = "/updateUser", method = RequestMethod.POST, produces = "application/json;charset=UTF-8") + @RequestMapping(value = "/updateUser.do", method = RequestMethod.POST, produces = "application/json;charset=UTF-8") @ResponseBody public Object updateUser(@RequestBody UserUpdateRequest request) { aclService.updateUser(request.getBrokerAddress(), request.getUserInfo()); return true; } - @PostMapping("/createUser") + @PostMapping("/createUser.do") public Object createUser(@RequestBody UserCreateRequest request) { aclService.createUser(request.getBrokerAddress(), request.getUserInfo()); return true; } - @DeleteMapping("/deleteAcl") + @DeleteMapping("/deleteAcl.do") public Object deleteAcl( @RequestParam(required = false) String brokerAddress, @RequestParam String subject, @@ -92,7 +92,7 @@ public class AclController { return true; } - @RequestMapping(value = "/updateAcl", method = RequestMethod.POST, produces = "application/json;charset=UTF-8") + @RequestMapping(value = "/updateAcl.do", method = RequestMethod.POST, produces = "application/json;charset=UTF-8") @ResponseBody public Object updateAcl(@RequestBody PolicyRequest request) { aclService.updateAcl(request); diff --git a/src/main/java/org/apache/rocketmq/dashboard/model/User.java b/src/main/java/org/apache/rocketmq/dashboard/model/User.java index e049297..4cb8099 100644 --- a/src/main/java/org/apache/rocketmq/dashboard/model/User.java +++ b/src/main/java/org/apache/rocketmq/dashboard/model/User.java @@ -19,8 +19,8 @@ package org.apache.rocketmq.dashboard.model; import org.hibernate.validator.constraints.Range; public class User { - public static final int ORDINARY = 0; - public static final int ADMIN = 1; + public static final int SUPER = 0; + public static final int NORMAL = 1; private long id; private String name; diff --git a/src/main/java/org/apache/rocketmq/dashboard/model/request/UserInfoParam.java b/src/main/java/org/apache/rocketmq/dashboard/model/request/UserInfoParam.java index f2dc8ab..a288c35 100644 --- a/src/main/java/org/apache/rocketmq/dashboard/model/request/UserInfoParam.java +++ b/src/main/java/org/apache/rocketmq/dashboard/model/request/UserInfoParam.java @@ -17,9 +17,11 @@ package org.apache.rocketmq.dashboard.model.request; +import lombok.AllArgsConstructor; import lombok.Data; @Data +@AllArgsConstructor public class UserInfoParam { private String username; private String password; diff --git a/src/main/java/org/apache/rocketmq/dashboard/service/impl/AbstractFileStore.java b/src/main/java/org/apache/rocketmq/dashboard/service/impl/AbstractFileStore.java index 0d65ae1..ea85e42 100644 --- a/src/main/java/org/apache/rocketmq/dashboard/service/impl/AbstractFileStore.java +++ b/src/main/java/org/apache/rocketmq/dashboard/service/impl/AbstractFileStore.java @@ -58,7 +58,7 @@ public abstract class AbstractFileStore { } } - abstract void load(InputStream inputStream); + protected abstract void load(InputStream inputStream); private void load() { load(null); @@ -66,7 +66,7 @@ public abstract class AbstractFileStore { private boolean watch() { try { - FileWatchService fileWatchService = new FileWatchService(new String[] {filePath}, new FileWatchService.Listener() { + FileWatchService fileWatchService = new FileWatchService(new String[]{filePath}, new FileWatchService.Listener() { @Override public void onChanged(String path) { log.info("The file changed, reload the context"); diff --git a/src/main/java/org/apache/rocketmq/dashboard/service/impl/LoginServiceImpl.java b/src/main/java/org/apache/rocketmq/dashboard/service/impl/LoginServiceImpl.java index 31f1613..c217a88 100644 --- a/src/main/java/org/apache/rocketmq/dashboard/service/impl/LoginServiceImpl.java +++ b/src/main/java/org/apache/rocketmq/dashboard/service/impl/LoginServiceImpl.java @@ -17,13 +17,10 @@ package org.apache.rocketmq.dashboard.service.impl; -import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.apache.rocketmq.dashboard.config.RMQConfigure; import org.apache.rocketmq.dashboard.service.LoginService; -import org.apache.rocketmq.dashboard.service.UserService; -import org.apache.rocketmq.dashboard.service.provider.UserInfoProvider; +import org.apache.rocketmq.dashboard.service.strategy.UserContext; import org.apache.rocketmq.dashboard.util.UserInfoContext; import org.apache.rocketmq.dashboard.util.WebUtil; import org.apache.rocketmq.remoting.protocol.body.UserInfo; @@ -33,34 +30,29 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; @Service public class LoginServiceImpl implements LoginService { private final Logger logger = LoggerFactory.getLogger(this.getClass()); - @Resource - private RMQConfigure rmqConfigure; - @Autowired - private UserService userService; - - @Autowired - private UserInfoProvider userInfoProvider; + private UserContext userContext; @Override public boolean login(HttpServletRequest request, HttpServletResponse response) { String username = (String) WebUtil.getValueFromSession(request, WebUtil.USER_NAME); if (username != null) { - UserInfo userInfo = userInfoProvider.getUserInfoByUsername(username); + UserInfo userInfo = userContext.queryByUsername(username); if (userInfo == null) { auth(request, response); return false; } UserInfoContext.set(WebUtil.USER_NAME, userInfo); return true; + } auth(request, response); return false; @@ -69,11 +61,7 @@ public class LoginServiceImpl implements LoginService { protected void auth(HttpServletRequest request, HttpServletResponse response) { try { String url = WebUtil.getUrl(request); - try { - url = URLEncoder.encode(url, "UTF-8"); - } catch (UnsupportedEncodingException e) { - logger.error("url encode:{}", url, e); - } + url = URLEncoder.encode(url, StandardCharsets.UTF_8); logger.debug("redirect url : {}", url); WebUtil.redirect(response, request, "/#/login?redirect=" + url); } catch (IOException e) { diff --git a/src/main/java/org/apache/rocketmq/dashboard/service/strategy/AclUserStrategy.java b/src/main/java/org/apache/rocketmq/dashboard/service/strategy/AclUserStrategy.java new file mode 100644 index 0000000..4a8f9d5 --- /dev/null +++ b/src/main/java/org/apache/rocketmq/dashboard/service/strategy/AclUserStrategy.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.dashboard.service.strategy; + +import lombok.AllArgsConstructor; +import org.apache.rocketmq.dashboard.service.ClusterInfoService; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.body.UserInfo; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.tools.admin.MQAdminExt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class AclUserStrategy implements UserStrategy { + + private static final Logger log = LoggerFactory.getLogger(AclUserStrategy.class); + + private final MQAdminExt mqAdminExt; + + private final ClusterInfoService clusterInfoService; + + @Override + public UserInfo getUserInfoByUsername(String username) { + ClusterInfo clusterInfo = clusterInfoService.get(); + + if (clusterInfo == null || clusterInfo.getBrokerAddrTable() == null || clusterInfo.getBrokerAddrTable().isEmpty()) { + log.warn("Cluster information is not available or has no broker addresses."); + return null; + } + for (BrokerData brokerLiveInfo : clusterInfo.getBrokerAddrTable().values()) { + if (brokerLiveInfo == null || brokerLiveInfo.getBrokerAddrs() == null || brokerLiveInfo.getBrokerAddrs().isEmpty()) { + continue; + } + String brokerAddr = brokerLiveInfo.getBrokerAddrs().get(0L); // Assuming 0L is the primary address + if (brokerAddr == null) { + continue; + } + try { + UserInfo userInfo = mqAdminExt.getUser(brokerAddr, username); + if (userInfo != null) { + return userInfo; + } + } catch (Exception e) { + log.warn("Failed to get user {} from broker {}. Trying next broker if available. Error: {}", username, brokerAddr, e.getMessage()); + } + } + return null; + } +} diff --git a/src/main/java/org/apache/rocketmq/dashboard/service/strategy/FileUserStrategy.java b/src/main/java/org/apache/rocketmq/dashboard/service/strategy/FileUserStrategy.java new file mode 100644 index 0000000..d5dac40 --- /dev/null +++ b/src/main/java/org/apache/rocketmq/dashboard/service/strategy/FileUserStrategy.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.dashboard.service.strategy; + +import jakarta.annotation.Resource; +import jakarta.validation.constraints.NotNull; +import org.apache.rocketmq.dashboard.config.RMQConfigure; +import org.apache.rocketmq.dashboard.exception.ServiceException; +import org.apache.rocketmq.dashboard.model.User; +import org.apache.rocketmq.dashboard.service.impl.AbstractFileStore; +import org.apache.rocketmq.remoting.protocol.body.UserInfo; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Service; + +import java.io.FileReader; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + +@Service +public class FileUserStrategy implements UserStrategy, InitializingBean { + @Resource + private RMQConfigure configure; + + private FileBasedUserInfoStore fileBasedUserInfoStore; + + + @Override + public void afterPropertiesSet() throws Exception { + if (configure.isLoginRequired()) { + fileBasedUserInfoStore = new FileBasedUserInfoStore(configure); + } + } + + @Override + public UserInfo getUserInfoByUsername(String username) { + User user = fileBasedUserInfoStore.queryByUsernameAndPassword(username); + if (user != null) { + return UserInfo.of(user.getName(), user.getPassword(), user.getType() == 0 ? "normal" : "super"); + } + return null; + } + + public static class FileBasedUserInfoStore extends AbstractFileStore { + private static final String FILE_NAME = "users.properties"; + + private static Map userMap = new ConcurrentHashMap<>(); + + public FileBasedUserInfoStore(RMQConfigure configure) { + super(configure, FILE_NAME); + } + + @Override + public void load(InputStream inputStream) { + Properties prop = new Properties(); + try { + if (inputStream == null) { + prop.load(new FileReader(filePath)); + } else { + prop.load(inputStream); + } + } catch (Exception e) { + log.error("load user.properties failed", e); + throw new ServiceException(0, String.format("Failed to load loginUserInfo property file: %s", filePath)); + } + + Map loadUserMap = new HashMap<>(); + String[] arrs; + int role; + for (String key : prop.stringPropertyNames()) { + String v = prop.getProperty(key); + if (v == null) + continue; + arrs = v.split(",", 2); + if (arrs.length == 0) { + continue; + } else if (arrs.length == 1) { + role = 0; + } else { + role = Integer.parseInt(arrs[1].trim()); + } + + loadUserMap.put(key, new User(key, arrs[0].trim(), role)); + } + + userMap.clear(); + userMap.putAll(loadUserMap); + } + + public User queryByName(String name) { + return userMap.get(name); + } + + public User queryByUsernameAndPassword(@NotNull String username) { + User user = queryByName(username); + if (user != null) { + return user.cloneOne(); + } + return null; + } + } +} diff --git a/src/main/java/org/apache/rocketmq/dashboard/service/strategy/UserContext.java b/src/main/java/org/apache/rocketmq/dashboard/service/strategy/UserContext.java new file mode 100644 index 0000000..f6df1ad --- /dev/null +++ b/src/main/java/org/apache/rocketmq/dashboard/service/strategy/UserContext.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.dashboard.service.strategy; + +import jakarta.annotation.PostConstruct; +import org.apache.rocketmq.remoting.protocol.body.UserInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Component +public class UserContext { + private UserStrategy userStrategy; + + @Autowired + private Map userStrategies; + + @Value("${rocketmq.config.authMode}") + private String authMode; + + @PostConstruct + public void init() { + switch (authMode.toLowerCase()) { + case "acl": + this.userStrategy = userStrategies.get("aclUserStrategy"); + break; + case "file": + default: + this.userStrategy = userStrategies.get("fileUserStrategy"); + break; + } + } + + public UserInfo queryByUsername(String username) { + return userStrategy.getUserInfoByUsername(username); + } +} diff --git a/src/main/java/org/apache/rocketmq/dashboard/service/strategy/UserStrategy.java b/src/main/java/org/apache/rocketmq/dashboard/service/strategy/UserStrategy.java new file mode 100644 index 0000000..1603d7e --- /dev/null +++ b/src/main/java/org/apache/rocketmq/dashboard/service/strategy/UserStrategy.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.dashboard.service.strategy; + +import org.apache.rocketmq.remoting.protocol.body.UserInfo; + +public interface UserStrategy { + UserInfo getUserInfoByUsername(String username); +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ddf624a..47117c7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -58,6 +58,12 @@ rocketmq: ticketKey: ticket # must create userInfo file: ${rocketmq.config.dataPath}/users.properties if the login is required loginRequired: false + # Authentication mode for RocketMQ Dashboard + # Available options: + # - 'file': Use username/password stored in a file (requires 'auth.file.path') + # - 'acl': Use credentials from ACL system (requires 'acl.access.key' and 'acl.secret.key') + # Default: file + authMode: file useTLS: false proxyAddr: 127.0.0.1:8080 proxyAddrs: