mirror of
				https://github.com/YunaiV/ruoyi-vue-pro.git
				synced 2025-10-30 10:05:59 +08:00 
			
		
		
		
	实现管理后台登出时,删除 oauth 令牌
This commit is contained in:
		| @ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.security.core.filter; | ||||
|  | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import cn.iocoder.yudao.framework.common.exception.ServiceException; | ||||
| import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||||
| import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; | ||||
| import cn.iocoder.yudao.framework.security.config.SecurityProperties; | ||||
| @ -44,23 +45,14 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { | ||||
|         if (StrUtil.isNotEmpty(token)) { | ||||
|             Integer userType = WebFrameworkUtils.getLoginUserType(request); | ||||
|             try { | ||||
|                 // 验证 token 有效性 | ||||
|                 OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token); | ||||
|                 if (accessToken != null && ObjectUtil.notEqual(accessToken.getUserType(), userType)) { // 用户类型不匹配,无权限 | ||||
|                     throw new AccessDeniedException("错误的用户类型"); | ||||
|                 } | ||||
|                 LoginUser loginUser = null; | ||||
|                 if (accessToken != null) { // 如果不为空,说明认证通过,则转换成登录用户 | ||||
|                     loginUser = new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) | ||||
|                             .setTenantId(accessToken.getTenantId()); | ||||
|                 } | ||||
|  | ||||
|                 // 模拟 Login 功能,方便日常开发调试 | ||||
|                 // 1.1 基于 token 构建登录用户 | ||||
|                 LoginUser loginUser = buildLoginUserByToken(token, userType); | ||||
|                 // 1.2 模拟 Login 功能,方便日常开发调试 | ||||
|                 if (loginUser == null) { | ||||
|                     loginUser = mockLoginUser(request, token, userType); | ||||
|                 } | ||||
|  | ||||
|                 // 设置当前用户 | ||||
|                 // 2. 设置当前用户 | ||||
|                 if (loginUser != null) { | ||||
|                     SecurityFrameworkUtils.setLoginUser(loginUser, request); | ||||
|                 } | ||||
| @ -75,6 +67,25 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { | ||||
|         chain.doFilter(request, response); | ||||
|     } | ||||
|  | ||||
|     private LoginUser buildLoginUserByToken(String token, Integer userType) { | ||||
|         try { | ||||
|             OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token); | ||||
|             if (accessToken == null) { | ||||
|                 return null; | ||||
|             } | ||||
|             // 用户类型不匹配,无权限 | ||||
|             if (ObjectUtil.notEqual(accessToken.getUserType(), userType)) { | ||||
|                 throw new AccessDeniedException("错误的用户类型"); | ||||
|             } | ||||
|             // 构建登录用户 | ||||
|             return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) | ||||
|                     .setTenantId(accessToken.getTenantId()); | ||||
|         } catch (ServiceException serviceException) { | ||||
|             // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可 | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 模拟登录用户,方便日常开发调试 | ||||
|      * | ||||
|  | ||||
| @ -16,7 +16,6 @@ public enum LoginLogTypeEnum { | ||||
|     LOGIN_SMS(104), // 使用短信登陆 | ||||
|  | ||||
|     LOGOUT_SELF(200),  // 自己主动登出 | ||||
|     LOGOUT_TIMEOUT(201), // 超时登出 | ||||
|     LOGOUT_DELETE(202), // 强制退出 | ||||
|     ; | ||||
|  | ||||
|  | ||||
| @ -33,8 +33,6 @@ import java.util.List; | ||||
| import java.util.Set; | ||||
|  | ||||
| import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | ||||
| import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; | ||||
| import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getUserAgent; | ||||
| import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; | ||||
| import static java.util.Collections.singleton; | ||||
|  | ||||
| @ -63,7 +61,7 @@ public class AuthController { | ||||
|     @ApiOperation("使用账号密码登录") | ||||
|     @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 | ||||
|     public CommonResult<AuthLoginRespVO> login(@RequestBody @Valid AuthLoginReqVO reqVO) { | ||||
|         String token = authService.login(reqVO, getClientIP(), getUserAgent()); | ||||
|         String token = authService.login(reqVO); | ||||
|         return success(AuthLoginRespVO.builder().token(token).build()); | ||||
|     } | ||||
|  | ||||
| @ -116,7 +114,7 @@ public class AuthController { | ||||
|     @ApiOperation("使用短信验证码登录") | ||||
|     @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 | ||||
|     public CommonResult<AuthLoginRespVO> smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) { | ||||
|         String token = authService.smsLogin(reqVO, getClientIP(), getUserAgent()); | ||||
|         String token = authService.smsLogin(reqVO); | ||||
|         // 返回结果 | ||||
|         return success(AuthLoginRespVO.builder().token(token).build()); | ||||
|     } | ||||
| @ -146,7 +144,7 @@ public class AuthController { | ||||
|     @ApiOperation("社交快捷登录,使用 code 授权码") | ||||
|     @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 | ||||
|     public CommonResult<AuthLoginRespVO> socialQuickLogin(@RequestBody @Valid AuthSocialQuickLoginReqVO reqVO) { | ||||
|         String token = authService.socialQuickLogin(reqVO, getClientIP(), getUserAgent()); | ||||
|         String token = authService.socialQuickLogin(reqVO); | ||||
|         return success(AuthLoginRespVO.builder().token(token).build()); | ||||
|     } | ||||
|  | ||||
| @ -154,7 +152,7 @@ public class AuthController { | ||||
|     @ApiOperation("社交绑定登录,使用 code 授权码 + 账号密码") | ||||
|     @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 | ||||
|     public CommonResult<AuthLoginRespVO> socialBindLogin(@RequestBody @Valid AuthSocialBindLoginReqVO reqVO) { | ||||
|         String token = authService.socialBindLogin(reqVO, getClientIP(), getUserAgent()); | ||||
|         String token = authService.socialBindLogin(reqVO); | ||||
|         return success(AuthLoginRespVO.builder().token(token).build()); | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -1,16 +1,16 @@ | ||||
| package cn.iocoder.yudao.module.system.dal.mysql.auth; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | ||||
| import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2RefreshTokenDO; | ||||
| import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | ||||
| import com.baomidou.mybatisplus.core.mapper.BaseMapper; | ||||
| import org.apache.ibatis.annotations.Mapper; | ||||
|  | ||||
| @Mapper | ||||
| public interface OAuth2RefreshTokenMapper extends BaseMapper<OAuth2RefreshTokenDO> { | ||||
|  | ||||
|     default int deleteByUserIdAndUserType(Integer userId, Integer userType) { | ||||
|         return delete(new QueryWrapper<OAuth2RefreshTokenDO>() | ||||
|                 .eq("user_id", userId).eq("user_type", userType)); | ||||
|     default int deleteByRefreshToken(String refreshToken) { | ||||
|         return delete(new LambdaQueryWrapperX<OAuth2RefreshTokenDO>() | ||||
|                 .eq(OAuth2RefreshTokenDO::getRefreshToken, refreshToken)); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -1,32 +0,0 @@ | ||||
| package cn.iocoder.yudao.module.system.job.auth; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; | ||||
| import cn.iocoder.yudao.module.system.service.auth.UserSessionService; | ||||
| import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.springframework.stereotype.Component; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
|  | ||||
| /** | ||||
|  * 用户 Session 超时 Job | ||||
|  * | ||||
|  * @author 願 | ||||
|  */ | ||||
| @Component | ||||
| @TenantJob | ||||
| @Slf4j | ||||
| public class UserSessionTimeoutJob implements JobHandler { | ||||
|  | ||||
|     @Resource | ||||
|     private UserSessionService userSessionService; | ||||
|  | ||||
|     @Override | ||||
|     public String execute(String param) throws Exception { | ||||
|         // 执行过期 | ||||
|         Long timeoutCount = userSessionService.deleteTimeoutSession(); | ||||
|         // 返回结果,记录每次的超时数量 | ||||
|         return String.format("移除在线会话数量为 %s 个", timeoutCount); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -17,11 +17,9 @@ public interface AdminAuthService { | ||||
|      * 账号登录 | ||||
|      * | ||||
|      * @param reqVO 登录信息 | ||||
|      * @param userIp 用户 IP | ||||
|      * @param userAgent 用户 UA | ||||
|      * @return 身份令牌,使用 JWT 方式 | ||||
|      */ | ||||
|     String login(@Valid AuthLoginReqVO reqVO, String userIp, String userAgent); | ||||
|     String login(@Valid AuthLoginReqVO reqVO); | ||||
|  | ||||
|     /** | ||||
|      * 基于 token 退出登录 | ||||
| @ -41,21 +39,17 @@ public interface AdminAuthService { | ||||
|      * 短信登录 | ||||
|      * | ||||
|      * @param reqVO 登录信息 | ||||
|      * @param userIp 用户 IP | ||||
|      * @param userAgent 用户 UA | ||||
|      * @return 身份令牌,使用 JWT 方式 | ||||
|      */ | ||||
|     String smsLogin(AuthSmsLoginReqVO reqVO, String userIp, String userAgent) ; | ||||
|     String smsLogin(AuthSmsLoginReqVO reqVO) ; | ||||
|  | ||||
|     /** | ||||
|      * 社交快捷登录,使用 code 授权码 | ||||
|      * | ||||
|      * @param reqVO 登录信息 | ||||
|      * @param userIp 用户 IP | ||||
|      * @param userAgent 用户 UA | ||||
|      * @return 身份令牌,使用 JWT 方式 | ||||
|      */ | ||||
|     String socialQuickLogin(@Valid AuthSocialQuickLoginReqVO reqVO, String userIp, String userAgent); | ||||
|     String socialQuickLogin(@Valid AuthSocialQuickLoginReqVO reqVO); | ||||
|  | ||||
|     /** | ||||
|      * 社交绑定登录,使用 code 授权码 + 账号密码 | ||||
| @ -65,6 +59,6 @@ public interface AdminAuthService { | ||||
|      * @param userAgent 用户 UA | ||||
|      * @return 身份令牌,使用 JWT 方式 | ||||
|      */ | ||||
|     String socialBindLogin(@Valid AuthSocialBindLoginReqVO reqVO, String userIp, String userAgent); | ||||
|     String socialBindLogin(@Valid AuthSocialBindLoginReqVO reqVO); | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -6,11 +6,11 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | ||||
| import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; | ||||
| import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; | ||||
| import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; | ||||
| import cn.iocoder.yudao.framework.security.core.LoginUser; | ||||
| import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO; | ||||
| import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi; | ||||
| import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.*; | ||||
| import cn.iocoder.yudao.module.system.convert.auth.AuthConvert; | ||||
| import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; | ||||
| import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; | ||||
| import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; | ||||
| import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; | ||||
| @ -47,8 +47,6 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|     @Resource | ||||
|     private LoginLogService loginLogService; | ||||
|     @Resource | ||||
|     private UserSessionService userSessionService; | ||||
|     @Resource | ||||
|     private OAuth2TokenService oauth2TokenService; | ||||
|     @Resource | ||||
|     private SocialUserService socialUserService; | ||||
| @ -60,16 +58,15 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|     private SmsCodeApi smsCodeApi; | ||||
|  | ||||
|     @Override | ||||
|     public String login(AuthLoginReqVO reqVO, String userIp, String userAgent) { | ||||
|     public String login(AuthLoginReqVO reqVO) { | ||||
|         // 判断验证码是否正确 | ||||
|         verifyCaptcha(reqVO); | ||||
|  | ||||
|         // 使用账号密码,进行登录 | ||||
|         LoginUser loginUser = login0(reqVO.getUsername(), reqVO.getPassword()); | ||||
|         AdminUserDO user = login0(reqVO.getUsername(), reqVO.getPassword()); | ||||
|  | ||||
|         // 缓存登陆用户到 Redis 中,返回 Token 令牌 | ||||
|         return createUserSessionAfterLoginSuccess(loginUser, reqVO.getUsername(), | ||||
|                 LoginLogTypeEnum.LOGIN_USERNAME, userIp, userAgent); | ||||
|         // 创建 Token 令牌,记录登录日志 | ||||
|         return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @ -83,9 +80,9 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String smsLogin(AuthSmsLoginReqVO reqVO, String userIp, String userAgent) { | ||||
|     public String smsLogin(AuthSmsLoginReqVO reqVO) { | ||||
|         // 校验验证码 | ||||
|         smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), userIp)); | ||||
|         smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), getClientIP())); | ||||
|  | ||||
|         // 获得用户信息 | ||||
|         AdminUserDO user = userService.getUserByMobile(reqVO.getMobile()); | ||||
| @ -93,12 +90,8 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|             throw exception(USER_NOT_EXISTS); | ||||
|         } | ||||
|  | ||||
|         // 创建 LoginUser 对象 | ||||
|         LoginUser loginUser = buildLoginUser(user); | ||||
|  | ||||
|         // 缓存登陆用户到 Redis 中,返回 sessionId 编号 | ||||
|         return createUserSessionAfterLoginSuccess(loginUser, reqVO.getMobile(), | ||||
|                 LoginLogTypeEnum.LOGIN_MOBILE, userIp, userAgent); | ||||
|         return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE); | ||||
|     } | ||||
|  | ||||
|     @VisibleForTesting | ||||
| @ -128,7 +121,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|     } | ||||
|  | ||||
|     @VisibleForTesting | ||||
|     LoginUser login0(String username, String password) { | ||||
|     AdminUserDO login0(String username, String password) { | ||||
|         final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; | ||||
|         // 校验账号是否存在 | ||||
|         AdminUserDO user = userService.getUserByUsername(username); | ||||
| @ -145,9 +138,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|             createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED); | ||||
|             throw exception(AUTH_LOGIN_USER_DISABLED); | ||||
|         } | ||||
|  | ||||
|         // 构建 User 对象 | ||||
|         return buildLoginUser(user); | ||||
|         return user; | ||||
|     } | ||||
|  | ||||
|     private void createLoginLog(Long userId, String username, | ||||
| @ -170,7 +161,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String socialQuickLogin(AuthSocialQuickLoginReqVO reqVO, String userIp, String userAgent) { | ||||
|     public String socialQuickLogin(AuthSocialQuickLoginReqVO reqVO) { | ||||
|         // 使用 code 授权码,进行登录。然后,获得到绑定的用户编号 | ||||
|         Long userId = socialUserService.getBindUserId(UserTypeEnum.ADMIN.getValue(), reqVO.getType(), | ||||
|                 reqVO.getCode(), reqVO.getState()); | ||||
| @ -178,56 +169,46 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|             throw exception(AUTH_THIRD_LOGIN_NOT_BIND); | ||||
|         } | ||||
|  | ||||
|         // 自动登录 | ||||
|         // 获得用户 | ||||
|         AdminUserDO user = userService.getUser(userId); | ||||
|         if (user == null) { | ||||
|             throw exception(USER_NOT_EXISTS); | ||||
|         } | ||||
|  | ||||
|         // 创建 LoginUser 对象 | ||||
|         LoginUser loginUser = buildLoginUser(user); | ||||
|  | ||||
|         // 缓存登录用户到 Redis 中,返回 Token 令牌 | ||||
|         return createUserSessionAfterLoginSuccess(loginUser, null, | ||||
|                 LoginLogTypeEnum.LOGIN_SOCIAL, userIp, userAgent); | ||||
|         // 创建 Token 令牌,记录登录日志 | ||||
|         return createTokenAfterLoginSuccess(user.getId(), null, LoginLogTypeEnum.LOGIN_SOCIAL); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String socialBindLogin(AuthSocialBindLoginReqVO reqVO, String userIp, String userAgent) { | ||||
|     public String socialBindLogin(AuthSocialBindLoginReqVO reqVO) { | ||||
|         // 使用账号密码,进行登录。 | ||||
|         LoginUser loginUser = login0(reqVO.getUsername(), reqVO.getPassword()); | ||||
|         AdminUserDO user = login0(reqVO.getUsername(), reqVO.getPassword()); | ||||
|  | ||||
|         // 绑定社交用户 | ||||
|         socialUserService.bindSocialUser(AuthConvert.INSTANCE.convert(loginUser.getId(), getUserType().getValue(), reqVO)); | ||||
|         socialUserService.bindSocialUser(AuthConvert.INSTANCE.convert(user.getId(), getUserType().getValue(), reqVO)); | ||||
|  | ||||
|         // 缓存登录用户到 Redis 中,返回 Token 令牌 | ||||
|         return createUserSessionAfterLoginSuccess(loginUser, reqVO.getUsername(), | ||||
|                 LoginLogTypeEnum.LOGIN_SOCIAL, userIp, userAgent); | ||||
|         // 创建 Token 令牌,记录登录日志 | ||||
|         return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); | ||||
|     } | ||||
|  | ||||
|     private String createUserSessionAfterLoginSuccess(LoginUser loginUser, String username, | ||||
|                                                       LoginLogTypeEnum logType, String userIp, String userAgent) { | ||||
|     private String createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) { | ||||
|         // 插入登陆日志 | ||||
|         createLoginLog(loginUser.getId(), username, logType, LoginResultEnum.SUCCESS); | ||||
|         createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS); | ||||
|         // 创建访问令牌 | ||||
|         // TODO userIp、userAgent | ||||
|         // TODO clientId | ||||
|         return oauth2TokenService.createAccessToken(loginUser.getId(), getUserType().getValue(), 1L) | ||||
|         return oauth2TokenService.createAccessToken(userId, getUserType().getValue(), 1L) | ||||
|                 .getAccessToken(); | ||||
| //        return userSessionService.createUserSession(loginUser, userIp, userAgent); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void logout(String token) { | ||||
|         // 查询用户信息 | ||||
|         LoginUser loginUser = userSessionService.getLoginUser(token); | ||||
|         if (loginUser == null) { | ||||
|         // 删除访问令牌 | ||||
|         OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(token); | ||||
|         if (accessTokenDO == null) { | ||||
|             return; | ||||
|         } | ||||
|         // 删除 session | ||||
|         userSessionService.deleteUserSession(token); | ||||
|         // 记录登出日志 | ||||
|         createLogoutLog(loginUser.getId()); | ||||
|         // 删除成功,则记录登出日志 | ||||
|         createLogoutLog(accessTokenDO.getUserId()); | ||||
|     } | ||||
|  | ||||
|     private void createLogoutLog(Long userId) { | ||||
| @ -243,10 +224,6 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|         loginLogService.createLoginLog(reqDTO); | ||||
|     } | ||||
|  | ||||
|     private LoginUser buildLoginUser(AdminUserDO user) { | ||||
|         return AuthConvert.INSTANCE.convert(user).setUserType(getUserType().getValue()); | ||||
|     } | ||||
|  | ||||
|     private String getUsername(Long userId) { | ||||
|         if (userId == null) { | ||||
|             return null; | ||||
|  | ||||
| @ -59,8 +59,8 @@ public interface OAuth2TokenService { | ||||
|      * 参考 DefaultTokenServices 的 revokeToken 方法 | ||||
|      * | ||||
|      * @param accessToken 刷新令牌 | ||||
|      * @return 是否移除到 | ||||
|      * @return 访问令牌的信息 | ||||
|      */ | ||||
|     boolean removeAccessToken(String accessToken); | ||||
|     OAuth2AccessTokenDO removeAccessToken(String accessToken); | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -85,23 +85,19 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean removeAccessToken(String accessToken) { | ||||
|         return false; | ||||
|     public OAuth2AccessTokenDO removeAccessToken(String accessToken) { | ||||
|         // 删除访问令牌 | ||||
|         OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); | ||||
|         if (accessTokenDO == null) { | ||||
|             return null; | ||||
|         } | ||||
|         oauth2AccessTokenMapper.deleteById(accessTokenDO.getId()); | ||||
|         oauth2AccessTokenRedisDAO.delete(accessToken); | ||||
|         // 删除刷新令牌 | ||||
|         oauth2RefreshTokenMapper.deleteByRefreshToken(accessTokenDO.getRefreshToken()); | ||||
|         return accessTokenDO; | ||||
|     } | ||||
|  | ||||
| //    @Override | ||||
| //    @Transactional | ||||
| //    public OAuth2AccessTokenRespDTO checkAccessToken(String accessToken) { | ||||
| //        OAuth2AccessTokenDO accessTokenDO = this.getOAuth2AccessToken(accessToken); | ||||
| //        if (accessTokenDO == null) { // 不存在 | ||||
| //            throw ServiceExceptionUtil.exception(OAUTH2_ACCESS_TOKEN_NOT_FOUND); | ||||
| //        } | ||||
| //        if (accessTokenDO.getExpiresTime().getTime() < System.currentTimeMillis()) { // 已过期 | ||||
| //            throw ServiceExceptionUtil.exception(OAUTH2_ACCESS_TOKEN_TOKEN_EXPIRED); | ||||
| //        } | ||||
| //        // 返回访问令牌 | ||||
| //        return OAuth2Convert.INSTANCE.convert(accessTokenDO); | ||||
| //    } | ||||
|  | ||||
| //    @Override | ||||
| //    @Transactional | ||||
| @ -124,20 +120,6 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { | ||||
| //        OAuth2AccessTokenDO oauth2AccessTokenDO = createOAuth2AccessToken(refreshTokenDO, refreshAccessTokenDTO.getCreateIp()); | ||||
| //        // 返回访问令牌 | ||||
| //        return OAuth2Convert.INSTANCE.convert(oauth2AccessTokenDO); | ||||
| //    } | ||||
| // | ||||
| //    @Override | ||||
| //    @Transactional | ||||
| //    public void removeToken(OAuth2RemoveTokenByUserReqDTO removeTokenDTO) { | ||||
| //        // 删除 Access Token | ||||
| //        OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByUserIdAndUserType( | ||||
| //                removeTokenDTO.getUserId(), removeTokenDTO.getUserType()); | ||||
| //        if (accessTokenDO != null) { | ||||
| //            this.deleteOAuth2AccessToken(accessTokenDO.getId()); | ||||
| //        } | ||||
| // | ||||
| //        // 删除 Refresh Token | ||||
| //        oauth2RefreshTokenMapper.deleteByUserIdAndUserType(removeTokenDTO.getUserId(), removeTokenDTO.getUserType()); | ||||
| //    } | ||||
|  | ||||
|     private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) { | ||||
| @ -158,19 +140,6 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { | ||||
|         return refreshToken; | ||||
|     } | ||||
|  | ||||
|  | ||||
| //    /** | ||||
| //     * 删除 accessToken 的 MySQL 与 Redis 的数据 | ||||
| //     * | ||||
| //     * @param accessToken 访问令牌 | ||||
| //     */ | ||||
| //    private void deleteOAuth2AccessToken(String accessToken) { | ||||
| //        // 删除 MySQL | ||||
| //        oauth2AccessTokenMapper.deleteById(accessToken); | ||||
| //        // 删除 Redis | ||||
| //        oauth2AccessTokenRedisDAO.delete(accessToken); | ||||
| //    } | ||||
| // | ||||
|     private static String generateAccessToken() { | ||||
|         return IdUtil.fastSimpleUUID(); | ||||
|     } | ||||
|  | ||||
| @ -1,9 +1,8 @@ | ||||
| package cn.iocoder.yudao.module.system.service.auth; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.security.core.LoginUser; | ||||
| import cn.iocoder.yudao.framework.common.pojo.PageResult; | ||||
| 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; | ||||
|  | ||||
| /** | ||||
|  * 在线用户 Session Service 接口 | ||||
| @ -20,31 +19,6 @@ public interface UserSessionService { | ||||
|      */ | ||||
|     PageResult<UserSessionDO> getUserSessionPage(UserSessionPageReqVO reqVO); | ||||
|  | ||||
|     /** | ||||
|      * 移除超时的在线用户 | ||||
|      * | ||||
|      * @return {@link Long } 移出的超时用户数量 | ||||
|      **/ | ||||
|     long deleteTimeoutSession(); | ||||
|  | ||||
|     /** | ||||
|      * 创建在线用户 Session | ||||
|      * | ||||
|      * @param loginUser 登录用户 | ||||
|      * @param userIp 用户 IP | ||||
|      * @param userAgent 用户 UA | ||||
|      * @return Token 令牌 | ||||
|      */ | ||||
|     String createUserSession(LoginUser loginUser, String userIp, String userAgent); | ||||
|  | ||||
|     /** | ||||
|      * 刷新在线用户 Session 的更新时间 | ||||
|      * | ||||
|      * @param token 令牌 | ||||
|      * @param loginUser 登录用户 | ||||
|      */ | ||||
|     void refreshUserSession(String token, LoginUser loginUser); | ||||
|  | ||||
|     /** | ||||
|      * 删除在线用户 Session | ||||
|      * | ||||
| @ -59,19 +33,4 @@ public interface UserSessionService { | ||||
|      */ | ||||
|     void deleteUserSession(Long id); | ||||
|  | ||||
|     /** | ||||
|      * 获得 Token 对应的在线用户 | ||||
|      * | ||||
|      * @param token 令牌 | ||||
|      * @return 在线用户 | ||||
|      */ | ||||
|     LoginUser getLoginUser(String token); | ||||
|  | ||||
|     /** | ||||
|      * 获得 Session 超时时间,单位:毫秒 | ||||
|      * | ||||
|      * @return 超时时间 | ||||
|      */ | ||||
|     Long getSessionTimeoutMillis(); | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -1,12 +1,10 @@ | ||||
| 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; | ||||
| @ -21,12 +19,9 @@ import lombok.extern.slf4j.Slf4j; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
| import java.time.Duration; | ||||
| import java.util.Collection; | ||||
| 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; | ||||
|  | ||||
| /** | ||||
|  * 在线用户 Session Service 实现类 | ||||
| @ -64,29 +59,6 @@ public class UserSessionServiceImpl implements UserSessionService { | ||||
|         return userSessionMapper.selectPage(reqVO, userIds); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public long deleteTimeoutSession() { | ||||
|         // 获取 db 里已经超时的用户列表 | ||||
|         List<UserSessionDO> timeoutSessions = userSessionMapper.selectListBySessionTimoutLt(); | ||||
|         if (CollUtil.isEmpty(timeoutSessions)) { | ||||
|             return 0L; | ||||
|         } | ||||
|  | ||||
|         // 由于过期的用户一般不多,所以顺序遍历,进行清理 | ||||
|         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()); | ||||
| @ -100,28 +72,6 @@ public class UserSessionServiceImpl implements UserSessionService { | ||||
|         loginLogService.createLoginLog(reqDTO); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String createUserSession(LoginUser loginUser, String userIp, String userAgent) { | ||||
|         // 生成 Session 编号 | ||||
|         String token = generateToken(); | ||||
|         // 写入 Redis 缓存 | ||||
|         loginUserRedisDAO.set(token, loginUser); | ||||
|         // 写入 DB 中 | ||||
|         UserSessionDO userSession = UserSessionDO.builder().token(token) | ||||
|                 .userId(loginUser.getId()).userType(loginUser.getUserType()) | ||||
|                 .userIp(userIp).userAgent(userAgent).username("") | ||||
|                 .sessionTimeout(addTime(Duration.ofMillis(getSessionTimeoutMillis()))) | ||||
|                 .build(); | ||||
|         userSessionMapper.insert(userSession); | ||||
|         // 返回 Token 令牌 | ||||
|         return token; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void refreshUserSession(String token, LoginUser loginUser) { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void deleteUserSession(String token) { | ||||
|         // 删除 Redis 缓存 | ||||
| @ -145,23 +95,4 @@ public class UserSessionServiceImpl implements UserSessionService { | ||||
|         createLogoutLog(session, LoginLogTypeEnum.LOGOUT_DELETE); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public LoginUser getLoginUser(String token) { | ||||
|         return loginUserRedisDAO.get(token); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public Long getSessionTimeoutMillis() { | ||||
|         return securityProperties.getSessionTimeout().toMillis(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 生成 Token 令牌,目前采用 UUID 算法 | ||||
|      * | ||||
|      * @return Session 编号 | ||||
|      */ | ||||
|     private static String generateToken() { | ||||
|         return IdUtil.fastSimpleUUID(); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -70,7 +70,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { | ||||
|         when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); | ||||
|  | ||||
|         // 调用 | ||||
|         LoginUser loginUser = authService.login0(username, password); | ||||
|         AdminUserDO loginUser = authService.login0(username, password); | ||||
|         // 校验 | ||||
|         assertPojoEquals(user, loginUser); | ||||
|     } | ||||
| @ -182,8 +182,6 @@ public class AuthServiceImplTest extends BaseDbUnitTest { | ||||
|     @Test | ||||
|     public void testLogin_success() { | ||||
|         // 准备参数 | ||||
|         String userIp = randomString(); | ||||
|         String userAgent = randomString(); | ||||
|         AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class, o -> | ||||
|                 o.setUsername("test_username").setPassword("test_password")); | ||||
|  | ||||
| @ -197,13 +195,14 @@ public class AuthServiceImplTest extends BaseDbUnitTest { | ||||
|         when(userService.isPasswordMatch(eq("test_password"), eq(user.getPassword()))).thenReturn(true); | ||||
|         // mock 缓存登录用户到 Redis | ||||
|         String token = randomString(); | ||||
|         when(userSessionService.createUserSession(argThat(argument -> { | ||||
|             AssertUtils.assertPojoEquals(user, argument); | ||||
|             return true; | ||||
|         }), eq(userIp), eq(userAgent))).thenReturn(token); | ||||
| //        when(userSessionService.createUserSession(argThat(argument -> { | ||||
| //            AssertUtils.assertPojoEquals(user, argument); | ||||
| //            return true; | ||||
| //        }), eq(userIp), eq(userAgent))).thenReturn(token); | ||||
|         // TODO 芋艿:oauth2 | ||||
|  | ||||
|         // 调用, 并断言异常 | ||||
|         String result = authService.login(reqVO, userIp, userAgent); | ||||
|         String result = authService.login(reqVO); | ||||
|         assertEquals(token, result); | ||||
|         // 校验调用参数 | ||||
|         verify(loginLogService).createLoginLog( | ||||
| @ -219,7 +218,8 @@ public class AuthServiceImplTest extends BaseDbUnitTest { | ||||
|         String token = randomString(); | ||||
|         LoginUser loginUser = randomPojo(LoginUser.class); | ||||
|         // mock | ||||
|         when(userSessionService.getLoginUser(token)).thenReturn(loginUser); | ||||
| //        when(userSessionService.getLoginUser(token)).thenReturn(loginUser); | ||||
|         // TODO @芋艿:oauth2 | ||||
|         // 调用 | ||||
|         authService.logout(token); | ||||
|         // 校验调用参数 | ||||
|  | ||||
| @ -3,7 +3,6 @@ package cn.iocoder.yudao.module.system.service.auth; | ||||
| 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; | ||||
| @ -14,8 +13,6 @@ 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.common.SexEnum; | ||||
| 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; | ||||
| @ -25,17 +22,15 @@ import org.springframework.context.annotation.Import; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
| import java.time.Duration; | ||||
| import java.util.Calendar; | ||||
|  | ||||
| import static cn.hutool.core.util.RandomUtil.randomEle; | ||||
| import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime; | ||||
| import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; | ||||
| import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; | ||||
| import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; | ||||
| import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; | ||||
| import static java.util.Collections.singletonList; | ||||
| import static org.junit.jupiter.api.Assertions.*; | ||||
| import static org.mockito.ArgumentMatchers.argThat; | ||||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||||
| import static org.junit.jupiter.api.Assertions.assertNull; | ||||
| import static org.mockito.ArgumentMatchers.eq; | ||||
| import static org.mockito.Mockito.verify; | ||||
| import static org.mockito.Mockito.when; | ||||
|  | ||||
| /** | ||||
| @ -100,112 +95,6 @@ public class UserSessionServiceImplTest extends BaseDbAndRedisUnitTest { | ||||
|         assertPojoEquals(dbSession, pageResult.getList().get(0)); | ||||
|     } | ||||
|  | ||||
|     @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() { | ||||
|         // 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); | ||||
|  | ||||
|         // 清空超时数据 | ||||
|         long count = userSessionService.deleteTimeoutSession(); | ||||
|         // 校验 | ||||
|         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 | ||||
|     public void testCreateUserSession_success() { | ||||
|         // 准备参数 | ||||
|         String userIp = randomString(); | ||||
|         String userAgent = randomString(); | ||||
|         LoginUser loginUser = randomPojo(LoginUser.class, o -> { | ||||
|             o.setUserType(randomEle(UserTypeEnum.values()).getValue()); | ||||
|             o.setTenantId(0L); // 租户设置为 0,因为暂未启用多租户组件 | ||||
|         }); | ||||
|  | ||||
|         // 调用 | ||||
|         String token = userSessionService.createUserSession(loginUser, userIp, userAgent); | ||||
|         // 校验 UserSessionDO 记录 | ||||
|         UserSessionDO userSessionDO = userSessionMapper.selectOne(UserSessionDO::getToken, token); | ||||
|         assertPojoEquals(loginUser, userSessionDO, "id", "updateTime"); | ||||
|         assertEquals(token, userSessionDO.getToken()); | ||||
|         assertEquals(userIp, userSessionDO.getUserIp()); | ||||
|         assertEquals(userAgent, userSessionDO.getUserAgent()); | ||||
|         // 校验 LoginUser 缓存 | ||||
|         LoginUser redisLoginUser = loginUserRedisDAO.get(token); | ||||
|         assertPojoEquals(loginUser, redisLoginUser, "username", "password"); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testCreateRefreshUserSession() { | ||||
|         // 准备参数 | ||||
|         String token = randomString(); | ||||
|  | ||||
|         // mock redis 数据 | ||||
|         LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setUserType(randomEle(UserTypeEnum.values()).getValue())); | ||||
|         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(token, loginUser); | ||||
|         // 校验 LoginUser 缓存 | ||||
|         LoginUser redisLoginUser = loginUserRedisDAO.get(token); | ||||
|         assertPojoEquals(redisLoginUser, loginUser, "username", "password"); | ||||
|         // 校验 UserSessionDO 记录 | ||||
|         UserSessionDO updateDO = userSessionMapper.selectOne(UserSessionDO::getToken, token); | ||||
| //        assertEquals(updateDO.getUsername(), loginUser.getUsername()); | ||||
|         assertNotNull(userSession.getUpdateTime()); | ||||
|         assertNotNull(userSession.getSessionTimeout()); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testDeleteUserSession_Token() { | ||||
|         // 准备参数 | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 YunaiV
					YunaiV