From f238ee120726cb2c6aa5039f998e66ed5c8a3ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B7=E5=86=B7?= Date: Thu, 3 Sep 2020 21:36:20 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20Introducing=20new=20features.=20#1?= =?UTF-8?q?620=20=E3=80=90oauth2=E3=80=91=E6=89=A9=E5=B1=95=20RedisTokenSt?= =?UTF-8?q?ore=20=E8=AE=B0=E5=BD=95=E7=94=A8=E6=88=B7=E5=90=8D=20=E5=92=8C?= =?UTF-8?q?token=20=E5=85=B3=E7=B3=BB=EF=BC=8C=E6=96=B9=E4=BE=BF=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=89=A9=E5=B1=95=E8=87=AA=E5=B7=B1=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/AuthorizationServerConfig.java | 35 +- .../pigx/auth/endpoint/PigxTokenEndpoint.java | 93 +--- .../auth/service/PigxRedisTokenStore.java | 502 ++++++++++++++++++ .../service/PigxTokenDealServiceImpl.java | 156 ++++++ .../common/core/constant/CacheConstants.java | 4 +- .../core/constant/SecurityConstants.java | 5 + 6 files changed, 690 insertions(+), 105 deletions(-) create mode 100644 pigx-auth/src/main/java/com/pig4cloud/pigx/auth/service/PigxRedisTokenStore.java create mode 100644 pigx-auth/src/main/java/com/pig4cloud/pigx/auth/service/PigxTokenDealServiceImpl.java diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/config/AuthorizationServerConfig.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/config/AuthorizationServerConfig.java index 1e13a359d..f4d04a963 100644 --- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/config/AuthorizationServerConfig.java +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/config/AuthorizationServerConfig.java @@ -20,14 +20,13 @@ package com.pig4cloud.pigx.auth.config; import cn.hutool.core.util.StrUtil; +import com.pig4cloud.pigx.auth.service.PigxRedisTokenStore; import com.pig4cloud.pigx.common.core.constant.SecurityConstants; import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder; import com.pig4cloud.pigx.common.security.component.PigxWebResponseExceptionTranslator; import lombok.AllArgsConstructor; import lombok.SneakyThrows; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.userdetails.UserDetailsService; @@ -41,8 +40,6 @@ import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices; import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator; import org.springframework.security.oauth2.provider.token.TokenEnhancer; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; /** * @author lengleng @@ -57,12 +54,12 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap private final AuthenticationManager authenticationManagerBean; - private final RedisConnectionFactory redisConnectionFactory; - private final UserDetailsService pigxUserDetailsService; private final AuthorizationCodeServices authorizationCodeServices; + private final PigxRedisTokenStore redisTokenStore; + private final TokenEnhancer pigxTokenEnhancer; @Override @@ -78,24 +75,22 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { - endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST).tokenStore(tokenStore()) + // 设置tokenStore 前缀相关 + String prefix = String.format(":%s%s", SecurityConstants.PIGX_PREFIX, SecurityConstants.OAUTH_PREFIX); + redisTokenStore.setPrefix(prefix); + // 设置区分key 条件 + redisTokenStore.setAuthenticationKeyGenerator(new DefaultAuthenticationKeyGenerator() { + @Override + public String extractKey(OAuth2Authentication authentication) { + return super.extractKey(authentication) + StrUtil.COLON + TenantContextHolder.getTenantId(); + } + }); + + endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST).tokenStore(redisTokenStore) .tokenEnhancer(pigxTokenEnhancer).userDetailsService(pigxUserDetailsService) .authorizationCodeServices(authorizationCodeServices).authenticationManager(authenticationManagerBean) .reuseRefreshTokens(false).pathMapping("/oauth/confirm_access", "/token/confirm_access") .exceptionTranslator(new PigxWebResponseExceptionTranslator()); } - @Bean - public TokenStore tokenStore() { - RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory); - tokenStore.setPrefix(SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_PREFIX); - tokenStore.setAuthenticationKeyGenerator(new DefaultAuthenticationKeyGenerator() { - @Override - public String extractKey(OAuth2Authentication authentication) { - return super.extractKey(authentication) + StrUtil.COLON + TenantContextHolder.getTenantId(); - } - }); - return tokenStore; - } - } diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/PigxTokenEndpoint.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/PigxTokenEndpoint.java index da534e297..907543e1b 100644 --- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/PigxTokenEndpoint.java +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/PigxTokenEndpoint.java @@ -22,39 +22,23 @@ package com.pig4cloud.pigx.auth.endpoint; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.pig4cloud.pigx.common.core.constant.CacheConstants; +import com.pig4cloud.pigx.auth.service.PigxTokenDealServiceImpl; import com.pig4cloud.pigx.common.core.constant.PaginationConstants; -import com.pig4cloud.pigx.common.core.constant.SecurityConstants; import com.pig4cloud.pigx.common.core.util.R; -import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder; import com.pig4cloud.pigx.common.security.annotation.Inner; import com.pig4cloud.pigx.common.security.util.SecurityUtils; -import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.CacheManager; -import org.springframework.data.redis.core.ConvertingCursor; -import org.springframework.data.redis.core.Cursor; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ScanOptions; -import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; -import org.springframework.data.redis.serializer.RedisSerializer; -import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.http.HttpHeaders; import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.security.oauth2.common.OAuth2RefreshToken; import org.springframework.security.oauth2.provider.AuthorizationRequest; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetailsService; -import org.springframework.security.oauth2.provider.OAuth2Authentication; -import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; import java.util.Map; /** @@ -63,21 +47,14 @@ import java.util.Map; */ @Slf4j @RestController -@AllArgsConstructor +@RequiredArgsConstructor @RequestMapping("/token") public class PigxTokenEndpoint { - private static final String PIGX_OAUTH_ACCESS = SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_PREFIX - + "auth_to_access:"; + private final PigxTokenDealServiceImpl dealService; private final ClientDetailsService clientDetailsService; - private final RedisTemplate redisTemplate; - - private final TokenStore tokenStore; - - private final CacheManager cacheManager; - /** * 认证页面 * @param modelAndView @@ -137,22 +114,7 @@ public class PigxTokenEndpoint { @Inner @DeleteMapping("/{token}") public R delToken(@PathVariable("token") String token) { - OAuth2AccessToken accessToken = tokenStore.readAccessToken(token); - if (accessToken == null || StrUtil.isBlank(accessToken.getValue())) { - return R.ok(Boolean.TRUE, "退出失败,token 无效"); - } - - OAuth2Authentication auth2Authentication = tokenStore.readAuthentication(accessToken); - // 清空用户信息 - cacheManager.getCache(CacheConstants.USER_DETAILS).evict(auth2Authentication.getName()); - - // 清空access token - tokenStore.removeAccessToken(accessToken); - - // 清空 refresh token - OAuth2RefreshToken refreshToken = accessToken.getRefreshToken(); - tokenStore.removeRefreshToken(refreshToken); - return R.ok(); + return dealService.removeToken(token); } /** @@ -163,51 +125,16 @@ public class PigxTokenEndpoint { @Inner @PostMapping("/page") public R tokenList(@RequestBody Map params) { - // 根据分页参数获取对应数据 - String key = String.format("%s*:%s", PIGX_OAUTH_ACCESS, TenantContextHolder.getTenantId()); - List pages = findKeysForPage(key, MapUtil.getInt(params, PaginationConstants.CURRENT), - MapUtil.getInt(params, PaginationConstants.SIZE)); - - redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer()); Page result = new Page(MapUtil.getInt(params, PaginationConstants.CURRENT), MapUtil.getInt(params, PaginationConstants.SIZE)); - result.setRecords(redisTemplate.opsForValue().multiGet(pages)); - result.setTotal(redisTemplate.keys(key).size()); - return R.ok(result); - } - private List findKeysForPage(String patternKey, int pageNum, int pageSize) { - ScanOptions options = ScanOptions.scanOptions().count(1000L).match(patternKey).build(); - RedisSerializer redisSerializer = (RedisSerializer) redisTemplate.getKeySerializer(); - Cursor cursor = (Cursor) redisTemplate.executeWithStickyConnection( - redisConnection -> new ConvertingCursor<>(redisConnection.scan(options), redisSerializer::deserialize)); - List result = new ArrayList<>(); - int tmpIndex = 0; - int startIndex = (pageNum - 1) * pageSize; - int end = pageNum * pageSize; - - assert cursor != null; - while (cursor.hasNext()) { - if (tmpIndex >= startIndex && tmpIndex < end) { - result.add(cursor.next().toString()); - tmpIndex++; - continue; - } - if (tmpIndex >= end) { - break; - } - tmpIndex++; - cursor.next(); + // 根据username 查询 token 列表 + Object username = params.get("username"); + if (username != null) { + return dealService.queryTokenByUsername(result, (String) username); } - try { - cursor.close(); - } - catch (IOException e) { - log.error("关闭cursor 失败"); - } - return result; + return dealService.queryToken(result); } } diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/service/PigxRedisTokenStore.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/service/PigxRedisTokenStore.java new file mode 100644 index 000000000..28a725d95 --- /dev/null +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/service/PigxRedisTokenStore.java @@ -0,0 +1,502 @@ +package com.pig4cloud.pigx.auth.service; + +import com.pig4cloud.pigx.common.core.constant.SecurityConstants; +import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStringCommands; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.Expiration; +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.stereotype.Service; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Method; +import java.util.*; + +/** + * @author lengleng + * @date 2020/9/3 + *

