瀏覽代碼

优化 UserSession 的实现,将 id 变成自增,额外增加 token 字段

YunaiV 3 年之前
父節點
當前提交
8606f5c605
共有 24 個文件被更改,包括 322 次插入235 次删除
  1. 1 0
      sql/dm/README.md
  2. 4 0
      yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml
  3. 4 4
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java
  4. 2 2
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java
  5. 3 3
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java
  6. 2 1
      yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/DefaultDatabaseQueryTest.java
  7. 7 9
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java
  8. 8 8
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/UserSessionApi.java
  9. 6 6
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/auth/UserSessionApiImpl.java
  10. 2 3
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/UserSessionController.java
  11. 9 9
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/UserSessionDO.java
  12. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/user/AdminUserDO.java
  13. 17 7
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/UserSessionMapper.java
  14. 1 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java
  15. 13 8
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/auth/LoginUserRedisDAO.java
  16. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/job/auth/UserSessionTimeoutJob.java
  17. 5 5
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
  18. 16 9
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/UserSessionService.java
  19. 68 51
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/UserSessionServiceImpl.java
  20. 17 8
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java
  21. 122 91
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/UserSessionServiceImplTest.java
  22. 2 1
      yudao-module-system/yudao-module-system-biz/src/test/resources/sql/create_tables.sql
  23. 5 6
      yudao-server/pom.xml
  24. 6 0
      yudao-server/src/main/resources/application-local.yaml

+ 1 - 0
sql/dm/README.md

@@ -0,0 +1 @@
+暂未适配国产 DM 数据库,如果你有需要,可以微信联系 wangwenbin-server 一起建设。

+ 4 - 0
yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml

@@ -46,6 +46,10 @@
             <groupId>org.postgresql</groupId>
             <artifactId>postgresql</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.microsoft.sqlserver</groupId>
+            <artifactId>mssql-jdbc</artifactId>
+        </dependency>
 
         <dependency>
             <groupId>com.alibaba</groupId>

+ 4 - 4
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java

@@ -3,7 +3,7 @@ package cn.iocoder.yudao.framework.security.config;
 import cn.iocoder.yudao.framework.security.core.aop.PreAuthenticatedAspect;
 import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider;
 import cn.iocoder.yudao.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy;
-import cn.iocoder.yudao.framework.security.core.filter.JWTAuthenticationTokenFilter;
+import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter;
 import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl;
 import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl;
 import cn.iocoder.yudao.framework.security.core.handler.LogoutSuccessHandlerImpl;
@@ -86,9 +86,9 @@ public class YudaoSecurityAutoConfiguration {
      * Token 认证过滤器 Bean
      */
     @Bean
-    public JWTAuthenticationTokenFilter authenticationTokenFilter(MultiUserDetailsAuthenticationProvider authenticationProvider,
-                                                                  GlobalExceptionHandler globalExceptionHandler) {
-        return new JWTAuthenticationTokenFilter(securityProperties, authenticationProvider, globalExceptionHandler);
+    public TokenAuthenticationFilter authenticationTokenFilter(MultiUserDetailsAuthenticationProvider authenticationProvider,
+                                                               GlobalExceptionHandler globalExceptionHandler) {
+        return new TokenAuthenticationFilter(securityProperties, authenticationProvider, globalExceptionHandler);
     }
 
     /**

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java

@@ -2,7 +2,7 @@ package cn.iocoder.yudao.framework.security.config;
 
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider;
-import cn.iocoder.yudao.framework.security.core.filter.JWTAuthenticationTokenFilter;
+import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter;
 import cn.iocoder.yudao.framework.web.config.WebProperties;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
@@ -55,7 +55,7 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
      * Token 认证过滤器 Bean
      */
     @Resource
-    private JWTAuthenticationTokenFilter authenticationTokenFilter;
+    private TokenAuthenticationFilter authenticationTokenFilter;
 
     /**
      * 自定义的权限映射 Bean 们

+ 3 - 3
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/JWTAuthenticationTokenFilter.java → yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java

@@ -18,13 +18,13 @@ import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 
 /**
- * JWT 过滤器,验证 token 的有效性
+ * Token 过滤器,验证 token 的有效性
  * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文
  *
  * @author 芋道源码
  */
 @RequiredArgsConstructor
-public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
+public class TokenAuthenticationFilter extends OncePerRequestFilter {
 
     private final SecurityProperties securityProperties;
 
@@ -43,7 +43,7 @@ public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
                 LoginUser loginUser = authenticationProvider.verifyTokenAndRefresh(request, token);
                 // 模拟 Login 功能,方便日常开发调试
                 if (loginUser == null) {
-                    loginUser = this.mockLoginUser(request, token);
+                    loginUser = mockLoginUser(request, token);
                 }
                 // 设置当前用户
                 if (loginUser != null) {

+ 2 - 1
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/DefaultDatabaseQueryTest.java

@@ -27,7 +27,8 @@ public class DefaultDatabaseQueryTest {
             if (StrUtil.startWithAny(tableInfo.getName().toLowerCase(), "act_", "flw_", "qrtz_")) {
                 continue;
             }
-            System.out.println(String.format("CREATE SEQUENCE %s_seq MINVALUE 0;", tableInfo.getName()));
+//            System.out.println(String.format("CREATE SEQUENCE %s_seq MINVALUE 0;", tableInfo.getName()));
+            System.out.println(String.format("DELETE FROM %s WHERE deleted = '1';", tableInfo.getName()));
         }
         System.out.println(tableInfos.size());
         System.out.println(System.currentTimeMillis() - time);

+ 7 - 9
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java

@@ -8,8 +8,6 @@ import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
 import cn.iocoder.yudao.framework.security.core.authentication.MultiUsernamePasswordAuthenticationToken;
 import cn.iocoder.yudao.module.member.controller.app.auth.vo.*;
-import cn.iocoder.yudao.module.member.controller.app.social.vo.AppSocialUserBindReqVO;
-import cn.iocoder.yudao.module.member.controller.app.social.vo.AppSocialUserUnbindReqVO;
 import cn.iocoder.yudao.module.member.convert.auth.AuthConvert;
 import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
 import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper;
@@ -88,7 +86,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
         // 使用手机 + 密码,进行登录。
         LoginUser loginUser = this.login0(reqVO.getMobile(), reqVO.getPassword());
 
-        // 缓存登录用户到 Redis 中,返回 sessionId 编号
+        // 缓存登录用户到 Redis 中,返回 Token 令牌
         return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_USERNAME, userIp, userAgent);
     }
 
@@ -105,7 +103,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
         // 执行登陆
         LoginUser loginUser = AuthConvert.INSTANCE.convert(user);
 
-        // 缓存登录用户到 Redis 中,返回 sessionId 编号
+        // 缓存登录用户到 Redis 中,返回 Token 令牌
         return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_SMS, userIp, userAgent);
     }
 
