From 308b0eb9cd264fbe5ebf5da6c5105135ebc69512 Mon Sep 17 00:00:00 2001 From: panda <1565636758@qq.com> Date: Thu, 24 Apr 2025 11:13:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8D=95=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=99=90=E5=88=B6=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E9=99=90=E5=88=B6=E5=90=8E=E5=8F=B0=E8=B4=A6=E5=8F=B7=EF=BC=88?= =?UTF-8?q?admin=E7=B1=BB=E5=9E=8B=EF=BC=89=E5=9C=A8=E5=90=8C=E4=B8=80?= =?UTF-8?q?=E7=AB=AF=EF=BC=88PC=E3=80=81APP=E3=80=81H5=E7=AD=89=EF=BC=89?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E5=90=8C=E6=97=B6=E7=99=BB=E5=BD=95=EF=BC=8C?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=BC=80=E5=90=AF=E5=9C=A8=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E4=B8=AD=E6=9C=89=E9=85=8D=E7=BD=AE=E5=BC=80?= =?UTF-8?q?=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/mysql/singlelogin.sql | 11 +++ .../enums/GlobalErrorCodeConstants.java | 2 +- .../enums/logger/LoginFromTypeEnum.java | 52 ++++++++++++ .../enums/logger/LoginOutReasonEnum.java | 44 ++++++++++ .../system/api/oauth2/OAuth2TokenApiImpl.java | 3 +- .../admin/auth/vo/AuthLoginReqVO.java | 5 ++ .../admin/auth/vo/AuthRegisterReqVO.java | 7 ++ .../admin/auth/vo/AuthSmsLoginReqVO.java | 6 ++ .../admin/auth/vo/AuthSocialLoginReqVO.java | 5 ++ .../oauth2/OAuth2RefreshTokenDO.java | 13 +++ .../oauth2/OAuth2RefreshTokenMapper.java | 10 +++ .../system/dal/redis/RedisKeyConstants.java | 11 +++ .../oauth2/OAuth2LoginFromTypeRedisDAO.java | 49 +++++++++++ .../service/auth/AdminAuthServiceImpl.java | 13 +-- .../oauth2/OAuth2GrantServiceImpl.java | 7 +- .../service/oauth2/OAuth2TokenService.java | 4 +- .../oauth2/OAuth2TokenServiceImpl.java | 81 +++++++++++++++++-- .../auth/AdminAuthServiceImplTest.java | 9 ++- 18 files changed, 310 insertions(+), 22 deletions(-) create mode 100644 sql/mysql/singlelogin.sql create mode 100644 yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginFromTypeEnum.java create mode 100644 yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginOutReasonEnum.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/oauth2/OAuth2LoginFromTypeRedisDAO.java diff --git a/sql/mysql/singlelogin.sql b/sql/mysql/singlelogin.sql new file mode 100644 index 0000000000..0f51adcfe0 --- /dev/null +++ b/sql/mysql/singlelogin.sql @@ -0,0 +1,11 @@ +-- by panda 25.04.23 +-- 单账号登录新增字段 +SET FOREIGN_KEY_CHECKS=0; +-- 登录来源 +ALTER TABLE `ruoyi-vue-pro`.`system_oauth2_refresh_token` ADD COLUMN `login_from_type` int NULL DEFAULT NULL COMMENT '登录来源' AFTER `expires_time`; +-- 失效原因,用于判断通知前台便于准确提示 +ALTER TABLE `ruoyi-vue-pro`.`system_oauth2_refresh_token` ADD COLUMN `failure_reason` int NULL DEFAULT NULL COMMENT '失效原因' AFTER `login_from_type`; +-- 配置表中新增单账号登录的开关 +INSERT INTO `ruoyi-vue-pro`.`infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (13, 'biz', 2, '单账户登录', 'oauth2.loginFromType.enable', 'true', b'1', '一个账号只能在某端一次登录有效,前者登录的会被后者踢掉', '1', '2025-04-23 15:36:35', '1', '2025-04-23 18:08:44', b'0'); + +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/GlobalErrorCodeConstants.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/GlobalErrorCodeConstants.java index edf31f24aa..f5041800fe 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/GlobalErrorCodeConstants.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/GlobalErrorCodeConstants.java @@ -25,7 +25,7 @@ public interface GlobalErrorCodeConstants { ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确"); ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许 ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试"); - + ErrorCode LOGIN_OUT_FOR_SINGLE = new ErrorCode(499, "该账号已在其他地方登录,被迫下线"); // ========== 服务端错误段 ========== ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常"); diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginFromTypeEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginFromTypeEnum.java new file mode 100644 index 0000000000..7aacc41f32 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginFromTypeEnum.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.system.enums.logger; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 登录来源的类型枚举,用来表示用户从那端登录,用于单账号登录互踢 + * 一个来源只能登录一次,前者会被踢下 + * 前提是在配置管理中开启了单账户登录 + * by panda 25.04.23 + */ +@Getter +@AllArgsConstructor +public enum LoginFromTypeEnum implements ArrayValuable { + + LOGIN_PC(100,"PC"), // 从PC端登录 + LOGIN_APP(110,"APP"), // 从APP端登录 + LOGIN_H5(120,"H5"), // 从H5端登录 + LOGIN_MINI(130,"MINI"), // 从小程序登录 + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(LoginFromTypeEnum::getType).toArray(Integer[]::new); + + /** + * 登录来源类型 + */ + private final Integer type; + + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static String getTypeName(Integer type) { + return Arrays.stream(values()).filter(item -> item.getType().equals(type)).findFirst().map(LoginFromTypeEnum::getName).orElse(""); + } + + public static LoginFromTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), + values()); + } + + public static LoginFromTypeEnum getDefault() { + return LOGIN_PC; + } +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginOutReasonEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginOutReasonEnum.java new file mode 100644 index 0000000000..e31ebf70a7 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginOutReasonEnum.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.system.enums.logger; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 登录退出的类型 + * by panda 25.04.23 + */ +@Getter +@AllArgsConstructor +public enum LoginOutReasonEnum implements ArrayValuable { + + LOGIN_SINGLE(100,"Single"), // 单账号登录被踢下线 + LOGIN_OTHER(900,"other"), // 其他 + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(LoginOutReasonEnum::getCode).toArray(Integer[]::new); + + /** + * 登录来源类型 + */ + private final Integer code; + + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static String getTypeName(Integer type) { + return Arrays.stream(values()).filter(item -> item.getCode().equals(type)).findFirst().map(LoginOutReasonEnum::getName).orElse(""); + } + + public static LoginOutReasonEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getCode().equals(type), + values()); + } +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApiImpl.java index 4fdec601a9..cc220be638 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApiImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApiImpl.java @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespD import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenCreateReqDTO; import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO; import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.enums.logger.LoginFromTypeEnum; import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; import org.springframework.stereotype.Service; @@ -24,7 +25,7 @@ public class OAuth2TokenApiImpl implements OAuth2TokenApi { @Override public OAuth2AccessTokenRespDTO createAccessToken(OAuth2AccessTokenCreateReqDTO reqDTO) { OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken( - reqDTO.getUserId(), reqDTO.getUserType(), reqDTO.getClientId(), reqDTO.getScopes()); + reqDTO.getUserId(), reqDTO.getUserType(), reqDTO.getClientId(), reqDTO.getScopes(), LoginFromTypeEnum.LOGIN_PC);//auth 只存在PC端,所以固定 by panda 25.04.23; return BeanUtils.toBean(accessTokenDO, OAuth2AccessTokenRespDTO.class); } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginReqVO.java index a29660ec4e..7621d6ddc7 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginReqVO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginReqVO.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.controller.admin.auth.vo; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.system.enums.logger.LoginFromTypeEnum; import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; @@ -54,4 +55,8 @@ public class AuthLoginReqVO extends CaptchaVerificationReqVO { return socialType == null || StrUtil.isNotEmpty(socialState); } + // ========== 登录来源判断 by panda 25.04.23,默认PC端========== + @Schema(description = "登录来源,参见 LoginFromTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(LoginFromTypeEnum.class) + private Integer loginFromType; } \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthRegisterReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthRegisterReqVO.java index 97edc75a23..40577f9197 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthRegisterReqVO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthRegisterReqVO.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.system.controller.admin.auth.vo; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.system.enums.logger.LoginFromTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.hibernate.validator.constraints.Length; @@ -26,4 +28,9 @@ public class AuthRegisterReqVO extends CaptchaVerificationReqVO { @NotEmpty(message = "密码不能为空") @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String password; + + // ========== 登录来源判断 by panda 25.04.23,默认PC端========== + @Schema(description = "登录来源,参见 LoginFromTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1",defaultValue = "100") + @InEnum(LoginFromTypeEnum.class) + private Integer loginFromType; } \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSmsLoginReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSmsLoginReqVO.java index e5730d2850..d2eece061b 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSmsLoginReqVO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSmsLoginReqVO.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.system.controller.admin.auth.vo; +import cn.iocoder.yudao.framework.common.validation.InEnum; import cn.iocoder.yudao.framework.common.validation.Mobile; +import cn.iocoder.yudao.module.system.enums.logger.LoginFromTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; @@ -25,4 +27,8 @@ public class AuthSmsLoginReqVO { @NotEmpty(message = "验证码不能为空") private String code; + // ========== 登录来源判断 by panda 25.04.23,默认PC端========== + @Schema(description = "登录来源,参见 LoginFromTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1",defaultValue = "100") + @InEnum(LoginFromTypeEnum.class) + private Integer loginFromType; } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSocialLoginReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSocialLoginReqVO.java index da2247008a..d7fdb4b763 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSocialLoginReqVO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSocialLoginReqVO.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.system.controller.admin.auth.vo; import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.system.enums.logger.LoginFromTypeEnum; import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; @@ -31,4 +32,8 @@ public class AuthSocialLoginReqVO { @NotEmpty(message = "state 不能为空") private String state; + // ========== 登录来源判断 by panda 25.04.23,默认PC端========== + @Schema(description = "登录来源,参见 LoginFromTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1",defaultValue = "100") + @InEnum(LoginFromTypeEnum.class) + private Integer loginFromType; } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java index 99d153e8bf..9d659c402c 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.dal.dataobject.oauth2; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.system.enums.logger.LoginFromTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; @@ -60,4 +61,16 @@ public class OAuth2RefreshTokenDO extends TenantBaseDO { */ private LocalDateTime expiresTime; + /** + * 登录来源 by panda 25.04.23 + * + * 枚举 {@link LoginFromTypeEnum} + */ + private Integer loginFromType; + /** + * 失效原因 by panda 25.04.23 + * + * 枚举 {@link cn.iocoder.yudao.module.system.enums.logger.LoginOutReasonEnum} + */ + private Integer failureReason; } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java index bf91457cd4..2c256bfb9f 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java @@ -3,8 +3,10 @@ package cn.iocoder.yudao.module.system.dal.mysql.oauth2; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; @Mapper public interface OAuth2RefreshTokenMapper extends BaseMapperX { @@ -19,4 +21,12 @@ public interface OAuth2RefreshTokenMapper extends BaseMapperX + * KEY 格式:oauth2_login_from_type:{token} + * VALUE 数据类型:String 访问令牌信息 {@link OAuth2AccessTokenDO} + *

+ * 由于动态过期时间,使用 RedisTemplate 操作 + */ + String OAUTH2_LOGIN_FROM_TYPE = "oauth2_login_from_type:%s"; + /** * 站内信模版的缓存 *

diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/oauth2/OAuth2LoginFromTypeRedisDAO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/oauth2/OAuth2LoginFromTypeRedisDAO.java new file mode 100644 index 0000000000..b182c22291 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/oauth2/OAuth2LoginFromTypeRedisDAO.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.system.dal.redis.oauth2; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import jakarta.annotation.Resource; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants.OAUTH2_ACCESS_TOKEN; +import static cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants.OAUTH2_LOGIN_FROM_TYPE; + +/** + * {@link OAuth2AccessTokenDO} 的 RedisDAO + * 登录来源的缓存标记 by panda 25.04.23 + * 1. 缓存标记的key为:OAUTH2_LOGIN_FROM_TYPE : userId + "_" + loginFromTypeName + * @author 芋道源码 + */ +@Repository +public class OAuth2LoginFromTypeRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public String get(String loginFromTypeName, OAuth2AccessTokenDO accessTokenDO) { + String redisKey = formatKey(accessTokenDO.getUserId()+"_"+loginFromTypeName); + return stringRedisTemplate.opsForValue().get(redisKey); + } + + public void set(String loginFromTypeName, OAuth2AccessTokenDO accessTokenDO) { + String redisKey = formatKey(accessTokenDO.getUserId()+"_"+loginFromTypeName); + long time = LocalDateTimeUtil.between(LocalDateTime.now(), accessTokenDO.getExpiresTime(), ChronoUnit.SECONDS); + if (time > 0) { + stringRedisTemplate.opsForValue().set(redisKey, accessTokenDO.getAccessToken(), time, TimeUnit.SECONDS); + } + } + + private static String formatKey(String userIdLoginFromTypeName) { + return String.format(OAUTH2_LOGIN_FROM_TYPE, userIdLoginFromTypeName); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java index 4b09980235..b47ff05123 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java @@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*; import cn.iocoder.yudao.module.system.convert.auth.AuthConvert; import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.enums.logger.LoginFromTypeEnum; import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; import cn.iocoder.yudao.module.system.enums.oauth2.OAuth2ClientConstants; @@ -110,7 +111,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); } // 创建 Token 令牌,记录登录日志 - return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); + return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME,LoginFromTypeEnum.valueOf(reqVO.getLoginFromType())); } @Override @@ -143,7 +144,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { } // 创建 Token 令牌,记录登录日志 - return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE); + return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE,LoginFromTypeEnum.valueOf(reqVO.getLoginFromType())); } private void createLoginLog(Long userId, String username, @@ -181,7 +182,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { } // 创建 Token 令牌,记录登录日志 - return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); + return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL,LoginFromTypeEnum.valueOf(reqVO.getLoginFromType())); } @VisibleForTesting @@ -206,12 +207,12 @@ public class AdminAuthServiceImpl implements AdminAuthService { return captchaService.verification(captchaVO); } - private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) { + private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType,LoginFromTypeEnum loginFromType) { // 插入登陆日志 createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS); // 创建访问令牌 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(), - OAuth2ClientConstants.CLIENT_ID_DEFAULT, null); + OAuth2ClientConstants.CLIENT_ID_DEFAULT, null,loginFromType); // 构建返回结果 return AuthConvert.INSTANCE.convert(accessTokenDO); } @@ -271,7 +272,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { Long userId = userService.registerUser(registerReqVO); // 3. 创建 Token 令牌,记录登录日志 - return createTokenAfterLoginSuccess(userId, registerReqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); + return createTokenAfterLoginSuccess(userId, registerReqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME,LoginFromTypeEnum.valueOf(registerReqVO.getLoginFromType())); } @VisibleForTesting diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImpl.java index e95fecccc6..edeca5b8ff 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImpl.java @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2CodeDO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; import cn.iocoder.yudao.module.system.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.system.enums.logger.LoginFromTypeEnum; import cn.iocoder.yudao.module.system.service.auth.AdminAuthService; import org.springframework.stereotype.Service; @@ -34,7 +35,7 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService { @Override public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, String clientId, List scopes) { - return oauth2TokenService.createAccessToken(userId, userType, clientId, scopes); + return oauth2TokenService.createAccessToken(userId, userType, clientId, scopes, LoginFromTypeEnum.LOGIN_PC);//auth 只存在PC端,所以固定 by panda 25.04.23 } @Override @@ -66,7 +67,7 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService { // 创建访问令牌 return oauth2TokenService.createAccessToken(codeDO.getUserId(), codeDO.getUserType(), - codeDO.getClientId(), codeDO.getScopes()); + codeDO.getClientId(), codeDO.getScopes(), LoginFromTypeEnum.LOGIN_PC);//auth 只存在PC端,所以固定 by panda 25.04.23; } @Override @@ -76,7 +77,7 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService { Assert.notNull(user, "用户不能为空!"); // 防御性编程 // 创建访问令牌 - return oauth2TokenService.createAccessToken(user.getId(), UserTypeEnum.ADMIN.getValue(), clientId, scopes); + return oauth2TokenService.createAccessToken(user.getId(), UserTypeEnum.ADMIN.getValue(), clientId, scopes, LoginFromTypeEnum.LOGIN_PC);//auth 只存在PC端,所以固定 by panda 25.04.23; } @Override diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenService.java index 977d935397..b4b44945f7 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenService.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenService.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.system.service.oauth2; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.enums.logger.LoginFromTypeEnum; import java.util.List; @@ -25,9 +26,10 @@ public interface OAuth2TokenService { * @param userType 用户类型 * @param clientId 客户端编号 * @param scopes 授权范围 + * @param loginFromType 登录来源 by panda 25.04.23 * @return 访问令牌的信息 */ - OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List scopes); + OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List scopes, LoginFromTypeEnum loginFromType); /** * 刷新访问令牌 diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java index fb0e756a20..9656192653 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.system.service.oauth2; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjectUtil; @@ -10,9 +11,11 @@ import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstant 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.BeanUtils; +import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; import cn.iocoder.yudao.framework.security.core.LoginUser; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.infra.api.config.ConfigApi; import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO; @@ -21,6 +24,9 @@ import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2AccessTokenMapper; import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2RefreshTokenMapper; import cn.iocoder.yudao.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO; +import cn.iocoder.yudao.module.system.dal.redis.oauth2.OAuth2LoginFromTypeRedisDAO; +import cn.iocoder.yudao.module.system.enums.logger.LoginFromTypeEnum; +import cn.iocoder.yudao.module.system.enums.logger.LoginOutReasonEnum; import cn.iocoder.yudao.module.system.service.user.AdminUserService; import jakarta.annotation.Resource; import org.springframework.context.annotation.Lazy; @@ -32,6 +38,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; @@ -57,12 +65,28 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { @Lazy // 懒加载,避免循环依赖 private AdminUserService adminUserService; + @Resource + private ConfigApi configApi; + + @Resource + private OAuth2LoginFromTypeRedisDAO oAuth2LoginFromTypeRedisDAO; + + /** + * 创建访问令牌,新增了LoginFromType,用来做互踢 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @param loginFromType 登录来源 by panda 25.04.23 + * @return 访问令牌 + */ @Override @Transactional(rollbackFor = Exception.class) - public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List scopes) { + public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List scopes, LoginFromTypeEnum loginFromType) { OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); // 创建刷新令牌 - OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes); + OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes, loginFromType); // 创建访问令牌 return createOAuth2AccessToken(refreshTokenDO, clientDO); } @@ -70,8 +94,14 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { @Override @Transactional(rollbackFor = Exception.class) public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) { + // 对踢下线的做处理 by panda 25.04.23 + OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectHasDeleteByAccessToken(refreshToken); + if (refreshTokenDO != null && ObjectUtil.equals(refreshTokenDO.getFailureReason(), LoginOutReasonEnum.LOGIN_SINGLE.getCode())) { + throw exception(GlobalErrorCodeConstants.LOGIN_OUT_FOR_SINGLE);//被迫下线 + } + // 查询访问令牌 - OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken); + refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken); if (refreshTokenDO == null) { throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "无效的刷新令牌"); } @@ -166,16 +196,19 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { .setRefreshToken(refreshTokenDO.getRefreshToken()) .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds())); accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号 + //校验用户的token互踢操作,by panda 25.04.23 + checkLoginFromType(refreshTokenDO.getLoginFromType(), accessTokenDO); oauth2AccessTokenMapper.insert(accessTokenDO); // 记录到 Redis 中 oauth2AccessTokenRedisDAO.set(accessTokenDO); return accessTokenDO; } - private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO, List scopes) { + private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO, List scopes, LoginFromTypeEnum loginFromType) { OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken()) .setUserId(userId).setUserType(userType) .setClientId(clientDO.getClientId()).setScopes(scopes) + .setLoginFromType(ObjectUtil.isEmpty(loginFromType) ? LoginFromTypeEnum.getDefault().getType() : loginFromType.getType())//by panda 25.04.23 添加登录来源,用来互踢,默认为PC .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getRefreshTokenValiditySeconds())); oauth2RefreshTokenMapper.insert(refreshToken); return refreshToken; @@ -185,14 +218,14 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { OAuth2AccessTokenDO accessTokenDO = BeanUtils.toBean(refreshTokenDO, OAuth2AccessTokenDO.class) .setAccessToken(refreshTokenDO.getRefreshToken()); TenantUtils.execute(refreshTokenDO.getTenantId(), - () -> accessTokenDO.setUserInfo(buildUserInfo(refreshTokenDO.getUserId(), refreshTokenDO.getUserType()))); + () -> accessTokenDO.setUserInfo(buildUserInfo(refreshTokenDO.getUserId(), refreshTokenDO.getUserType()))); return accessTokenDO; } /** * 加载用户信息,方便 {@link cn.iocoder.yudao.framework.security.core.LoginUser} 获取到昵称、部门等信息 * - * @param userId 用户编号 + * @param userId 用户编号 * @param userType 用户类型 * @return 用户信息 */ @@ -216,4 +249,40 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { return IdUtil.fastSimpleUUID(); } + /** + * 校验登录来源,用来做互踢 + * 1.开启互踢功能 + * 2.当前用户在loginFromType 上是否已经存在token + * 3.如果存在,踢掉之前的 + * 4.更新失效原因,用于刷新token是判断是否是被踢下线,便于返回前端做正确提示 + * 5.更新当前登录来源的缓存 + * + * @param loginFromType 登录来源 by panda 25.04.23 + * 枚举 {@link LoginFromTypeEnum} + * @param accessTokenDO 最新的访问令牌,取里面的用户来判断,同时缓存 + */ + private void checkLoginFromType(Integer loginFromType, OAuth2AccessTokenDO accessTokenDO) { + // 是否开启了互踢 + if (ObjectUtil.equal(Convert.toBool(configApi.getConfigValueByKey("oauth2.loginFromType.enable")), true)) { + //当前用户在loginFromType 上是否已经存在token + String loginTypeToken = oAuth2LoginFromTypeRedisDAO.get(LoginFromTypeEnum.getTypeName(loginFromType), accessTokenDO); + //校验下当前缓存的token是否真实存在,否则就不一致,这种情况出现在先开启单账户功能使用,后关闭了然后使用,但在redis有效期内,再开启时,redis中有缓存(概率很小但是还是考虑进去,否则异常) + OAuth2AccessTokenDO oldAccessTokenDO = getAccessToken(loginTypeToken); + if (ObjectUtil.isEmpty(oldAccessTokenDO)) { + //没有登录过,直接保存 + oAuth2LoginFromTypeRedisDAO.set(LoginFromTypeEnum.getTypeName(loginFromType), accessTokenDO); + } else { + //先查出老的token信息(一定要先更新,后删除,否则先删除了就查不到了!) + OAuth2RefreshTokenDO oldRefreshTokenDo = oauth2RefreshTokenMapper.selectByRefreshToken(oldAccessTokenDO.getRefreshToken()); + //更新失效原因,用于刷新token时判断是否是被踢下线,便于返回前端做正确提示 + if (ObjectUtil.isNotEmpty(oldRefreshTokenDo)) + oauth2RefreshTokenMapper.updateById(oldRefreshTokenDo.setFailureReason(LoginOutReasonEnum.LOGIN_SINGLE.getCode())); + //删除token让其失效,达到踢下线 + SpringUtils.getBean(OAuth2TokenService.class).removeAccessToken(loginTypeToken); + //更新当前登录来源的缓存 + oAuth2LoginFromTypeRedisDAO.set(LoginFromTypeEnum.getTypeName(loginFromType), accessTokenDO); + + } + } + } } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java index 151150bc5a..2f3c29c914 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java @@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO; import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*; import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.enums.logger.LoginFromTypeEnum; import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; @@ -164,7 +165,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { // mock 缓存登录用户到 Redis OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) .setUserType(UserTypeEnum.ADMIN.getValue())); - when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull())) + when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull(),eq(LoginFromTypeEnum.LOGIN_PC))) .thenReturn(accessTokenDO); // 调用,并校验 @@ -206,7 +207,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { // 准备参数 String mobile = randomString(); String code = randomString(); - AuthSmsLoginReqVO reqVO = new AuthSmsLoginReqVO(mobile, code); + AuthSmsLoginReqVO reqVO = new AuthSmsLoginReqVO(mobile, code,LoginFromTypeEnum.LOGIN_PC.getType()); // mock 方法(验证码) doNothing().when(smsCodeApi).useSmsCode((argThat(smsCodeUseReqDTO -> { assertEquals(mobile, smsCodeUseReqDTO.getMobile()); @@ -220,7 +221,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { // mock 缓存登录用户到 Redis OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) .setUserType(UserTypeEnum.ADMIN.getValue())); - when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull())) + when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull(),eq(LoginFromTypeEnum.LOGIN_PC))) .thenReturn(accessTokenDO); // 调用,并断言 @@ -248,7 +249,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { // mock 缓存登录用户到 Redis OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) .setUserType(UserTypeEnum.ADMIN.getValue())); - when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull())) + when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull(),eq(LoginFromTypeEnum.LOGIN_PC))) .thenReturn(accessTokenDO); // 调用,并断言