Commit 8606f5c6053b0947213f4e4ed336b4cbc17b2502

Authored by YunaiV
1 parent 0f21b55e

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

Showing 24 changed files with 322 additions and 235 deletions
sql/dm/README.md 0 → 100644
  1 +暂未适配国产 DM 数据库,如果你有需要,可以微信联系 wangwenbin-server 一起建设。
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml
... ... @@ -46,6 +46,10 @@
46 46 <groupId>org.postgresql</groupId>
47 47 <artifactId>postgresql</artifactId>
48 48 </dependency>
  49 + <dependency>
  50 + <groupId>com.microsoft.sqlserver</groupId>
  51 + <artifactId>mssql-jdbc</artifactId>
  52 + </dependency>
49 53  
50 54 <dependency>
51 55 <groupId>com.alibaba</groupId>
... ...
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;
3 3 import cn.iocoder.yudao.framework.security.core.aop.PreAuthenticatedAspect;
4 4 import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider;
5 5 import cn.iocoder.yudao.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy;
6   -import cn.iocoder.yudao.framework.security.core.filter.JWTAuthenticationTokenFilter;
  6 +import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter;
7 7 import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl;
8 8 import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl;
9 9 import cn.iocoder.yudao.framework.security.core.handler.LogoutSuccessHandlerImpl;
... ... @@ -86,9 +86,9 @@ public class YudaoSecurityAutoConfiguration {
86 86 * Token 认证过滤器 Bean
87 87 */
88 88 @Bean
89   - public JWTAuthenticationTokenFilter authenticationTokenFilter(MultiUserDetailsAuthenticationProvider authenticationProvider,
90   - GlobalExceptionHandler globalExceptionHandler) {
91   - return new JWTAuthenticationTokenFilter(securityProperties, authenticationProvider, globalExceptionHandler);
  89 + public TokenAuthenticationFilter authenticationTokenFilter(MultiUserDetailsAuthenticationProvider authenticationProvider,
  90 + GlobalExceptionHandler globalExceptionHandler) {
  91 + return new TokenAuthenticationFilter(securityProperties, authenticationProvider, globalExceptionHandler);
92 92 }
93 93  
94 94 /**
... ...
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;
2 2  
3 3 import cn.hutool.core.util.StrUtil;
4 4 import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider;
5   -import cn.iocoder.yudao.framework.security.core.filter.JWTAuthenticationTokenFilter;
  5 +import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter;
6 6 import cn.iocoder.yudao.framework.web.config.WebProperties;
7 7 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
8 8 import org.springframework.context.annotation.Bean;
... ... @@ -55,7 +55,7 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
55 55 * Token 认证过滤器 Bean
56 56 */
57 57 @Resource
58   - private JWTAuthenticationTokenFilter authenticationTokenFilter;
  58 + private TokenAuthenticationFilter authenticationTokenFilter;
59 59  
60 60 /**
61 61 * 自定义的权限映射 Bean 们
... ...
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/JWTAuthenticationTokenFilter.java renamed to 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;
18 18 import java.io.IOException;
19 19  
20 20 /**
21   - * JWT 过滤器,验证 token 的有效性
  21 + * Token 过滤器,验证 token 的有效性
22 22 * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文
23 23 *
24 24 * @author 芋道源码
25 25 */
26 26 @RequiredArgsConstructor
27   -public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
  27 +public class TokenAuthenticationFilter extends OncePerRequestFilter {
28 28  
29 29 private final SecurityProperties securityProperties;
30 30  
... ... @@ -43,7 +43,7 @@ public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
43 43 LoginUser loginUser = authenticationProvider.verifyTokenAndRefresh(request, token);
44 44 // 模拟 Login 功能,方便日常开发调试
45 45 if (loginUser == null) {
46   - loginUser = this.mockLoginUser(request, token);
  46 + loginUser = mockLoginUser(request, token);
47 47 }
48 48 // 设置当前用户
49 49 if (loginUser != null) {
... ...
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 {
27 27 if (StrUtil.startWithAny(tableInfo.getName().toLowerCase(), "act_", "flw_", "qrtz_")) {
28 28 continue;
29 29 }
30   - System.out.println(String.format("CREATE SEQUENCE %s_seq MINVALUE 0;", tableInfo.getName()));
  30 +// System.out.println(String.format("CREATE SEQUENCE %s_seq MINVALUE 0;", tableInfo.getName()));
  31 + System.out.println(String.format("DELETE FROM %s WHERE deleted = '1';", tableInfo.getName()));
31 32 }
32 33 System.out.println(tableInfos.size());
33 34 System.out.println(System.currentTimeMillis() - time);
... ...
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;
8 8 import cn.iocoder.yudao.framework.security.core.LoginUser;
9 9 import cn.iocoder.yudao.framework.security.core.authentication.MultiUsernamePasswordAuthenticationToken;
10 10 import cn.iocoder.yudao.module.member.controller.app.auth.vo.*;
11   -import cn.iocoder.yudao.module.member.controller.app.social.vo.AppSocialUserBindReqVO;
12   -import cn.iocoder.yudao.module.member.controller.app.social.vo.AppSocialUserUnbindReqVO;
13 11 import cn.iocoder.yudao.module.member.convert.auth.AuthConvert;
14 12 import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
15 13 import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper;
... ... @@ -88,7 +86,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
88 86 // 使用手机 + 密码,进行登录。
89 87 LoginUser loginUser = this.login0(reqVO.getMobile(), reqVO.getPassword());
90 88  
91   - // 缓存登录用户到 Redis 中,返回 sessionId 编号
  89 + // 缓存登录用户到 Redis 中,返回 Token 令牌
92 90 return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_USERNAME, userIp, userAgent);
93 91 }
94 92  
... ... @@ -105,7 +103,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
105 103 // 执行登陆
106 104 LoginUser loginUser = AuthConvert.INSTANCE.convert(user);
107 105  
108   - // 缓存登录用户到 Redis 中,返回 sessionId 编号
  106 + // 缓存登录用户到 Redis 中,返回 Token 令牌
109 107 return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_SMS, userIp, userAgent);
110 108 }
111 109  
... ... @@ -127,7 +125,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
127 125 // 创建 LoginUser 对象
128 126 LoginUser loginUser = AuthConvert.INSTANCE.convert(user);
129 127  
130   - // 缓存登录用户到 Redis 中,返回 sessionId 编号
  128 + // 缓存登录用户到 Redis 中,返回 Token 令牌