@@ -127,7 +125,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
         // 创建 LoginUser 对象
         LoginUser loginUser = AuthConvert.INSTANCE.convert(user);
 
-        // 缓存登录用户到 Redis 中,返回 sessionId 编号
+        // 缓存登录用户到 Redis 中,返回 Token 令牌
         return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_SOCIAL, userIp, userAgent);
     }
 
@@ -136,18 +134,18 @@ public class MemberAuthServiceImpl implements MemberAuthService {
         // 使用手机号、手机验证码登录
         AppAuthSmsLoginReqVO loginReqVO = AppAuthSmsLoginReqVO.builder()
                 .mobile(reqVO.getMobile()).code(reqVO.getSmsCode()).build();
-        String sessionId = this.smsLogin(loginReqVO, userIp, userAgent);
-        LoginUser loginUser = userSessionApi.getLoginUser(sessionId);
+        String token = this.smsLogin(loginReqVO, userIp, userAgent);
+        LoginUser loginUser = userSessionApi.getLoginUser(token);
 
         // 绑定社交用户
         socialUserApi.bindSocialUser(AuthConvert.INSTANCE.convert(loginUser.getId(), getUserType().getValue(), reqVO));
-        return sessionId;
+        return token;
     }
 
     private String createUserSessionAfterLoginSuccess(LoginUser loginUser, LoginLogTypeEnum logType, String userIp, String userAgent) {
         // 插入登陆日志
         createLoginLog(loginUser.getUsername(), logType, LoginResultEnum.SUCCESS);
-        // 缓存登录用户到 Redis 中,返回 sessionId 编号
+        // 缓存登录用户到 Redis 中,返回 Token 令牌
         return userSessionApi.createUserSession(loginUser, userIp, userAgent);
     }
 

+ 8 - 8
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/UserSessionApi.java

