diff --git a/.gitee/ISSUE_TEMPLATE.zh-CN.md b/.gitee/ISSUE_TEMPLATE.zh-CN.md index 871dc68f..24087998 100644 --- a/.gitee/ISSUE_TEMPLATE.zh-CN.md +++ b/.gitee/ISSUE_TEMPLATE.zh-CN.md @@ -7,5 +7,4 @@ ### 回显步骤 - -### 报错信息 +### 报错信息 、截图 截图 !!! diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index bea10ed1..daa368b9 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -55,12 +55,3 @@ jobs: # 💤🤷‍♀️ failure 🙅‍♂️💣 [pig-mesh/pig](https://github.com/pig-mesh/pig) > Github Action: https://github.com/pig-mesh/pig failure > (⋟﹏⋞) from github action message - - uses: shaowenchen/debugger-action@v2 - if: github.repository == 'pig-mesh/pig' - name: debugger - continue-on-error: true - with: - frp_server_addr: ${{ secrets.FRP_SERVER_ADDR }} - frp_server_port: ${{ secrets.FRP_SERVER_PORT }} - frp_token: ${{ secrets.FRP_TOKEN }} - ssh_port: ${{ secrets.SSH_PORT }} diff --git a/.gitignore b/.gitignore index 6dcb5e26..877ad0ba 100755 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ target/ *.zip *.tar *.tar.gz +*.versionsBackup ### vscode ### .vscode diff --git a/LICENSE b/LICENSE index ceee3cb6..6220fa71 100644 --- a/LICENSE +++ b/LICENSE @@ -193,7 +193,7 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/README.md b/README.md index bb7c85ec..549c2a60 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,20 @@ - 提供对常见容器化支持 Docker、Kubernetes、Rancher2 支持 - 提供 lambda 、stream api 、webflux 的生产实践 +## 文档视频 + +[文档视频 wiki.pig4cloud.com](https://wiki.pig4cloud.com) + +[PIGX 在线体验 pigx.pig4cloud.com](http://pigx.pig4cloud.com) + +[产品白皮书 paper.pig4cloud.com](https://paper.pig4cloud.com) + +## 微信群 [禁广告] + +![1628762721](https://minio.pigx.vip/oss/1628762721.png) + +## 快速开始 + ### 核心依赖 | 依赖 | 版本 | @@ -22,7 +36,7 @@ | Spring Cloud Alibaba | 2021.1 | | Spring Security OAuth2 | 2.3.6 | | Mybatis Plus | 3.4.3.5 | -| hutool | 5.7.13 | +| hutool | 5.7.14 | | Avue | 2.6.18 | ### 模块说明 @@ -55,16 +69,6 @@ pig └── pig-xxl-job-admin -- 分布式定时任务管理台 [5004] ``` -## 文档视频 - -[文档视频 wiki.pig4cloud.com](https://wiki.pig4cloud.com) - -[PIGX 在线体验 pigx.pig4cloud.com](http://pigx.pig4cloud.com) - -[产品白皮书 paper.pig4cloud.com](https://paper.pig4cloud.com) - -## 快速开始 - ### 本地开发 运行 pig 提供了详细的[部署文档 wiki.pig4cloud.com](https://www.yuque.com/pig4cloud/pig/vsdox9),包括开发环境安装、服务端代码运行、前端代码运行等。 @@ -73,7 +77,8 @@ pig 提供了详细的[部署文档 wiki.pig4cloud.com](https://www.yuque.com/pi ### 定制自己微服务 -[PIG DIY](https://diy.pig4cloud.com) +[PIG DIY](https://diy.pig4cloud.com) + [PIG ARCHETYPE](https://archetype.pig4cloud.com) ### Docker 运行 @@ -105,11 +110,6 @@ npm run build:docker && docker-compose up -d -## 微信群 [禁广告] - -![1628762721](https://minio.pigx.vip/oss/1628762721.png) - - ## 开源共建 ### 开源协议 diff --git a/docker-compose.yml b/docker-compose.yml index eab049fa..c2cf8b68 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,7 @@ services: - 3306:3306 pig-redis: - image: redis:6.0 + image: redis:6.2.6 ports: - 6379:6379 restart: always @@ -90,4 +90,4 @@ services: hostname: pig-job image: pig-job ports: - - 5004:5004 \ No newline at end of file + - 5004:5004 diff --git a/pig-auth/pom.xml b/pig-auth/pom.xml index 70b2bbdd..762edf98 100755 --- a/pig-auth/pom.xml +++ b/pig-auth/pom.xml @@ -21,7 +21,7 @@ com.pig4cloud pig - 3.3.5 + 3.4.0 pig-auth diff --git a/pig-auth/src/main/java/com/pig4cloud/pig/auth/config/AuthorizationServerConfig.java b/pig-auth/src/main/java/com/pig4cloud/pig/auth/config/AuthorizationServerConfig.java index 0363506c..1ddee299 100755 --- a/pig-auth/src/main/java/com/pig4cloud/pig/auth/config/AuthorizationServerConfig.java +++ b/pig-auth/src/main/java/com/pig4cloud/pig/auth/config/AuthorizationServerConfig.java @@ -16,19 +16,19 @@ package com.pig4cloud.pig.auth.config; -import com.pig4cloud.pig.common.core.constant.CacheConstants; import com.pig4cloud.pig.common.core.constant.SecurityConstants; import com.pig4cloud.pig.common.security.component.PigWebResponseExceptionTranslator; import com.pig4cloud.pig.common.security.grant.ResourceOwnerCustomeAppTokenGranter; import com.pig4cloud.pig.common.security.service.PigClientDetailsService; -import com.pig4cloud.pig.common.security.service.PigUser; +import com.pig4cloud.pig.common.security.service.PigCustomTokenServices; import lombok.RequiredArgsConstructor; 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.authentication.ProviderManager; +import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; @@ -36,17 +36,15 @@ import org.springframework.security.oauth2.config.annotation.web.configuration.A import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; +import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.CompositeTokenGranter; import org.springframework.security.oauth2.provider.TokenGranter; 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; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider; import javax.sql.DataSource; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; +import java.util.*; /** * @author lengleng @@ -63,7 +61,7 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap private final AuthenticationManager authenticationManager; - private final RedisConnectionFactory redisConnectionFactory; + private final TokenStore redisTokenStore; @Override @SneakyThrows @@ -78,8 +76,8 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { - endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST).tokenStore(tokenStore()) - .tokenEnhancer(tokenEnhancer()).userDetailsService(userDetailsService) + endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST).tokenServices(tokenServices()) + .tokenStore(redisTokenStore).tokenEnhancer(tokenEnhancer()).userDetailsService(userDetailsService) .authenticationManager(authenticationManager).reuseRefreshTokens(false) .pathMapping("/oauth/confirm_access", "/token/confirm_access") .exceptionTranslator(new PigWebResponseExceptionTranslator()); @@ -98,33 +96,42 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap endpoints.tokenGranter(compositeTokenGranter); } - @Bean - public TokenStore tokenStore() { - RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory); - tokenStore.setPrefix(CacheConstants.PROJECT_OAUTH_ACCESS); - return tokenStore; - } - @Bean public TokenEnhancer tokenEnhancer() { return (accessToken, authentication) -> { final Map additionalInfo = new HashMap<>(4); - PigUser pigUser = (PigUser) authentication.getUserAuthentication().getPrincipal(); additionalInfo.put(SecurityConstants.DETAILS_LICENSE, SecurityConstants.PROJECT_LICENSE); - additionalInfo.put(SecurityConstants.DETAILS_USER_ID, pigUser.getId()); - additionalInfo.put(SecurityConstants.DETAILS_USERNAME, pigUser.getUsername()); - additionalInfo.put(SecurityConstants.DETAILS_DEPT_ID, pigUser.getDeptId()); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); return accessToken; }; } @Bean - public PigClientDetailsService pigClientDetailsService() { + public ClientDetailsService pigClientDetailsService() { PigClientDetailsService clientDetailsService = new PigClientDetailsService(dataSource); clientDetailsService.setSelectClientDetailsSql(SecurityConstants.DEFAULT_SELECT_STATEMENT); clientDetailsService.setFindClientDetailsSql(SecurityConstants.DEFAULT_FIND_STATEMENT); return clientDetailsService; } + @Bean + public PigCustomTokenServices tokenServices() { + PigCustomTokenServices tokenServices = new PigCustomTokenServices(); + tokenServices.setTokenStore(redisTokenStore); + tokenServices.setSupportRefreshToken(true); + tokenServices.setReuseRefreshToken(false); + tokenServices.setClientDetailsService(pigClientDetailsService()); + tokenServices.setTokenEnhancer(tokenEnhancer()); + addUserDetailsService(tokenServices, userDetailsService); + return tokenServices; + } + + private void addUserDetailsService(PigCustomTokenServices tokenServices, UserDetailsService userDetailsService) { + if (userDetailsService != null) { + PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider(); + provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<>(userDetailsService)); + tokenServices.setAuthenticationManager(new ProviderManager(Collections.singletonList(provider))); + } + } + } diff --git a/pig-common/pig-common-bom/pom.xml b/pig-common/pig-common-bom/pom.xml index 8e6aeca5..87df74f9 100644 --- a/pig-common/pig-common-bom/pom.xml +++ b/pig-common/pig-common-bom/pom.xml @@ -6,7 +6,7 @@ com.pig4cloud pig-common-bom - 3.3.5 + 3.4.0 pom pig-common-bom @@ -20,9 +20,9 @@ 2.1.8.RELEASE 1.8 1.8 - 2.2.5 - 0.0.28 - 1.2.75 + 5.0.0 + 0.0.29 + 1.2.78 1.5.24 3.4.3.4 2.0.3 @@ -150,8 +150,8 @@ - pl.project13.maven - git-commit-id-plugin + io.github.git-commit-id + git-commit-id-maven-plugin ${git.commit.plugin} diff --git a/pig-common/pig-common-core/pom.xml b/pig-common/pig-common-core/pom.xml index 7ce01119..4bb31f2a 100755 --- a/pig-common/pig-common-core/pom.xml +++ b/pig-common/pig-common-core/pom.xml @@ -21,7 +21,7 @@ com.pig4cloud pig-common - 3.3.5 + 3.4.0 pig-common-core diff --git a/pig-common/pig-common-datasource/pom.xml b/pig-common/pig-common-datasource/pom.xml index 0e2f5802..7bf9f85d 100644 --- a/pig-common/pig-common-datasource/pom.xml +++ b/pig-common/pig-common-datasource/pom.xml @@ -21,7 +21,7 @@ pig-common com.pig4cloud - 3.3.5 + 3.4.0 4.0.0 diff --git a/pig-common/pig-common-feign/pom.xml b/pig-common/pig-common-feign/pom.xml index f172916a..54446f1f 100755 --- a/pig-common/pig-common-feign/pom.xml +++ b/pig-common/pig-common-feign/pom.xml @@ -21,7 +21,7 @@ com.pig4cloud pig-common - 3.3.5 + 3.4.0 4.0.0 diff --git a/pig-common/pig-common-job/pom.xml b/pig-common/pig-common-job/pom.xml index becfe697..9e4effd9 100755 --- a/pig-common/pig-common-job/pom.xml +++ b/pig-common/pig-common-job/pom.xml @@ -23,7 +23,7 @@ com.pig4cloud pig-common - 3.3.5 + 3.4.0 pig-common-job diff --git a/pig-common/pig-common-log/pom.xml b/pig-common/pig-common-log/pom.xml index e5911ef7..a5808e86 100755 --- a/pig-common/pig-common-log/pom.xml +++ b/pig-common/pig-common-log/pom.xml @@ -21,7 +21,7 @@ com.pig4cloud pig-common - 3.3.5 + 3.4.0 pig-common-log diff --git a/pig-common/pig-common-mybatis/pom.xml b/pig-common/pig-common-mybatis/pom.xml index 97e06db0..13c68b12 100755 --- a/pig-common/pig-common-mybatis/pom.xml +++ b/pig-common/pig-common-mybatis/pom.xml @@ -21,7 +21,7 @@ com.pig4cloud pig-common - 3.3.5 + 3.4.0 pig-common-mybatis diff --git a/pig-common/pig-common-security/pom.xml b/pig-common/pig-common-security/pom.xml index 5e35fbed..cfa1d1a1 100755 --- a/pig-common/pig-common-security/pom.xml +++ b/pig-common/pig-common-security/pom.xml @@ -21,7 +21,7 @@ com.pig4cloud pig-common - 3.3.5 + 3.4.0 pig-common-security diff --git a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigLocalResourceServerTokenServices.java b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigLocalResourceServerTokenServices.java new file mode 100644 index 00000000..ab48d2ff --- /dev/null +++ b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigLocalResourceServerTokenServices.java @@ -0,0 +1,66 @@ +package com.pig4cloud.pig.common.security.component; + +import com.pig4cloud.pig.common.security.exception.UnauthorizedException; +import com.pig4cloud.pig.common.security.service.PigUser; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; +import org.springframework.security.oauth2.provider.token.TokenStore; + +/** + * @author lengleng + * @date 2020/9/29 + */ +@RequiredArgsConstructor +public class PigLocalResourceServerTokenServices implements ResourceServerTokenServices { + + private final TokenStore tokenStore; + + private final UserDetailsService userDetailsService; + + @Override + public OAuth2Authentication loadAuthentication(String accessToken) + throws AuthenticationException, InvalidTokenException { + OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(accessToken); + if (oAuth2Authentication == null) { + return null; + } + + OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request(); + if (!(oAuth2Authentication.getPrincipal() instanceof PigUser)) { + return oAuth2Authentication; + } + + // 根据 username 查询 spring cache 最新的值 并返回 + PigUser pigxUser = (PigUser) oAuth2Authentication.getPrincipal(); + + UserDetails userDetails; + try { + userDetails = userDetailsService.loadUserByUsername(pigxUser.getUsername()); + } + catch (UsernameNotFoundException notFoundException) { + throw new UnauthorizedException(String.format("%s username not found", pigxUser.getUsername()), + notFoundException); + } + Authentication userAuthentication = new UsernamePasswordAuthenticationToken(userDetails, "N/A", + userDetails.getAuthorities()); + OAuth2Authentication authentication = new OAuth2Authentication(oAuth2Request, userAuthentication); + authentication.setAuthenticated(true); + return authentication; + } + + @Override + public OAuth2AccessToken readAccessToken(String accessToken) { + throw new UnsupportedOperationException("Not supported: read access token"); + } + +} diff --git a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigRedisTokenStore.java b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigRedisTokenStore.java new file mode 100644 index 00000000..b0bfffd1 --- /dev/null +++ b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigRedisTokenStore.java @@ -0,0 +1,478 @@ +package com.pig4cloud.pig.common.security.component; + +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 PigRedisTokenStore 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 AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator(); + + private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy(); + + private String prefix = ""; + + private Method redisConnectionSet_2_0; + + public PigRedisTokenStore(RedisConnectionFactory connectionFactory) { + 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(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; + try (RedisConnection conn = getConnection()) { + bytes = conn.get(serializedKey); + } + 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; + try (RedisConnection conn = getConnection()) { + bytes = conn.get(serializeKey(AUTH + token)); + } + return deserializeAuthentication(bytes); + } + + @Override + public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) { + return readAuthenticationForRefreshToken(token.getValue()); + } + + public OAuth2Authentication readAuthenticationForRefreshToken(String token) { + try (RedisConnection conn = getConnection()) { + byte[] bytes = conn.get(serializeKey(REFRESH_AUTH + token)); + return deserializeAuthentication(bytes); + } + } + + @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()); + + try (RedisConnection conn = getConnection()) { + 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[] 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(); + } + } + + 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; + try (RedisConnection conn = getConnection()) { + bytes = conn.get(key); + } + return deserializeAccessToken(bytes); + } + + public void removeAccessToken(String tokenValue) { + byte[] accessKey = serializeKey(ACCESS + tokenValue); + byte[] authKey = serializeKey(AUTH + tokenValue); + try (RedisConnection conn = getConnection()) { + 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(); + } + } + } + + @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); + try (RedisConnection conn = getConnection()) { + 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(); + } + } + + @Override + public OAuth2RefreshToken readRefreshToken(String tokenValue) { + byte[] key = serializeKey(REFRESH + tokenValue); + byte[] bytes; + try (RedisConnection conn = getConnection()) { + bytes = conn.get(key); + } + return deserializeRefreshToken(bytes); + } + + @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); + try (RedisConnection conn = getConnection()) { + conn.openPipeline(); + conn.del(refreshKey); + conn.del(refreshAuthKey); + conn.del(refresh2AccessKey); + conn.closePipeline(); + } + } + + @Override + public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) { + removeAccessTokenUsingRefreshToken(refreshToken.getValue()); + } + + private void removeAccessTokenUsingRefreshToken(String refreshToken) { + byte[] key = serializeKey(REFRESH_TO_ACCESS + refreshToken); + List results; + try (RedisConnection conn = getConnection()) { + conn.openPipeline(); + conn.get(key); + conn.del(key); + results = conn.closePipeline(); + } + 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); + assert size != null; + 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; + try (RedisConnection conn = getConnection()) { + // client_id_to_acccess maintenance + Cursor clientToAccessKeys = conn + .scan(ScanOptions.scanOptions().match(prefix + CLIENT_ID_TO_ACCESS + "*").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(prefix + UNAME_TO_ACCESS + "*").build()); + while (unameToAccessKeys.hasNext()) { + byte[] unameToAccessKey = unameToAccessKeys.next(); + + removed += conn.zRemRangeByScore(unameToAccessKey, 0, System.currentTimeMillis()); + } + } + return removed; + } + + @Override + public Collection findTokensByClientIdAndUserName(String clientId, String userName) { + byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(clientId, userName)); + List byteList; + try (RedisConnection conn = getConnection()) { + byteList = getZByteLists(approvalKey, conn); + } + 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; + try (RedisConnection conn = getConnection()) { + byteList = getZByteLists(key, conn); + } + 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/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigResourceServerAutoConfiguration.java b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigResourceServerAutoConfiguration.java index b3b6af55..374a7cce 100644 --- a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigResourceServerAutoConfiguration.java +++ b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigResourceServerAutoConfiguration.java @@ -17,19 +17,12 @@ package com.pig4cloud.pig.common.security.component; import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.SneakyThrows; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.web.client.DefaultResponseErrorHandler; -import org.springframework.web.client.RestTemplate; - -import java.util.Collections; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; +import org.springframework.security.oauth2.provider.token.TokenStore; /** * @author lengleng @@ -60,32 +53,9 @@ public class PigResourceServerAutoConfiguration { @Bean @Primary - @LoadBalanced - public RestTemplate lbRestTemplate() { - RestTemplate restTemplate = new RestTemplate(); - - // 传递ACCEPT JSON - restTemplate.setInterceptors(Collections.singletonList((request, body, execution) -> { - request.getHeaders().set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); - return execution.execute(request, body); - })); - - // 处理400 异常 - restTemplate.setErrorHandler(new DefaultResponseErrorHandler() { - @Override - @SneakyThrows - public void handleError(ClientHttpResponse response) { - // 当认证中心返回 400 或者 424 错误码不抛异常,交给资源服务自行处理 - if (response.getRawStatusCode() == HttpStatus.FAILED_DEPENDENCY.value() - || response.getRawStatusCode() == HttpStatus.BAD_REQUEST.value()) { - return; - } - - // 原有异常处理逻辑 - super.handleError(response); - } - }); - return restTemplate; + public ResourceServerTokenServices resourceServerTokenServices(TokenStore tokenStore, + UserDetailsService userDetailsService) { + return new PigLocalResourceServerTokenServices(tokenStore, userDetailsService); } } diff --git a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigResourceServerConfigurerAdapter.java b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigResourceServerConfigurerAdapter.java index f6e4041c..6eba2367 100644 --- a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigResourceServerConfigurerAdapter.java +++ b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigResourceServerConfigurerAdapter.java @@ -23,11 +23,9 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter; import org.springframework.security.oauth2.provider.token.RemoteTokenServices; -import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter; +import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; import org.springframework.security.web.access.AccessDeniedHandler; -import org.springframework.web.client.RestTemplate; /** * @author lengleng @@ -52,10 +50,10 @@ public class PigResourceServerConfigurerAdapter extends ResourceServerConfigurer private PermitAllUrlProperties permitAllUrl; @Autowired - private RestTemplate lbRestTemplate; + private PigBearerTokenExtractor pigBearerTokenExtractor; @Autowired - private PigBearerTokenExtractor pigBearerTokenExtractor; + private ResourceServerTokenServices resourceServerTokenServices; /** * 默认的配置,对外暴露 @@ -74,14 +72,8 @@ public class PigResourceServerConfigurerAdapter extends ResourceServerConfigurer @Override public void configure(ResourceServerSecurityConfigurer resources) { - DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter(); - UserAuthenticationConverter userTokenConverter = new PigUserAuthenticationConverter(); - accessTokenConverter.setUserTokenConverter(userTokenConverter); - - remoteTokenServices.setRestTemplate(lbRestTemplate); - remoteTokenServices.setAccessTokenConverter(accessTokenConverter); resources.authenticationEntryPoint(resourceAuthExceptionEntryPoint).tokenExtractor(pigBearerTokenExtractor) - .accessDeniedHandler(pigAccessDeniedHandler).tokenServices(remoteTokenServices); + .accessDeniedHandler(pigAccessDeniedHandler).tokenServices(resourceServerTokenServices); } } diff --git a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigTokenStoreAutoCleanSchedule.java b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigTokenStoreAutoCleanSchedule.java new file mode 100644 index 00000000..5b55a067 --- /dev/null +++ b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigTokenStoreAutoCleanSchedule.java @@ -0,0 +1,31 @@ +package com.pig4cloud.pig.common.security.component; + +import com.pig4cloud.pig.common.core.util.SpringContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; + +/** + * @author lengleng + * @date 2020/9/29 + *

