实现单设备登录功能

This commit is contained in:
2025-06-16 16:20:20 +08:00
parent 45ce929bd4
commit 2b1bf8be00
5 changed files with 45 additions and 2 deletions

View File

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; 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.OAuth2AccessTokenDO;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -32,4 +33,12 @@ public interface OAuth2AccessTokenMapper extends BaseMapperX<OAuth2AccessTokenDO
.orderByDesc(OAuth2AccessTokenDO::getId)); .orderByDesc(OAuth2AccessTokenDO::getId));
} }
default void deleteByUserId(Long userId) {
delete(Wrappers.lambdaUpdate(OAuth2AccessTokenDO.class).eq(OAuth2AccessTokenDO::getUserId, userId));
}
default List<OAuth2AccessTokenDO> selectListByUserId(Long userId) {
return selectList(OAuth2AccessTokenDO::getUserId, userId);
}
} }

View File

@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO; import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@Mapper @Mapper
@@ -18,5 +19,7 @@ public interface OAuth2RefreshTokenMapper extends BaseMapperX<OAuth2RefreshToken
default OAuth2RefreshTokenDO selectByRefreshToken(String refreshToken) { default OAuth2RefreshTokenDO selectByRefreshToken(String refreshToken) {
return selectOne(OAuth2RefreshTokenDO::getRefreshToken, refreshToken); return selectOne(OAuth2RefreshTokenDO::getRefreshToken, refreshToken);
} }
default void deleteByUserId(Long userId) {
delete(Wrappers.lambdaUpdate(OAuth2RefreshTokenDO.class).eq(OAuth2RefreshTokenDO::getUserId, userId));
}
} }

View File

@@ -40,6 +40,11 @@ public interface OAuth2TokenService {
*/ */
OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId); OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId);
/**
* 删除用户所有返回令牌
* @param userId 用户 Id
*/
void removeAccessTokenByUserId(Long userId);
/** /**
* 获得访问令牌 * 获得访问令牌
* *

View File

@@ -8,6 +8,7 @@ import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.date.DateUtils; 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.object.BeanUtils;
import cn.iocoder.yudao.framework.security.core.LoginUser; import cn.iocoder.yudao.framework.security.core.LoginUser;
@@ -22,6 +23,7 @@ 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.mysql.oauth2.OAuth2RefreshTokenMapper;
import cn.iocoder.yudao.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO; import cn.iocoder.yudao.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO;
import cn.iocoder.yudao.module.system.service.user.AdminUserService; import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -31,6 +33,7 @@ import java.time.LocalDateTime;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@@ -57,11 +60,19 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
@Lazy // 懒加载,避免循环依赖 @Lazy // 懒加载,避免循环依赖
private AdminUserService adminUserService; private AdminUserService adminUserService;
@Value("${multiple-device-login}")
private Boolean multipleDeviceLoginConfig;
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes) { public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes) {
// 在 yaml multiple-device-login = True 时 删除用户上次登录令牌,实现单设备登录
if (multipleDeviceLoginConfig){
removeAccessTokenByUserId(userId);
}
OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
// 创建刷新令牌 // 创建刷新令牌
OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes); OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes);
// 创建访问令牌 // 创建访问令牌
return createOAuth2AccessToken(refreshTokenDO, clientDO); return createOAuth2AccessToken(refreshTokenDO, clientDO);
@@ -99,6 +110,18 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
return createOAuth2AccessToken(refreshTokenDO, clientDO); return createOAuth2AccessToken(refreshTokenDO, clientDO);
} }
@Override
@Transactional(rollbackFor = Exception.class)
public void removeAccessTokenByUserId(Long userId) {
List<OAuth2AccessTokenDO> oAuth2AccessTokenDOList = oauth2AccessTokenMapper.selectListByUserId(userId);
if (!CollectionUtils.isAnyEmpty(oAuth2AccessTokenDOList)) {
oauth2AccessTokenRedisDAO.deleteList(oAuth2AccessTokenDOList.stream().map(OAuth2AccessTokenDO::getAccessToken).collect(Collectors.toList()));
}
oauth2AccessTokenMapper.deleteByUserId(userId);
oauth2RefreshTokenMapper.deleteByUserId(userId);
}
@Override @Override
public OAuth2AccessTokenDO getAccessToken(String accessToken) { public OAuth2AccessTokenDO getAccessToken(String accessToken) {
// 优先从 Redis 中获取 // 优先从 Redis 中获取
@@ -126,6 +149,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
return accessTokenDO; return accessTokenDO;
} }
@Override @Override
public OAuth2AccessTokenDO checkAccessToken(String accessToken) { public OAuth2AccessTokenDO checkAccessToken(String accessToken) {
OAuth2AccessTokenDO accessTokenDO = getAccessToken(accessToken); OAuth2AccessTokenDO accessTokenDO = getAccessToken(accessToken);

View File

@@ -228,3 +228,5 @@ pf4j:
md5: md5:
salt: (-FhqvXO,wMz salt: (-FhqvXO,wMz
multiple-device-login: true