@@ -18,33 +18,33 @@ public interface UserSessionApi {
      * @param loginUser 登录用户
      * @param userIp 用户 IP
      * @param userAgent 用户 UA
-     * @return Session 编号
+     * @return Token 令牌
      */
     String createUserSession(@NotNull(message = "登录用户不能为空") LoginUser loginUser, String userIp, String userAgent);
 
     /**
      * 刷新在线用户 Session 的更新时间
      *
-     * @param sessionId Session 编号
+     * @param token Token 令牌
      * @param loginUser 登录用户
      */
-    void refreshUserSession(@NotEmpty(message = "Session编号不能为空") String sessionId,
+    void refreshUserSession(@NotEmpty(message = "Token 令牌不能为空") String token,
                             @NotNull(message = "登录用户不能为空") LoginUser loginUser);
 
     /**
      * 删除在线用户 Session
      *
-     * @param sessionId Session 编号
+     * @param token Token 令牌
      */
-    void deleteUserSession(String sessionId);
+    void deleteUserSession(String token);
 
     /**
-     * 获得 Session 编号对应的在线用户
+     * 获得 Token 令牌对应的在线用户
      *
-     * @param sessionId Session 编号
+     * @param token Token 令牌
      * @return 在线用户
      */
-    LoginUser getLoginUser(String sessionId);
+    LoginUser getLoginUser(String token);
 
     /**
      * 获得 Session 超时时间,单位:毫秒

+ 6 - 6
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/auth/UserSessionApiImpl.java

@@ -25,18 +25,18 @@ public class UserSessionApiImpl implements UserSessionApi {
     }
 
     @Override
-    public void refreshUserSession(String sessionId, LoginUser loginUser) {
-        userSessionService.refreshUserSession(sessionId, loginUser);
+    public void refreshUserSession(String token, LoginUser loginUser) {
+        userSessionService.refreshUserSession(token, loginUser);
     }
 
     @Override
-    public void deleteUserSession(String sessionId) {
-        userSessionService.deleteUserSession(sessionId);
+    public void deleteUserSession(String token) {
+        userSessionService.deleteUserSession(token);
     }
 
     @Override
-    public LoginUser getLoginUser(String sessionId) {
-        return userSessionService.getLoginUser(sessionId);
+    public LoginUser getLoginUser(String token) {
+        return userSessionService.getLoginUser(token);
     }
 
     @Override

+ 2 - 3
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/UserSessionController.java

@@ -69,10 +69,9 @@ public class UserSessionController {
 
     @DeleteMapping("/delete")
     @ApiOperation("删除 Session")
-    @ApiImplicitParam(name = "id", value = "Session 编号", required = true, dataTypeClass = String.class,
-            example = "fe50b9f6-d177-44b1-8da9-72ea34f63db7")
+    @ApiImplicitParam(name = "id", value = "Session 编号", required = true, dataTypeClass = Long.class, example = "1024")
     @PreAuthorize("@ss.hasPermission('system:user-session:delete')")
-    public CommonResult<Boolean> deleteUserSession(@RequestParam("id") String id) {
+    public CommonResult<Boolean> deleteUserSession(@RequestParam("id") Long id) {
         userSessionService.deleteUserSession(id);
         return success(true);
     }

+ 9 - 9
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/UserSessionDO.java

@@ -3,10 +3,7 @@ package cn.iocoder.yudao.module.system.dal.dataobject.auth;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.KeySequence;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.annotation.*;
 import lombok.Builder;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
@@ -22,18 +19,21 @@ import java.util.Date;
  *
  * @author 芋道源码
  */
-@TableName(value = "system_user_session", autoResultMap = true)
-@KeySequence("system_user_session_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@TableName(value = "system_user_session")
 @Data
 @Builder
 @EqualsAndHashCode(callSuper = true)
 public class UserSessionDO extends BaseDO {
 
     /**
-     * 会话编号, 即 sessionId
+     * 会话编号
      */
-    @TableId(type = IdType.INPUT)
-    private String id;
+    private Long id;
+    /**
+     * 令牌
+     */
+    private String token;
+
     /**
      * 用户编号
      *

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/user/AdminUserDO.java

@@ -19,7 +19,7 @@ import java.util.Set;
  *
  * @author 芋道源码
  */
-@TableName(value = "system_user", autoResultMap = true)
+@TableName(value = "system_users", autoResultMap = true) // 由于 SQL Server 的 system_user 是关键字,所以使用 system_users
 @KeySequence("system_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
 @Data
 @EqualsAndHashCode(callSuper = true)

+ 17 - 7
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/UserSessionMapper.java

@@ -1,10 +1,10 @@
 package cn.iocoder.yudao.module.system.dal.mysql.auth;
 
-import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageReqVO;
-import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
-import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.util.Collection;
@@ -15,13 +15,23 @@ import java.util.List;
 public interface UserSessionMapper extends BaseMapperX<UserSessionDO> {
 
     default PageResult<UserSessionDO> selectPage(UserSessionPageReqVO reqVO, Collection<Long> userIds) {
-        return selectPage(reqVO, new QueryWrapperX<UserSessionDO>()
-                .inIfPresent("user_id", userIds)
-                .likeIfPresent("user_ip", reqVO.getUserIp()));
+        return selectPage(reqVO, new LambdaQueryWrapperX<UserSessionDO>()
+                .inIfPresent(UserSessionDO::getUserId, userIds)
+                .likeIfPresent(UserSessionDO::getUserIp, reqVO.getUserIp()));
     }
 
     default List<UserSessionDO> selectListBySessionTimoutLt() {
-        return selectList(new QueryWrapperX<UserSessionDO>().lt("session_timeout",new Date()));
+        return selectList(new LambdaQueryWrapperX<UserSessionDO>()
+                .lt(UserSessionDO::getSessionTimeout, new Date()));
+    }
+
+    default void updateByToken(String token, UserSessionDO updateObj) {
+        update(updateObj, new LambdaQueryWrapperX<UserSessionDO>()
+                .eq(UserSessionDO::getToken, token));
+    }
+
+    default void deleteByToken(String token) {
+        delete(new LambdaQueryWrapperX<UserSessionDO>().eq(UserSessionDO::getToken, token));
     }
 
 }

+ 1 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java

@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.system.dal.redis;
 
 import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
-import me.zhyd.oauth.model.AuthUser;
 
 import java.time.Duration;
 
@@ -20,7 +19,7 @@ public interface RedisKeyConstants {
             STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
 
     RedisKeyDefine LOGIN_USER = new RedisKeyDefine("登录用户的缓存",
-            "login_user:%s", // 参数为 sessionId
+            "login_user:%s", // 参数为 token 令牌
             STRING, LoginUser.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
 
     RedisKeyDefine SOCIAL_AUTH_STATE = new RedisKeyDefine("社交登陆的 state", // 注意,它是被 JustAuth 的 justauth.type.prefix 使用到

+ 13 - 8
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/auth/LoginUserRedisDAO.java

@@ -24,24 +24,29 @@ public class LoginUserRedisDAO {
     @Resource
     private SecurityProperties securityProperties;
 
-    public LoginUser get(String sessionId) {
-        String redisKey = formatKey(sessionId);
+    public LoginUser get(String token) {
+        String redisKey = formatKey(token);
         return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), LoginUser.class);
     }
 
-    public void set(String sessionId, LoginUser loginUser) {
-        String redisKey = formatKey(sessionId);
+    public Boolean exists(String token) {
+        String redisKey = formatKey(token);
+        return stringRedisTemplate.hasKey(redisKey);
+    }
+
+    public void set(String token, LoginUser loginUser) {
+        String redisKey = formatKey(token);
         stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(loginUser),
                 securityProperties.getSessionTimeout());
     }
 
-    public void delete(String sessionId) {
-        String redisKey = formatKey(sessionId);
+    public void delete(String token) {
+        String redisKey = formatKey(token);
         stringRedisTemplate.delete(redisKey);
     }
 
-    private static String formatKey(String sessionId) {
-        return LOGIN_USER.formatKey(sessionId);
+    private static String formatKey(String token) {
+        return LOGIN_USER.formatKey(token);
     }
 
 }

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/job/auth/UserSessionTimeoutJob.java

@@ -24,7 +24,7 @@ public class UserSessionTimeoutJob implements JobHandler {
     @Override
     public String execute(String param) throws Exception {
         // 执行过期
-        Long timeoutCount = userSessionService.clearSessionTimeout();
+        Long timeoutCount = userSessionService.deleteTimeoutSession();
         // 返回结果,记录每次的超时数量
         return String.format("移除在线会话数量为 %s 个", timeoutCount);
     }

+ 5 - 5
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java

@@ -104,7 +104,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
         // 使用账号密码,进行登录
         LoginUser loginUser = login0(reqVO.getUsername(), reqVO.getPassword());
 
-        // 缓存登陆用户到 Redis 中,返回 sessionId 编号
+        // 缓存登陆用户到 Redis 中,返回 Token 令牌
         return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_USERNAME, userIp, userAgent);
     }
 
@@ -207,7 +207,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
         // 创建 LoginUser 对象
         LoginUser loginUser = buildLoginUser(user);
 
-        // 缓存登录用户到 Redis 中,返回 sessionId 编号
+        // 缓存登录用户到 Redis 中,返回 Token 令牌
         return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_SOCIAL, userIp, userAgent);
     }
 
@@ -219,14 +219,14 @@ public class AdminAuthServiceImpl implements AdminAuthService {
         // 绑定社交用户
         socialUserService.bindSocialUser(AuthConvert.INSTANCE.convert(loginUser.getId(), getUserType().getValue(), reqVO));
 
-        // 缓存登录用户到 Redis 中,返回 sessionId 编号
+        // 缓存登录用户到 Redis 中,返回 Token 令牌
         return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_SOCIAL, userIp, userAgent);
     }
 
     private String createUserSessionAfterLoginSuccess(LoginUser loginUser, LoginLogTypeEnum logType, String userIp, String userAgent) {
         // 插入登陆日志
         createLoginLog(loginUser.getUsername(), logType, LoginResultEnum.SUCCESS);
-        // 缓存登录用户到 Redis 中,返回 sessionId 编号
+        // 缓存登录用户到 Redis 中,返回 Token 令牌
         return userSessionService.createUserSession(loginUser, userIp, userAgent);
     }
 
@@ -240,7 +240,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
         // 删除 session
         userSessionService.deleteUserSession(token);
         // 记录登出日志
-        this.createLogoutLog(loginUser.getId(), loginUser.getUsername());
+        createLogoutLog(loginUser.getId(), loginUser.getUsername());
     }
 
     @Override

+ 16 - 9
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/UserSessionService.java

@@ -25,7 +25,7 @@ public interface UserSessionService {
      *
      * @return {@link Long } 移出的超时用户数量
      **/
-    long clearSessionTimeout();
+    long deleteTimeoutSession();
 
     /**
      * 创建在线用户 Session
@@ -33,32 +33,39 @@ public interface UserSessionService {
      * @param loginUser 登录用户
      * @param userIp 用户 IP
      * @param userAgent 用户 UA
-     * @return Session 编号
+     * @return Token 令牌
      */
     String createUserSession(LoginUser loginUser, String userIp, String userAgent);
 
     /**
      * 刷新在线用户 Session 的更新时间
      *
-     * @param sessionId Session 编号
+     * @param token 令牌
      * @param loginUser 登录用户
      */
-    void refreshUserSession(String sessionId, LoginUser loginUser);
+    void refreshUserSession(String token, LoginUser loginUser);
 
     /**
      * 删除在线用户 Session
      *
-     * @param sessionId Session 编号
+     * @param token token 令牌
      */
-    void deleteUserSession(String sessionId);
+    void deleteUserSession(String token);
 
     /**
-     * 获得 Session 编号对应的在线用户
+     * 删除在线用户 Session
+     *
+     * @param id 编号
+     */
+    void deleteUserSession(Long id);
+
+    /**
+     * 获得 Token 对应的在线用户
      *
-     * @param sessionId Session 编号
+     * @param token 令牌
      * @return 在线用户
      */
-    LoginUser getLoginUser(String sessionId);
+    LoginUser getLoginUser(String token);
 
     /**
      * 获得 Session 超时时间,单位:毫秒

+ 68 - 51
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/UserSessionServiceImpl.java

@@ -3,28 +3,28 @@ package cn.iocoder.yudao.module.system.service.auth;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
 import cn.iocoder.yudao.framework.security.config.SecurityProperties;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
 import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageReqVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
 import cn.iocoder.yudao.module.system.dal.mysql.auth.UserSessionMapper;
+import cn.iocoder.yudao.module.system.dal.redis.auth.LoginUserRedisDAO;
 import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
 import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
 import cn.iocoder.yudao.module.system.service.logger.LoginLogService;
 import cn.iocoder.yudao.module.system.service.user.AdminUserService;
-import cn.iocoder.yudao.module.system.dal.redis.auth.LoginUserRedisDAO;
-import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
-import com.google.common.collect.Lists;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
 import java.time.Duration;
-import java.util.*;
-import java.util.stream.Collectors;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime;
@@ -65,82 +65,99 @@ public class UserSessionServiceImpl implements UserSessionService {
         return userSessionMapper.selectPage(reqVO, userIds);
     }
 
-    // TODO @芋艿:优化下该方法
     @Override
-    public long clearSessionTimeout() {
-        // 获取db里已经超时的用户列表
-        List<UserSessionDO> sessionTimeoutDOS = userSessionMapper.selectListBySessionTimoutLt();
-        Map<String, UserSessionDO> timeoutSessionDOMap = sessionTimeoutDOS
-                .stream()
-                .filter(sessionDO -> loginUserRedisDAO.get(sessionDO.getId()) == null)
-                .collect(Collectors.toMap(UserSessionDO::getId, o -> o));
-        // 确认已经超时,按批次移出在线用户列表
-        if (CollUtil.isNotEmpty(timeoutSessionDOMap)) {
-            Lists.partition(new ArrayList<>(timeoutSessionDOMap.keySet()), 100)
-                    .forEach(userSessionMapper::deleteBatchIds);
-            // 记录用户超时退出日志
-            createTimeoutLogoutLog(timeoutSessionDOMap.values());
+    public long deleteTimeoutSession() {
+        // 获取 db 里已经超时的用户列表
+        List<UserSessionDO> timeoutSessions = userSessionMapper.selectListBySessionTimoutLt();
+        if (CollUtil.isEmpty(timeoutSessions)) {
+            return 0L;
         }
-        return timeoutSessionDOMap.size();
-    }
 
-    private void createTimeoutLogoutLog(Collection<UserSessionDO> timeoutSessionDOS) {
-        for (UserSessionDO timeoutSessionDO : timeoutSessionDOS) {
-            LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
-            reqDTO.setLogType(LoginLogTypeEnum.LOGOUT_TIMEOUT.getType());
-            reqDTO.setTraceId(TracerUtils.getTraceId());
-            reqDTO.setUserId(timeoutSessionDO.getUserId());
-            reqDTO.setUserType(timeoutSessionDO.getUserType());
-            reqDTO.setUsername(timeoutSessionDO.getUsername());
-            reqDTO.setUserAgent(timeoutSessionDO.getUserAgent());
-            reqDTO.setUserIp(timeoutSessionDO.getUserIp());
-            reqDTO.setResult(LoginResultEnum.SUCCESS.getResult());
-            loginLogService.createLoginLog(reqDTO);
+        // 由于过期的用户一般不多,所以顺序遍历,进行清理
+        int count = 0;
+        for (UserSessionDO session : timeoutSessions) {
+            // 基于 Redis 二次判断,同时也保证 Redis Key 的立即过期,避免延迟导致浪费内存空间
+            if (loginUserRedisDAO.exists(session.getToken())) {
+                continue;
+            }
+            userSessionMapper.deleteById(session.getId());
+            // 记录退出日志
+            createLogoutLog(session, LoginLogTypeEnum.LOGOUT_TIMEOUT);
+            count++;
         }
+        return count;
+    }
+
+    private void createLogoutLog(UserSessionDO session, LoginLogTypeEnum type) {
+        LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
+        reqDTO.setLogType(type.getType());
+        reqDTO.setTraceId(TracerUtils.getTraceId());
+        reqDTO.setUserId(session.getUserId());
+        reqDTO.setUserType(session.getUserType());
+        reqDTO.setUsername(session.getUsername());
+        reqDTO.setUserAgent(session.getUserAgent());
+        reqDTO.setUserIp(session.getUserIp());
+        reqDTO.setResult(LoginResultEnum.SUCCESS.getResult());
+        loginLogService.createLoginLog(reqDTO);
     }
 
     @Override
     public String createUserSession(LoginUser loginUser, String userIp, String userAgent) {
         // 生成 Session 编号
-        String sessionId = generateSessionId();
+        String token = generateToken();
         // 写入 Redis 缓存
         loginUser.setUpdateTime(new Date());
-        loginUserRedisDAO.set(sessionId, loginUser);
+        loginUserRedisDAO.set(token, loginUser);
         // 写入 DB 中
-        UserSessionDO userSession = UserSessionDO.builder().id(sessionId)
+        UserSessionDO userSession = UserSessionDO.builder().token(token)
                 .userId(loginUser.getId()).userType(loginUser.getUserType())
                 .userIp(userIp).userAgent(userAgent).username(loginUser.getUsername())
                 .sessionTimeout(addTime(Duration.ofMillis(getSessionTimeoutMillis())))
                 .build();
         userSessionMapper.insert(userSession);
-        // 返回 Session 编号
-        return sessionId;
+        // 返回 Token 令牌
+        return token;
     }
 
     @Override
-    public void refreshUserSession(String sessionId, LoginUser loginUser) {
+    public void refreshUserSession(String token, LoginUser loginUser) {
         // 写入 Redis 缓存
         loginUser.setUpdateTime(new Date());
-        loginUserRedisDAO.set(sessionId, loginUser);
+        loginUserRedisDAO.set(token, loginUser);
         // 更新 DB 中
-        UserSessionDO updateObj = UserSessionDO.builder().id(sessionId).build();
+        UserSessionDO updateObj = UserSessionDO.builder().build();
         updateObj.setUsername(loginUser.getUsername());
         updateObj.setUpdateTime(new Date());
         updateObj.setSessionTimeout(addTime(Duration.ofMillis(getSessionTimeoutMillis())));
-        userSessionMapper.updateById(updateObj);
+        userSessionMapper.updateByToken(token, updateObj);
     }
 
     @Override
-    public void deleteUserSession(String sessionId) {
+    public void deleteUserSession(String token) {
+        // 删除 Redis 缓存
+        loginUserRedisDAO.delete(token);
+        // 删除 DB 记录
+        userSessionMapper.deleteByToken(token);
+        // 无需记录日志,因为退出那已经记录
+    }
+
+    @Override
+    public void deleteUserSession(Long id) {
+        UserSessionDO session = userSessionMapper.selectById(id);
+        if (session == null) {
+            return;
+        }
         // 删除 Redis 缓存
-        loginUserRedisDAO.delete(sessionId);
+        loginUserRedisDAO.delete(session.getToken());
         // 删除 DB 记录
-        userSessionMapper.deleteById(sessionId);
+        userSessionMapper.deleteById(id);
+        // 记录退出日志
+        createLogoutLog(session, LoginLogTypeEnum.LOGOUT_DELETE);
     }
 
     @Override
-    public LoginUser getLoginUser(String sessionId) {
-        return loginUserRedisDAO.get(sessionId);
+    public LoginUser getLoginUser(String token) {
+        return loginUserRedisDAO.get(token);
     }
 
     @Override
@@ -149,11 +166,11 @@ public class UserSessionServiceImpl implements UserSessionService {
     }
 
     /**
-     * 生成 Session 编号,目前采用 UUID 算法
+     * 生成 Token 令牌,目前采用 UUID 算法
      *
      * @return Session 编号
      */
-    private static String generateSessionId() {
+    private static String generateToken() {
         return IdUtil.fastSimpleUUID();
     }
 

+ 17 - 8
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java

@@ -134,6 +134,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
         AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
         String userIp = randomString();
         String userAgent = randomString();
+
         // 调用, 并断言异常
         assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_CAPTCHA_NOT_FOUND);
         // 校验调用参数
@@ -148,10 +149,12 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
         // 准备参数
         String userIp = randomString();
         String userAgent = randomString();
-        String code = randomString();
         AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
+
         // mock 验证码不正确
+        String code = randomString();
         when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(code);
+
         // 调用, 并断言异常
         assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_CAPTCHA_CODE_ERROR);
         // 校验调用参数
@@ -172,6 +175,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
         // mock 抛出异常
         when(authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(reqVO.getUsername(), reqVO.getPassword())))
                 .thenThrow(new BadCredentialsException("测试账号或密码不正确"));
+
         // 调用, 并断言异常
         assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_BAD_CREDENTIALS);
         // 校验调用参数
@@ -188,11 +192,13 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
         String userIp = randomString();
         String userAgent = randomString();
         AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
+
         // mock 验证码正确
         when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode());
         // mock 抛出异常
         when(authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(reqVO.getUsername(), reqVO.getPassword())))
                 .thenThrow(new DisabledException("测试用户被禁用"));
