diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/PigTokenEndpoint.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/PigTokenEndpoint.java index 1ee0e4723..7730e49d6 100644 --- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/PigTokenEndpoint.java +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/PigTokenEndpoint.java @@ -30,6 +30,7 @@ import com.pig4cloud.pigx.auth.support.handler.PigAuthenticationFailureEventHand import com.pig4cloud.pigx.common.core.constant.CacheConstants; import com.pig4cloud.pigx.common.core.constant.CommonConstants; import com.pig4cloud.pigx.common.core.constant.SecurityConstants; +import com.pig4cloud.pigx.common.core.util.KeyStrResolver; import com.pig4cloud.pigx.common.core.util.R; import com.pig4cloud.pigx.common.core.util.RetOps; import com.pig4cloud.pigx.common.core.util.SpringContextHolder; @@ -86,6 +87,8 @@ public class PigTokenEndpoint { private final RemoteTenantService tenantService; + private final KeyStrResolver tenantKeyStrResolver; + private final CacheManager cacheManager; /** @@ -196,7 +199,7 @@ public class PigTokenEndpoint { @PostMapping("/page") public R> tokenList(@RequestBody Map params) { // 根据分页参数获取对应数据 - String key = String.format("%s::*", CacheConstants.PROJECT_OAUTH_ACCESS); + String key = String.format("%s::%s::*", tenantKeyStrResolver.key(), CacheConstants.PROJECT_OAUTH_ACCESS); int current = MapUtil.getInt(params, CommonConstants.CURRENT); int size = MapUtil.getInt(params, CommonConstants.SIZE); Set keys = redisTemplate.keys(key); @@ -221,10 +224,17 @@ public class PigTokenEndpoint { tokenVo.setIssuedAt(issuedAt); Map attributes = authorization.getAttributes(); - Authentication authentication = (Authentication)attributes.get(Principal.class.getName()); + Authentication authentication = (Authentication) attributes.get(Principal.class.getName()); PigxUser pigxUser = (PigxUser) authentication.getPrincipal(); tokenVo.setUserId(pigxUser.getId()); return tokenVo; + }).filter(tokenVo -> { + // 根据用户名过滤 + String username = MapUtil.getStr(params, SecurityConstants.DETAILS_USERNAME); + if (StrUtil.isBlank(username)) { + return true; + } + return tokenVo.getUsername().contains(username); }).collect(Collectors.toList()); result.setRecords(tokenVoList); result.setTotal(keys.size()); diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/base/OAuth2ResourceOwnerBaseAuthenticationProvider.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/base/OAuth2ResourceOwnerBaseAuthenticationProvider.java index 0c106af67..efdca4e91 100644 --- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/base/OAuth2ResourceOwnerBaseAuthenticationProvider.java +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/base/OAuth2ResourceOwnerBaseAuthenticationProvider.java @@ -1,10 +1,14 @@ package com.pig4cloud.pigx.auth.support.base; +import cn.hutool.core.util.BooleanUtil; import cn.hutool.extra.spring.SpringUtil; +import com.pig4cloud.pigx.common.core.constant.CommonConstants; +import com.pig4cloud.pigx.common.security.service.PigRedisOAuth2AuthorizationService; import com.pig4cloud.pigx.common.security.util.OAuth2ErrorCodesExpand; import com.pig4cloud.pigx.common.security.util.ScopeException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.security.authentication.*; import org.springframework.security.core.Authentication; @@ -141,78 +145,20 @@ public abstract class OAuth2ResourceOwnerBaseAuthenticationProvider metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, - ((ClaimAccessor) generatedAccessToken).getClaims())) - // 0.4.0 新增的方法 - .authorizedScopes(authorizedScopes) - .attribute(Principal.class.getName(), usernamePasswordAuthentication); - } - else { - authorizationBuilder.id(accessToken.getTokenValue()).accessToken(accessToken); + Object onlineQuantity = registeredClient.getClientSettings().getSettings() + .get(CommonConstants.ONLINE_QUANTITY); + // 没有设置并发控制走原有逻辑生成 || 设置同时在线为 true + if (Objects.isNull(onlineQuantity) || BooleanUtil.toBooleanObject((String) onlineQuantity)) { + return generatAuthenticationToken(resouceOwnerBaseAuthentication, clientPrincipal, registeredClient, + authorizedScopes, usernamePasswordAuthentication); } - // ----- Refresh token ----- - OAuth2RefreshToken refreshToken = null; - if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) && - // Do not issue refresh token to public client - !clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) { + // 不允许同时在线,删除原有username 关联的所有token + PigRedisOAuth2AuthorizationService redisOAuth2AuthorizationService = (PigRedisOAuth2AuthorizationService) this.authorizationService; + redisOAuth2AuthorizationService.removeByUsername(usernamePasswordAuthentication); - if (this.refreshTokenGenerator != null) { - Instant issuedAt = Instant.now(); - Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getRefreshTokenTimeToLive()); - refreshToken = new OAuth2RefreshToken(this.refreshTokenGenerator.get(), issuedAt, expiresAt); - } - else { - tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build(); - OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext); - if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) { - OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, - "The token generator failed to generate the refresh token.", ERROR_URI); - throw new OAuth2AuthenticationException(error); - } - refreshToken = (OAuth2RefreshToken) generatedRefreshToken; - } - authorizationBuilder.refreshToken(refreshToken); - } - - OAuth2Authorization authorization = authorizationBuilder.build(); - - this.authorizationService.save(authorization); - - LOGGER.debug("returning OAuth2AccessTokenAuthenticationToken"); - - return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, - refreshToken, Objects.requireNonNull(authorization.getAccessToken().getClaims())); + return generatAuthenticationToken(resouceOwnerBaseAuthentication, clientPrincipal, registeredClient, + authorizedScopes, usernamePasswordAuthentication); } catch (Exception ex) { @@ -221,6 +167,93 @@ public abstract class OAuth2ResourceOwnerBaseAuthenticationProvider authorizedScopes, Authentication usernamePasswordAuthentication) { + // @formatter:off + DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder() + .registeredClient(registeredClient) + .principal(usernamePasswordAuthentication) + .authorizationServerContext(AuthorizationServerContextHolder.getContext()) + .authorizedScopes(authorizedScopes) + .authorizationGrantType(AuthorizationGrantType.PASSWORD) + .authorizationGrant(resouceOwnerBaseAuthentication); + // @formatter:on + + OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) + .principalName(usernamePasswordAuthentication.getName()) + .authorizationGrantType(AuthorizationGrantType.PASSWORD) + // 0.4.0 新增的方法 + .authorizedScopes(authorizedScopes); + + // ----- Access token ----- + OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build(); + OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext); + if (generatedAccessToken == null) { + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, + "The token generator failed to generate the access token.", ERROR_URI); + throw new OAuth2AuthenticationException(error); + } + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), + generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); + if (generatedAccessToken instanceof ClaimAccessor) { + authorizationBuilder.id(accessToken.getTokenValue()) + .token(accessToken, + (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, + ((ClaimAccessor) generatedAccessToken).getClaims())) + // 0.4.0 新增的方法 + .authorizedScopes(authorizedScopes) + .attribute(Principal.class.getName(), usernamePasswordAuthentication); + } + else { + authorizationBuilder.id(accessToken.getTokenValue()).accessToken(accessToken); + } + + // ----- Refresh token ----- + OAuth2RefreshToken refreshToken = null; + if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) && + // Do not issue refresh token to public client + !clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) { + + if (this.refreshTokenGenerator != null) { + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getRefreshTokenTimeToLive()); + refreshToken = new OAuth2RefreshToken(this.refreshTokenGenerator.get(), issuedAt, expiresAt); + } + else { + tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build(); + OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext); + if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) { + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, + "The token generator failed to generate the refresh token.", ERROR_URI); + throw new OAuth2AuthenticationException(error); + } + refreshToken = (OAuth2RefreshToken) generatedRefreshToken; + } + authorizationBuilder.refreshToken(refreshToken); + } + + OAuth2Authorization authorization = authorizationBuilder.build(); + + this.authorizationService.save(authorization); + + LOGGER.debug("returning OAuth2AccessTokenAuthenticationToken"); + + return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, + Objects.requireNonNull(authorization.getAccessToken().getClaims())); + } + /** * 登录异常转换为oauth2异常 * @param authentication 身份验证 diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigRedisOAuth2AuthorizationService.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigRedisOAuth2AuthorizationService.java index ad7a8bb67..502dfc28a 100644 --- a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigRedisOAuth2AuthorizationService.java +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigRedisOAuth2AuthorizationService.java @@ -1,9 +1,13 @@ package com.pig4cloud.pigx.common.security.service; +import cn.hutool.core.collection.CollUtil; +import com.pig4cloud.pigx.common.core.constant.SecurityConstants; +import com.pig4cloud.pigx.common.core.util.KeyStrResolver; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.lang.Nullable; +import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -17,6 +21,7 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.concurrent.TimeUnit; /** @@ -32,6 +37,8 @@ public class PigRedisOAuth2AuthorizationService implements OAuth2AuthorizationSe private final RedisTemplate redisTemplate; + private final KeyStrResolver tenantKeyStrResolver; + @Override public void save(OAuth2Authorization authorization) { Assert.notNull(authorization, "authorization cannot be null"); @@ -68,6 +75,11 @@ public class PigRedisOAuth2AuthorizationService implements OAuth2AuthorizationSe redisTemplate.setValueSerializer(RedisSerializer.java()); redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()), authorization, between, TimeUnit.SECONDS); + + // 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx + String tokenUsername = String.format("%s::%s::%s::%s::%s", tenantKeyStrResolver.key(), AUTHORIZATION, + SecurityConstants.DETAILS_USERNAME, authorization.getPrincipalName(), accessToken.getTokenValue()); + redisTemplate.opsForValue().set(tokenUsername, accessToken.getTokenValue(), between, TimeUnit.SECONDS); } } @@ -96,7 +108,13 @@ public class PigRedisOAuth2AuthorizationService implements OAuth2AuthorizationSe if (isAccessToken(authorization)) { OAuth2AccessToken accessToken = authorization.getAccessToken().getToken(); keys.add(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue())); + + // 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx + String key = String.format("%s::%s::%s::%s::%s", tenantKeyStrResolver.key(), AUTHORIZATION, + SecurityConstants.DETAILS_USERNAME, authorization.getPrincipalName(), accessToken.getTokenValue()); + keys.add(key); } + redisTemplate.delete(keys); } @@ -116,7 +134,7 @@ public class PigRedisOAuth2AuthorizationService implements OAuth2AuthorizationSe } private String buildKey(String type, String id) { - return String.format("%s::%s::%s", AUTHORIZATION, type, id); + return String.format("%s::%s::%s::%s", tenantKeyStrResolver.key(), AUTHORIZATION, type, id); } private static boolean isState(OAuth2Authorization authorization) { @@ -137,4 +155,32 @@ public class PigRedisOAuth2AuthorizationService implements OAuth2AuthorizationSe return Objects.nonNull(authorization.getAccessToken()); } + /** + * 扩展方法根据 username 查询是否存在存储的 + * @param authentication + * @return + */ + public void removeByUsername(Authentication authentication) { + // 根据 username查询对应access-token + String authenticationName = authentication.getName(); + + // 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx + String tokenUsernameKey = String.format("%s::%s::%s::%s::*", tenantKeyStrResolver.key(), AUTHORIZATION, + SecurityConstants.DETAILS_USERNAME, authenticationName); + Set keys = redisTemplate.keys(tokenUsernameKey); + if (CollUtil.isEmpty(keys)) { + return; + } + + List tokenList = redisTemplate.opsForValue().multiGet(keys); + + for (Object token : tokenList) { + // 根据token 查询存储的 OAuth2Authorization + OAuth2Authorization authorization = this.findByToken((String) token, OAuth2TokenType.ACCESS_TOKEN); + // 根据 OAuth2Authorization 删除相关令牌 + this.remove(authorization); + } + + } + } diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigRemoteRegisteredClientRepository.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigRemoteRegisteredClientRepository.java index b29ee99a7..80680f9bf 100644 --- a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigRemoteRegisteredClientRepository.java +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigRemoteRegisteredClientRepository.java @@ -2,12 +2,15 @@ package com.pig4cloud.pigx.common.security.service; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; import com.pig4cloud.pigx.admin.api.entity.SysOauthClientDetails; import com.pig4cloud.pigx.admin.api.feign.RemoteClientDetailsService; +import com.pig4cloud.pigx.common.core.constant.CacheConstants; import com.pig4cloud.pigx.common.core.constant.SecurityConstants; import com.pig4cloud.pigx.common.core.util.RetOps; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import org.springframework.cache.annotation.Cacheable; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2Error; @@ -20,6 +23,7 @@ import org.springframework.security.oauth2.server.authorization.settings.TokenSe import java.time.Duration; import java.util.Arrays; +import java.util.Map; import java.util.Optional; /** @@ -81,8 +85,7 @@ public class PigRemoteRegisteredClientRepository implements RegisteredClientRepo */ @Override @SneakyThrows - // @Cacheable(value = CacheConstants.CLIENT_DETAILS_KEY, key = "#clientId", unless = - // "#result == null") + @Cacheable(value = CacheConstants.CLIENT_DETAILS_KEY, key = "#clientId", unless = "#result == null") public RegisteredClient findByClientId(String clientId) { SysOauthClientDetails clientDetails = RetOps @@ -107,6 +110,13 @@ public class PigRemoteRegisteredClientRepository implements RegisteredClientRepo Optional.ofNullable(clientDetails.getScope()).ifPresent( scope -> Arrays.stream(scope.split(StrUtil.COMMA)).filter(StrUtil::isNotBlank).forEach(builder::scope)); + // 注入扩展配置 + Optional.ofNullable(clientDetails.getAdditionalInformation()).ifPresent(ext -> { + Map map = JSONUtil.parseObj(ext).toBean(Map.class); + builder.clientSettings(ClientSettings.withSettings(map) + .requireAuthorizationConsent(!BooleanUtil.toBoolean(clientDetails.getAutoapprove())).build()); + }); + return builder .tokenSettings(TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.REFERENCE) .accessTokenTimeToLive(Duration.ofSeconds(Optional @@ -115,8 +125,7 @@ public class PigRemoteRegisteredClientRepository implements RegisteredClientRepo Duration.ofSeconds(Optional.ofNullable(clientDetails.getRefreshTokenValidity()) .orElse(refreshTokenValiditySeconds))) .build()) - .clientSettings(ClientSettings.builder() - .requireAuthorizationConsent(!BooleanUtil.toBoolean(clientDetails.getAutoapprove())).build()) + .build(); }