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

wuzhiguo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/bigtop-manager.git


The following commit(s) were added to refs/heads/main by this push:
     new 37c7e500 BIGTOP-4479: Add token version control (#251)
37c7e500 is described below

commit 37c7e500578be85c705032f3ba7e40271fc641d5
Author: ChunFuWu <[email protected]>
AuthorDate: Wed Aug 13 12:06:13 2025 +0800

    BIGTOP-4479: Add token version control (#251)
---
 .../bigtop/manager/common/constants/Caches.java    |  19 +-
 .../org/apache/bigtop/manager/dao/po/UserPO.java   |   3 +
 .../src/main/resources/mapper/mysql/UserMapper.xml |   2 +-
 .../resources/mapper/postgresql/UserMapper.xml     |   2 +-
 .../manager/server/controller/LoginController.java |   5 +-
 .../server/interceptor/AuthInterceptor.java        |  36 ++-
 .../bigtop/manager/server/model/vo/UserVO.java     |   2 +
 .../bigtop/manager/server/service/UserService.java |   8 +
 .../server/service/impl/LoginServiceImpl.java      |  16 +-
 .../server/service/impl/UserServiceImpl.java       |  22 +-
 .../bigtop/manager/server/utils/CacheUtils.java    | 293 +++++++++++++++++++--
 .../bigtop/manager/server/utils/JWTUtils.java      |  20 +-
 .../src/main/resources/ddl/MySQL-DDL-CREATE.sql    |   2 +
 .../main/resources/ddl/PostgreSQL-DDL-CREATE.sql   |   2 +
 .../manager/server/service/LoginServiceTest.java   |   7 +-
 .../manager/server/service/UserServiceTest.java    |  20 +-
 .../bigtop/manager/server/utils/JWTUtilsTest.java  |  13 +-
 17 files changed, 404 insertions(+), 68 deletions(-)

diff --git 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/model/vo/UserVO.java
 
b/bigtop-manager-common/src/main/java/org/apache/bigtop/manager/common/constants/Caches.java
similarity index 72%
copy from 
bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/model/vo/UserVO.java
copy to 
bigtop-manager-common/src/main/java/org/apache/bigtop/manager/common/constants/Caches.java
index 994f283a..a9b61858 100644
--- 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/model/vo/UserVO.java
+++ 
b/bigtop-manager-common/src/main/java/org/apache/bigtop/manager/common/constants/Caches.java
@@ -16,22 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.bigtop.manager.server.model.vo;
+package org.apache.bigtop.manager.common.constants;
 
-import lombok.Data;
+public class Caches {
 
-@Data
-public class UserVO {
+    public static final String CACHE_NONCE = "auth:nonce";
 
-    private Long id;
+    public static final int NONCE_EXPIRE_TIME_MINUTES = 5;
 
-    private String username;
+    public static final String CACHE_USER = "auth:user";
 
-    private String nickname;
-
-    private String createTime;
-
-    private String updateTime;
-
-    private Boolean status;
+    public static final int USER_EXPIRE_TIME_DAYS = 1;
 }
diff --git 
a/bigtop-manager-dao/src/main/java/org/apache/bigtop/manager/dao/po/UserPO.java 
b/bigtop-manager-dao/src/main/java/org/apache/bigtop/manager/dao/po/UserPO.java
index e684ea70..650ffa74 100644
--- 
a/bigtop-manager-dao/src/main/java/org/apache/bigtop/manager/dao/po/UserPO.java
+++ 
b/bigtop-manager-dao/src/main/java/org/apache/bigtop/manager/dao/po/UserPO.java
@@ -46,4 +46,7 @@ public class UserPO extends BasePO implements Serializable {
 
     @Column(name = "status")
     private Boolean status;
+
+    @Column(name = "token_version")
+    private Integer tokenVersion;
 }
diff --git a/bigtop-manager-dao/src/main/resources/mapper/mysql/UserMapper.xml 
b/bigtop-manager-dao/src/main/resources/mapper/mysql/UserMapper.xml
index e05f9262..22d75539 100644
--- a/bigtop-manager-dao/src/main/resources/mapper/mysql/UserMapper.xml
+++ b/bigtop-manager-dao/src/main/resources/mapper/mysql/UserMapper.xml
@@ -24,7 +24,7 @@
 <mapper namespace="org.apache.bigtop.manager.dao.repository.UserDao">
 
     <sql id="baseColumns">
-        id, username, password, nickname, status
+        id, username, password, nickname, status, token_version
     </sql>
 
     <select id="findByUsername" 
resultType="org.apache.bigtop.manager.dao.po.UserPO">
diff --git 
a/bigtop-manager-dao/src/main/resources/mapper/postgresql/UserMapper.xml 
b/bigtop-manager-dao/src/main/resources/mapper/postgresql/UserMapper.xml
index 24a32e28..f1ccbc04 100644
--- a/bigtop-manager-dao/src/main/resources/mapper/postgresql/UserMapper.xml
+++ b/bigtop-manager-dao/src/main/resources/mapper/postgresql/UserMapper.xml
@@ -24,7 +24,7 @@
 <mapper namespace="org.apache.bigtop.manager.dao.repository.UserDao">
 
     <sql id="baseColumns">
-        "id", "username", "password", "nickname", "status"
+        "id", "username", "password", "nickname", "status", "token_version"
     </sql>
 
     <select id="findByUsername" 
resultType="org.apache.bigtop.manager.dao.po.UserPO">
diff --git 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/controller/LoginController.java
 
b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/controller/LoginController.java
index 6b1a02c5..08be7024 100644
--- 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/controller/LoginController.java
+++ 
b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/controller/LoginController.java
@@ -18,6 +18,7 @@
  */
 package org.apache.bigtop.manager.server.controller;
 
+import org.apache.bigtop.manager.common.constants.Caches;
 import org.apache.bigtop.manager.server.annotations.Audit;
 import org.apache.bigtop.manager.server.enums.ApiExceptionEnum;
 import org.apache.bigtop.manager.server.exception.ApiException;
@@ -41,6 +42,7 @@ import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 
 import jakarta.annotation.Resource;
+import java.util.concurrent.TimeUnit;
 
 @Tag(name = "Login Controller")
 @RestController
@@ -60,7 +62,8 @@ public class LoginController {
     @GetMapping(value = "/nonce")
     public ResponseEntity<String> nonce(String username) {
         String nonce = PasswordUtils.randomString(16);
-        CacheUtils.setCache(username, nonce);
+        String cacheKey = username + ":" + nonce;
+        CacheUtils.setCache(Caches.CACHE_NONCE, cacheKey, nonce, 
Caches.NONCE_EXPIRE_TIME_MINUTES, TimeUnit.MINUTES);
         return ResponseEntity.success(nonce);
     }
 
diff --git 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/interceptor/AuthInterceptor.java
 
b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/interceptor/AuthInterceptor.java
index 1bbb2a83..a7cc2e84 100644
--- 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/interceptor/AuthInterceptor.java
+++ 
b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/interceptor/AuthInterceptor.java
@@ -18,14 +18,19 @@
  */
 package org.apache.bigtop.manager.server.interceptor;
 
+import org.apache.bigtop.manager.common.constants.Caches;
 import org.apache.bigtop.manager.common.utils.JsonUtils;
 import org.apache.bigtop.manager.server.enums.ApiExceptionEnum;
 import org.apache.bigtop.manager.server.holder.SessionUserHolder;
+import org.apache.bigtop.manager.server.model.vo.UserVO;
+import org.apache.bigtop.manager.server.service.UserService;
+import org.apache.bigtop.manager.server.utils.CacheUtils;
 import org.apache.bigtop.manager.server.utils.JWTUtils;
 import org.apache.bigtop.manager.server.utils.ResponseEntity;
 
 import org.apache.commons.lang3.StringUtils;
 
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 import org.springframework.web.servlet.HandlerInterceptor;
 import org.springframework.web.servlet.ModelAndView;
@@ -34,10 +39,15 @@ import com.auth0.jwt.interfaces.DecodedJWT;
 
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
 
 @Component
 public class AuthInterceptor implements HandlerInterceptor {
 
+    @Autowired
+    private UserService userService;
+
     private ResponseEntity<?> responseEntity;
 
     @Override
@@ -80,7 +90,31 @@ public class AuthInterceptor implements HandlerInterceptor {
 
         try {
             DecodedJWT decodedJWT = JWTUtils.resolveToken(token);
-            
SessionUserHolder.setUserId(decodedJWT.getClaim(JWTUtils.CLAIM_ID).asLong());
+            Long userId = decodedJWT.getClaim(JWTUtils.CLAIM_ID).asLong();
+            Integer tokenVersion =
+                    decodedJWT.getClaim(JWTUtils.CLAIM_TOKEN_VERSION).asInt();
+
+            // Check if the user exists
+            UserVO userVO = CacheUtils.getCache(Caches.CACHE_USER, 
userId.toString(), UserVO.class);
+            if (userVO == null) {
+                userVO = userService.get(userId);
+                if (userVO == null) {
+                    responseEntity = 
ResponseEntity.error(ApiExceptionEnum.NEED_LOGIN);
+                    return false;
+                }
+                // Explicitly set cache expiration time
+                CacheUtils.setCache(
+                        Caches.CACHE_USER, userId.toString(), userVO, 
Caches.USER_EXPIRE_TIME_DAYS, TimeUnit.DAYS);
+            }
+
+            // Check if the token version matches
+            if (!Objects.equals(tokenVersion, userVO.getTokenVersion())) {
+                CacheUtils.removeCache(Caches.CACHE_USER, userId.toString());
+                responseEntity = 
ResponseEntity.error(ApiExceptionEnum.NEED_LOGIN);
+                return false;
+            }
+
+            SessionUserHolder.setUserId(userId);
         } catch (Exception e) {
             responseEntity = ResponseEntity.error(ApiExceptionEnum.NEED_LOGIN);
             return false;
diff --git 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/model/vo/UserVO.java
 
b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/model/vo/UserVO.java
index 994f283a..e19aced4 100644
--- 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/model/vo/UserVO.java
+++ 
b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/model/vo/UserVO.java
@@ -34,4 +34,6 @@ public class UserVO {
     private String updateTime;
 
     private Boolean status;
+
+    private Integer tokenVersion;
 }
diff --git 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/UserService.java
 
b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/UserService.java
index 421f6bd8..6924ed42 100644
--- 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/UserService.java
+++ 
b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/UserService.java
@@ -31,6 +31,14 @@ public interface UserService {
      */
     UserVO current();
 
+    /**
+     * Get a user
+     *
+     * @param id user id
+     * @return user
+     */
+    UserVO get(Long id);
+
     /**
      * Update a user
      *
diff --git 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LoginServiceImpl.java
 
b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LoginServiceImpl.java
index 2c1bbcf5..88d4b55f 100644
--- 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LoginServiceImpl.java
+++ 
b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LoginServiceImpl.java
@@ -18,12 +18,15 @@
  */
 package org.apache.bigtop.manager.server.service.impl;
 
+import org.apache.bigtop.manager.common.constants.Caches;
 import org.apache.bigtop.manager.dao.po.UserPO;
 import org.apache.bigtop.manager.dao.repository.UserDao;
 import org.apache.bigtop.manager.server.enums.ApiExceptionEnum;
 import org.apache.bigtop.manager.server.exception.ApiException;
+import org.apache.bigtop.manager.server.model.converter.UserConverter;
 import org.apache.bigtop.manager.server.model.dto.LoginDTO;
 import org.apache.bigtop.manager.server.model.vo.LoginVO;
+import org.apache.bigtop.manager.server.model.vo.UserVO;
 import org.apache.bigtop.manager.server.service.LoginService;
 import org.apache.bigtop.manager.server.utils.CacheUtils;
 import org.apache.bigtop.manager.server.utils.JWTUtils;
@@ -32,6 +35,7 @@ import org.apache.bigtop.manager.server.utils.PasswordUtils;
 import org.springframework.stereotype.Service;
 
 import jakarta.annotation.Resource;
+import java.util.concurrent.TimeUnit;
 
 @Service
 public class LoginServiceImpl implements LoginService {
@@ -54,7 +58,8 @@ public class LoginServiceImpl implements LoginService {
             throw new ApiException(ApiExceptionEnum.USER_IS_DISABLED);
         }
 
-        String cache = CacheUtils.getCache(username);
+        String cacheKey = username + ":" + nonce;
+        String cache = CacheUtils.getCache(Caches.CACHE_NONCE, cacheKey, 
String.class);
         if (cache == null || !cache.equals(nonce)) {
             throw new 
ApiException(ApiExceptionEnum.INCORRECT_USERNAME_OR_PASSWORD);
         }
@@ -63,9 +68,14 @@ public class LoginServiceImpl implements LoginService {
             throw new 
ApiException(ApiExceptionEnum.INCORRECT_USERNAME_OR_PASSWORD);
         }
 
-        CacheUtils.removeCache(username);
+        CacheUtils.removeCache(Caches.CACHE_NONCE, username);
 
-        String token = JWTUtils.generateToken(user.getId(), 
user.getUsername());
+        // After successful login, update the user cache to ensure that the 
information in the cache is up-to-date
+        UserVO userVO = UserConverter.INSTANCE.fromPO2VO(user);
+        CacheUtils.setCache(
+                Caches.CACHE_USER, user.getId().toString(), userVO, 
Caches.USER_EXPIRE_TIME_DAYS, TimeUnit.DAYS);
+
+        String token = JWTUtils.generateToken(user.getId(), 
user.getUsername(), user.getTokenVersion());
         LoginVO loginVO = new LoginVO();
         loginVO.setToken(token);
         return loginVO;
diff --git 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/UserServiceImpl.java
 
b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/UserServiceImpl.java
index 9631fd40..47bcb97e 100644
--- 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/UserServiceImpl.java
+++ 
b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/UserServiceImpl.java
@@ -18,6 +18,7 @@
  */
 package org.apache.bigtop.manager.server.service.impl;
 
+import org.apache.bigtop.manager.common.constants.Caches;
 import org.apache.bigtop.manager.dao.po.UserPO;
 import org.apache.bigtop.manager.dao.repository.UserDao;
 import org.apache.bigtop.manager.server.enums.ApiExceptionEnum;
@@ -28,12 +29,14 @@ import 
org.apache.bigtop.manager.server.model.dto.ChangePasswordDTO;
 import org.apache.bigtop.manager.server.model.dto.UserDTO;
 import org.apache.bigtop.manager.server.model.vo.UserVO;
 import org.apache.bigtop.manager.server.service.UserService;
+import org.apache.bigtop.manager.server.utils.CacheUtils;
 import org.apache.bigtop.manager.server.utils.PasswordUtils;
 import org.apache.bigtop.manager.server.utils.Pbkdf2Utils;
 
 import org.springframework.stereotype.Service;
 
 import jakarta.annotation.Resource;
+import java.util.concurrent.TimeUnit;
 
 @Service
 public class UserServiceImpl implements UserService {
@@ -48,13 +51,23 @@ public class UserServiceImpl implements UserService {
         return UserConverter.INSTANCE.fromPO2VO(userPO);
     }
 
+    @Override
+    public UserVO get(Long id) {
+        UserPO userPO = userDao.findById(id);
+        return UserConverter.INSTANCE.fromPO2VO(userPO);
+    }
+
     @Override
     public UserVO update(UserDTO userDTO) {
         Long id = SessionUserHolder.getUserId();
         UserPO userPO = userDao.findOptionalById(id).orElseThrow(() -> new 
ApiException(ApiExceptionEnum.NEED_LOGIN));
         userPO.setNickname(userDTO.getNickname());
         userDao.partialUpdateById(userPO);
-        return UserConverter.INSTANCE.fromPO2VO(userPO);
+
+        // Update user information in cache
+        UserVO userVO = UserConverter.INSTANCE.fromPO2VO(userPO);
+        CacheUtils.setCache(Caches.CACHE_USER, id.toString(), userVO, 
Caches.USER_EXPIRE_TIME_DAYS, TimeUnit.DAYS);
+        return userVO;
     }
 
     @Override
@@ -77,7 +90,12 @@ public class UserServiceImpl implements UserService {
 
         String newPassword = 
Pbkdf2Utils.getBcryptPassword(userPO.getUsername(), 
changePasswordDTO.getNewPassword());
         userPO.setPassword(newPassword);
+        userPO.setTokenVersion(userPO.getTokenVersion() + 1);
         userDao.partialUpdateById(userPO);
-        return UserConverter.INSTANCE.fromPO2VO(userPO);
+
+        // Update user information in cache
+        UserVO userVO = UserConverter.INSTANCE.fromPO2VO(userPO);
+        CacheUtils.setCache(Caches.CACHE_USER, id.toString(), userVO, 
Caches.USER_EXPIRE_TIME_DAYS, TimeUnit.DAYS);
+        return userVO;
     }
 }
diff --git 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/utils/CacheUtils.java
 
b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/utils/CacheUtils.java
index 2aa4103a..25b9b251 100644
--- 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/utils/CacheUtils.java
+++ 
b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/utils/CacheUtils.java
@@ -22,72 +22,311 @@ import org.apache.commons.lang3.StringUtils;
 
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheStats;
 
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 
 /**
- * Cache utility class
+ * Multi-cache utility class supporting different expiration times and 
enhanced generic support
  */
 public class CacheUtils {
 
     /**
-     * Create cache, expires in 5 minutes, maximum capacity 10000
+     * Default cache configuration
      */
-    private static final Cache<String, String> CACHE = 
CacheBuilder.newBuilder()
-            .expireAfterWrite(5, TimeUnit.MINUTES)
-            .maximumSize(10000)
-            .build();
+    private static final int DEFAULT_EXPIRE_TIME = 5;
+
+    private static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.MINUTES;
+    private static final int DEFAULT_MAXIMUM_SIZE = 10000;
+
+    /**
+     * Multiple cache instances mapped by cache name
+     */
+    private static final ConcurrentHashMap<String, Cache<String, Object>> 
CACHES = new ConcurrentHashMap<>();
+
+    /**
+     * Create or get cache with specific configuration
+     * Note: If a cache with the same name already exists, it will be returned 
regardless
+     * of the provided configuration parameters.
+     *
+     * @param cacheName   cache name/identifier
+     * @param expireTime  expire time
+     * @param timeUnit    time unit
+     * @param maximumSize maximum cache size
+     * @return cache instance
+     */
+    public static Cache<String, Object> getOrCreateCache(
+            String cacheName, int expireTime, TimeUnit timeUnit, int 
maximumSize) {
+        if (StringUtils.isBlank(cacheName)) {
+            throw new IllegalArgumentException("Cache name cannot be blank");
+        }
+
+        return CACHES.computeIfAbsent(cacheName, name -> 
CacheBuilder.newBuilder()
+                .expireAfterWrite(expireTime, timeUnit)
+                .maximumSize(maximumSize)
+                .recordStats()
+                .build());
+    }
+
+    /**
+     * Create or get cache with custom expire time (in minutes) and default 
size
+     *
+     * @param cacheName     cache name/identifier
+     * @param expireMinutes expire time in minutes
+     * @return cache instance
+     */
+    public static Cache<String, Object> getOrCreateCache(String cacheName, int 
expireMinutes) {
+        return getOrCreateCache(cacheName, expireMinutes, DEFAULT_TIME_UNIT, 
DEFAULT_MAXIMUM_SIZE);
+    }
+
+    /**
+     * Create or get cache with custom expire time and time unit, using 
default size
+     *
+     * @param cacheName  cache name/identifier
+     * @param expireTime expire time
+     * @param timeUnit   time unit
+     * @return cache instance
+     */
+    public static Cache<String, Object> getOrCreateCache(String cacheName, int 
expireTime, TimeUnit timeUnit) {
+        return getOrCreateCache(cacheName, expireTime, timeUnit, 
DEFAULT_MAXIMUM_SIZE);
+    }
+
+    /**
+     * Get cache with default configuration
+     *
+     * @param cacheName cache name/identifier
+     * @return cache instance
+     */
+    public static Cache<String, Object> getOrCreateCache(String cacheName) {
+        return getOrCreateCache(cacheName, DEFAULT_EXPIRE_TIME, 
DEFAULT_TIME_UNIT, DEFAULT_MAXIMUM_SIZE);
+    }
 
     /**
-     * Store cache
+     * Store cache value
      *
-     * @param key   cache key
-     * @param value cache value
+     * @param cacheName cache name/identifier
+     * @param key       cache key
+     * @param value     cache value
      */
-    public static void setCache(String key, String value) {
-        if (StringUtils.isBlank(key) || value == null) {
+    public static void setCache(String cacheName, String key, Object value) {
+        if (StringUtils.isAnyBlank(cacheName, key) || value == null) {
             return;
         }
-        CACHE.put(key, value);
+        getOrCreateCache(cacheName).put(key, value);
     }
 
     /**
-     * Get cache
+     * Store cache value with specific cache configuration
      *
-     * @param key cache key
+     * @param cacheName  cache name/identifier
+     * @param key        cache key
+     * @param value      cache value
+     * @param expireTime expire time
+     */
+    public static void setCache(String cacheName, String key, Object value, 
int expireTime) {
+        if (StringUtils.isAnyBlank(cacheName, key) || value == null) {
+            return;
+        }
+        getOrCreateCache(cacheName, expireTime).put(key, value);
+    }
+
+    /**
+     * Store cache value with specific cache configuration
+     *
+     * @param cacheName  cache name/identifier
+     * @param key        cache key
+     * @param value      cache value
+     * @param expireTime expire time
+     * @param timeUnit   time unit
+     */
+    public static void setCache(String cacheName, String key, Object value, 
int expireTime, TimeUnit timeUnit) {
+        if (StringUtils.isAnyBlank(cacheName, key) || value == null) {
+            return;
+        }
+        getOrCreateCache(cacheName, expireTime, timeUnit).put(key, value);
+    }
+
+    /**
+     * Store cache value with specific cache configuration
+     *
+     * @param cacheName   cache name/identifier
+     * @param key         cache key
+     * @param value       cache value
+     * @param expireTime  expire time
+     * @param timeUnit    time unit
+     * @param maximumSize maximum cache size
+     */
+    public static void setCache(
+            String cacheName, String key, Object value, int expireTime, 
TimeUnit timeUnit, int maximumSize) {
+        if (StringUtils.isAnyBlank(cacheName, key) || value == null) {
+            return;
+        }
+        getOrCreateCache(cacheName, expireTime, timeUnit, 
maximumSize).put(key, value);
+    }
+
+    /**
+     * Batch set cache values
+     *
+     * @param cacheName cache name/identifier
+     * @param values    map of key-value pairs to cache
+     */
+    public static void setCacheBatch(String cacheName, Map<String, Object> 
values) {
+        if (StringUtils.isBlank(cacheName) || values == null || 
values.isEmpty()) {
+            return;
+        }
+        Cache<String, Object> cache = getOrCreateCache(cacheName);
+        cache.putAll(values);
+    }
+
+    /**
+     * Get cache value with type safety
+     *
+     * @param cacheName cache name/identifier
+     * @param key       cache key
+     * @param clazz     expected class type
+     * @param <T>       generic type
+     * @return cache value of specified type (returns null if not found, 
expired or type mismatch)
+     */
+    public static <T> T getCache(String cacheName, String key, Class<T> clazz) 
{
+        if (StringUtils.isAnyBlank(cacheName, key) || clazz == null) {
+            return null;
+        }
+
+        Object value = getCache(cacheName, key);
+        if (value != null && clazz.isInstance(value)) {
+            return clazz.cast(value);
+        }
+        return null;
+    }
+
+    /**
+     * Generic version of getCache method
+     *
+     * @param cacheName cache name/identifier
+     * @param key       cache key
+     * @param <T>       generic type
      * @return cache value (returns null if not found or expired)
      */
-    public static String getCache(String key) {
-        if (StringUtils.isBlank(key)) {
+    @SuppressWarnings("unchecked")
+    public static <T> T getCache(String cacheName, String key) {
+        if (StringUtils.isAnyBlank(cacheName, key)) {
             return null;
         }
-        return CACHE.getIfPresent(key);
+        Cache<String, Object> cache = CACHES.get(cacheName);
+        return cache != null ? (T) cache.getIfPresent(key) : null;
+    }
+
+    /**
+     * Check if cache with given name exists
+     *
+     * @param cacheName cache name/identifier
+     * @return true if cache exists, false otherwise
+     */
+    public static boolean exists(String cacheName) {
+        return StringUtils.isNotBlank(cacheName) && 
CACHES.containsKey(cacheName);
+    }
+
+    /**
+     * Remove specified cache entry
+     *
+     * @param cacheName cache name/identifier
+     * @param key       cache key
+     */
+    public static void removeCache(String cacheName, String key) {
+        if (StringUtils.isAnyBlank(cacheName, key)) {
+            return;
+        }
+        Cache<String, Object> cache = CACHES.get(cacheName);
+        if (cache != null) {
+            cache.invalidate(key);
+        }
     }
 
     /**
-     * Remove specified cache
+     * Clear all entries in specific cache
      *
-     * @param key cache key
+     * @param cacheName cache name/identifier
      */
-    public static void removeCache(String key) {
-        if (StringUtils.isBlank(key)) {
+    public static void clearCache(String cacheName) {
+        if (StringUtils.isBlank(cacheName)) {
             return;
         }
-        CACHE.invalidate(key);
+        Cache<String, Object> cache = CACHES.get(cacheName);
+        if (cache != null) {
+            cache.invalidateAll();
+            cache.cleanUp();
+        }
     }
 
     /**
      * Clear all caches
      */
     public static void clearAll() {
-        CACHE.invalidateAll();
-        CACHE.cleanUp();
+        CACHES.values().forEach(cache -> {
+            cache.invalidateAll();
+            cache.cleanUp();
+        });
+        CACHES.clear();
     }
 
     /**
-     * Get cache size
+     * Get cache size for specific cache
+     *
+     * @param cacheName cache name/identifier
+     * @return cache size
      */
-    public static long size() {
-        return CACHE.size();
+    public static long size(String cacheName) {
+        if (StringUtils.isBlank(cacheName)) {
+            return 0;
+        }
+        Cache<String, Object> cache = CACHES.get(cacheName);
+        return cache != null ? cache.size() : 0;
+    }
+
+    /**
+     * Get total size of all caches
+     *
+     * @return total cache size
+     */
+    public static long totalSize() {
+        return CACHES.values().stream().mapToLong(Cache::size).sum();
+    }
+
+    /**
+     * Get cache statistics
+     *
+     * @param cacheName cache name/identifier
+     * @return cache statistics
+     */
+    public static CacheStats getStats(String cacheName) {
+        if (StringUtils.isBlank(cacheName)) {
+            return null;
+        }
+        Cache<String, Object> cache = CACHES.get(cacheName);
+        return cache != null ? cache.stats() : null;
+    }
+
+    /**
+     * Force cleanup of expired entries in all caches
+     */
+    public static void cleanupAll() {
+        CACHES.values().forEach(Cache::cleanUp);
+    }
+
+    /**
+     * Force cleanup of expired entries in specific cache
+     *
+     * @param cacheName cache name/identifier
+     */
+    public static void cleanup(String cacheName) {
+        if (StringUtils.isBlank(cacheName)) {
+            return;
+        }
+        Cache<String, Object> cache = CACHES.get(cacheName);
+        if (cache != null) {
+            cache.cleanUp();
+        }
     }
 }
diff --git 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/utils/JWTUtils.java
 
b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/utils/JWTUtils.java
index 02ca8c63..a0f7e261 100644
--- 
a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/utils/JWTUtils.java
+++ 
b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/utils/JWTUtils.java
@@ -22,8 +22,8 @@ import com.auth0.jwt.JWT;
 import com.auth0.jwt.algorithms.Algorithm;
 import com.auth0.jwt.interfaces.DecodedJWT;
 
-import java.util.Calendar;
-import java.util.Date;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 
 public class JWTUtils {
 
@@ -31,17 +31,21 @@ public class JWTUtils {
 
     public static final String CLAIM_USERNAME = "username";
 
+    public static final String CLAIM_TOKEN_VERSION = "token_version";
+
     protected static final String SIGN = "r0PGVyvjKOxUBwGt";
 
-    public static String generateToken(Long id, String username) {
-        Calendar calendar = Calendar.getInstance();
-        calendar.add(Calendar.DAY_OF_MONTH, 7);
-        Date date = calendar.getTime();
+    // Token validity period (days)
+    private static final int TOKEN_EXPIRATION_DAYS = 7;
+
+    public static String generateToken(Long userId, String username, Integer 
tokenVersion) {
+        Instant expireTime = Instant.now().plus(TOKEN_EXPIRATION_DAYS, 
ChronoUnit.DAYS);
 
         return JWT.create()
-                .withClaim(CLAIM_ID, id)
+                .withClaim(CLAIM_ID, userId)
                 .withClaim(CLAIM_USERNAME, username)
-                .withExpiresAt(date)
+                .withClaim(CLAIM_TOKEN_VERSION, tokenVersion)
+                .withExpiresAt(expireTime)
                 .sign(Algorithm.HMAC256(SIGN));
     }
 
diff --git a/bigtop-manager-server/src/main/resources/ddl/MySQL-DDL-CREATE.sql 
b/bigtop-manager-server/src/main/resources/ddl/MySQL-DDL-CREATE.sql
index e9118535..aeab6931 100644
--- a/bigtop-manager-server/src/main/resources/ddl/MySQL-DDL-CREATE.sql
+++ b/bigtop-manager-server/src/main/resources/ddl/MySQL-DDL-CREATE.sql
@@ -338,6 +338,8 @@ CREATE TABLE `llm_chat_message`
 INSERT INTO user (username, password, nickname, status)
 VALUES ('admin', 
'$2b$10$bdTvADKA0dSJYT3wMU3LFeIEnxzKQHeWN3XcHJ5jQpsIo7ju1U5Yi', 
'Administrator', true);
 
+ALTER TABLE user ADD COLUMN token_version INTEGER DEFAULT 1;
+
 INSERT INTO repo (name, arch, base_url, pkg_name, checksum, type)
 VALUES
 ('general', 'x86_64,aarch64', 'http://your-repo/', null, null, 1),
diff --git 
a/bigtop-manager-server/src/main/resources/ddl/PostgreSQL-DDL-CREATE.sql 
b/bigtop-manager-server/src/main/resources/ddl/PostgreSQL-DDL-CREATE.sql
index 71001cf4..35feb63f 100644
--- a/bigtop-manager-server/src/main/resources/ddl/PostgreSQL-DDL-CREATE.sql
+++ b/bigtop-manager-server/src/main/resources/ddl/PostgreSQL-DDL-CREATE.sql
@@ -351,6 +351,8 @@ CREATE INDEX idx_message_user_id ON llm_chat_message 
(user_id);
 INSERT INTO "user" (username, password, nickname, status)
 VALUES ('admin', 
'$2b$10$bdTvADKA0dSJYT3wMU3LFeIEnxzKQHeWN3XcHJ5jQpsIo7ju1U5Yi', 
'Administrator', true);
 
+ALTER TABLE "user" ADD token_version INT DEFAULT 1;
+
 INSERT INTO repo (name, arch, base_url, pkg_name, checksum, type)
 VALUES
 ('general', 'x86_64,aarch64', 'http://your-repo/', null, null, 1),
diff --git 
a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/service/LoginServiceTest.java
 
b/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/service/LoginServiceTest.java
index e5430717..86c06db1 100644
--- 
a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/service/LoginServiceTest.java
+++ 
b/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/service/LoginServiceTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.bigtop.manager.server.service;
 
+import org.apache.bigtop.manager.common.constants.Caches;
 import org.apache.bigtop.manager.dao.po.UserPO;
 import org.apache.bigtop.manager.dao.repository.UserDao;
 import org.apache.bigtop.manager.server.enums.ApiExceptionEnum;
@@ -36,6 +37,8 @@ import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
+import java.util.concurrent.TimeUnit;
+
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -60,9 +63,11 @@ public class LoginServiceTest {
     @BeforeEach
     public void setUp() {
         nonce = PasswordUtils.randomString(16);
-        CacheUtils.setCache(USERNAME, nonce);
+        String cacheKey = USERNAME + ":" + nonce;
+        CacheUtils.setCache(Caches.CACHE_NONCE, cacheKey, nonce, 
Caches.NONCE_EXPIRE_TIME_MINUTES, TimeUnit.MINUTES);
 
         mockUser = new UserPO();
+        mockUser.setId(1L);
         mockUser.setUsername(USERNAME);
         mockUser.setPassword(Pbkdf2Utils.getBcryptPassword(USERNAME, 
RAW_PASSWORD));
         mockUser.setStatus(true);
diff --git 
a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/service/UserServiceTest.java
 
b/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/service/UserServiceTest.java
index c3b17e77..1b9b150c 100644
--- 
a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/service/UserServiceTest.java
+++ 
b/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/service/UserServiceTest.java
@@ -23,6 +23,7 @@ import org.apache.bigtop.manager.dao.po.UserPO;
 import org.apache.bigtop.manager.dao.repository.UserDao;
 import org.apache.bigtop.manager.server.enums.ApiExceptionEnum;
 import org.apache.bigtop.manager.server.exception.ApiException;
+import org.apache.bigtop.manager.server.holder.SessionUserHolder;
 import org.apache.bigtop.manager.server.model.dto.UserDTO;
 import org.apache.bigtop.manager.server.service.impl.UserServiceImpl;
 
@@ -30,6 +31,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
+import org.mockito.MockedStatic;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 import java.util.Optional;
@@ -37,6 +39,7 @@ import java.util.Optional;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mockStatic;
 import static org.mockito.Mockito.when;
 
 @ExtendWith(MockitoExtension.class)
@@ -50,12 +53,19 @@ public class UserServiceTest {
 
     @Test
     public void testCurrentAndUpdate() {
-        when(userDao.findOptionalById(any())).thenReturn(Optional.of(new 
UserPO()));
-        assert userService.current() != null;
+        // Behavior of Mock SessionUserHolder
+        try (MockedStatic<SessionUserHolder> mockedSessionHolder = 
mockStatic(SessionUserHolder.class)) {
 
-        UserDTO userDTO = new UserDTO();
-        userDTO.setNickname("test");
-        assert userService.update(userDTO).getNickname().equals("test");
+            // Set SessionUserHolder. getUserId() to return 1L
+            
mockedSessionHolder.when(SessionUserHolder::getUserId).thenReturn(1L);
+
+            when(userDao.findOptionalById(1L)).thenReturn(Optional.of(new 
UserPO()));
+            assert userService.current() != null;
+
+            UserDTO userDTO = new UserDTO();
+            userDTO.setNickname("test");
+            assert userService.update(userDTO).getNickname().equals("test");
+        }
     }
 
     @Test
diff --git 
a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/utils/JWTUtilsTest.java
 
b/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/utils/JWTUtilsTest.java
index 53152e57..e9316093 100644
--- 
a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/utils/JWTUtilsTest.java
+++ 
b/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/utils/JWTUtilsTest.java
@@ -39,12 +39,15 @@ public class JWTUtilsTest {
     public void testGenerateTokenNormal() {
         Long id = 1L;
         String username = "testUser";
-        String token = JWTUtils.generateToken(id, username);
+        Integer tokenVersion = 1;
+        String token = JWTUtils.generateToken(id, username, tokenVersion);
         assertNotNull(token);
 
         DecodedJWT decodedJWT = JWTUtils.resolveToken(token);
-        assertEquals(decodedJWT.getClaim(JWTUtils.CLAIM_ID).asLong(), id);
-        assertEquals(decodedJWT.getClaim(JWTUtils.CLAIM_USERNAME).asString(), 
username);
+        assertEquals(id, decodedJWT.getClaim(JWTUtils.CLAIM_ID).asLong());
+        assertEquals(username, 
decodedJWT.getClaim(JWTUtils.CLAIM_USERNAME).asString());
+        assertEquals(
+                tokenVersion, 
decodedJWT.getClaim(JWTUtils.CLAIM_TOKEN_VERSION).asInt());
     }
 
     @Test
@@ -79,10 +82,10 @@ public class JWTUtilsTest {
 
     @Test
     public void testGenerateTokenUsernameEmpty() {
-        String token = JWTUtils.generateToken(1L, "");
+        String token = JWTUtils.generateToken(1L, "", 1);
         assertNotNull(token);
 
         DecodedJWT decodedJWT = JWTUtils.resolveToken(token);
-        assertEquals(decodedJWT.getClaim(JWTUtils.CLAIM_USERNAME).asString(), 
"");
+        assertEquals("", 
decodedJWT.getClaim(JWTUtils.CLAIM_USERNAME).asString());
     }
 }


Reply via email to