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();