diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/service/PigxTokenDealServiceImpl.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/service/PigxTokenDealServiceImpl.java index 8237a9c7b..737aa79b7 100644 --- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/service/PigxTokenDealServiceImpl.java +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/service/PigxTokenDealServiceImpl.java @@ -78,12 +78,12 @@ public class PigxTokenDealServiceImpl { */ public R queryTokenByUsername(Page page, String username) { String key = keyStrResolver.extract( - SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_PREFIX + "uname_to_access:", StrUtil.COLON); + SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_PREFIX + "uname_to_access_z:", StrUtil.COLON); Object collect = redisTemplate.keys("*:" + username).stream().filter(k -> ((String) k).contains(key)) .flatMap(k -> { redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer()); - return redisTemplate.opsForSet().members(k).stream(); + return redisTemplate.opsForZSet().range(k, 0, System.currentTimeMillis()).stream(); }).collect(Collectors.toList()); if (collect instanceof List) { diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxRedisTokenStore.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxRedisTokenStore.java new file mode 100644 index 000000000..a15f36213 --- /dev/null +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxRedisTokenStore.java @@ -0,0 +1,539 @@ +package com.pig4cloud.pigx.common.security.component; + +import cn.hutool.core.util.StrUtil; +import com.pig4cloud.pigx.common.core.util.KeyStrResolver; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisZSetCommands; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.OAuth2RefreshToken; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator; +import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.provider.token.store.redis.JdkSerializationStrategy; +import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; +import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStoreSerializationStrategy; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Method; +import java.util.*; + +/** + * @author efenderbosch + * @date 2020/9/30 + *

+ * @link https://github.com/spring-projects/spring-security-oauth/pull/1660 + * 重写RedisTokenStore ,主要解决 #1814 oauth2中client_id_to_access数据膨胀问题 + */ +public class PigxRedisTokenStore implements TokenStore { + + private static final String ACCESS = "access:"; + + private static final String AUTH_TO_ACCESS = "auth_to_access:"; + + private static final String AUTH = "auth:"; + + private static final String REFRESH_AUTH = "refresh_auth:"; + + private static final String REFRESH = "refresh:"; + + private static final String REFRESH_TO_ACCESS = "refresh_to_access:"; + + private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access_z:"; + + private static final String UNAME_TO_ACCESS = "uname_to_access_z:"; + + private static final boolean springDataRedis_2_0 = ClassUtils.isPresent( + "org.springframework.data.redis.connection.RedisStandaloneConfiguration", + RedisTokenStore.class.getClassLoader()); + + private final RedisConnectionFactory connectionFactory; + + private final KeyStrResolver keyStrResolver; + + private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator(); + + private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy(); + + private String prefix = ""; + + private Method redisConnectionSet_2_0; + + public PigxRedisTokenStore(RedisConnectionFactory connectionFactory, KeyStrResolver resolver) { + this.keyStrResolver = resolver; + this.connectionFactory = connectionFactory; + if (springDataRedis_2_0) { + this.loadRedisConnectionMethods_2_0(); + } + } + + public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) { + this.authenticationKeyGenerator = authenticationKeyGenerator; + } + + public void setSerializationStrategy(RedisTokenStoreSerializationStrategy serializationStrategy) { + this.serializationStrategy = serializationStrategy; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + private void loadRedisConnectionMethods_2_0() { + this.redisConnectionSet_2_0 = ReflectionUtils.findMethod(RedisConnection.class, "set", byte[].class, + byte[].class); + } + + private RedisConnection getConnection() { + return connectionFactory.getConnection(); + } + + private byte[] serialize(Object object) { + return serializationStrategy.serialize(object); + } + + private byte[] serializeKey(String object) { + return serialize(keyStrResolver.extract((prefix + object), StrUtil.COLON)); + } + + private OAuth2AccessToken deserializeAccessToken(byte[] bytes) { + return serializationStrategy.deserialize(bytes, OAuth2AccessToken.class); + } + + private OAuth2Authentication deserializeAuthentication(byte[] bytes) { + return serializationStrategy.deserialize(bytes, OAuth2Authentication.class); + } + + private OAuth2RefreshToken deserializeRefreshToken(byte[] bytes) { + return serializationStrategy.deserialize(bytes, OAuth2RefreshToken.class); + } + + private byte[] serialize(String string) { + return serializationStrategy.serialize(string); + } + + private String deserializeString(byte[] bytes) { + return serializationStrategy.deserializeString(bytes); + } + + @Override + public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) { + String key = authenticationKeyGenerator.extractKey(authentication); + byte[] serializedKey = serializeKey(AUTH_TO_ACCESS + key); + byte[] bytes = null; + RedisConnection conn = getConnection(); + try { + bytes = conn.get(serializedKey); + } + finally { + conn.close(); + } + OAuth2AccessToken accessToken = deserializeAccessToken(bytes); + if (accessToken != null) { + OAuth2Authentication storedAuthentication = readAuthentication(accessToken.getValue()); + if ((storedAuthentication == null + || !key.equals(authenticationKeyGenerator.extractKey(storedAuthentication)))) { + // Keep the stores consistent (maybe the same user is + // represented by this authentication but the details have + // changed) + storeAccessToken(accessToken, authentication); + } + + } + return accessToken; + } + + @Override + public OAuth2Authentication readAuthentication(OAuth2AccessToken token) { + return readAuthentication(token.getValue()); + } + + @Override + public OAuth2Authentication readAuthentication(String token) { + byte[] bytes; + RedisConnection conn = getConnection(); + try { + bytes = conn.get(serializeKey(AUTH + token)); + } + finally { + conn.close(); + } + OAuth2Authentication auth = deserializeAuthentication(bytes); + return auth; + } + + @Override + public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) { + return readAuthenticationForRefreshToken(token.getValue()); + } + + public OAuth2Authentication readAuthenticationForRefreshToken(String token) { + RedisConnection conn = getConnection(); + try { + byte[] bytes = conn.get(serializeKey(REFRESH_AUTH + token)); + OAuth2Authentication auth = deserializeAuthentication(bytes); + return auth; + } + finally { + conn.close(); + } + } + + @Override + public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { + byte[] serializedAccessToken = serialize(token); + byte[] serializedAuth = serialize(authentication); + byte[] accessKey = serializeKey(ACCESS + token.getValue()); + byte[] authKey = serializeKey(AUTH + token.getValue()); + byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication)); + byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication)); + byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId()); + + RedisConnection conn = getConnection(); + try { + conn.openPipeline(); + if (springDataRedis_2_0) { + try { + this.redisConnectionSet_2_0.invoke(conn, accessKey, serializedAccessToken); + this.redisConnectionSet_2_0.invoke(conn, authKey, serializedAuth); + this.redisConnectionSet_2_0.invoke(conn, authToAccessKey, serializedAccessToken); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + else { + conn.set(accessKey, serializedAccessToken); + conn.set(authKey, serializedAuth); + conn.set(authToAccessKey, serializedAccessToken); + } + + if (token.getExpiration() != null) { + int seconds = token.getExpiresIn(); + long expirationTime = token.getExpiration().getTime(); + + if (!authentication.isClientOnly()) { + conn.zAdd(approvalKey, expirationTime, serializedAccessToken); + } + conn.zAdd(clientId, expirationTime, serializedAccessToken); + + conn.expire(accessKey, seconds); + conn.expire(authKey, seconds); + conn.expire(authToAccessKey, seconds); + conn.expire(clientId, seconds); + conn.expire(approvalKey, seconds); + } + else { + conn.zAdd(clientId, -1, serializedAccessToken); + if (!authentication.isClientOnly()) { + conn.zAdd(approvalKey, -1, serializedAccessToken); + } + } + OAuth2RefreshToken refreshToken = token.getRefreshToken(); + if (refreshToken != null && refreshToken.getValue() != null) { + byte[] refresh = serialize(token.getRefreshToken().getValue()); + byte[] auth = serialize(token.getValue()); + byte[] refreshToAccessKey = serializeKey(REFRESH_TO_ACCESS + token.getRefreshToken().getValue()); + if (springDataRedis_2_0) { + try { + this.redisConnectionSet_2_0.invoke(conn, refreshToAccessKey, auth); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + else { + conn.set(refreshToAccessKey, auth); + } + if (refreshToken instanceof ExpiringOAuth2RefreshToken) { + ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken; + Date expiration = expiringRefreshToken.getExpiration(); + if (expiration != null) { + int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L) + .intValue(); + conn.expire(refreshToAccessKey, seconds); + } + } + } + conn.closePipeline(); + } + finally { + conn.close(); + } + } + + private static String getApprovalKey(OAuth2Authentication authentication) { + String userName = authentication.getUserAuthentication() == null ? "" + : authentication.getUserAuthentication().getName(); + return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName); + } + + private static String getApprovalKey(String clientId, String userName) { + return clientId + (userName == null ? "" : ":" + userName); + } + + @Override + public void removeAccessToken(OAuth2AccessToken accessToken) { + removeAccessToken(accessToken.getValue()); + } + + @Override + public OAuth2AccessToken readAccessToken(String tokenValue) { + byte[] key = serializeKey(ACCESS + tokenValue); + byte[] bytes = null; + RedisConnection conn = getConnection(); + try { + bytes = conn.get(key); + } + finally { + conn.close(); + } + OAuth2AccessToken accessToken = deserializeAccessToken(bytes); + return accessToken; + } + + public void removeAccessToken(String tokenValue) { + byte[] accessKey = serializeKey(ACCESS + tokenValue); + byte[] authKey = serializeKey(AUTH + tokenValue); + RedisConnection conn = getConnection(); + try { + conn.openPipeline(); + conn.get(accessKey); + conn.get(authKey); + conn.del(accessKey); + // Don't remove the refresh token - it's up to the caller to do that + conn.del(authKey); + List results = conn.closePipeline(); + byte[] access = (byte[]) results.get(0); + byte[] auth = (byte[]) results.get(1); + + OAuth2Authentication authentication = deserializeAuthentication(auth); + if (authentication != null) { + String key = authenticationKeyGenerator.extractKey(authentication); + byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + key); + byte[] unameKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication)); + byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId()); + conn.openPipeline(); + conn.del(authToAccessKey); + conn.zRem(unameKey, access); + conn.zRem(clientId, access); + conn.del(serialize(ACCESS + key)); + conn.closePipeline(); + } + } + finally { + conn.close(); + } + } + + @Override + public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) { + byte[] refreshKey = serializeKey(REFRESH + refreshToken.getValue()); + byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + refreshToken.getValue()); + byte[] serializedRefreshToken = serialize(refreshToken); + RedisConnection conn = getConnection(); + try { + conn.openPipeline(); + if (springDataRedis_2_0) { + try { + this.redisConnectionSet_2_0.invoke(conn, refreshKey, serializedRefreshToken); + this.redisConnectionSet_2_0.invoke(conn, refreshAuthKey, serialize(authentication)); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + else { + conn.set(refreshKey, serializedRefreshToken); + conn.set(refreshAuthKey, serialize(authentication)); + } + if (refreshToken instanceof ExpiringOAuth2RefreshToken) { + ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken; + Date expiration = expiringRefreshToken.getExpiration(); + if (expiration != null) { + int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L).intValue(); + conn.expire(refreshKey, seconds); + conn.expire(refreshAuthKey, seconds); + } + } + conn.closePipeline(); + } + finally { + conn.close(); + } + } + + @Override + public OAuth2RefreshToken readRefreshToken(String tokenValue) { + byte[] key = serializeKey(REFRESH + tokenValue); + byte[] bytes = null; + RedisConnection conn = getConnection(); + try { + bytes = conn.get(key); + } + finally { + conn.close(); + } + OAuth2RefreshToken refreshToken = deserializeRefreshToken(bytes); + return refreshToken; + } + + @Override + public void removeRefreshToken(OAuth2RefreshToken refreshToken) { + removeRefreshToken(refreshToken.getValue()); + } + + public void removeRefreshToken(String tokenValue) { + byte[] refreshKey = serializeKey(REFRESH + tokenValue); + byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + tokenValue); + byte[] refresh2AccessKey = serializeKey(REFRESH_TO_ACCESS + tokenValue); + RedisConnection conn = getConnection(); + try { + conn.openPipeline(); + conn.del(refreshKey); + conn.del(refreshAuthKey); + conn.del(refresh2AccessKey); + conn.closePipeline(); + } + finally { + conn.close(); + } + } + + @Override + public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) { + removeAccessTokenUsingRefreshToken(refreshToken.getValue()); + } + + private void removeAccessTokenUsingRefreshToken(String refreshToken) { + byte[] key = serializeKey(REFRESH_TO_ACCESS + refreshToken); + List results = null; + RedisConnection conn = getConnection(); + try { + conn.openPipeline(); + conn.get(key); + conn.del(key); + results = conn.closePipeline(); + } + finally { + conn.close(); + } + byte[] bytes = (byte[]) results.get(0); + String accessToken = deserializeString(bytes); + if (accessToken != null) { + removeAccessToken(accessToken); + } + } + + private List getZByteLists(byte[] key, RedisConnection conn) { + // Sorted Set expiration maintenance + long currentTime = System.currentTimeMillis(); + conn.zRemRangeByScore(key, 0, currentTime); + + List byteList; + Long size = conn.zCard(key); + byteList = new ArrayList<>(size.intValue()); + Cursor cursor = conn.zScan(key, ScanOptions.NONE); + + while (cursor.hasNext()) { + RedisZSetCommands.Tuple t = cursor.next(); + + // Probably not necessary because of the maintenance at the beginning but why + // not... + if (t.getScore() == -1 || t.getScore() > currentTime) { + byteList.add(t.getValue()); + } + } + return byteList; + } + + /** + * Runs a maintenance of the RedisTokenStore. + *

+ * SortedSets UNAME_TO_ACCESS and CLIENT_ID_TO_ACCESS contains access tokens that can + * expire. This expiration is set as a score of the Redis SortedSet data structure. + * Redis does not support expiration of items in a container data structure. It + * supports only expiration of whole key. In case there is still new access tokens + * being stored into the RedisTokenStore before whole key gets expired, the expiration + * is prolonged and the key is not effectively deleted. To do "garbage collection" + * this method should be called once upon a time. + * @return how many items were removed + */ + public long doMaintenance() { + long removed = 0; + RedisConnection conn = getConnection(); + try { + // client_id_to_acccess maintenance + Cursor clientToAccessKeys = conn.scan(ScanOptions.scanOptions() + .match(keyStrResolver.extract(prefix + CLIENT_ID_TO_ACCESS + "*", StrUtil.COLON)).build()); + while (clientToAccessKeys.hasNext()) { + byte[] clientIdToAccessKey = clientToAccessKeys.next(); + + removed += conn.zRemRangeByScore(clientIdToAccessKey, 0, System.currentTimeMillis()); + } + + // uname_to_access maintenance + Cursor unameToAccessKeys = conn.scan(ScanOptions.scanOptions() + .match(keyStrResolver.extract(prefix + UNAME_TO_ACCESS + "*", StrUtil.COLON)).build()); + while (unameToAccessKeys.hasNext()) { + byte[] unameToAccessKey = unameToAccessKeys.next(); + + removed += conn.zRemRangeByScore(unameToAccessKey, 0, System.currentTimeMillis()); + } + } + finally { + conn.close(); + } + return removed; + } + + @Override + public Collection findTokensByClientIdAndUserName(String clientId, String userName) { + byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(clientId, userName)); + List byteList; + RedisConnection conn = getConnection(); + try { + byteList = getZByteLists(approvalKey, conn); + } + finally { + conn.close(); + } + if (byteList.size() == 0) { + return Collections.emptySet(); + } + List accessTokens = new ArrayList<>(byteList.size()); + for (byte[] bytes : byteList) { + OAuth2AccessToken accessToken = deserializeAccessToken(bytes); + accessTokens.add(accessToken); + } + return Collections.unmodifiableCollection(accessTokens); + } + + @Override + public Collection findTokensByClientId(String clientId) { + byte[] key = serializeKey(CLIENT_ID_TO_ACCESS + clientId); + List byteList = null; + RedisConnection conn = getConnection(); + try { + byteList = getZByteLists(key, conn); + } + finally { + conn.close(); + } + if (byteList.size() == 0) { + return Collections.emptySet(); + } + List accessTokens = new ArrayList<>(byteList.size()); + for (byte[] bytes : byteList) { + OAuth2AccessToken accessToken = deserializeAccessToken(bytes); + accessTokens.add(accessToken); + } + return Collections.unmodifiableCollection(accessTokens); + } + +} diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxTokenStoreAutoConfiguration.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxTokenStoreAutoConfiguration.java index 35a8584e8..926b29324 100644 --- a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxTokenStoreAutoConfiguration.java +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxTokenStoreAutoConfiguration.java @@ -10,7 +10,6 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator; import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; /** * @author lengleng @@ -28,16 +27,12 @@ public class PigxTokenStoreAutoConfiguration { @Bean public TokenStore tokenStore() { - RedisTokenStore tokenStore = new RedisTokenStore(connectionFactory); - tokenStore.setPrefix( - resolver.extract(SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_PREFIX, StrUtil.COLON)); + PigxRedisTokenStore tokenStore = new PigxRedisTokenStore(connectionFactory, resolver); + tokenStore.setPrefix(SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_PREFIX); tokenStore.setAuthenticationKeyGenerator(new DefaultAuthenticationKeyGenerator() { @Override public String extractKey(OAuth2Authentication authentication) { - if (resolver == null) { - return super.extractKey(authentication); - } // 增加租户隔离部分 租户ID:原生计算值 return resolver.extract(super.extractKey(authentication), StrUtil.COLON); }