This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 4f787c78 fix: handle case-insensitive bearer auth and expired sessions 
(#672)
4f787c78 is described below

commit 4f787c7808ff8c569a9e970d047a663c26f0e7a3
Author: yx9o <[email protected]>
AuthorDate: Fri Jul 31 16:15:03 2026 +0800

    fix: handle case-insensitive bearer auth and expired sessions (#672)
---
 .../apache/rocketmq/studio/auth/AuthService.java   | 25 +++++++++++----
 .../rocketmq/studio/auth/AuthServiceTest.java      | 37 ++++++++++++++++++++--
 2 files changed, 53 insertions(+), 9 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthService.java 
b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthService.java
index 5c083225..66ec35dd 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthService.java
@@ -18,10 +18,11 @@
 package org.apache.rocketmq.studio.auth;
 
 import org.apache.rocketmq.studio.common.exception.BusinessException;
-import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.time.Clock;
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
@@ -29,15 +30,25 @@ import java.util.UUID;
 
 @Slf4j
 @Service
-@RequiredArgsConstructor
 public class AuthService {
 
     private static final int TOKEN_TTL_SECONDS = 86400;
     private static final String TOKEN_PREFIX = "Bearer ";
 
     private final AuthProperties authProperties;
+    private final Clock clock;
     private final Map<String, AuthSession> activeTokens = new 
ConcurrentHashMap<>();
 
+    @Autowired
+    public AuthService(AuthProperties authProperties) {
+        this(authProperties, Clock.systemUTC());
+    }
+
+    AuthService(AuthProperties authProperties, Clock clock) {
+        this.authProperties = authProperties;
+        this.clock = clock;
+    }
+
     public LoginVO login(LoginDTO request) {
         log.info("Login attempt for user: {}", request.getUsername());
 
@@ -49,9 +60,10 @@ public class AuthService {
         }
 
         LoginVO.UserInfo user = authenticate(request);
+        long now = clock.millis();
+        activeTokens.entrySet().removeIf(entry -> 
entry.getValue().expiresAtMillis() <= now);
         String token = "studio-jwt-" + UUID.randomUUID();
-        activeTokens.put(token, new AuthSession(user, 
System.currentTimeMillis()
-                + TOKEN_TTL_SECONDS * 1000L));
+        activeTokens.put(token, new AuthSession(user, now + TOKEN_TTL_SECONDS 
* 1000L));
 
         LoginVO response = LoginVO.builder()
                 .token(token)
@@ -72,7 +84,7 @@ public class AuthService {
         if (session == null) {
             return false;
         }
-        if (session.expiresAtMillis() <= System.currentTimeMillis()) {
+        if (session.expiresAtMillis() <= clock.millis()) {
             activeTokens.remove(token.get());
             return false;
         }
@@ -106,7 +118,8 @@ public class AuthService {
     }
 
     private Optional<String> tokenFromAuthorization(String authorization) {
-        if (authorization == null || !authorization.startsWith(TOKEN_PREFIX)) {
+        if (authorization == null || !authorization.regionMatches(true, 0, 
TOKEN_PREFIX, 0,
+                TOKEN_PREFIX.length())) {
             return Optional.empty();
         }
         String token = authorization.substring(TOKEN_PREFIX.length()).trim();
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthServiceTest.java 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthServiceTest.java
index 5880e5ba..31959b52 100644
--- a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthServiceTest.java
+++ b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthServiceTest.java
@@ -22,11 +22,18 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
 
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneOffset;
 import java.util.List;
+import java.util.Map;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 @ExtendWith(MockitoExtension.class)
 class AuthServiceTest {
@@ -37,7 +44,7 @@ class AuthServiceTest {
     @BeforeEach
     void setUp() {
         authProperties = new AuthProperties();
-        authService = new AuthService(authProperties);
+        authService = new AuthService(authProperties, 
Clock.fixed(Instant.EPOCH, ZoneOffset.UTC));
     }
 
     @Test
@@ -59,6 +66,8 @@ class AuthServiceTest {
         assertThat(response.getUser().getUsername()).isEqualTo("testuser");
         assertThat(response.getUser().isAdmin()).isFalse();
         assertThat(authService.isAuthenticated("Bearer " + 
response.getToken())).isTrue();
+        assertThat(authService.isAuthenticated("bearer " + 
response.getToken())).isTrue();
+        assertThat(authService.isAuthenticated("bEaReR " + 
response.getToken())).isTrue();
     }
 
     @Test
@@ -142,7 +151,7 @@ class AuthServiceTest {
     }
 
     @Test
-    void logoutShouldRevokeActiveToken() {
+    void logoutShouldRevokeActiveTokenWithCaseInsensitiveBearerScheme() {
         AuthProperties.User user = new AuthProperties.User();
         user.setUsername("testuser");
         user.setPassword("testpass");
@@ -152,11 +161,33 @@ class AuthServiceTest {
         request.setPassword("testpass");
         LoginVO response = authService.login(request);
 
-        authService.logout("Bearer " + response.getToken());
+        authService.logout("bEaReR " + response.getToken());
 
         assertThat(authService.isAuthenticated("Bearer " + 
response.getToken())).isFalse();
     }
 
+    @Test
+    @SuppressWarnings("unchecked")
+    void loginShouldRemoveExpiredSessions() {
+        Clock clock = mock(Clock.class);
+        when(clock.millis()).thenReturn(0L);
+        authService = new AuthService(authProperties, clock);
+        AuthProperties.User user = new AuthProperties.User();
+        user.setUsername("testuser");
+        user.setPassword("testpass");
+        authProperties.setUsers(List.of(user));
+        LoginDTO request = new LoginDTO();
+        request.setUsername("testuser");
+        request.setPassword("testpass");
+        LoginVO expiredSession = authService.login(request);
+        when(clock.millis()).thenReturn(expiredSession.getExpiresIn() * 1000L);
+
+        LoginVO activeSession = authService.login(request);
+
+        Map<String, ?> activeTokens = (Map<String, ?>) 
ReflectionTestUtils.getField(authService, "activeTokens");
+        assertThat(activeTokens).containsOnlyKeys(activeSession.getToken());
+    }
+
     @Test
     void loginShouldThrowWhenUsernameIsNull() {
         LoginDTO request = new LoginDTO();

Reply via email to