131 129 return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_SOCIAL, userIp, userAgent);
132 130 }
133 131  
... ... @@ -136,18 +134,18 @@ public class MemberAuthServiceImpl implements MemberAuthService {
136 134 // 使用手机号、手机验证码登录
137 135 AppAuthSmsLoginReqVO loginReqVO = AppAuthSmsLoginReqVO.builder()
138 136 .mobile(reqVO.getMobile()).code(reqVO.getSmsCode()).build();
139   - String sessionId = this.smsLogin(loginReqVO, userIp, userAgent);
140   - LoginUser loginUser = userSessionApi.getLoginUser(sessionId);
  137 + String token = this.smsLogin(loginReqVO, userIp, userAgent);
  138 + LoginUser loginUser = userSessionApi.getLoginUser(token);
141 139  
142 140 // 绑定社交用户
143 141 socialUserApi.bindSocialUser(AuthConvert.INSTANCE.convert(loginUser.getId(), getUserType().getValue(), reqVO));
144   - return sessionId;
  142 + return token;
145 143 }
146 144  
147 145 private String createUserSessionAfterLoginSuccess(LoginUser loginUser, LoginLogTypeEnum logType, String userIp, String userAgent) {
148 146 // 插入登陆日志
149 147 createLoginLog(loginUser.getUsername(), logType, LoginResultEnum.SUCCESS);
150   - // 缓存登录用户到 Redis 中,返回 sessionId 编号
  148 + // 缓存登录用户到 Redis 中,返回 Token 令牌
151 149 return userSessionApi.createUserSession(loginUser, userIp, userAgent);
152 150 }
153 151  
... ...
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 {
18 18 * @param loginUser 登录用户
19 19 * @param userIp 用户 IP
20 20 * @param userAgent 用户 UA
21   - * @return Session 编号
  21 + * @return Token 令牌
22 22 */
23 23 String createUserSession(@NotNull(message = "登录用户不能为空") LoginUser loginUser, String userIp, String userAgent);
24 24  
25 25 /**
26 26 * 刷新在线用户 Session 的更新时间
27 27 *
28   - * @param sessionId Session 编号
  28 + * @param token Token 令牌
29 29 * @param loginUser 登录用户
30 30 */
31   - void refreshUserSession(@NotEmpty(message = "Session编号不能为空") String sessionId,
  31 + void refreshUserSession(@NotEmpty(message = "Token 令牌不能为空") String token,
32 32 @NotNull(message = "登录用户不能为空") LoginUser loginUser);
33 33  
34 34 /**
35 35 * 删除在线用户 Session
36 36 *
37   - * @param sessionId Session 编号
  37 + * @param token Token 令牌
38 38 */
39   - void deleteUserSession(String sessionId);
  39 + void deleteUserSession(String token);
40 40  
41 41 /**
42   - * 获得 Session 编号对应的在线用户
  42 + * 获得 Token 令牌对应的在线用户
43 43 *
44   - * @param sessionId Session 编号
  44 + * @param token Token 令牌
45 45 * @return 在线用户
46 46 */
47   - LoginUser getLoginUser(String sessionId);
  47 + LoginUser getLoginUser(String token);
48 48  
49 49 /**
50 50 * 获得 Session 超时时间,单位:毫秒
... ...
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 {
25 25 }
26 26  
27 27 @Override
28   - public void refreshUserSession(String sessionId, LoginUser loginUser) {
29   - userSessionService.refreshUserSession(sessionId, loginUser);
  28 + public void refreshUserSession(String token, LoginUser loginUser) {
  29 + userSessionService.refreshUserSession(token, loginUser);
30 30 }
31 31  
32 32 @Override
33   - public void deleteUserSession(String sessionId) {
34   - userSessionService.deleteUserSession(sessionId);
  33 + public void deleteUserSession(String token) {
  34 + userSessionService.deleteUserSession(token);
35 35 }
36 36  
37 37 @Override
38   - public LoginUser getLoginUser(String sessionId) {
39   - return userSessionService.getLoginUser(sessionId);
  38 + public LoginUser getLoginUser(String token) {
  39 + return userSessionService.getLoginUser(token);
40 40 }
41 41  
42 42 @Override
... ...
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 {
69 69  
70 70 @DeleteMapping("/delete")
71 71 @ApiOperation("删除 Session")
72   - @ApiImplicitParam(name = "id", value = "Session 编号", required = true, dataTypeClass = String.class,
73   - example = "fe50b9f6-d177-44b1-8da9-72ea34f63db7")
  72 + @ApiImplicitParam(name = "id", value = "Session 编号", required = true, dataTypeClass = Long.class, example = "1024")
74 73 @PreAuthorize("@ss.hasPermission('system:user-session:delete')")
75   - public CommonResult<Boolean> deleteUserSession(@RequestParam("id") String id) {
  74 + public CommonResult<Boolean> deleteUserSession(@RequestParam("id") Long id) {
76 75 userSessionService.deleteUserSession(id);
77 76 return success(true);
78 77 }
... ...
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;
3 3 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
4 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
5 5 import cn.iocoder.yudao.framework.security.core.LoginUser;
6   -import com.baomidou.mybatisplus.annotation.IdType;
7   -import com.baomidou.mybatisplus.annotation.KeySequence;
8   -import com.baomidou.mybatisplus.annotation.TableId;
9   -import com.baomidou.mybatisplus.annotation.TableName;
  6 +import com.baomidou.mybatisplus.annotation.*;
10 7 import lombok.Builder;
11 8 import lombok.Data;
12 9 import lombok.EqualsAndHashCode;
... ... @@ -22,18 +19,21 @@ import java.util.Date;
22 19 *
23 20 * @author 芋道源码
24 21 */
25   -@TableName(value = "system_user_session", autoResultMap = true)
26   -@KeySequence("system_user_session_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  22 +@TableName(value = "system_user_session")
27 23 @Data
28 24 @Builder
29 25 @EqualsAndHashCode(callSuper = true)
30 26 public class UserSessionDO extends BaseDO {
31 27  
32 28 /**
33   - * 会话编号, 即 sessionId
  29 + * 会话编号
34 30 */
35   - @TableId(type = IdType.INPUT)
36   - private String id;
  31 + private Long id;
  32 + /**
  33 + * 令牌
  34 + */
  35 + private String token;
  36 +
37 37 /**
38 38 * 用户编号
39 39 *
... ...
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;
19 19 *
20 20 * @author 芋道源码
21 21 */
22   -@TableName(value = "system_user", autoResultMap = true)
  22 +@TableName(value = "system_users", autoResultMap = true) // 由于 SQL Server 的 system_user 是关键字,所以使用 system_users
23 23 @KeySequence("system_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
24 24 @Data
25 25 @EqualsAndHashCode(callSuper = true)
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/UserSessionMapper.java
1 1 package cn.iocoder.yudao.module.system.dal.mysql.auth;
2 2  
3   -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageReqVO;
4   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO;
5 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
7   -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageReqVO;
  7 +import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO;
8 8 import org.apache.ibatis.annotations.Mapper;
9 9  
10 10 import java.util.Collection;
... ... @@ -15,13 +15,23 @@ import java.util.List;
15 15 public interface UserSessionMapper extends BaseMapperX<UserSessionDO> {
16 16  
17 17 default PageResult<UserSessionDO> selectPage(UserSessionPageReqVO reqVO, Collection<Long> userIds) {
18   - return selectPage(reqVO, new QueryWrapperX<UserSessionDO>()
19   - .inIfPresent("user_id", userIds)
20   - .likeIfPresent("user_ip", reqVO.getUserIp()));
  18 + return selectPage(reqVO, new LambdaQueryWrapperX<UserSessionDO>()
  19 + .inIfPresent(UserSessionDO::getUserId, userIds)
  20 + .likeIfPresent(UserSessionDO::getUserIp, reqVO.getUserIp()));
21 21 }
22 22  
23 23 default List<UserSessionDO> selectListBySessionTimoutLt() {
24   - return selectList(new QueryWrapperX<UserSessionDO>().lt("session_timeout",new Date()));
  24 + return selectList(new LambdaQueryWrapperX<UserSessionDO>()
  25 + .lt(UserSessionDO::getSessionTimeout, new Date()));
  26 + }
  27 +
  28 + default void updateByToken(String token, UserSessionDO updateObj) {
  29 + update(updateObj, new LambdaQueryWrapperX<UserSessionDO>()
  30 + .eq(UserSessionDO::getToken, token));
  31 + }
  32 +
  33 + default void deleteByToken(String token) {
  34 + delete(new LambdaQueryWrapperX<UserSessionDO>().eq(UserSessionDO::getToken, token));
25 35 }
26 36  
27 37 }
... ...
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;
2 2  
3 3 import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
4 4 import cn.iocoder.yudao.framework.security.core.LoginUser;
5   -import me.zhyd.oauth.model.AuthUser;
6 5  
7 6 import java.time.Duration;
8 7  
... ... @@ -20,7 +19,7 @@ public interface RedisKeyConstants {
20 19 STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
21 20  
22 21 RedisKeyDefine LOGIN_USER = new RedisKeyDefine("登录用户的缓存",
23   - "login_user:%s", // 参数为 sessionId
  22 + "login_user:%s", // 参数为 token 令牌
24 23 STRING, LoginUser.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
25 24  
26 25 RedisKeyDefine SOCIAL_AUTH_STATE = new RedisKeyDefine("社交登陆的 state", // 注意,它是被 JustAuth 的 justauth.type.prefix 使用到
... ...
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 {
24 24 @Resource
25 25 private SecurityProperties securityProperties;
26 26  
27   - public LoginUser get(String sessionId) {
28   - String redisKey = formatKey(sessionId);
  27 + public LoginUser get(String token) {
  28 + String redisKey = formatKey(token);
29 29 return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), LoginUser.class);
30 30 }
31 31  
32   - public void set(String sessionId, LoginUser loginUser) {
33   - String redisKey = formatKey(sessionId);
  32 + public Boolean exists(String token) {
  33 + String redisKey = formatKey(token);
  34 + return stringRedisTemplate.hasKey(redisKey);
  35 + }
  36 +
  37 + public void set(String token, LoginUser loginUser) {
  38 + String redisKey = formatKey(token);
34 39 stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(loginUser),
35 40 securityProperties.getSessionTimeout());
36 41 }
37 42  
38   - public void delete(String sessionId) {
39   - String redisKey = formatKey(sessionId);
  43 + public void delete(String token) {
  44 + String redisKey = formatKey(token);
40 45 stringRedisTemplate.delete(redisKey);
41 46 }
42 47  
43   - private static String formatKey(String sessionId) {
44   - return LOGIN_USER.formatKey(sessionId);
  48 + private static String formatKey(String token) {
  49 + return LOGIN_USER.formatKey(token);
45 50 }
46 51  
47 52 }
... ...
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 {
24 24 @Override
25 25 public String execute(String param) throws Exception {
26 26 // 执行过期
27   - Long timeoutCount = userSessionService.clearSessionTimeout();
  27 + Long timeoutCount = userSessionService.deleteTimeoutSession();
28 28 // 返回结果,记录每次的超时数量
29 29 return String.format("移除在线会话数量为 %s 个", timeoutCount);
30 30 }
... ...
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 {
104 104 // 使用账号密码,进行登录
105 105 LoginUser loginUser = login0(reqVO.getUsername(), reqVO.getPassword());
106 106  
107   - // 缓存登陆用户到 Redis 中,返回 sessionId 编号
  107 + // 缓存登陆用户到 Redis 中,返回 Token 令牌
108 108 return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_USERNAME, userIp, userAgent);
109 109 }
110 110  
... ... @@ -207,7 +207,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
207 207 // 创建 LoginUser 对象
208 208 LoginUser loginUser = buildLoginUser(user);
209 209  
210   - // 缓存登录用户到 Redis 中,返回 sessionId 编号
  210 + // 缓存登录用户到 Redis 中,返回 Token 令牌
211 211 return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_SOCIAL, userIp, userAgent);
212 212 }
213 213  
... ... @@ -219,14 +219,14 @@ public class AdminAuthServiceImpl implements AdminAuthService {
219 219 // 绑定社交用户
220 220 socialUserService.bindSocialUser(AuthConvert.INSTANCE.convert(loginUser.getId(), getUserType().getValue(), reqVO));
221 221  
222   - // 缓存登录用户到 Redis 中,返回 sessionId 编号
  222 + // 缓存登录用户到 Redis 中,返回 Token 令牌
223 223 return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_SOCIAL, userIp, userAgent);
224 224 }
225 225  
226 226 private String createUserSessionAfterLoginSuccess(LoginUser loginUser, LoginLogTypeEnum logType, String userIp, String userAgent) {
227 227 // 插入登陆日志
228 228 createLoginLog(loginUser.getUsername(), logType, LoginResultEnum.SUCCESS);
229   - // 缓存登录用户到 Redis 中,返回 sessionId 编号
  229 + // 缓存登录用户到 Redis 中,返回 Token 令牌
230 230 return userSessionService.createUserSession(loginUser, userIp, userAgent);
231 231 }
232 232  
... ... @@ -240,7 +240,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
240 240 // 删除 session
241 241 userSessionService.deleteUserSession(token);
242 242 // 记录登出日志
243   - this.createLogoutLog(loginUser.getId(), loginUser.getUsername());
  243 + createLogoutLog(loginUser.getId(), loginUser.getUsername());
244 244 }
245 245  
246 246 @Override
... ...
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 {
25 25 *
26 26 * @return {@link Long } 移出的超时用户数量
27 27 **/
28   - long clearSessionTimeout();
  28 + long deleteTimeoutSession();
29 29  
30 30 /**
31 31 * 创建在线用户 Session
... ... @@ -33,32 +33,39 @@ public interface UserSessionService {
33 33 * @param loginUser 登录用户
34 34 * @param userIp 用户 IP
35 35 * @param userAgent 用户 UA
36   - * @return Session 编号
  36 + * @return Token 令牌
37 37 */
38 38 String createUserSession(LoginUser loginUser, String userIp, String userAgent);
39 39  
40 40 /**
41 41 * 刷新在线用户 Session 的更新时间
42 42 *
43   - * @param sessionId Session 编号
  43 + * @param token 令牌
44 44 * @param loginUser 登录用户
45 45 */
46   - void refreshUserSession(String sessionId, LoginUser loginUser);
  46 + void refreshUserSession(String token, LoginUser loginUser);
47 47  
48 48 /**
49 49 * 删除在线用户 Session
50 50 *
51   - * @param sessionId Session 编号
  51 + * @param token token 令牌
52 52 */
53   - void deleteUserSession(String sessionId);
  53 + void deleteUserSession(String token);
54 54  
55 55 /**
56   - * 获得 Session 编号对应的在线用户
  56 + * 删除在线用户 Session
  57 + *
  58 + * @param id 编号
  59 + */
  60 + void deleteUserSession(Long id);
  61 +
  62 + /**
  63 + * 获得 Token 对应的在线用户
57 64 *
58   - * @param sessionId Session 编号
  65 + * @param token 令牌
59 66 * @return 在线用户
60 67 */
61   - LoginUser getLoginUser(String sessionId);
  68 + LoginUser getLoginUser(String token);
62 69  
63 70 /**
64 71 * 获得 Session 超时时间,单位:毫秒
... ...
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;
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.IdUtil;
5 5 import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  7 +import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
6 8 import cn.iocoder.yudao.framework.security.config.SecurityProperties;
7 9 import cn.iocoder.yudao.framework.security.core.LoginUser;
  10 +import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
8 11 import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageReqVO;
9 12 import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO;
10 13 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
11 14 import cn.iocoder.yudao.module.system.dal.mysql.auth.UserSessionMapper;
  15 +import cn.iocoder.yudao.module.system.dal.redis.auth.LoginUserRedisDAO;
12 16 import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
13 17 import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
14 18 import cn.iocoder.yudao.module.system.service.logger.LoginLogService;
15 19 import cn.iocoder.yudao.module.system.service.user.AdminUserService;
16   -import cn.iocoder.yudao.module.system.dal.redis.auth.LoginUserRedisDAO;
17   -import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
18   -import cn.iocoder.yudao.framework.common.pojo.PageResult;
19   -import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
20   -import com.google.common.collect.Lists;
21 20 import lombok.extern.slf4j.Slf4j;
22 21 import org.springframework.stereotype.Service;
23 22  
24 23 import javax.annotation.Resource;
25 24 import java.time.Duration;
26   -import java.util.*;
27   -import java.util.stream.Collectors;
  25 +import java.util.Collection;
  26 +import java.util.Date;
  27 +import java.util.List;
28 28  
29 29 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
30 30 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime;
... ... @@ -65,82 +65,99 @@ public class UserSessionServiceImpl implements UserSessionService {
65 65 return userSessionMapper.selectPage(reqVO, userIds);
66 66 }
67 67  
68   - // TODO @芋艿:优化下该方法
69 68 @Override
70   - public long clearSessionTimeout() {
71   - // 获取db里已经超时的用户列表
72   - List<UserSessionDO> sessionTimeoutDOS = userSessionMapper.selectListBySessionTimoutLt();
73   - Map<String, UserSessionDO> timeoutSessionDOMap = sessionTimeoutDOS
74   - .stream()
75   - .filter(sessionDO -> loginUserRedisDAO.get(sessionDO.getId()) == null)
76   - .collect(Collectors.toMap(UserSessionDO::getId, o -> o));
77   - // 确认已经超时,按批次移出在线用户列表
78   - if (CollUtil.isNotEmpty(timeoutSessionDOMap)) {
79   - Lists.partition(new ArrayList<>(timeoutSessionDOMap.keySet()), 100)
80   - .forEach(userSessionMapper::deleteBatchIds);
81   - // 记录用户超时退出日志
82   - createTimeoutLogoutLog(timeoutSessionDOMap.values());
  69 + public long deleteTimeoutSession() {
  70 + // 获取 db 里已经超时的用户列表
  71 + List<UserSessionDO> timeoutSessions = userSessionMapper.selectListBySessionTimoutLt();
  72 + if (CollUtil.isEmpty(timeoutSessions)) {
  73 + return 0L;
83 74 }
84   - return timeoutSessionDOMap.size();
85   - }
86 75  
87   - private void createTimeoutLogoutLog(Collection<UserSessionDO> timeoutSessionDOS) {
88   - for (UserSessionDO timeoutSessionDO : timeoutSessionDOS) {
89   - LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
90   - reqDTO.setLogType(LoginLogTypeEnum.LOGOUT_TIMEOUT.getType());
91   - reqDTO.setTraceId(TracerUtils.getTraceId());
92   - reqDTO.setUserId(timeoutSessionDO.getUserId());
93   - reqDTO.setUserType(timeoutSessionDO.getUserType());
94   - reqDTO.setUsername(timeoutSessionDO.getUsername());
95   - reqDTO.setUserAgent(timeoutSessionDO.getUserAgent());
96   - reqDTO.setUserIp(timeoutSessionDO.getUserIp());
97   - reqDTO.setResult(LoginResultEnum.SUCCESS.getResult());
98   - loginLogService.createLoginLog(reqDTO);
  76 + // 由于过期的用户一般不多,所以顺序遍历,进行清理
  77 + int count = 0;
  78 + for (UserSessionDO session : timeoutSessions) {
  79 + // 基于 Redis 二次判断,同时也保证 Redis Key 的立即过期,避免延迟导致浪费内存空间
  80 + if (loginUserRedisDAO.exists(session.getToken())) {
  81 + continue;
  82 + }
  83 + userSessionMapper.deleteById(session.getId());
  84 + // 记录退出日志
  85 + createLogoutLog(session, LoginLogTypeEnum.LOGOUT_TIMEOUT);
  86 + count++;
99 87 }
  88 + return count;
  89 + }
  90 +
  91 + private void createLogoutLog(UserSessionDO session, LoginLogTypeEnum type) {
  92 + LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
  93 + reqDTO.setLogType(type.getType());
  94 + reqDTO.setTraceId(TracerUtils.getTraceId());
  95 + reqDTO.setUserId(session.getUserId());
  96 + reqDTO.setUserType(session.getUserType());
  97 + reqDTO.setUsername(session.getUsername());
  98 + reqDTO.setUserAgent(session.getUserAgent());
  99 + reqDTO.setUserIp(session.getUserIp());
  100 + reqDTO.setResult(LoginResultEnum.SUCCESS.getResult());
  101 + loginLogService.createLoginLog(reqDTO);
100 102 }
101 103  
102 104 @Override
103 105 public String createUserSession(LoginUser loginUser, String userIp, String userAgent) {
104 106 // 生成 Session 编号
105   - String sessionId = generateSessionId();
  107 + String token = generateToken();
106 108 // 写入 Redis 缓存
107 109 loginUser.setUpdateTime(new Date());
108   - loginUserRedisDAO.set(sessionId, loginUser);
  110 + loginUserRedisDAO.set(token, loginUser);
109 111 // 写入 DB 中
110   - UserSessionDO userSession = UserSessionDO.builder().id(sessionId)
  112 + UserSessionDO userSession = UserSessionDO.builder().token(token)
111 113 .userId(loginUser.getId()).userType(loginUser.getUserType())
112 114 .userIp(userIp).userAgent(userAgent).username(loginUser.getUsername())
113 115 .sessionTimeout(addTime(Duration.ofMillis(getSessionTimeoutMillis())))
114 116 .build();
115 117 userSessionMapper.insert(userSession);
116   - // 返回 Session 编号
117   - return sessionId;
  118 + // 返回 Token 令牌
  119 + return token;
118 120 }
119 121  
120 122 @Override
121   - public void refreshUserSession(String sessionId, LoginUser loginUser) {
  123 + public void refreshUserSession(String token, LoginUser loginUser) {
122 124 // 写入 Redis 缓存
123 125 loginUser.setUpdateTime(new Date());
124   - loginUserRedisDAO.set(sessionId, loginUser);
  126 + loginUserRedisDAO.set(token, loginUser);
125 127 // 更新 DB 中
126   - UserSessionDO updateObj = UserSessionDO.builder().id(sessionId).build();
  128 + UserSessionDO updateObj = UserSessionDO.builder().build();
127 129 updateObj.setUsername(loginUser.getUsername());
128 130 updateObj.setUpdateTime(new Date());
129 131 updateObj.setSessionTimeout(addTime(Duration.ofMillis(getSessionTimeoutMillis())));
130   - userSessionMapper.updateById(updateObj);
  132 + userSessionMapper.updateByToken(token, updateObj);
131 133 }
132 134  
133 135 @Override
134   - public void deleteUserSession(String sessionId) {
  136 + public void deleteUserSession(String token) {
  137 + // 删除 Redis 缓存
  138 + loginUserRedisDAO.delete(token);
  139 + // 删除 DB 记录
  140 + userSessionMapper.deleteByToken(token);
  141 + // 无需记录日志,因为退出那已经记录
  142 + }
  143 +
  144 + @Override
  145 + public void deleteUserSession(Long id) {
  146 + UserSessionDO session = userSessionMapper.selectById(id);
  147 + if (session == null) {
  148 + return;
  149 + }
135 150 // 删除 Redis 缓存
136   - loginUserRedisDAO.delete(sessionId);
  151 + loginUserRedisDAO.delete(session.getToken());
137 152 // 删除 DB 记录
138   - userSessionMapper.deleteById(sessionId);
  153 + userSessionMapper.deleteById(id);
  154 + // 记录退出日志
  155 + createLogoutLog(session, LoginLogTypeEnum.LOGOUT_DELETE);
139 156 }
140 157  
141 158 @Override
142   - public LoginUser getLoginUser(String sessionId) {
143   - return loginUserRedisDAO.get(sessionId);
  159 + public LoginUser getLoginUser(String token) {
  160 + return loginUserRedisDAO.get(token);
144 161 }
145 162  
146 163 @Override
... ... @@ -149,11 +166,11 @@ public class UserSessionServiceImpl implements UserSessionService {
149 166 }
150 167  
151 168 /**
152   - * 生成 Session 编号,目前采用 UUID 算法
  169 + * 生成 Token 令牌,目前采用 UUID 算法
153 170 *
154 171 * @return Session 编号
155 172 */
156   - private static String generateSessionId() {
  173 + private static String generateToken() {
157 174 return IdUtil.fastSimpleUUID();
158 175 }
159 176  
... ...
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 {
134 134 AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
135 135 String userIp = randomString();
136 136 String userAgent = randomString();
  137 +
137 138 // 调用, 并断言异常
138 139 assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_CAPTCHA_NOT_FOUND);
139 140 // 校验调用参数
... ... @@ -148,10 +149,12 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
148 149 // 准备参数
149 150 String userIp = randomString();
150 151 String userAgent = randomString();
151   - String code = randomString();
152 152 AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
  153 +
153 154 // mock 验证码不正确
  155 + String code = randomString();
154 156 when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(code);
  157 +
155 158 // 调用, 并断言异常
156 159 assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_CAPTCHA_CODE_ERROR);
157 160 // 校验调用参数
... ... @@ -172,6 +175,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
172 175 // mock 抛出异常
173 176 when(authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(reqVO.getUsername(), reqVO.getPassword())))
174 177 .thenThrow(new BadCredentialsException("测试账号或密码不正确"));
  178 +
175 179 // 调用, 并断言异常
176 180 assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_BAD_CREDENTIALS);
177 181 // 校验调用参数
... ... @@ -188,11 +192,13 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
188 192 String userIp = randomString();
189 193 String userAgent = randomString();
190 194 AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
  195 +
191 196 // mock 验证码正确
192 197 when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode());
193 198 // mock 抛出异常
194 199 when(authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(reqVO.getUsername(), reqVO.getPassword())))
195 200 .thenThrow(new DisabledException("测试用户被禁用"));
  201 +
196 202 // 调用, 并断言异常
197 203 assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_USER_DISABLED);
198 204 // 校验调用参数
... ... @@ -214,6 +220,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
214 220 // mock 抛出异常
215 221 when(authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(reqVO.getUsername(), reqVO.getPassword())))
216 222 .thenThrow(new AuthenticationException("测试未知异常") {});
  223 +