+
         // 调用, 并断言异常
         assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_USER_DISABLED);
         // 校验调用参数
@@ -214,6 +220,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
         // mock 抛出异常
         when(authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(reqVO.getUsername(), reqVO.getPassword())))
                 .thenThrow(new AuthenticationException("测试未知异常") {});
+
         // 调用, 并断言异常
         assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_FAIL_UNKNOWN);
         // 校验调用参数
@@ -229,27 +236,29 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
         // 准备参数
         String userIp = randomString();
         String userAgent = randomString();
+        AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
+
+        // mock 验证码正确
+        when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode());
+        // mock authentication
         Long userId = randomLongId();
         Set<Long> userRoleIds = randomSet(Long.class);
-        String sessionId = randomString();
-        AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
         LoginUser loginUser = randomPojo(LoginUser.class, o -> {
             o.setId(userId);
             o.setRoleIds(userRoleIds);
         });
-        // mock 验证码正确
-        when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode());
-        // mock authentication
         when(authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(reqVO.getUsername(), reqVO.getPassword())))
                 .thenReturn(authentication);
         when(authentication.getPrincipal()).thenReturn(loginUser);
         // mock 获得 User 拥有的角色编号数组
         when(permissionService.getUserRoleIds(userId, singleton(CommonStatusEnum.ENABLE.getStatus()))).thenReturn(userRoleIds);
         // mock 缓存登录用户到 Redis
