diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java index 01a5fb71d..b39def5d2 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java @@ -5,6 +5,8 @@ import com.cf.imes.framework.common.util.collection.CollectionUtils; import com.cf.imes.framework.common.util.json.JsonUtils; import com.cf.imes.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import com.cf.imes.module.system.dal.redis.RedisKeyConstants; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; @@ -15,6 +17,8 @@ import java.util.Collection; import java.util.List; import java.util.concurrent.TimeUnit; +import static com.cf.imes.module.system.dal.redis.RedisKeyConstants.OAUTH2_ACCESS_TOKEN; + /** * {@link OAuth2AccessTokenDO} 的 RedisDAO * @@ -26,11 +30,27 @@ public class OAuth2AccessTokenRedisDAO { @Resource private StringRedisTemplate stringRedisTemplate; + /** + * 不带key获取 + * + * @param accessToken + * @return + */ public OAuth2AccessTokenDO get(String accessToken) { String redisKey = formatKey(accessToken); return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), OAuth2AccessTokenDO.class); } + /** + * 带key获取,oauth2_access_token:xxxx + * + * @param accessToken + * @return + */ + public OAuth2AccessTokenDO getWithKey(String accessToken) { + return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(accessToken), OAuth2AccessTokenDO.class); + } + public void set(OAuth2AccessTokenDO accessTokenDO) { String redisKey = formatKey(accessTokenDO.getAccessToken()); // 清理多余字段,避免缓存 @@ -42,11 +62,26 @@ public class OAuth2AccessTokenRedisDAO { } } + /** + * 不带key删除 + * + * @param accessToken + */ public void delete(String accessToken) { String redisKey = formatKey(accessToken); stringRedisTemplate.delete(redisKey); } + /** + * 带key删除,oauth2_access_token:xxxx + * + * @param accessToken + * @return + */ + public void deleteWithKey(String accessToken) { + stringRedisTemplate.delete(accessToken); + } + public void deleteList(Collection accessTokens) { List redisKeys = CollectionUtils.convertList(accessTokens, OAuth2AccessTokenRedisDAO::formatKey); stringRedisTemplate.delete(redisKeys); @@ -67,4 +102,12 @@ public class OAuth2AccessTokenRedisDAO { return String.format(RedisKeyConstants.OAUTH2_ACCESS_TOKEN, accessToken); } + /** + * 构建token游标,使用时轮训一次获取两百个token + * + * @return + */ + public Cursor scan(){ + return stringRedisTemplate.scan(ScanOptions.scanOptions().match(String.format(OAUTH2_ACCESS_TOKEN, "*")).count(200).build()); + } } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/oauth2/OAuth2TokenServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/oauth2/OAuth2TokenServiceImpl.java index 469f104e7..697c97da6 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/oauth2/OAuth2TokenServiceImpl.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/oauth2/OAuth2TokenServiceImpl.java @@ -17,12 +17,17 @@ import com.cf.imes.module.system.dal.mysql.oauth2.OAuth2AccessTokenMapper; import com.cf.imes.module.system.dal.mysql.oauth2.OAuth2RefreshTokenMapper; import com.cf.imes.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO; import com.cf.imes.module.system.service.organ.OrganService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.redis.core.Cursor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception0; @@ -34,6 +39,7 @@ import static com.cf.imes.framework.common.util.collection.CollectionUtils.conve * @author 晨丰科技 */ @Service +@Slf4j public class OAuth2TokenServiceImpl implements OAuth2TokenService { @Resource @@ -182,8 +188,10 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { // 在Redis中新增Token,除去刷新Token字段 private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2AccessTokenDO refreshTokenDO, OAuth2ClientDO clientDO) { - OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken()) - .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()) + String currentToken = generateAccessToken(); + Long userId = refreshTokenDO.getUserId(); + OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(currentToken) + .setUserId(userId).setUserType(refreshTokenDO.getUserType()) .setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes()) .setRefreshToken(refreshTokenDO.getRefreshToken()) .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds())) @@ -193,9 +201,41 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { .setDeptId(refreshTokenDO.getDeptId()); // 记录到 Redis 中 oauth2AccessTokenRedisDAO.set(accessTokenDO); + // 异步踢出其他token + CompletableFuture.runAsync(() -> { + kickOut(userId, currentToken); + }).exceptionally(e -> { + log.error("[OAuth2TokenServiceImpl] redis 清空token失败, 用户id:{}, 异常:{}", userId, e); + return null; + }); return accessTokenDO; } + /** + * 踢出除本次userId登录产生token以外的所有token(不处理超管的超管除外) + * + * @param userId + * @param currentToken + */ + public void kickOut(Long userId, String currentToken) { + List matchKeys = new ArrayList<>(); + Cursor cursor = oauth2AccessTokenRedisDAO.scan(); + while (cursor.hasNext()) { + matchKeys.add(cursor.next()); + } + cursor.close(); + if (CollUtil.isNotEmpty(matchKeys)) { + for (String key : matchKeys) { + // 获取key下的用户信息 + OAuth2AccessTokenDO oAuth2AccessTokenDO = oauth2AccessTokenRedisDAO.getWithKey(key); + if (ObjectUtil.equal(userId, oAuth2AccessTokenDO.getUserId()) && !StringUtils.equals(currentToken, oAuth2AccessTokenDO.getAccessToken()) && Boolean.FALSE.equals(oAuth2AccessTokenDO.getIsSupAdmin())) { + // 用户id匹配、非当次登录token、非超管 -> 删除redis中的token缓存 + oauth2AccessTokenRedisDAO.deleteWithKey(key); + } + } + } + } + private static String generateAccessToken() {