+ * 重写默认tokenStore 保存 username and token 关系 + */ +@Service +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 ACCESS_TO_REFRESH = "access_to_refresh:"; + + 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:"; + + private static final String UNAME_TO_ACCESS = "uname_to_access:"; + + private static final boolean springDataRedis_2_0 = ClassUtils.isPresent( + "org.springframework.data.redis.connection.RedisStandaloneConfiguration", + RedisTokenStore.class.getClassLoader()); + + private final RedisConnectionFactory connectionFactory; + + private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator(); + + private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy(); + + private String prefix = ""; + + private Method redisConnectionSet_2_0; + + public PigxRedisTokenStore(RedisConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + if (springDataRedis_2_0) { + this.loadRedisConnectionMethods_2_0(); + } + } + + 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); + } + + 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); + } + + // 增加key 的多租户区分 + private byte[] serializeKey(String object) { + return serialize(TenantContextHolder.getTenantId() + prefix + object); + } + + 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; + 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 = null; + 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 (!authentication.isClientOnly()) { + conn.sAdd(approvalKey, serializedAccessToken); + } + conn.sAdd(clientId, serializedAccessToken); + if (token.getExpiration() != null) { + int seconds = token.getExpiresIn(); + conn.expire(accessKey, seconds); + conn.expire(authKey, seconds); + conn.expire(authToAccessKey, seconds); + conn.expire(clientId, seconds); + conn.expire(approvalKey, seconds); + } + 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()); + byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + token.getValue()); + if (springDataRedis_2_0) { + try { + this.redisConnectionSet_2_0.invoke(conn, refreshToAccessKey, auth); + this.redisConnectionSet_2_0.invoke(conn, accessToRefreshKey, refresh); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + else { + conn.set(refreshToAccessKey, auth); + conn.set(accessToRefreshKey, refresh); + } + 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.expire(accessToRefreshKey, seconds); + } + } + } + + // KEY + byte[] key = serializeKey(String.format("%s%s", SecurityConstants.TOKEN_PREFIX, authentication.getName())); + // value + byte[] tokenVal = serialize(token.getValue()); + RedisStringCommands stringCommand = conn.stringCommands(); + stringCommand.set(key, tokenVal, Expiration.seconds(token.getExpiresIn()), + RedisStringCommands.SetOption.SET_IF_ABSENT); + conn.closePipeline(); + } + finally { + conn.close(); + } + } + + @Override + public void removeAccessToken(OAuth2AccessToken accessToken) { + removeAccessToken(accessToken.getValue()); + } + + @Override + public OAuth2AccessToken readAccessToken(String tokenValue) { + byte[] key = serializeKey(ACCESS + tokenValue); + byte[] bytes; + 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); + byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + tokenValue); + RedisConnection conn = getConnection(); + try { + conn.openPipeline(); + conn.get(accessKey); + conn.get(authKey); + conn.del(accessKey); + conn.del(accessToRefreshKey); + // 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.sRem(unameKey, access); + conn.sRem(clientId, access); + conn.del(serialize(ACCESS + key)); + + // keyName + byte[] keyName = serializeKey( + String.format("%s%s", SecurityConstants.TOKEN_PREFIX, authentication.getName())); + // 获取redis连接 + conn.del(keyName); + 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); + byte[] access2RefreshKey = serializeKey(ACCESS_TO_REFRESH + tokenValue); + RedisConnection conn = getConnection(); + try { + conn.openPipeline(); + conn.del(refreshKey); + conn.del(refreshAuthKey); + conn.del(refresh2AccessKey); + conn.del(access2RefreshKey); + 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 getByteLists(byte[] approvalKey, RedisConnection conn) { + List byteList; + Long size = conn.sCard(approvalKey); + byteList = new ArrayList(size.intValue()); + Cursor cursor = conn.sScan(approvalKey, ScanOptions.NONE); + while (cursor.hasNext()) { + byteList.add(cursor.next()); + } + return byteList; + } + + @Override + public Collection findTokensByClientIdAndUserName(String clientId, String userName) { + byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(clientId, userName)); + List byteList; + RedisConnection conn = getConnection(); + try { + byteList = getByteLists(approvalKey, conn); + } + finally { + conn.close(); + } + if (byteList == null || 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; + RedisConnection conn = getConnection(); + try { + byteList = getByteLists(key, conn); + } + finally { + conn.close(); + } + if (byteList == null || 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-auth/src/main/java/com/pig4cloud/pigx/auth/service/PigxTokenDealServiceImpl.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/service/PigxTokenDealServiceImpl.java new file mode 100644 index 000000000..16702ec1e --- /dev/null +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/service/PigxTokenDealServiceImpl.java @@ -0,0 +1,156 @@ +package com.pig4cloud.pigx.auth.service; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.pig4cloud.pigx.common.core.constant.CacheConstants; +import com.pig4cloud.pigx.common.core.constant.SecurityConstants; +import com.pig4cloud.pigx.common.core.util.R; +import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.CacheManager; +import org.springframework.data.redis.core.ConvertingCursor; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +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.TokenStore; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * @author lengleng + * @date 2020/9/3 + *