+ * redis token store 自动配置 + */ +@Slf4j +@EnableScheduling +@ConditionalOnBean(AuthorizationServerConfigurerAdapter.class) +public class PigTokenStoreAutoCleanSchedule { + + /** + * 每小时执行一致,避免 redis zset 容量问题 + */ + @Scheduled(cron = "@hourly") + public void doMaintenance() { + PigRedisTokenStore tokenStore = SpringContextHolder.getBean(PigRedisTokenStore.class); + long maintenance = tokenStore.doMaintenance(); + log.debug("清理Redis ZADD 过期 token 数量: {}", maintenance); + } + +} diff --git a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigTokenStoreAutoConfiguration.java b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigTokenStoreAutoConfiguration.java new file mode 100644 index 00000000..83fcc92a --- /dev/null +++ b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigTokenStoreAutoConfiguration.java @@ -0,0 +1,21 @@ +package com.pig4cloud.pig.common.security.component; + +import com.pig4cloud.pig.common.core.constant.CacheConstants; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.security.oauth2.provider.token.TokenStore; + +/** + * @author lengleng + * @date 2021/10/16 + */ +public class PigTokenStoreAutoConfiguration { + + @Bean + public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) { + PigRedisTokenStore tokenStore = new PigRedisTokenStore(redisConnectionFactory); + tokenStore.setPrefix(CacheConstants.PROJECT_OAUTH_ACCESS); + return tokenStore; + } + +} diff --git a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigUserAuthenticationConverter.java b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigUserAuthenticationConverter.java deleted file mode 100644 index 725c25f1..00000000 --- a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigUserAuthenticationConverter.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.pig4cloud.pig.common.security.component; - -import com.pig4cloud.pig.common.core.constant.SecurityConstants; -import com.pig4cloud.pig.common.security.service.PigUser; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter; -import org.springframework.util.StringUtils; - -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * @author lengleng - * @date 2019-03-07 - *

