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
 

Reply via email to