217 224 // 调用, 并断言异常
218 225 assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_FAIL_UNKNOWN);
219 226 // 校验调用参数
... ... @@ -229,27 +236,29 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
229 236 // 准备参数
230 237 String userIp = randomString();
231 238 String userAgent = randomString();
  239 + AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
  240 +
  241 + // mock 验证码正确
  242 + when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode());
  243 + // mock authentication
232 244 Long userId = randomLongId();
233 245 Set<Long> userRoleIds = randomSet(Long.class);
234   - String sessionId = randomString();
235   - AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
236 246 LoginUser loginUser = randomPojo(LoginUser.class, o -> {
237 247 o.setId(userId);
238 248 o.setRoleIds(userRoleIds);
239 249 });
240   - // mock 验证码正确
241   - when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode());
242   - // mock authentication
243 250 when(authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(reqVO.getUsername(), reqVO.getPassword())))
244 251 .thenReturn(authentication);
245 252 when(authentication.getPrincipal()).thenReturn(loginUser);
246 253 // mock 获得 User 拥有的角色编号数组
247 254 when(permissionService.getUserRoleIds(userId, singleton(CommonStatusEnum.ENABLE.getStatus()))).thenReturn(userRoleIds);
248 255 // mock 缓存登录用户到 Redis
249   - when(userSessionService.createUserSession(loginUser, userIp, userAgent)).thenReturn(sessionId);
  256 + String token = randomString();
  257 + when(userSessionService.createUserSession(loginUser, userIp, userAgent)).thenReturn(token);
  258 +