-        when(userSessionService.createUserSession(loginUser, userIp, userAgent)).thenReturn(sessionId);
+        String token = randomString();
+        when(userSessionService.createUserSession(loginUser, userIp, userAgent)).thenReturn(token);
+
         // 调用, 并断言异常
         String login = authService.login(reqVO, userIp, userAgent);
-        assertEquals(sessionId, login);
+        assertEquals(token, login);
         // 校验调用参数
         verify(captchaService, times(1)).deleteCaptchaCode(reqVO.getUuid());
         verify(loginLogService, times(1)).createLoginLog(

+ 122 - 91
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/UserSessionServiceImplTest.java

@@ -1,31 +1,31 @@
 package cn.iocoder.yudao.module.system.service.auth;
 
-import cn.hutool.core.date.DateUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.date.DateUtils;
+import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
 import cn.iocoder.yudao.framework.security.config.SecurityProperties;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest;
 import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
 import cn.iocoder.yudao.module.system.dal.mysql.auth.UserSessionMapper;
-import cn.iocoder.yudao.module.system.service.logger.LoginLogService;
-import cn.iocoder.yudao.module.system.service.user.AdminUserService;
-import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO;
 import cn.iocoder.yudao.module.system.dal.redis.auth.LoginUserRedisDAO;
 import cn.iocoder.yudao.module.system.enums.common.SexEnum;
-import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
-import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
-import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest;
+import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
+import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
+import cn.iocoder.yudao.module.system.service.logger.LoginLogService;
+import cn.iocoder.yudao.module.system.service.user.AdminUserService;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.context.annotation.Import;
 
 import javax.annotation.Resource;
 import java.time.Duration;
-import java.util.Date;
-import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
+import java.util.Calendar;
 
 import static cn.hutool.core.util.RandomUtil.randomEle;
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime;
@@ -33,8 +33,9 @@ import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEq
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
 import static java.util.Collections.singletonList;
 import static org.junit.jupiter.api.Assertions.*;
-import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 /**
@@ -61,6 +62,11 @@ public class UserSessionServiceImplTest extends BaseDbAndRedisUnitTest {
     @MockBean
     private SecurityProperties securityProperties;
 
+    @BeforeEach
+    public void setUp() {
+        when(securityProperties.getSessionTimeout()).thenReturn(Duration.ofDays(1L));
+    }
+
     @Test
     public void testGetUserSessionPage_success() {
         // mock 数据
@@ -78,15 +84,9 @@ public class UserSessionServiceImplTest extends BaseDbAndRedisUnitTest {
         });
         userSessionMapper.insert(dbSession);
         // 测试 username 不匹配
-        userSessionMapper.insert(ObjectUtils.cloneIgnoreId(dbSession, o -> {
-            o.setId(randomString());
-            o.setUserId(123456L);
-        }));
+        userSessionMapper.insert(ObjectUtils.cloneIgnoreId(dbSession, o -> o.setUserId(123456L)));
         // 测试 userIp 不匹配
-        userSessionMapper.insert(ObjectUtils.cloneIgnoreId(dbSession, o -> {
-            o.setId(randomString());
-            o.setUserIp("testUserIp");
-        }));
+        userSessionMapper.insert(ObjectUtils.cloneIgnoreId(dbSession, o -> o.setUserIp("testUserIp")));
         // 准备参数
         UserSessionPageReqVO reqVO = new UserSessionPageReqVO();
         reqVO.setUsername(dbUser.getUsername());
@@ -100,35 +100,60 @@ public class UserSessionServiceImplTest extends BaseDbAndRedisUnitTest {
         assertPojoEquals(dbSession, pageResult.getList().get(0));
     }
 
-    // TODO 芋艿:单测写的有问题
+    @Test
+    public void testClearSessionTimeout_none() {
+        // mock db 数据
+        UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> {
+            o.setUserType(randomEle(UserTypeEnum.values()).getValue());
+            o.setSessionTimeout(addTime(Duration.ofDays(1)));
+        });
+        userSessionMapper.insert(userSession);
+
+        // 调用
+        long count = userSessionService.deleteTimeoutSession();
+        // 断言
+        assertEquals(0, count);
+        assertPojoEquals(userSession, userSessionMapper.selectById(userSession.getId())); // 未删除
+    }
+
+    @Test // Redis 还存在的情况
+    public void testClearSessionTimeout_exists() {
+        // mock db 数据
+        UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> {
+            o.setUserType(randomEle(UserTypeEnum.values()).getValue());
+            o.setSessionTimeout(DateUtils.addDate(Calendar.DAY_OF_YEAR, -1));
+        });
+        userSessionMapper.insert(userSession);
+        // mock redis 数据
+        loginUserRedisDAO.set(userSession.getToken(), new LoginUser());
+
+        // 调用
+        long count = userSessionService.deleteTimeoutSession();
+        // 断言
+        assertEquals(0, count);
+        assertPojoEquals(userSession, userSessionMapper.selectById(userSession.getId())); // 未删除
+    }
+
     @Test
     public void testClearSessionTimeout_success() {
-        // 准备超时数据 120 条, 在线用户 1 条
-        int expectedTimeoutCount = 120, expectedTotal = 1;
-
-        // 准备数据
-        List<UserSessionDO> prepareData = Stream
-                .iterate(0, i -> i)
-                .limit(expectedTimeoutCount)
-                .map(i -> randomPojo(UserSessionDO.class, o -> {
-                    o.setUserType(randomEle(UserTypeEnum.values()).getValue());
-                    o.setSessionTimeout(DateUtil.offsetSecond(new Date(), -1));
-                }))
-                .collect(Collectors.toList());
-        UserSessionDO sessionDO = randomPojo(UserSessionDO.class, o -> {
+        // mock db 数据
+        UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> {
             o.setUserType(randomEle(UserTypeEnum.values()).getValue());
-            o.setSessionTimeout(DateUtil.offsetMinute(new Date(), 30));
+            o.setSessionTimeout(DateUtils.addDate(Calendar.DAY_OF_YEAR, -1));
         });
-        prepareData.add(sessionDO);
-        prepareData.forEach(userSessionMapper::insert);
+        userSessionMapper.insert(userSession);
 
         // 清空超时数据
-        long actualTimeoutCount = userSessionService.clearSessionTimeout();
+        long count = userSessionService.deleteTimeoutSession();
         // 校验
-        assertEquals(expectedTimeoutCount, actualTimeoutCount);
-        List<UserSessionDO> userSessionDOS = userSessionMapper.selectList();
-        assertEquals(expectedTotal, userSessionDOS.size());
-        assertPojoEquals(sessionDO, userSessionDOS.get(0), "updateTime");
+        assertEquals(1, count);
+        assertNull(userSessionMapper.selectById(userSession.getId())); // 已删除
+        verify(loginLogService).createLoginLog(argThat(loginLog -> {
+            assertPojoEquals(userSession, loginLog);
+            assertEquals(LoginLogTypeEnum.LOGOUT_TIMEOUT.getType(), loginLog.getLogType());
+            assertEquals(LoginResultEnum.SUCCESS.getResult(), loginLog.getResult());
+            return true;
+        }));
     }
 
     @Test
@@ -140,80 +165,86 @@ public class UserSessionServiceImplTest extends BaseDbAndRedisUnitTest {
             o.setUserType(randomEle(UserTypeEnum.values()).getValue());
             o.setTenantId(0L); // 租户设置为 0,因为暂未启用多租户组件
         });
-        // mock 方法
-        when(securityProperties.getSessionTimeout()).thenReturn(Duration.ofDays(1));
 
         // 调用
-        String sessionId = userSessionService.createUserSession(loginUser, userIp, userAgent);
+        String token = userSessionService.createUserSession(loginUser, userIp, userAgent);
         // 校验 UserSessionDO 记录
-        UserSessionDO userSessionDO = userSessionMapper.selectById(sessionId);
+        UserSessionDO userSessionDO = userSessionMapper.selectOne(UserSessionDO::getToken, token);
         assertPojoEquals(loginUser, userSessionDO, "id", "updateTime");
-        assertEquals(sessionId, userSessionDO.getId());
+        assertEquals(token, userSessionDO.getToken());
         assertEquals(userIp, userSessionDO.getUserIp());
         assertEquals(userAgent, userSessionDO.getUserAgent());
         // 校验 LoginUser 缓存
-        LoginUser redisLoginUser = loginUserRedisDAO.get(sessionId);
+        LoginUser redisLoginUser = loginUserRedisDAO.get(token);
         assertPojoEquals(loginUser, redisLoginUser, "username", "password");
     }
 
     @Test
-    public void testCreateRefreshUserSession_success() {
+    public void testCreateRefreshUserSession() {
         // 准备参数
-        String sessionId = randomString();
-        String userIp = randomString();
-        String userAgent = randomString();
-        long timeLong = randomLongId();
-        String userName = randomString();
-        Date date = randomDate();
+        String token = randomString();
+
+        // mock redis 数据
         LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setUserType(randomEle(UserTypeEnum.values()).getValue()));
-        // mock 方法
-        when(securityProperties.getSessionTimeout()).thenReturn(Duration.ofDays(1));
-        // mock 数据
-        loginUser.setUpdateTime(date);
-        loginUserRedisDAO.set(sessionId, loginUser);
-        UserSessionDO userSession = UserSessionDO.builder().id(sessionId)
-                .userId(loginUser.getId()).userType(loginUser.getUserType())
-                .userIp(userIp).userAgent(userAgent).username(userName)
-                .sessionTimeout(addTime(Duration.ofMillis(timeLong)))
-                .build();
+        loginUserRedisDAO.set(token, loginUser);
+        // mock db 数据
+        UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> {
+            o.setUserType(randomEle(UserTypeEnum.values()).getValue());
+            o.setToken(token);
+        });
         userSessionMapper.insert(userSession);
 
         // 调用
-        userSessionService.refreshUserSession(sessionId, loginUser);
+        userSessionService.refreshUserSession(token, loginUser);
         // 校验 LoginUser 缓存
-        LoginUser redisLoginUser = loginUserRedisDAO.get(sessionId);
-        assertNotEquals(redisLoginUser.getUpdateTime(), date);
+        LoginUser redisLoginUser = loginUserRedisDAO.get(token);
+        assertPojoEquals(redisLoginUser, loginUser, "username", "password");
         // 校验 UserSessionDO 记录
-        UserSessionDO updateDO = userSessionMapper.selectById(sessionId);
+        UserSessionDO updateDO = userSessionMapper.selectOne(UserSessionDO::getToken, token);
         assertEquals(updateDO.getUsername(), loginUser.getUsername());
-        assertNotEquals(updateDO.getUpdateTime(), userSession.getUpdateTime());
-        assertNotEquals(updateDO.getSessionTimeout(), addTime(Duration.ofMillis(timeLong)));
+        assertNotNull(userSession.getUpdateTime());
+        assertNotNull(userSession.getSessionTimeout());
     }
 
     @Test
-    public void testDeleteUserSession_success() {
+    public void testDeleteUserSession_Token() {
         // 准备参数
-        String sessionId = randomString();
-        String userIp = randomString();
-        String userAgent = randomString();
-        Long timeLong = randomLongId();
-        LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setUserType(randomEle(UserTypeEnum.values()).getValue()));
-        // mock 存入 Redis
-        when(securityProperties.getSessionTimeout()).thenReturn(Duration.ofDays(1));
-        // mock 数据
-        loginUserRedisDAO.set(sessionId, loginUser);
-        UserSessionDO userSession = UserSessionDO.builder().id(sessionId)
-                .userId(loginUser.getId()).userType(loginUser.getUserType())
-                .userIp(userIp).userAgent(userAgent).username(loginUser.getUsername())
-                .sessionTimeout(addTime(Duration.ofMillis(timeLong)))
-                .build();
+        String token = randomString();
+
+        // mock redis 数据
+        loginUserRedisDAO.set(token, new LoginUser());
+        // mock db 数据
+        UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> {
+            o.setUserType(randomEle(UserTypeEnum.values()).getValue());
+            o.setToken(token);
+        });
         userSessionMapper.insert(userSession);
 
         // 调用
-        userSessionService.deleteUserSession(sessionId);
+        userSessionService.deleteUserSession(token);
+        // 校验数据不存在了
+        assertNull(loginUserRedisDAO.get(token));
+        assertNull(userSessionMapper.selectOne(UserSessionDO::getToken, token));
+    }
+
+    @Test
+    public void testDeleteUserSession_Id() {
+        // mock db 数据
+        UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> {
+            o.setUserType(randomEle(UserTypeEnum.values()).getValue());
+        });
+        userSessionMapper.insert(userSession);
+        // mock redis 数据
+        loginUserRedisDAO.set(userSession.getToken(), new LoginUser());
+
+        // 准备参数
+        Long id = userSession.getId();
+
+        // 调用
+        userSessionService.deleteUserSession(id);
         // 校验数据不存在了
-        assertNull(loginUserRedisDAO.get(sessionId));
-        assertNull(userSessionMapper.selectById(sessionId));
+        assertNull(loginUserRedisDAO.get(userSession.getToken()));
+        assertNull(userSessionMapper.selectById(id));
     }
 
 }

+ 2 - 1
yudao-module-system/yudao-module-system-biz/src/test/resources/sql/create_tables.sql

@@ -115,7 +115,8 @@ CREATE TABLE IF NOT EXISTS "system_dict_type" (
 ) COMMENT '字典类型表';
 
 CREATE TABLE IF NOT EXISTS `system_user_session` (
-    `id` varchar(32) NOT NULL,
+    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    `token` varchar(32) NOT NULL,
     `user_id` bigint DEFAULT NULL,
     "user_type" tinyint NOT NULL,
     `username` varchar(50) NOT NULL DEFAULT '',

+ 5 - 6
yudao-server/pom.xml

@@ -21,7 +21,6 @@
     <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 
     <dependencies>
-        <!-- TODO 芋艿:多模块 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-module-member-biz</artifactId>
@@ -43,11 +42,11 @@
             <version>${revision}</version>
         </dependency>
         <!-- 默认引入 yudao-module-bpm-biz-flowable 实现,可以替换为 yudao-module-bpm-biz-activiti 实现-->
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-bpm-biz-flowable</artifactId>
-            <version>${revision}</version>
-        </dependency>
+<!--        <dependency>-->
+<!--            <groupId>cn.iocoder.boot</groupId>-->
+<!--            <artifactId>yudao-module-bpm-biz-flowable</artifactId>-->
+<!--            <version>${revision}</version>-->
+<!--        </dependency>-->
 <!--        <dependency>-->
 <!--            <groupId>cn.iocoder.boot</groupId>-->
 <!--            <artifactId>yudao-module-bpm-biz-activiti</artifactId>-->

+ 6 - 0
yudao-server/src/main/resources/application-local.yaml

@@ -49,6 +49,9 @@ spring:
 #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
           username: root
           password: 123456
+#          url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.master.name} # SQLServer 连接的示例
+#          username: sa
+#          password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W
         slave: # 模拟从库,可根据自己需要修改
           name: ruoyi-vue-pro
           url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL 连接的示例
@@ -56,6 +59,9 @@ spring:
 #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
           username: root
           password: 123456
+#          url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.slave.name} # SQLServer 连接的示例
+#          username: sa
+#          password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W
 
   # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
   redis: