This is an automated email from the ASF dual-hosted git repository.
jinrongtong pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new 8037cfc [ISSUE #353] fix Actuator vulnerability issues (#354)
8037cfc is described below
commit 8037cfcf052d70c4182359154e0c1c90cb4ef07d
Author: Crazylychee <[email protected]>
AuthorDate: Sat Aug 9 16:04:52 2025 +0800
[ISSUE #353] fix Actuator vulnerability issues (#354)
* [ISSUE #348] fix Some interaction issues with the consumer interface
* commit
* [ISSUE #353] fix Actuator vulnerability issues
* [ISSUE #353] fix Actuator vulnerability issues
* commit
---
frontend-new/src/api/remoteApi/remoteApi.js | 100 +++++++++++++++------
.../src/components/consumer/ClientInfoModal.jsx | 1 +
frontend-new/src/pages/Consumer/consumer.jsx | 2 +-
frontend-new/src/store/context/ThemeContext.js | 1 -
frontend-new/src/store/reducers/themeReducer.js | 1 -
pom.xml | 5 ++
.../config/AuthWebMVCConfigurerAdapter.java | 12 ---
.../rocketmq/dashboard/config/SecurityConfig.java | 74 +++++++++++++++
.../CsrfTokenController.java} | 31 ++++---
.../dashboard/controller/LoginController.java | 3 +-
.../dashboard/interceptor/AuthInterceptor.java | 6 ++
.../service/impl/ConsumerServiceImpl.java | 7 +-
src/main/resources/application.yml | 12 +++
13 files changed, 194 insertions(+), 61 deletions(-)
diff --git a/frontend-new/src/api/remoteApi/remoteApi.js
b/frontend-new/src/api/remoteApi/remoteApi.js
index 329628d..c727f55 100644
--- a/frontend-new/src/api/remoteApi/remoteApi.js
+++ b/frontend-new/src/api/remoteApi/remoteApi.js
@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
const appConfig = {
apiBaseUrl: 'http://localhost:8082'
};
@@ -33,29 +34,73 @@ const remoteApi = {
return `${appConfig.apiBaseUrl}/${endpoint}`;
},
- _fetch: async (url, options) => {
+
+ async getCsrfToken() {
+ const csrfToken = this.getCookie();
+
+ if (csrfToken) {
+ return csrfToken;
+ }
+
+ const response = await
fetch(remoteApi.buildUrl("/rocketmq-dashboard/csrf-token"), {
+ method: 'GET',
+ credentials: 'include'
+ });
+
+ const newCsrfToken = this.getCookie();
+ if (!newCsrfToken) {
+ console.error("Failed to get CSRF Token");
+ throw new Error("CSRF Token not available");
+ }
+ return newCsrfToken;
+ },
+
+ getCookie() {
+ return
document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/,
'$1')
+ },
+
+ _fetch: async (url, options = {}) => {
+ const headers = {
+ ...options.headers,
+ 'Content-Type': 'application/json',
+ };
+
+
+ const csrfToken = await remoteApi.getCsrfToken();
+ console.log(csrfToken)
+ if (!csrfToken) {
+ console.warn('CSRF Token not found');
+ }else{
+ headers["X-XSRF-TOKEN"] = csrfToken;
+ }
+ console.log(csrfToken)
+
+
try {
- // 在 options 中添加 credentials: 'include'
const response = await fetch(url, {
- ...options, // 保留原有的 options
- credentials: 'include' // 关键改动:允许发送 Cookie
+ ...options,
+ headers,
+ credentials: 'include',
});
-
- // 检查响应是否被重定向,并且最终的 URL 包含了登录页的路径。
- // 这是会话过期或需要认证时后端重定向到登录页的常见模式。
- // 注意:fetch 会自动跟随 GET 请求的 3xx 重定向,所以我们检查的是 response.redirected。
if (response.redirected) {
if (_redirectHandler) {
- _redirectHandler(); // 如果设置了重定向处理函数,则调用它
+ _redirectHandler();
}
return {__isRedirectHandled: true};
}
+ if(response.status == 403){
+ window.localStorage.removeItem("csrfToken");
+ console.log(111)
+ await remoteApi.getCsrfToken()
+ }
return response;
} catch (error) {
- console.error("Fetch 请求出错:", error);
- throw error;
+ console.error('fetch error:', error);
+ window.localStorage.removeItem("csrfToken");
+ console.log(111)
+ await remoteApi.getCsrfToken()
}
},
@@ -232,24 +277,19 @@ const remoteApi = {
throw new Error(`HTTP error! status: ${response.status}`);
}
- // 假设服务器总是返回 JSON
const data = await response.json();
- // 1. 打开一个新的空白窗口
const newWindow = window.open('', '_blank');
if (!newWindow) {
- // 浏览器可能会阻止弹窗,需要用户允许
return {status: 1, errMsg: "Failed to open new window. Please
allow pop-ups for this site."};
}
- // 2. 将 JSON 数据格式化后写入新窗口
newWindow.document.write('<html><head><title>DLQ
导出内容</title></head><body>');
newWindow.document.write('<h1>DLQ 导出 JSON 内容</h1>');
- // 使用 <pre> 标签保持格式,并使用 JSON.stringify 格式化 JSON 以便于阅读
newWindow.document.write('<pre>' + JSON.stringify(data, null, 2) +
'</pre>');
newWindow.document.write('</body></html>');
- newWindow.document.close(); // 关闭文档流,确保内容显示
+ newWindow.document.close();
return {status: 0, msg: "导出请求成功,内容已在新页面显示"};
} catch (error) {
@@ -382,6 +422,9 @@ const remoteApi = {
},
queryConsumerGroupList: async (skipSysGroup, address) => {
+ if (address === undefined) {
+ address = ""
+ }
try {
const response = await
remoteApi._fetch(remoteApi.buildUrl(`/consumer/groupList.query?skipSysGroup=${skipSysGroup}&address=${address}`));
const data = await response.json();
@@ -404,9 +447,12 @@ const remoteApi = {
}
},
- refreshAllConsumerGroup: async () => {
+ refreshAllConsumerGroup: async (address) => {
+ if (address === undefined) {
+ address = ""
+ }
try {
- const response = await
remoteApi._fetch(remoteApi.buildUrl("/consumer/group.refresh.all"));
+ const response = await
remoteApi._fetch(remoteApi.buildUrl(`/consumer/group.refresh.all?address=${address}`));
const data = await response.json();
return data;
} catch (error) {
@@ -875,21 +921,17 @@ const remoteApi = {
login: async (username, password) => {
try {
-
- // 2. 发送请求,注意 body 可以是空字符串或 null,或者直接省略 body
- // 这里使用 GET 方法,因为参数在 URL 上
const response = await
remoteApi._fetch(remoteApi.buildUrl("/login/login.do"), {
method: 'POST',
+ body: JSON.stringify({
+ username: username,
+ password: password
+ }),
headers: {
- 'Content-Type': 'application/x-www-form-urlencoded' // 这个
header 可能不再需要,或者需要调整
- },
- body: new URLSearchParams({
- username: username, // 假设 username 是变量名
- password: password // 假设 password 是变量名
- }).toString()
+ 'Content-Type': 'application/json'
+ }
});
- // 3. 处理响应
const data = await response.json();
return data;
} catch (error) {
diff --git a/frontend-new/src/components/consumer/ClientInfoModal.jsx
b/frontend-new/src/components/consumer/ClientInfoModal.jsx
index 21d21a5..3974a51 100644
--- a/frontend-new/src/components/consumer/ClientInfoModal.jsx
+++ b/frontend-new/src/components/consumer/ClientInfoModal.jsx
@@ -38,6 +38,7 @@ const ClientInfoModal = ({visible, group, address, onCancel,
messageApi}) => {
setConnectionData(connResponse.data);
}else{
messageApi.error(connResponse.errMsg);
+ setConnectionData(null);
}
} finally {
setLoading(false);
diff --git a/frontend-new/src/pages/Consumer/consumer.jsx
b/frontend-new/src/pages/Consumer/consumer.jsx
index 3377e7a..e2c1be2 100644
--- a/frontend-new/src/pages/Consumer/consumer.jsx
+++ b/frontend-new/src/pages/Consumer/consumer.jsx
@@ -270,7 +270,7 @@ const ConsumerGroupList = () => {
const handleRefreshConsumerData = async () => {
setLoading(true);
- const refreshResult = await remoteApi.refreshAllConsumerGroup();
+ const refreshResult = await
remoteApi.refreshAllConsumerGroup(selectedProxy);
setLoading(false);
if (refreshResult && refreshResult.status === 0) {
diff --git a/frontend-new/src/store/context/ThemeContext.js
b/frontend-new/src/store/context/ThemeContext.js
index 7249876..5975179 100644
--- a/frontend-new/src/store/context/ThemeContext.js
+++ b/frontend-new/src/store/context/ThemeContext.js
@@ -20,7 +20,6 @@ import {defaultTheme, themes} from
'../../assets/styles/theme';
import {setTheme} from '../actions/themeActions';
export const useTheme = () => {
- // 从 Redux store 中取出 currentThemeName
const currentThemeName = useSelector(state =>
state.theme.currentThemeName);
const dispatch = useDispatch();
diff --git a/frontend-new/src/store/reducers/themeReducer.js
b/frontend-new/src/store/reducers/themeReducer.js
index 5601f94..463bb1e 100644
--- a/frontend-new/src/store/reducers/themeReducer.js
+++ b/frontend-new/src/store/reducers/themeReducer.js
@@ -28,7 +28,6 @@ const initialState = {
const themeReducer = (state = initialState, action) => {
switch (action.type) {
case SET_THEME:
- // 注意:reducer 应该返回新的状态对象,而不是直接修改旧状态
return {
...state,
currentThemeName: action.payload,
diff --git a/pom.xml b/pom.xml
index 065d065..c24d964 100644
--- a/pom.xml
+++ b/pom.xml
@@ -149,6 +149,11 @@
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring.boot.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-security</artifactId>
+ <version>${spring.boot.version}</version>
+ </dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
diff --git
a/src/main/java/org/apache/rocketmq/dashboard/config/AuthWebMVCConfigurerAdapter.java
b/src/main/java/org/apache/rocketmq/dashboard/config/AuthWebMVCConfigurerAdapter.java
index b902575..1e65ef0 100644
---
a/src/main/java/org/apache/rocketmq/dashboard/config/AuthWebMVCConfigurerAdapter.java
+++
b/src/main/java/org/apache/rocketmq/dashboard/config/AuthWebMVCConfigurerAdapter.java
@@ -31,7 +31,6 @@ import
org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import
org.springframework.web.multipart.support.MissingServletRequestPartException;
-import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import
org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -90,17 +89,6 @@ public class AuthWebMVCConfigurerAdapter implements
WebMvcConfigurer {
});
}
- @Override
- public void addCorsMappings(CorsRegistry registry) {
-
- registry.addMapping("/**")
- .allowedOriginPatterns("http://localhost:3003")
- .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE",
"OPTIONS")
- .maxAge(3600)
- .allowCredentials(true)
- .allowedHeaders("content-type", "Authorization",
"X-Requested-With", "Origin", "Accept")
- .exposedHeaders("authorization");
- }
@Override
diff --git
a/src/main/java/org/apache/rocketmq/dashboard/config/SecurityConfig.java
b/src/main/java/org/apache/rocketmq/dashboard/config/SecurityConfig.java
new file mode 100644
index 0000000..f729f94
--- /dev/null
+++ b/src/main/java/org/apache/rocketmq/dashboard/config/SecurityConfig.java
@@ -0,0 +1,74 @@
+/*
+ * 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.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import
org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import
org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
+import org.springframework.security.web.csrf.CsrfTokenRepository;
+import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+
+import java.util.Arrays;
+
+import static org.springframework.security.config.Customizer.withDefaults;
+
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws
Exception {
+ http
+ .cors(withDefaults())
+ .csrf(csrf -> csrf
+ .ignoringRequestMatchers("/actuator/**")
+
.ignoringRequestMatchers("/rocketmq-dashboard/csrf-token")
+ .csrfTokenRepository(csrfTokenRepository())
+ .csrfTokenRequestHandler(new
CsrfTokenRequestAttributeHandler())
+ )
+ .authorizeHttpRequests(authorize -> authorize
+ .requestMatchers("/actuator/**").hasRole("ADMIN")
+ .anyRequest().permitAll()
+ )
+ .httpBasic(withDefaults());
+ return http.build();
+ }
+
+ @Bean
+ public CorsConfigurationSource corsConfigurationSource() {
+ CorsConfiguration configuration = new CorsConfiguration();
+
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3003"));
+ configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT",
"DELETE", "OPTIONS"));
+ configuration.setAllowedHeaders(Arrays.asList("content-type",
"Authorization", "X-Requested-With", "Origin", "Accept", "X-XSRF-TOKEN"));
+ configuration.setAllowCredentials(true);
+ configuration.setMaxAge(3600L);
+
+ UrlBasedCorsConfigurationSource source = new
UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", configuration);
+ return source;
+ }
+ @Bean
+ public CsrfTokenRepository csrfTokenRepository() {
+ return CookieCsrfTokenRepository.withHttpOnlyFalse();
+ }
+}
diff --git
a/src/main/java/org/apache/rocketmq/dashboard/interceptor/AuthInterceptor.java
b/src/main/java/org/apache/rocketmq/dashboard/controller/CsrfTokenController.java
similarity index 52%
copy from
src/main/java/org/apache/rocketmq/dashboard/interceptor/AuthInterceptor.java
copy to
src/main/java/org/apache/rocketmq/dashboard/controller/CsrfTokenController.java
index b85c4a2..725e40c 100644
---
a/src/main/java/org/apache/rocketmq/dashboard/interceptor/AuthInterceptor.java
+++
b/src/main/java/org/apache/rocketmq/dashboard/controller/CsrfTokenController.java
@@ -15,26 +15,31 @@
* limitations under the License.
*/
-package org.apache.rocketmq.dashboard.interceptor;
+package org.apache.rocketmq.dashboard.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
-import org.apache.rocketmq.dashboard.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.security.web.csrf.CsrfToken;
+import org.springframework.security.web.csrf.CsrfTokenRepository;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
-
-@Component
-public class AuthInterceptor implements HandlerInterceptor {
+@RestController
+@RequestMapping(value = "/rocketmq-dashboard")
+public class CsrfTokenController {
@Autowired
- private LoginService loginService;
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws Exception {
- return loginService.login(request, response);
- }
+ private CsrfTokenRepository csrfTokenRepository;
+ @RequestMapping(value = "/csrf-token", method = RequestMethod.GET)
+ @ResponseBody
+ public Object getCsrfToken(HttpServletRequest request, HttpServletResponse
response) {
+ CsrfToken token = csrfTokenRepository.generateToken(request);
+ csrfTokenRepository.saveToken(token, request, response);
+ return token;
+ }
}
diff --git
a/src/main/java/org/apache/rocketmq/dashboard/controller/LoginController.java
b/src/main/java/org/apache/rocketmq/dashboard/controller/LoginController.java
index 9345acc..df4077d 100644
---
a/src/main/java/org/apache/rocketmq/dashboard/controller/LoginController.java
+++
b/src/main/java/org/apache/rocketmq/dashboard/controller/LoginController.java
@@ -33,6 +33,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@@ -65,7 +66,7 @@ public class LoginController {
@RequestMapping(value = "/login.do", method = RequestMethod.POST)
@ResponseBody
- public Object login(org.apache.rocketmq.remoting.protocol.body.UserInfo
userInfoRequest,
+ public Object login(@RequestBody
org.apache.rocketmq.remoting.protocol.body.UserInfo userInfoRequest,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
logger.info("user:{} login", userInfoRequest.getUsername());
diff --git
a/src/main/java/org/apache/rocketmq/dashboard/interceptor/AuthInterceptor.java
b/src/main/java/org/apache/rocketmq/dashboard/interceptor/AuthInterceptor.java
index b85c4a2..3abab56 100644
---
a/src/main/java/org/apache/rocketmq/dashboard/interceptor/AuthInterceptor.java
+++
b/src/main/java/org/apache/rocketmq/dashboard/interceptor/AuthInterceptor.java
@@ -33,6 +33,12 @@ public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws Exception {
+ if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
+ return true;
+ }
+ if
(request.getRequestURL().toString().contains("/rocketmq-dashboard/csrf-token"))
{
+ return true;
+ }
return loginService.login(request, response);
}
diff --git
a/src/main/java/org/apache/rocketmq/dashboard/service/impl/ConsumerServiceImpl.java
b/src/main/java/org/apache/rocketmq/dashboard/service/impl/ConsumerServiceImpl.java
index 160da1a..58fb5f5 100644
---
a/src/main/java/org/apache/rocketmq/dashboard/service/impl/ConsumerServiceImpl.java
+++
b/src/main/java/org/apache/rocketmq/dashboard/service/impl/ConsumerServiceImpl.java
@@ -135,6 +135,7 @@ public class ConsumerServiceImpl extends
AbstractCommonService implements Consum
SYSTEM_GROUP_SET.add(MixAll.CID_ONSAPI_PERMISSION_GROUP);
SYSTEM_GROUP_SET.add(MixAll.CID_ONSAPI_OWNER_GROUP);
SYSTEM_GROUP_SET.add(MixAll.CID_SYS_RMQ_TRANS);
+ SYSTEM_GROUP_SET.add("CID_DefaultHeartBeatSyncerTopic");
}
@Override
@@ -147,7 +148,7 @@ public class ConsumerServiceImpl extends
AbstractCommonService implements Consum
if (cacheConsumeInfoList.isEmpty() && !isCacheBeingBuilt) {
isCacheBeingBuilt = true;
try {
- makeGroupListCache();
+ makeGroupListCache(address);
} finally {
isCacheBeingBuilt = false;
}
@@ -173,7 +174,7 @@ public class ConsumerServiceImpl extends
AbstractCommonService implements Consum
}
- public void makeGroupListCache() {
+ public void makeGroupListCache(String address) {
SubscriptionGroupWrapper subscriptionGroupWrapper = null;
try {
ClusterInfo clusterInfo = clusterInfoService.get();
@@ -205,7 +206,7 @@ public class ConsumerServiceImpl extends
AbstractCommonService implements Consum
String consumerGroup = entry.getKey();
executorService.submit(() -> {
try {
- GroupConsumeInfo consumeInfo = queryGroup(consumerGroup,
"");
+ GroupConsumeInfo consumeInfo = queryGroup(consumerGroup,
address);
consumeInfo.setAddress(entry.getValue());
if (SYSTEM_GROUP_SET.contains(consumerGroup)) {
consumeInfo.setSubGroupType("SYSTEM");
diff --git a/src/main/resources/application.yml
b/src/main/resources/application.yml
index 47117c7..2e891d5 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -33,6 +33,18 @@ spring:
application:
name: rocketmq-dashboard
+ security:
+ user:
+ name: rocketmq
+ password: 1234567
+ roles: ADMIN
+
+management:
+ endpoints:
+ web:
+ exposure:
+ include: "*"
+
logging:
config: classpath:logback.xml