250 259 // 调用, 并断言异常
251 260 String login = authService.login(reqVO, userIp, userAgent);
252   - assertEquals(sessionId, login);
  261 + assertEquals(token, login);
253 262 // 校验调用参数
254 263 verify(captchaService, times(1)).deleteCaptchaCode(reqVO.getUuid());
255 264 verify(loginLogService, times(1)).createLoginLog(
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/UserSessionServiceImplTest.java
1 1 package cn.iocoder.yudao.module.system.service.auth;
2 2  
3   -import cn.hutool.core.date.DateUtil;
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.date.DateUtils;
  7 +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
4 8 import cn.iocoder.yudao.framework.security.config.SecurityProperties;
5 9 import cn.iocoder.yudao.framework.security.core.LoginUser;
  10 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest;
6 11 import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageReqVO;
  12 +import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO;
7 13 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
8 14 import cn.iocoder.yudao.module.system.dal.mysql.auth.UserSessionMapper;
9   -import cn.iocoder.yudao.module.system.service.logger.LoginLogService;
10   -import cn.iocoder.yudao.module.system.service.user.AdminUserService;
11   -import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO;
12 15 import cn.iocoder.yudao.module.system.dal.redis.auth.LoginUserRedisDAO;
13 16 import cn.iocoder.yudao.module.system.enums.common.SexEnum;
14   -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
15   -import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
16   -import cn.iocoder.yudao.framework.common.pojo.PageResult;
17   -import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
18   -import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest;
  17 +import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
  18 +import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
  19 +import cn.iocoder.yudao.module.system.service.logger.LoginLogService;
  20 +import cn.iocoder.yudao.module.system.service.user.AdminUserService;
  21 +import org.junit.jupiter.api.BeforeEach;
19 22 import org.junit.jupiter.api.Test;
20 23 import org.springframework.boot.test.mock.mockito.MockBean;
21 24 import org.springframework.context.annotation.Import;
22 25  
23 26 import javax.annotation.Resource;
24 27 import java.time.Duration;
25   -import java.util.Date;
26   -import java.util.List;
27   -import java.util.stream.Collectors;
28   -import java.util.stream.Stream;
  28 +import java.util.Calendar;
29 29  
30 30 import static cn.hutool.core.util.RandomUtil.randomEle;
31 31 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
33 33 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
34 34 import static java.util.Collections.singletonList;
35 35 import static org.junit.jupiter.api.Assertions.*;
36   -import static org.junit.jupiter.api.Assertions.assertNull;
  36 +import static org.mockito.ArgumentMatchers.argThat;
37 37 import static org.mockito.ArgumentMatchers.eq;
  38 +import static org.mockito.Mockito.verify;
38 39 import static org.mockito.Mockito.when;
39 40  
40 41 /**
... ... @@ -61,6 +62,11 @@ public class UserSessionServiceImplTest extends BaseDbAndRedisUnitTest {
61 62 @MockBean
62 63 private SecurityProperties securityProperties;
63 64  
  65 + @BeforeEach
  66 + public void setUp() {
  67 + when(securityProperties.getSessionTimeout()).thenReturn(Duration.ofDays(1L));
  68 + }
  69 +
64 70 @Test
65 71 public void testGetUserSessionPage_success() {
66 72 // mock 数据
... ... @@ -78,15 +84,9 @@ public class UserSessionServiceImplTest extends BaseDbAndRedisUnitTest {
78 84 });
79 85 userSessionMapper.insert(dbSession);
80 86 // 测试 username 不匹配
81   - userSessionMapper.insert(ObjectUtils.cloneIgnoreId(dbSession, o -> {
82   - o.setId(randomString());
83   - o.setUserId(123456L);
84   - }));
  87 + userSessionMapper.insert(ObjectUtils.cloneIgnoreId(dbSession, o -> o.setUserId(123456L)));
85 88 // 测试 userIp 不匹配
86   - userSessionMapper.insert(ObjectUtils.cloneIgnoreId(dbSession, o -> {
87   - o.setId(randomString());
88   - o.setUserIp("testUserIp");
89   - }));
  89 + userSessionMapper.insert(ObjectUtils.cloneIgnoreId(dbSession, o -> o.setUserIp("testUserIp")));
90 90 // 准备参数
91 91 UserSessionPageReqVO reqVO = new UserSessionPageReqVO();
92 92 reqVO.setUsername(dbUser.getUsername());
... ... @@ -100,35 +100,60 @@ public class UserSessionServiceImplTest extends BaseDbAndRedisUnitTest {
100 100 assertPojoEquals(dbSession, pageResult.getList().get(0));
101 101 }
102 102  
103   - // TODO 芋艿:单测写的有问题
  103 + @Test
  104 + public void testClearSessionTimeout_none() {
  105 + // mock db 数据
  106 + UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> {
  107 + o.setUserType(randomEle(UserTypeEnum.values()).getValue());
  108 + o.setSessionTimeout(addTime(Duration.ofDays(1)));
  109 + });
  110 + userSessionMapper.insert(userSession);
  111 +
  112 + // 调用
  113 + long count = userSessionService.deleteTimeoutSession();
  114 + // 断言
  115 + assertEquals(0, count);
  116 + assertPojoEquals(userSession, userSessionMapper.selectById(userSession.getId())); // 未删除
  117 + }
  118 +
  119 + @Test // Redis 还存在的情况
  120 + public void testClearSessionTimeout_exists() {
  121 + // mock db 数据
  122 + UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> {
  123 + o.setUserType(randomEle(UserTypeEnum.values()).getValue());
  124 + o.setSessionTimeout(DateUtils.addDate(Calendar.DAY_OF_YEAR, -1));
  125 + });
  126 + userSessionMapper.insert(userSession);
  127 + // mock redis 数据
  128 + loginUserRedisDAO.set(userSession.getToken(), new LoginUser());
  129 +
  130 + // 调用
  131 + long count = userSessionService.deleteTimeoutSession();
  132 + // 断言
  133 + assertEquals(0, count);
  134 + assertPojoEquals(userSession, userSessionMapper.selectById(userSession.getId())); // 未删除
  135 + }
  136 +
104 137 @Test
105 138 public void testClearSessionTimeout_success() {
106   - // 准备超时数据 120 条, 在线用户 1 条
107   - int expectedTimeoutCount = 120, expectedTotal = 1;
108   -
109   - // 准备数据
110   - List<UserSessionDO> prepareData = Stream
111   - .iterate(0, i -> i)
112   - .limit(expectedTimeoutCount)
113   - .map(i -> randomPojo(UserSessionDO.class, o -> {
114   - o.setUserType(randomEle(UserTypeEnum.values()).getValue());
115   - o.setSessionTimeout(DateUtil.offsetSecond(new Date(), -1));
116   - }))
117   - .collect(Collectors.toList());
118   - UserSessionDO sessionDO = randomPojo(UserSessionDO.class, o -> {
  139 + // mock db 数据
  140 + UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> {
119 141 o.setUserType(randomEle(UserTypeEnum.values()).getValue());
120   - o.setSessionTimeout(DateUtil.offsetMinute(new Date(), 30));
  142 + o.setSessionTimeout(DateUtils.addDate(Calendar.DAY_OF_YEAR, -1));
121 143 });
122   - prepareData.add(sessionDO);
123   - prepareData.forEach(userSessionMapper::insert);
  144 + userSessionMapper.insert(userSession);
124 145  
125 146 // 清空超时数据
126   - long actualTimeoutCount = userSessionService.clearSessionTimeout();
  147 + long count = userSessionService.deleteTimeoutSession();
127 148 // 校验
128   - assertEquals(expectedTimeoutCount, actualTimeoutCount);
129   - List<UserSessionDO> userSessionDOS = userSessionMapper.selectList();
130   - assertEquals(expectedTotal, userSessionDOS.size());
131   - assertPojoEquals(sessionDO, userSessionDOS.get(0), "updateTime");
  149 + assertEquals(1, count);
  150 + assertNull(userSessionMapper.selectById(userSession.getId())); // 已删除
  151 + verify(loginLogService).createLoginLog(argThat(loginLog -> {
  152 + assertPojoEquals(userSession, loginLog);
  153 + assertEquals(LoginLogTypeEnum.LOGOUT_TIMEOUT.getType(), loginLog.getLogType());
  154 + assertEquals(LoginResultEnum.SUCCESS.getResult(), loginLog.getResult());
  155 + return true;
  156 + }));
132 157 }
133 158  
134 159 @Test
... ... @@ -140,80 +165,86 @@ public class UserSessionServiceImplTest extends BaseDbAndRedisUnitTest {
140 165 o.setUserType(randomEle(UserTypeEnum.values()).getValue());
141 166 o.setTenantId(0L); // 租户设置为 0,因为暂未启用多租户组件
142 167 });
143   - // mock 方法
144   - when(securityProperties.getSessionTimeout()).thenReturn(Duration.ofDays(1));
145 168  
146 169 // 调用
147   - String sessionId = userSessionService.createUserSession(loginUser, userIp, userAgent);
  170 + String token = userSessionService.createUserSession(loginUser, userIp, userAgent);
148 171 // 校验 UserSessionDO 记录
149   - UserSessionDO userSessionDO = userSessionMapper.selectById(sessionId);
  172 + UserSessionDO userSessionDO = userSessionMapper.selectOne(UserSessionDO::getToken, token);
150 173 assertPojoEquals(loginUser, userSessionDO, "id", "updateTime");
151   - assertEquals(sessionId, userSessionDO.getId());
  174 + assertEquals(token, userSessionDO.getToken());
152 175 assertEquals(userIp, userSessionDO.getUserIp());
153 176 assertEquals(userAgent, userSessionDO.getUserAgent());
154 177 // 校验 LoginUser 缓存
155   - LoginUser redisLoginUser = loginUserRedisDAO.get(sessionId);
  178 + LoginUser redisLoginUser = loginUserRedisDAO.get(token);
156 179 assertPojoEquals(loginUser, redisLoginUser, "username", "password");
157 180 }
158 181  
159 182 @Test
160   - public void testCreateRefreshUserSession_success() {
  183 + public void testCreateRefreshUserSession() {
161 184 // 准备参数
162   - String sessionId = randomString();
163   - String userIp = randomString();
164   - String userAgent = randomString();
165   - long timeLong = randomLongId();
166   - String userName = randomString();
167   - Date date = randomDate();
  185 + String token = randomString();
  186 +
  187 + // mock redis 数据
168 188 LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setUserType(randomEle(UserTypeEnum.values()).getValue()));
169   - // mock 方法
170   - when(securityProperties.getSessionTimeout()).thenReturn(Duration.ofDays(1));
171   - // mock 数据
172   - loginUser.setUpdateTime(date);
173   - loginUserRedisDAO.set(sessionId, loginUser);
174   - UserSessionDO userSession = UserSessionDO.builder().id(sessionId)
175   - .userId(loginUser.getId()).userType(loginUser.getUserType())
176   - .userIp(userIp).userAgent(userAgent).username(userName)
177   - .sessionTimeout(addTime(Duration.ofMillis(timeLong)))
178   - .build();
  189 + loginUserRedisDAO.set(token, loginUser);
  190 + // mock db 数据
  191 + UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> {
  192 + o.setUserType(randomEle(UserTypeEnum.values()).getValue());
  193 + o.setToken(token);
  194 + });
179 195 userSessionMapper.insert(userSession);
180 196  
181 197 // 调用
182   - userSessionService.refreshUserSession(sessionId, loginUser);
  198 + userSessionService.refreshUserSession(token, loginUser);
183 199 // 校验 LoginUser 缓存
184   - LoginUser redisLoginUser = loginUserRedisDAO.get(sessionId);
185   - assertNotEquals(redisLoginUser.getUpdateTime(), date);
  200 + LoginUser redisLoginUser = loginUserRedisDAO.get(token);
  201 + assertPojoEquals(redisLoginUser, loginUser, "username", "password");
186 202 // 校验 UserSessionDO 记录
187   - UserSessionDO updateDO = userSessionMapper.selectById(sessionId);
  203 + UserSessionDO updateDO = userSessionMapper.selectOne(UserSessionDO::getToken, token);
188 204 assertEquals(updateDO.getUsername(), loginUser.getUsername());
189   - assertNotEquals(updateDO.getUpdateTime(), userSession.getUpdateTime());
190   - assertNotEquals(updateDO.getSessionTimeout(), addTime(Duration.ofMillis(timeLong)));
  205 + assertNotNull(userSession.getUpdateTime());
  206 + assertNotNull(userSession.getSessionTimeout());
191 207 }
192 208  
193 209 @Test
194   - public void testDeleteUserSession_success() {
  210 + public void testDeleteUserSession_Token() {
195 211 // 准备参数
196   - String sessionId = randomString();
197   - String userIp = randomString();
198   - String userAgent = randomString();
199   - Long timeLong = randomLongId();
200   - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setUserType(randomEle(UserTypeEnum.values()).getValue()));
201   - // mock 存入 Redis
202   - when(securityProperties.getSessionTimeout()).thenReturn(Duration.ofDays(1));
203   - // mock 数据
204   - loginUserRedisDAO.set(sessionId, loginUser);
205   - UserSessionDO userSession = UserSessionDO.builder().id(sessionId)
206   - .userId(loginUser.getId()).userType(loginUser.getUserType())
207   - .userIp(userIp).userAgent(userAgent).username(loginUser.getUsername())
208   - .sessionTimeout(addTime(Duration.ofMillis(timeLong)))
209   - .build();
  212 + String token = randomString();
  213 +
  214 + // mock redis 数据
  215 + loginUserRedisDAO.set(token, new LoginUser());
  216 + // mock db 数据
  217 + UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> {
  218 + o.setUserType(randomEle(UserTypeEnum.values()).getValue());
  219 + o.setToken(token);
  220 + });
210 221 userSessionMapper.insert(userSession);
211 222  
212 223 // 调用
213   - userSessionService.deleteUserSession(sessionId);
  224 + userSessionService.deleteUserSession(token);
  225 + // 校验数据不存在了
  226 + assertNull(loginUserRedisDAO.get(token));
  227 + assertNull(userSessionMapper.selectOne(UserSessionDO::getToken, token));
  228 + }
  229 +
  230 + @Test
  231 + public void testDeleteUserSession_Id() {
  232 + // mock db 数据
  233 + UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> {
  234 + o.setUserType(randomEle(UserTypeEnum.values()).getValue());
  235 + });
  236 + userSessionMapper.insert(userSession);
  237 + // mock redis 数据
  238 + loginUserRedisDAO.set(userSession.getToken(), new LoginUser());
  239 +
  240 + // 准备参数
  241 + Long id = userSession.getId();
  242 +
  243 + // 调用
  244 + userSessionService.deleteUserSession(id);
214 245 // 校验数据不存在了
215   - assertNull(loginUserRedisDAO.get(sessionId));
216   - assertNull(userSessionMapper.selectById(sessionId));
  246 + assertNull(loginUserRedisDAO.get(userSession.getToken()));
  247 + assertNull(userSessionMapper.selectById(id));
217 248 }
218 249  
219 250 }
... ...
yudao-module-system/yudao-module-system-biz/src/test/resources/sql/create_tables.sql
... ... @@ -115,7 +115,8 @@ CREATE TABLE IF NOT EXISTS &quot;system_dict_type&quot; (
115 115 ) COMMENT '字典类型表';
116 116  
117 117 CREATE TABLE IF NOT EXISTS `system_user_session` (
118   - `id` varchar(32) NOT NULL,
  118 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  119 + `token` varchar(32) NOT NULL,
119 120 `user_id` bigint DEFAULT NULL,
120 121 "user_type" tinyint NOT NULL,
121 122 `username` varchar(50) NOT NULL DEFAULT '',
... ...
yudao-server/pom.xml
... ... @@ -21,7 +21,6 @@
21 21 <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
22 22  
23 23 <dependencies>
24   - <!-- TODO 芋艿:多模块 -->
25 24 <dependency>
26 25 <groupId>cn.iocoder.boot</groupId>
27 26 <artifactId>yudao-module-member-biz</artifactId>
... ... @@ -43,11 +42,11 @@
43 42 <version>${revision}</version>
44 43 </dependency>
45 44 <!-- 默认引入 yudao-module-bpm-biz-flowable 实现,可以替换为 yudao-module-bpm-biz-activiti 实现-->
46   - <dependency>
47   - <groupId>cn.iocoder.boot</groupId>
48   - <artifactId>yudao-module-bpm-biz-flowable</artifactId>
49   - <version>${revision}</version>
50   - </dependency>
  45 +<!-- <dependency>-->
  46 +<!-- <groupId>cn.iocoder.boot</groupId>-->
  47 +<!-- <artifactId>yudao-module-bpm-biz-flowable</artifactId>-->
  48 +<!-- <version>${revision}</version>-->
  49 +<!-- </dependency>-->
51 50 <!-- <dependency>-->
52 51 <!-- <groupId>cn.iocoder.boot</groupId>-->
53 52 <!-- <artifactId>yudao-module-bpm-biz-activiti</artifactId>-->
... ...
yudao-server/src/main/resources/application-local.yaml
... ... @@ -49,6 +49,9 @@ spring:
49 49 # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
50 50 username: root
51 51 password: 123456
  52 +# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.master.name} # SQLServer 连接的示例
  53 +# username: sa
  54 +# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W
52 55 slave: # 模拟从库,可根据自己需要修改
53 56 name: ruoyi-vue-pro
54 57 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:
56 59 # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
57 60 username: root
58 61 password: 123456
  62 +# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.slave.name} # SQLServer 连接的示例
  63 +# username: sa
  64 +# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W
59 65  
60 66 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
61 67 redis:
... ...