- * 根据checktoken 的结果转化用户信息 - */ -public class PigUserAuthenticationConverter implements UserAuthenticationConverter { - - private static final String N_A = "N/A"; - - /** - * Extract information about the user to be used in an access token (i.e. for resource - * servers). - * @param authentication an authentication representing a user - * @return a map of key values representing the unique information about the user - */ - @Override - public Map convertUserAuthentication(Authentication authentication) { - Map response = new LinkedHashMap<>(); - response.put(USERNAME, authentication.getName()); - if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) { - response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities())); - } - return response; - } - - /** - * Inverse of {@link #convertUserAuthentication(Authentication)}. Extracts an - * Authentication from a map. - * @param map a map of user information - * @return an Authentication representing the user or null if there is none - */ - @Override - public Authentication extractAuthentication(Map map) { - if (map.containsKey(USERNAME)) { - Collection authorities = getAuthorities(map); - - String username = (String) map.get(SecurityConstants.DETAILS_USERNAME); - Integer id = (Integer) map.get(SecurityConstants.DETAILS_USER_ID); - Integer deptId = (Integer) map.get(SecurityConstants.DETAILS_DEPT_ID); - PigUser user = new PigUser(id, deptId, username, N_A, true, true, true, true, authorities); - return new UsernamePasswordAuthenticationToken(user, N_A, authorities); - } - return null; - } - - private Collection getAuthorities(Map map) { - Object authorities = map.get(AUTHORITIES); - if (authorities instanceof String) { - return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities); - } - if (authorities instanceof Collection) { - return AuthorityUtils.commaSeparatedStringToAuthorityList( - StringUtils.collectionToCommaDelimitedString((Collection) authorities)); - } - return AuthorityUtils.NO_AUTHORITIES; - } - -} diff --git a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/service/PigCustomTokenServices.java b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/service/PigCustomTokenServices.java new file mode 100644 index 00000000..505c191e --- /dev/null +++ b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/service/PigCustomTokenServices.java @@ -0,0 +1,382 @@ +package com.pig4cloud.pig.common.security.service; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.common.*; +import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; +import org.springframework.security.oauth2.common.exceptions.InvalidScopeException; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.security.oauth2.provider.*; +import org.springframework.security.oauth2.provider.token.*; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import java.util.Date; +import java.util.Set; +import java.util.UUID; + +/** + * 自定义 token 放发处理逻辑 + * + * @author lengleng + * @date 2021/10/15 + */ +public class PigCustomTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices, + ConsumerTokenServices, InitializingBean { + + private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days. + + private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours. + + private boolean supportRefreshToken = false; + + private boolean reuseRefreshToken = true; + + private TokenStore tokenStore; + + private ClientDetailsService clientDetailsService; + + private TokenEnhancer accessTokenEnhancer; + + private AuthenticationManager authenticationManager; + + /** + * Initialize these token services. If no random generator is set, one will be + * created. + */ + public void afterPropertiesSet() { + Assert.notNull(tokenStore, "tokenStore must be set"); + } + + @Override + public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException { + OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication); + OAuth2RefreshToken refreshToken = null; + + // 若已产生token , 过期时删除相关token,执行下边的重新生成逻辑 + if (existingAccessToken != null) { + tokenStore.removeAccessToken(existingAccessToken); + if (existingAccessToken.getRefreshToken() != null) { + refreshToken = existingAccessToken.getRefreshToken(); + tokenStore.removeRefreshToken(refreshToken); + } + } + + if (refreshToken == null) { + refreshToken = createRefreshToken(authentication); + } + + else if (refreshToken instanceof ExpiringOAuth2RefreshToken) { + ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken; + if (System.currentTimeMillis() > expiring.getExpiration().getTime()) { + refreshToken = createRefreshToken(authentication); + } + } + + OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken); + tokenStore.storeAccessToken(accessToken, authentication); + + refreshToken = accessToken.getRefreshToken(); + if (refreshToken != null) { + tokenStore.storeRefreshToken(refreshToken, authentication); + } + return accessToken; + } + + @Transactional(noRollbackFor = { InvalidTokenException.class, InvalidGrantException.class }) + public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) + throws AuthenticationException { + + if (!supportRefreshToken) { + throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue); + } + + OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue); + if (refreshToken == null) { + throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue); + } + + OAuth2Authentication authentication = tokenStore.readAuthenticationForRefreshToken(refreshToken); + if (this.authenticationManager != null && !authentication.isClientOnly()) { + // The client has already been authenticated, but the user authentication + // might be old now, so give it a + // chance to re-authenticate. + Authentication user = new PreAuthenticatedAuthenticationToken(authentication.getUserAuthentication(), "", + authentication.getAuthorities()); + user = authenticationManager.authenticate(user); + Object details = authentication.getDetails(); + authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user); + authentication.setDetails(details); + } + String clientId = authentication.getOAuth2Request().getClientId(); + if (clientId == null || !clientId.equals(tokenRequest.getClientId())) { + throw new InvalidGrantException("Wrong client for this refresh token: " + refreshTokenValue); + } + + // clear out any access tokens already associated with the refresh + // token. + tokenStore.removeAccessTokenUsingRefreshToken(refreshToken); + + if (isExpired(refreshToken)) { + tokenStore.removeRefreshToken(refreshToken); + throw new InvalidTokenException("Invalid refresh token (expired): " + refreshToken); + } + + authentication = createRefreshedAuthentication(authentication, tokenRequest); + + if (!reuseRefreshToken) { + tokenStore.removeRefreshToken(refreshToken); + refreshToken = createRefreshToken(authentication); + } + + OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken); + tokenStore.storeAccessToken(accessToken, authentication); + if (!reuseRefreshToken) { + tokenStore.storeRefreshToken(accessToken.getRefreshToken(), authentication); + } + return accessToken; + } + + public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) { + return tokenStore.getAccessToken(authentication); + } + + /** + * Create a refreshed authentication. + * @param authentication The authentication. + * @param request The scope for the refreshed token. + * @return The refreshed authentication. + * @throws InvalidScopeException If the scope requested is invalid or wider than the + * original scope. + */ + private OAuth2Authentication createRefreshedAuthentication(OAuth2Authentication authentication, + TokenRequest request) { + OAuth2Authentication narrowed = authentication; + Set scope = request.getScope(); + OAuth2Request clientAuth = authentication.getOAuth2Request().refresh(request); + if (scope != null && !scope.isEmpty()) { + Set originalScope = clientAuth.getScope(); + if (originalScope == null || !originalScope.containsAll(scope)) { + throw new InvalidScopeException( + "Unable to narrow the scope of the client authentication to " + scope + ".", originalScope); + } + else { + clientAuth = clientAuth.narrowScope(scope); + } + } + narrowed = new OAuth2Authentication(clientAuth, authentication.getUserAuthentication()); + return narrowed; + } + + protected boolean isExpired(OAuth2RefreshToken refreshToken) { + if (refreshToken instanceof ExpiringOAuth2RefreshToken) { + ExpiringOAuth2RefreshToken expiringToken = (ExpiringOAuth2RefreshToken) refreshToken; + return expiringToken.getExpiration() == null + || System.currentTimeMillis() > expiringToken.getExpiration().getTime(); + } + return false; + } + + public OAuth2AccessToken readAccessToken(String accessToken) { + return tokenStore.readAccessToken(accessToken); + } + + public OAuth2Authentication loadAuthentication(String accessTokenValue) + throws AuthenticationException, InvalidTokenException { + OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue); + if (accessToken == null) { + throw new InvalidTokenException("Invalid access token: " + accessTokenValue); + } + else if (accessToken.isExpired()) { + tokenStore.removeAccessToken(accessToken); + throw new InvalidTokenException("Access token expired: " + accessTokenValue); + } + + OAuth2Authentication result = tokenStore.readAuthentication(accessToken); + if (result == null) { + // in case of race condition + throw new InvalidTokenException("Invalid access token: " + accessTokenValue); + } + if (clientDetailsService != null) { + String clientId = result.getOAuth2Request().getClientId(); + try { + clientDetailsService.loadClientByClientId(clientId); + } + catch (ClientRegistrationException e) { + throw new InvalidTokenException("Client not valid: " + clientId, e); + } + } + return result; + } + + public String getClientId(String tokenValue) { + OAuth2Authentication authentication = tokenStore.readAuthentication(tokenValue); + if (authentication == null) { + throw new InvalidTokenException("Invalid access token: " + tokenValue); + } + OAuth2Request clientAuth = authentication.getOAuth2Request(); + if (clientAuth == null) { + throw new InvalidTokenException("Invalid access token (no client id): " + tokenValue); + } + return clientAuth.getClientId(); + } + + public boolean revokeToken(String tokenValue) { + OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue); + if (accessToken == null) { + return false; + } + if (accessToken.getRefreshToken() != null) { + tokenStore.removeRefreshToken(accessToken.getRefreshToken()); + } + tokenStore.removeAccessToken(accessToken); + return true; + } + + private OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) { + if (!isSupportRefreshToken(authentication.getOAuth2Request())) { + return null; + } + int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request()); + String value = UUID.randomUUID().toString(); + if (validitySeconds > 0) { + return new DefaultExpiringOAuth2RefreshToken(value, + new Date(System.currentTimeMillis() + (validitySeconds * 1000L))); + } + return new DefaultOAuth2RefreshToken(value); + } + + private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) { + DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString()); + int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request()); + if (validitySeconds > 0) { + token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L))); + } + token.setRefreshToken(refreshToken); + token.setScope(authentication.getOAuth2Request().getScope()); + + return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token; + } + + /** + * The access token validity period in seconds + * @param clientAuth the current authorization request + * @return the access token validity period in seconds + */ + protected int getAccessTokenValiditySeconds(OAuth2Request clientAuth) { + if (clientDetailsService != null) { + ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId()); + Integer validity = client.getAccessTokenValiditySeconds(); + if (validity != null) { + return validity; + } + } + return accessTokenValiditySeconds; + } + + /** + * The refresh token validity period in seconds + * @param clientAuth the current authorization request + * @return the refresh token validity period in seconds + */ + protected int getRefreshTokenValiditySeconds(OAuth2Request clientAuth) { + if (clientDetailsService != null) { + ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId()); + Integer validity = client.getRefreshTokenValiditySeconds(); + if (validity != null) { + return validity; + } + } + return refreshTokenValiditySeconds; + } + + /** + * Is a refresh token supported for this client (or the global setting if + * {@link #setClientDetailsService(ClientDetailsService) clientDetailsService} is not + * set. + * @param clientAuth the current authorization request + * @return boolean to indicate if refresh token is supported + */ + protected boolean isSupportRefreshToken(OAuth2Request clientAuth) { + if (clientDetailsService != null) { + ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId()); + return client.getAuthorizedGrantTypes().contains("refresh_token"); + } + return this.supportRefreshToken; + } + + /** + * An access token enhancer that will be applied to a new token before it is saved in + * the token store. + * @param accessTokenEnhancer the access token enhancer to set + */ + public void setTokenEnhancer(TokenEnhancer accessTokenEnhancer) { + this.accessTokenEnhancer = accessTokenEnhancer; + } + + /** + * The validity (in seconds) of the refresh token. If less than or equal to zero then + * the tokens will be non-expiring. + * @param refreshTokenValiditySeconds The validity (in seconds) of the refresh token. + */ + public void setRefreshTokenValiditySeconds(int refreshTokenValiditySeconds) { + this.refreshTokenValiditySeconds = refreshTokenValiditySeconds; + } + + /** + * The default validity (in seconds) of the access token. Zero or negative for + * non-expiring tokens. If a client details service is set the validity period will be + * read from the client, defaulting to this value if not defined by the client. + * @param accessTokenValiditySeconds The validity (in seconds) of the access token. + */ + public void setAccessTokenValiditySeconds(int accessTokenValiditySeconds) { + this.accessTokenValiditySeconds = accessTokenValiditySeconds; + } + + /** + * Whether to support the refresh token. + * @param supportRefreshToken Whether to support the refresh token. + */ + public void setSupportRefreshToken(boolean supportRefreshToken) { + this.supportRefreshToken = supportRefreshToken; + } + + /** + * Whether to reuse refresh tokens (until expired). + * @param reuseRefreshToken Whether to reuse refresh tokens (until expired). + */ + public void setReuseRefreshToken(boolean reuseRefreshToken) { + this.reuseRefreshToken = reuseRefreshToken; + } + + /** + * The persistence strategy for token storage. + * @param tokenStore the store for access and refresh tokens. + */ + public void setTokenStore(TokenStore tokenStore) { + this.tokenStore = tokenStore; + } + + /** + * An authentication manager that will be used (if provided) to check the user + * authentication when a token is refreshed. + * @param authenticationManager the authenticationManager to set + */ + public void setAuthenticationManager(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + /** + * The client details service to use for looking up clients (if necessary). Optional + * if the access token expiry is set globally via + * {@link #setAccessTokenValiditySeconds(int)}. + * @param clientDetailsService the client details service + */ + public void setClientDetailsService(ClientDetailsService clientDetailsService) { + this.clientDetailsService = clientDetailsService; + } + +} diff --git a/pig-common/pig-common-security/src/main/resources/META-INF/spring.factories b/pig-common/pig-common-security/src/main/resources/META-INF/spring.factories index ba4694e7..88439207 100755 --- a/pig-common/pig-common-security/src/main/resources/META-INF/spring.factories +++ b/pig-common/pig-common-security/src/main/resources/META-INF/spring.factories @@ -1,4 +1,6 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.pig4cloud.pig.common.security.service.PigUserDetailsServiceImpl,\ - com.pig4cloud.pig.common.security.component.PigSecurityInnerAspect + com.pig4cloud.pig.common.security.component.PigSecurityInnerAspect,\ + com.pig4cloud.pig.common.security.component.PigTokenStoreAutoConfiguration,\ + com.pig4cloud.pig.common.security.component.PigTokenStoreAutoCleanSchedule diff --git a/pig-common/pig-common-swagger/pom.xml b/pig-common/pig-common-swagger/pom.xml index 24c1c389..420b4047 100644 --- a/pig-common/pig-common-swagger/pom.xml +++ b/pig-common/pig-common-swagger/pom.xml @@ -24,7 +24,7 @@ com.pig4cloud pig-common - 3.3.5 + 3.4.0 pig-common-swagger diff --git a/pig-common/pig-common-test/pom.xml b/pig-common/pig-common-test/pom.xml index 9cb24d2a..dcd30061 100755 --- a/pig-common/pig-common-test/pom.xml +++ b/pig-common/pig-common-test/pom.xml @@ -6,7 +6,7 @@ com.pig4cloud pig-common - 3.3.5 + 3.4.0 pig-common-test diff --git a/pig-common/pom.xml b/pig-common/pom.xml index 756f4436..5d117eb1 100755 --- a/pig-common/pom.xml +++ b/pig-common/pom.xml @@ -21,7 +21,7 @@ com.pig4cloud pig - 3.3.5 + 3.4.0 pig-common diff --git a/pig-gateway/pom.xml b/pig-gateway/pom.xml index 93e8040e..0690d7e0 100755 --- a/pig-gateway/pom.xml +++ b/pig-gateway/pom.xml @@ -21,7 +21,7 @@ com.pig4cloud pig - 3.3.5 + 3.4.0 pig-gateway diff --git a/pig-register/pom.xml b/pig-register/pom.xml index 70e03253..9582a5e1 100755 --- a/pig-register/pom.xml +++ b/pig-register/pom.xml @@ -18,7 +18,7 @@ com.pig4cloud pig - 3.3.5 + 3.4.0 pig-register diff --git a/pig-register/src/main/java/com/alibaba/nacos/config/ConfigConstants.java b/pig-register/src/main/java/com/alibaba/nacos/config/ConfigConstants.java index 3b38bc7a..91133f54 100755 --- a/pig-register/src/main/java/com/alibaba/nacos/config/ConfigConstants.java +++ b/pig-register/src/main/java/com/alibaba/nacos/config/ConfigConstants.java @@ -40,7 +40,6 @@ public interface ConfigConstants { */ String LOG_BASEDIR = "server.tomcat.basedir"; - /** * access_log日志开关 */ diff --git a/pig-upms/pig-upms-api/pom.xml b/pig-upms/pig-upms-api/pom.xml index f8faba07..9c3ec04f 100755 --- a/pig-upms/pig-upms-api/pom.xml +++ b/pig-upms/pig-upms-api/pom.xml @@ -21,7 +21,7 @@ com.pig4cloud pig-upms - 3.3.5 + 3.4.0 pig-upms-api diff --git a/pig-upms/pig-upms-biz/pom.xml b/pig-upms/pig-upms-biz/pom.xml index 2c3a6e60..ad4ca62e 100644 --- a/pig-upms/pig-upms-biz/pom.xml +++ b/pig-upms/pig-upms-biz/pom.xml @@ -21,7 +21,7 @@ com.pig4cloud pig-upms - 3.3.5 + 3.4.0 pig-upms-biz diff --git a/pig-upms/pom.xml b/pig-upms/pom.xml index 2ded333b..ac60c482 100755 --- a/pig-upms/pom.xml +++ b/pig-upms/pom.xml @@ -21,7 +21,7 @@ com.pig4cloud pig - 3.3.5 + 3.4.0 pig-upms diff --git a/pig-visual/pig-codegen/pom.xml b/pig-visual/pig-codegen/pom.xml index a1a6e15a..c07a6a9d 100755 --- a/pig-visual/pig-codegen/pom.xml +++ b/pig-visual/pig-codegen/pom.xml @@ -22,7 +22,7 @@ com.pig4cloud pig-visual - 3.3.5 + 3.4.0 pig-codegen diff --git a/pig-visual/pig-monitor/pom.xml b/pig-visual/pig-monitor/pom.xml index 3e41b551..b4f8b902 100755 --- a/pig-visual/pig-monitor/pom.xml +++ b/pig-visual/pig-monitor/pom.xml @@ -21,7 +21,7 @@ com.pig4cloud pig-visual - 3.3.5 + 3.4.0 pig-monitor diff --git a/pig-visual/pig-sentinel-dashboard/pom.xml b/pig-visual/pig-sentinel-dashboard/pom.xml index 7209c310..c8e97f08 100755 --- a/pig-visual/pig-sentinel-dashboard/pom.xml +++ b/pig-visual/pig-sentinel-dashboard/pom.xml @@ -6,7 +6,7 @@ com.pig4cloud pig-visual - 3.3.5 + 3.4.0 pig-sentinel-dashboard diff --git a/pig-visual/pig-xxl-job-admin/pom.xml b/pig-visual/pig-xxl-job-admin/pom.xml index 6d523ea3..2bcab3dc 100644 --- a/pig-visual/pig-xxl-job-admin/pom.xml +++ b/pig-visual/pig-xxl-job-admin/pom.xml @@ -4,7 +4,7 @@ com.pig4cloud pig-visual - 3.3.5 + 3.4.0 pig-xxl-job-admin diff --git a/pig-visual/pom.xml b/pig-visual/pom.xml index 604a9668..105d387a 100755 --- a/pig-visual/pom.xml +++ b/pig-visual/pom.xml @@ -21,7 +21,7 @@ com.pig4cloud pig - 3.3.5 + 3.4.0 pig-visual diff --git a/pom.xml b/pom.xml index ecb59063..4b01258d 100755 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ com.pig4cloud pig ${project.artifactId} - 3.3.5 + 3.4.0 pom https://www.pig4cloud.com @@ -34,9 +34,9 @@ 1.8 1.8 2.5.2 - 5.7.13 + 5.7.14 3.4.1 - 2.2.0 + 2.2.1 1.7 1.10 2.0.3 @@ -49,8 +49,8 @@ pig4cloud username password - 2.2.5 - 0.0.28 + 5.0.0 + 0.0.29 @@ -203,9 +203,21 @@ - pl.project13.maven - git-commit-id-plugin + io.github.git-commit-id + git-commit-id-maven-plugin ${git.commit.plugin} + + + get-the-git-infos + + revision + + initialize + + + + true +