+ * token 端点相关的业务处理 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PigxTokenDealServiceImpl { + + private final RedisTemplate redisTemplate; + + private final CacheManager cacheManager; + + private final TokenStore tokenStore; + + /** + * 删除 请求令牌 和 刷新令牌 + * @param token 请求令牌 + * @return R + */ + public R removeToken(String token) { + + OAuth2AccessToken accessToken = tokenStore.readAccessToken(token); + if (accessToken == null || StrUtil.isBlank(accessToken.getValue())) { + return R.ok(Boolean.TRUE, "退出失败,token 无效"); + } + + OAuth2Authentication auth2Authentication = tokenStore.readAuthentication(accessToken); + // 清空用户信息 + cacheManager.getCache(CacheConstants.USER_DETAILS).evict(auth2Authentication.getName()); + + // 清空access token + tokenStore.removeAccessToken(accessToken); + + // 清空 refresh token + OAuth2RefreshToken refreshToken = accessToken.getRefreshToken(); + tokenStore.removeRefreshToken(refreshToken); + return R.ok(); + } + + /** + * 根据用户名查询 令牌相关信息 + * @param page 分页信息 + * @param username 用户名 + * @return R + */ + public R queryTokenByUsername(Page page, String username) { + String key = String.format("%s:%s%s%s%s", TenantContextHolder.getTenantId(), SecurityConstants.PIGX_PREFIX, + SecurityConstants.OAUTH_PREFIX, SecurityConstants.TOKEN_PREFIX, username); + // 根据用户名查询token + redisTemplate.setValueSerializer(new StringRedisSerializer()); + Object token = redisTemplate.opsForValue().get(key); + + // 根据token 查询详细信息 + String keyVal = String.format("%s:%s%s%s%s", TenantContextHolder.getTenantId(), SecurityConstants.PIGX_PREFIX, + SecurityConstants.OAUTH_PREFIX, "access:", token); + + redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer()); + Object o = redisTemplate.opsForValue().get(keyVal); + + if (o != null) { + page.setRecords(CollUtil.newArrayList(o)); + page.setTotal(1L); + } + return R.ok(page); + } + + /** + * 分页查询token 列表 + * @param page page + * @return + */ + public R queryToken(Page page) { + // 根据分页参数获取对应数据 + String key = String.format("%s:%s%s%s*", TenantContextHolder.getTenantId(), SecurityConstants.PIGX_PREFIX, + SecurityConstants.OAUTH_PREFIX, "access:"); + List pages = findKeysForPage(key, page.getCurrent(), page.getSize()); + + redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer()); + page.setRecords(redisTemplate.opsForValue().multiGet(pages)); + page.setTotal(redisTemplate.keys(key).size()); + return R.ok(page); + } + + /** + * 使用游标 分页查询key + * @param patternKey key 通配符 + * @param pageNum 当前页 + * @param pageSize 每页大小 + * @return 符合的 value列表 + */ + private List findKeysForPage(String patternKey, long pageNum, long pageSize) { + ScanOptions options = ScanOptions.scanOptions().count(1000L).match(patternKey).build(); + RedisSerializer redisSerializer = (RedisSerializer) redisTemplate.getKeySerializer(); + Cursor cursor = (Cursor) redisTemplate.executeWithStickyConnection( + redisConnection -> new ConvertingCursor<>(redisConnection.scan(options), redisSerializer::deserialize)); + List result = new ArrayList<>(); + int tmpIndex = 0; + long startIndex = (pageNum - 1) * pageSize; + long end = pageNum * pageSize; + + assert cursor != null; + while (cursor.hasNext()) { + if (tmpIndex >= startIndex && tmpIndex < end) { + result.add(cursor.next().toString()); + tmpIndex++; + continue; + } + if (tmpIndex >= end) { + break; + } + tmpIndex++; + cursor.next(); + } + + try { + cursor.close(); + } + catch (IOException e) { + log.error("关闭cursor 失败"); + } + return result; + } + +} diff --git a/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/CacheConstants.java b/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/CacheConstants.java index 50ca9e408..688eb5be8 100644 --- a/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/CacheConstants.java +++ b/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/CacheConstants.java @@ -48,12 +48,12 @@ public interface CacheConstants { /** * spring boot admin 事件key */ - String EVENT_KEY = "event_key"; + String EVENT_KEY = GLOBALLY + "event_key"; /** * 路由存放 */ - String ROUTE_KEY = "gateway_route_key"; + String ROUTE_KEY = GLOBALLY + "gateway_route_key"; /** * redis reload 事件 diff --git a/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/SecurityConstants.java b/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/SecurityConstants.java index 3a9af36b4..13b1e8adc 100644 --- a/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/SecurityConstants.java +++ b/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/SecurityConstants.java @@ -50,6 +50,11 @@ public interface SecurityConstants { */ String PIGX_PREFIX = "pigx_"; + /** + * token 相关前缀 + */ + String TOKEN_PREFIX = "token:"; + /** * oauth 相关前缀 */