feat(security): 重构Redis存储的OAuth2授权服务

- 将PigxRedisOAuth2AuthorizationService迁移至common-security模块
- 使用RedisTemplate替代RedisUtils进行缓存操作
- 增加Jackson序列化支持RouteDefinitionVo
- 优化token存储逻辑与过期时间处理
- 统一授权服务与路由管理的Redis配置
This commit is contained in:
PIG AI committed 2025-07-04 12:07:00 +08:00
1 parent d392f2f03a
commit c8edf16536
5 files changed
+57 -38

No files matched your search

@@ -2,8 +2,8 @@ package com.pig4cloud.pigx.auth.support.base;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.pig4cloud.pigx.auth.support.store.PigxRedisOAuth2AuthorizationService;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
import com.pig4cloud.pigx.common.security.service.PigxRedisOAuth2AuthorizationService;
import com.pig4cloud.pigx.common.security.util.OAuth2ErrorCodesExpand;
import com.pig4cloud.pigx.common.security.util.ScopeException;
import lombok.extern.slf4j.Slf4j;
@@ -1,37 +1,41 @@
package com.pig4cloud.pigx.auth.support.store;
package com.pig4cloud.pigx.common.security.service;
import com.pig4cloud.pigx.common.data.cache.RedisUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
@Service
import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
public class PigxRedisOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
private final RedisTemplate<String, Object> redisTemplate;
private final static Long TIMEOUT = 10L;
@Override
public void save(OAuth2AuthorizationConsent authorizationConsent) {
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
RedisUtils.set(buildKey(authorizationConsent), authorizationConsent, TIMEOUT * 60);
redisTemplate.opsForValue()
.set(buildKey(authorizationConsent), authorizationConsent, TIMEOUT, TimeUnit.MINUTES);
}
@Override
public void remove(OAuth2AuthorizationConsent authorizationConsent) {
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
RedisUtils.delete(buildKey(authorizationConsent));
redisTemplate.delete(buildKey(authorizationConsent));
}
@Override
public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
return RedisUtils.get(buildKey(registeredClientId, principalName));
return (OAuth2AuthorizationConsent) redisTemplate.opsForValue()
.get(buildKey(registeredClientId, principalName));
}
private static String buildKey(String registeredClientId, String principalName) {
@@ -1,10 +1,10 @@
package com.pig4cloud.pigx.auth.support.store;
package com.pig4cloud.pigx.common.security.service;
import cn.hutool.core.collection.CollUtil;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.data.cache.RedisUtils;
import com.pig4cloud.pigx.common.security.service.PigxUser;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
@@ -15,7 +15,6 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import java.security.Principal;
@@ -24,14 +23,12 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* 基于Redis实现的OAuth2授权服务类,提供授权信息的存储、查询和删除功能
*
* @author lengleng
* @date 2025/06/30
* @date 2022/5/27
*/
@Service
@RequiredArgsConstructor
public class PigxRedisOAuth2AuthorizationService implements OAuth2AuthorizationService {
@@ -39,6 +36,8 @@ public class PigxRedisOAuth2AuthorizationService implements OAuth2AuthorizationS
private static final String AUTHORIZATION = "token";
private final RedisTemplate<String, Object> redisTemplate;
private static boolean isState(OAuth2Authorization authorization) {
return Objects.nonNull(authorization.getAttribute("state"));
}
@@ -63,7 +62,9 @@ public class PigxRedisOAuth2AuthorizationService implements OAuth2AuthorizationS
if (isState(authorization)) {
String token = authorization.getAttribute("state");
RedisUtils.set(buildKey(OAuth2ParameterNames.STATE, token), authorization, TIMEOUT * 60);
redisTemplate.setValueSerializer(RedisSerializer.java());
redisTemplate.opsForValue()
.set(buildKey(OAuth2ParameterNames.STATE, token), authorization, TIMEOUT, TimeUnit.MINUTES);
}
if (isCode(authorization)) {
@@ -72,20 +73,28 @@ public class PigxRedisOAuth2AuthorizationService implements OAuth2AuthorizationS
OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
long between = ChronoUnit.MINUTES.between(authorizationCodeToken.getIssuedAt(),
authorizationCodeToken.getExpiresAt());
RedisUtils.set(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()), authorization,
(int) (between * 60));
redisTemplate.setValueSerializer(RedisSerializer.java());
redisTemplate.opsForValue()
.set(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()), authorization,
between, TimeUnit.MINUTES);
}
if (isRefreshToken(authorization)) {
OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
long between = ChronoUnit.SECONDS.between(refreshToken.getIssuedAt(), refreshToken.getExpiresAt());
RedisUtils.set(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()), authorization, (int) between);
redisTemplate.setValueSerializer(RedisSerializer.java());
redisTemplate.opsForValue()
.set(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()), authorization, between,
TimeUnit.SECONDS);
}
if (isAccessToken(authorization)) {
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
long between = ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt());
RedisUtils.set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()), authorization, (int) between);
redisTemplate.setValueSerializer(RedisSerializer.java());
redisTemplate.opsForValue()
.set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()), authorization, between,
TimeUnit.SECONDS);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = authorization.getAttribute(Principal.class.getName());
@@ -93,7 +102,7 @@ public class PigxRedisOAuth2AuthorizationService implements OAuth2AuthorizationS
// 扩展记录 access-token 、username 的关系 token::username::admin::tenantId::xxx
String tokenUsername = String.format("%s::%s::%s::%s::%s::%s", AUTHORIZATION,
SecurityConstants.DETAILS_USERNAME, authorization.getPrincipalName(), authorization.getRegisteredClientId(), pigxUser.getTenantId(), accessToken.getTokenValue());
RedisUtils.set(tokenUsername, accessToken.getTokenValue(), (int) between);
redisTemplate.opsForValue().set(tokenUsername, accessToken.getTokenValue(), between, TimeUnit.SECONDS);
}
}
}
@@ -130,7 +139,7 @@ public class PigxRedisOAuth2AuthorizationService implements OAuth2AuthorizationS
keys.add(key);
}
RedisUtils.delete(keys.toArray(new String[0]));
redisTemplate.delete(keys);
}
@Override
@@ -143,7 +152,9 @@ public class PigxRedisOAuth2AuthorizationService implements OAuth2AuthorizationS
@Nullable
public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
Assert.hasText(token, "token cannot be empty");
return RedisUtils.get(buildKey(Objects.requireNonNullElse(tokenType, OAuth2TokenType.ACCESS_TOKEN).getValue(), token));
redisTemplate.setValueSerializer(RedisSerializer.java());
return (OAuth2Authorization) redisTemplate.opsForValue()
.get(buildKey(Objects.requireNonNullElse(tokenType, OAuth2TokenType.ACCESS_TOKEN).getValue(), token));
}
private String buildKey(String type, String id) {
@@ -162,16 +173,16 @@ public class PigxRedisOAuth2AuthorizationService implements OAuth2AuthorizationS
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
String tokenUsernameKey = String.format("%s::%s::%s::*", AUTHORIZATION,
SecurityConstants.DETAILS_USERNAME, authenticationName);
Set<String> keys = RedisUtils.keys(tokenUsernameKey);
Set<String> keys = redisTemplate.keys(tokenUsernameKey);
if (CollUtil.isEmpty(keys)) {
return;
}
List<String> tokenList = RedisUtils.multiGet(keys.stream().toList());
List<Object> tokenList = redisTemplate.opsForValue().multiGet(keys);
for (String token : tokenList) {
for (Object token : tokenList) {
// 根据token 查询存储的 OAuth2Authorization
OAuth2Authorization authorization = this.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
OAuth2Authorization authorization = this.findByToken((String) token, OAuth2TokenType.ACCESS_TOKEN);
// 根据 OAuth2Authorization 删除相关令牌
this.remove(authorization);
}
@@ -2,6 +2,8 @@ com.pig4cloud.pigx.common.security.service.PigxDefaultUserDetailsServiceImpl
com.pig4cloud.pigx.common.security.service.PigxTocDefaultUserDetailsServiceImpl
com.pig4cloud.pigx.common.security.service.PigxMobileUserDetailServiceImpl
com.pig4cloud.pigx.common.security.service.PigxTocMobileUserDetailsServiceImpl
com.pig4cloud.pigx.common.security.service.PigxRedisOAuth2AuthorizationService
com.pig4cloud.pigx.common.security.service.PigxRedisOAuth2AuthorizationConsentService
com.pig4cloud.pigx.common.security.component.PigxSecurityInnerAspect
com.pig4cloud.pigx.common.security.component.PigxSecurityMessageSourceConfiguration
com.pig4cloud.pigx.common.security.service.PigxRemoteRegisteredClientRepository
@@ -21,7 +21,6 @@ import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.pig4cloud.pigx.admin.service.SysRouteConfService;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.data.cache.RedisUtils;
import com.pig4cloud.pigx.common.gateway.support.DynamicRouteInitEvent;
import com.pig4cloud.pigx.common.gateway.vo.RouteDefinitionVo;
import lombok.RequiredArgsConstructor;
@@ -34,9 +33,11 @@ import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.scheduling.annotation.Async;
import org.springframework.transaction.event.TransactionalEventListener;
@@ -44,10 +45,10 @@ import java.net.URI;
import java.util.Map;
/**
* 动态路由初始化运行器:容器启动后保存配置文件里面的路由信息到Redis
*
* @author lengleng
* @date 2018/10/31
* <p>
* 容器启动后保存配置文件里面的路由信息到Redis
*/
@Slf4j
@Configuration
@@ -55,6 +56,8 @@ import java.util.Map;
@ConditionalOnDiscoveryEnabled
public class DynamicRouteInitRunner implements InitializingBean {
private final RedisTemplate<String, String> redisTemplate;
private final SysRouteConfService routeConfService;
private final RedisMessageListenerContainer listenerContainer;
@@ -73,7 +76,7 @@ public class DynamicRouteInitRunner implements InitializingBean {
@Order
@TransactionalEventListener({DynamicRouteInitEvent.class})
public void initRoute() {
RedisUtils.delete(CacheConstants.ROUTE_KEY);
redisTemplate.delete(CacheConstants.ROUTE_KEY);
log.info("开始初始化网关路由");
routeConfService.list().forEach(route -> {
@@ -90,14 +93,13 @@ public class DynamicRouteInitRunner implements InitializingBean {
vo.setMetadata(JSONUtil.toBean(route.getMetadata(), Map.class));
log.info("加载路由ID:{},{}", route.getRouteId(), vo);
RedisUtils.hset(CacheConstants.ROUTE_KEY, route.getRouteId(), vo);
redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(RouteDefinitionVo.class));
HashOperations<String, String, RouteDefinitionVo> stringStringValueOperations = redisTemplate.opsForHash();
stringStringValueOperations.put(CacheConstants.ROUTE_KEY, route.getRouteId(), vo);
});
// 通知网关重置路由
RedisUtils.execute((RedisCallback<Void>) connection -> {
connection.publish(CacheConstants.ROUTE_JVM_RELOAD_TOPIC.getBytes(), "路由信息,网关缓存更新".getBytes());
return null;
});
redisTemplate.convertAndSend(CacheConstants.ROUTE_JVM_RELOAD_TOPIC, "路由信息,网关缓存更新");
log.debug("初始化网关路由结束 ");
}