diff --git a/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/pom.xml b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/pom.xml new file mode 100644 index 00000000..9cf3e878 --- /dev/null +++ b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/pom.xml @@ -0,0 +1,53 @@ + + + + shenyu-plugin-security + org.apache.shenyu + 2.6.0-SNAPSHOT + + 4.0.0 + + shenyu-plugin-maxkey + + + + org.apache.shenyu + shenyu-plugin-base + ${project.version} + + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.client + 1.0.2 + + + + cn.hutool + hutool-all + 5.8.16 + + + + + org.springframework.security + spring-security-test + test + + + + org.springframework + spring-test + test + + + + io.projectreactor + reactor-test + test + + + + \ No newline at end of file diff --git a/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/MaxKeyPlugin.java b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/MaxKeyPlugin.java new file mode 100644 index 00000000..85aa4ffb --- /dev/null +++ b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/MaxKeyPlugin.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyu.plugin.maxkey; + +import cn.hutool.core.codec.Base64; +import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.common.dto.SelectorData; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.common.utils.GsonUtils; +import org.apache.shenyu.common.utils.Singleton; +import org.apache.shenyu.plugin.api.ShenyuPluginChain; +import org.apache.shenyu.plugin.api.result.ShenyuResultEnum; +import org.apache.shenyu.plugin.api.result.ShenyuResultWrap; +import org.apache.shenyu.plugin.api.utils.WebFluxResultUtils; +import org.apache.shenyu.plugin.base.AbstractShenyuPlugin; +import org.apache.shenyu.plugin.maxkey.config.MaxkeyConfig; +import org.apache.shenyu.plugin.maxkey.service.MaxkeyService; +import org.apache.shenyu.plugin.maxkey.service.MaxkeyUser; +import org.springframework.http.HttpHeaders; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.Objects; + +/** + * The type Maxkey plugin. + */ +public class MaxKeyPlugin extends AbstractShenyuPlugin { + + @Override + protected Mono doExecute(final ServerWebExchange exchange, final ShenyuPluginChain chain, final SelectorData selector, final RuleData rule) { + MaxkeyService maxkeyService = Singleton.INST.get(MaxkeyService.class); + MaxkeyConfig config = maxkeyService.getMaxkeyConfig(); + ServerHttpRequest request = exchange.getRequest(); + + // 这里处理需要token的逻辑 + if (config.isBearerOnly()) { + String token = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); + boolean isActive = maxkeyService.introspectAccessToken(token); + if (isActive) { + // 根据配置决定是否获取userInfo + return chain.execute(handlerUserInfo(exchange, token, maxkeyService, config.isSetUserInfoHeader())); + } + Object error = ShenyuResultWrap.error(exchange, ShenyuResultEnum.ERROR_TOKEN); + return WebFluxResultUtils.result(exchange, error); + } + + // 走到这里说明没有token 需要处理code授权码的逻辑 + String code = request.getQueryParams().getFirst("code"); + String state = request.getQueryParams().getFirst("state"); + if (Objects.nonNull(code)) { + String token = maxkeyService.getOAuthToken(code); + // 根据配置决定是否获取userInfo + return chain.execute(handlerUserInfo(exchange, token, maxkeyService, config.isSetUserInfoHeader())); + } + + // 走到这里说明没有code 需要重定向至IdP服务获取code + return maxkeyService.redirect(exchange, state); + } + + @Override + public int getOrder() { + return PluginEnum.MAXKEY.getCode(); + } + + @Override + public String named() { + return PluginEnum.MAXKEY.getName(); + } + + @Override + public boolean skip(final ServerWebExchange exchange) { + return false; + } + + // 判断直接传递token还是传递userInfo + private ServerWebExchange handlerUserInfo(final ServerWebExchange exchange, final String token, final MaxkeyService maxkeyService, final boolean setUserInfo) { + if (setUserInfo) { + MaxkeyUser maxkeyUser = maxkeyService.getMaxkeyUser(token); + return handleToken(exchange, maxkeyUser); + } else { + return handleToken(exchange, token); + } + } + + // 直接使用AccessToken访问收保护的资源 + private ServerWebExchange handleToken(final ServerWebExchange exchange, final String accessToken) { + ServerHttpRequest.Builder mutate = exchange.getRequest().mutate(); + mutate.headers(httpHeaders -> httpHeaders.remove(HttpHeaders.ACCEPT_ENCODING)); + mutate.header(HttpHeaders.AUTHORIZATION, accessToken); + return exchange.mutate().request(mutate.build()).build(); + } + + // 获取原始请求对象 根据MaxKey认证解析后的userInfo 重新构建请求头 访问收保护的资源 + private ServerWebExchange handleToken(final ServerWebExchange exchange, final MaxkeyUser maxkeyUser) { + ServerHttpRequest.Builder mutate = exchange.getRequest().mutate(); + mutate.headers(httpHeaders -> httpHeaders.remove(HttpHeaders.ACCEPT_ENCODING)); + String maxkeyUserInfoJson = GsonUtils.getInstance().toJson(maxkeyUser); + mutate.header("X-Userinfo", Base64.encode(maxkeyUserInfoJson)); + return exchange.mutate().request(mutate.build()).build(); + } + +} diff --git a/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/config/MaxkeyConfig.java b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/config/MaxkeyConfig.java new file mode 100644 index 00000000..482d2159 --- /dev/null +++ b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/config/MaxkeyConfig.java @@ -0,0 +1,377 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyu.plugin.maxkey.config; + +public class MaxkeyConfig { + + private String clientId; + + private String clientSecret; + + private String authorizationEndpoint; + + private String scope; + + private String responseType; + + private String redirectUrl; + + private String realm; + + private String grantType; + + private String tokenEndpoint; + + private boolean bearerOnly; + + private String introspectionEndpoint; + + private boolean setUserInfoHeader; + + private String userInfoEndpoint; + + private String introspectionEndpointAuthMethodsSupported; + + private String discovery; + + public MaxkeyConfig() { + } + + public MaxkeyConfig(final String clientId, + final String clientSecret, + final String authorizationEndpoint, + final String scope, + final String responseType, + final String redirectUrl, + final String realm, + final String grantType, + final String tokenEndpoint, + final boolean bearerOnly, + final String introspectionEndpoint, + final boolean setUserInfoHeader, + final String userInfoEndpoint, + final String introspectionEndpointAuthMethodsSupported, + final String discovery) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.authorizationEndpoint = authorizationEndpoint; + this.scope = scope; + this.responseType = responseType; + this.redirectUrl = redirectUrl; + this.realm = realm; + this.grantType = grantType; + this.tokenEndpoint = tokenEndpoint; + this.bearerOnly = bearerOnly; + this.introspectionEndpoint = introspectionEndpoint; + this.setUserInfoHeader = setUserInfoHeader; + this.userInfoEndpoint = userInfoEndpoint; + this.introspectionEndpointAuthMethodsSupported = introspectionEndpointAuthMethodsSupported; + this.discovery = discovery; + } + + /** + * Gets clientId. + * + * @return the clientId + */ + public String getClientId() { + return clientId; + } + + /** + * Sets clientId. + * + * @param clientId the clientId + */ + public void setClientId(final String clientId) { + this.clientId = clientId; + } + + /** + * Gets clientSecret. + * + * @return the clientSecret + */ + public String getClientSecret() { + return clientSecret; + } + + /** + * Sets clientSecret. + * + * @param clientSecret the clientSecret + */ + public void setClientSecret(final String clientSecret) { + this.clientSecret = clientSecret; + } + + /** + * Gets authorizationEndpoint. + * + * @return the authorizationEndpoint + */ + public String getAuthorizationEndpoint() { + return authorizationEndpoint; + } + + /** + * Sets authorizationEndpoint. + * + * @param authorizationEndpoint the authorizationEndpoint + */ + public void setAuthorizationEndpoint(final String authorizationEndpoint) { + this.authorizationEndpoint = authorizationEndpoint; + } + + /** + * Gets scope. + * + * @return the scope + */ + public String getScope() { + return scope; + } + + /** + * Sets scope. + * + * @param scope the scope + */ + public void setScope(final String scope) { + this.scope = scope; + } + + /** + * Gets responseType. + * + * @return the responseType + */ + public String getResponseType() { + return responseType; + } + + /** + * Sets responseType. + * + * @param responseType the responseType + */ + public void setResponseType(final String responseType) { + this.responseType = responseType; + } + + /** + * Gets redirectUrl. + * + * @return the redirectUrl + */ + public String getRedirectUrl() { + return redirectUrl; + } + + /** + * Sets redirectUrl. + * + * @param redirectUrl the redirectUrl + */ + public void setRedirectUrl(final String redirectUrl) { + this.redirectUrl = redirectUrl; + } + + /** + * Gets realm. + * + * @return the realm + */ + public String getRealm() { + return realm; + } + + /** + * Sets realm. + * + * @param realm the realm + */ + public void setRealm(final String realm) { + this.realm = realm; + } + + /** + * Gets tokenType. + * + * @return the tokenType + */ + public String getGrantType() { + return grantType; + } + + /** + * Sets grantType. + * + * @param grantType the grantType + */ + public void setGrantType(final String grantType) { + this.grantType = grantType; + } + + /** + * Gets tokenEndpoint. + * + * @return the tokenEndpoint + */ + public String getTokenEndpoint() { + return tokenEndpoint; + } + + /** + * Sets tokenEndpoint. + * + * @param tokenEndpoint the tokenEndpoint + */ + public void setTokenEndpoint(final String tokenEndpoint) { + this.tokenEndpoint = tokenEndpoint; + } + + /** + * Is bearerOnly. + * + * @return is bearerOnly + */ + public boolean isBearerOnly() { + return bearerOnly; + } + + /** + * Sets bearerOnly. + * + * @param bearerOnly the bearerOnly + */ + public void setBearerOnly(final boolean bearerOnly) { + this.bearerOnly = bearerOnly; + } + + /** + * Gets introspectionEndpoint. + * + * @return the introspectionEndpoint + */ + public String getIntrospectionEndpoint() { + return introspectionEndpoint; + } + + /** + * Sets introspectionEndpoint. + * + * @param introspectionEndpoint the introspectionEndpoint + */ + public void setIntrospectionEndpoint(final String introspectionEndpoint) { + this.introspectionEndpoint = introspectionEndpoint; + } + + /** + * Is setUserInfoHeader. + * + * @return is setUserInfoHeader + */ + public boolean isSetUserInfoHeader() { + return setUserInfoHeader; + } + + /** + * Sets setUserInfoHeader. + * + * @param setUserInfoHeader the setUserInfoHeader + */ + public void setSetUserInfoHeader(final boolean setUserInfoHeader) { + this.setUserInfoHeader = setUserInfoHeader; + } + + /** + * Gets userInfoEndpoint. + * + * @return the userInfoEndpoint + */ + public String getUserInfoEndpoint() { + return userInfoEndpoint; + } + + /** + * Sets userInfoEndpoint. + * + * @param userInfoEndpoint the userInfoEndpoint + */ + public void setUserInfoEndpoint(final String userInfoEndpoint) { + this.userInfoEndpoint = userInfoEndpoint; + } + + /** + * Gets introspectionEndpointAuthMethodsSupported. + * + * @return the introspectionEndpointAuthMethodsSupported + */ + public String getIntrospectionEndpointAuthMethodsSupported() { + return introspectionEndpointAuthMethodsSupported; + } + + /** + * Sets introspectionEndpointAuthMethodsSupported. + * + * @param introspectionEndpointAuthMethodsSupported the accessToken + */ + public void setIntrospectionEndpointAuthMethodsSupported(final String introspectionEndpointAuthMethodsSupported) { + this.introspectionEndpointAuthMethodsSupported = introspectionEndpointAuthMethodsSupported; + } + + /** + * Gets discovery. + * + * @return the discovery + */ + public String getDiscovery() { + return discovery; + } + + /** + * Sets discovery. + * + * @param discovery the discovery + */ + public void setDiscovery(final String discovery) { + this.discovery = discovery; + } + + @Override + public String toString() { + return "MaxkeyConfig{" + + "clientId='" + clientId + '\'' + + ", clientSecret='" + clientSecret + '\'' + + ", authorizationEndpoint='" + authorizationEndpoint + '\'' + + ", scope='" + scope + '\'' + + ", responseType='" + responseType + '\'' + + ", redirectUrl='" + redirectUrl + '\'' + + ", realm='" + realm + '\'' + + ", grantType='" + grantType + '\'' + + ", tokenEndpoint='" + tokenEndpoint + '\'' + + ", bearerOnly=" + bearerOnly + + ", introspectionEndpoint='" + introspectionEndpoint + '\'' + + ", setUserInfoHeader=" + setUserInfoHeader + + ", userInfoEndpoint='" + userInfoEndpoint + '\'' + + ", introspectionEndpointAuthMethodsSupported='" + introspectionEndpointAuthMethodsSupported + '\'' + + ", discovery='" + discovery + '\'' + + '}'; + } +} diff --git a/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/handle/MaxkeyPluginDataHandler.java b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/handle/MaxkeyPluginDataHandler.java new file mode 100644 index 00000000..89f795d5 --- /dev/null +++ b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/handle/MaxkeyPluginDataHandler.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyu.plugin.maxkey.handle; + +import org.apache.shenyu.common.dto.PluginData; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.common.utils.GsonUtils; +import org.apache.shenyu.common.utils.Singleton; +import org.apache.shenyu.plugin.base.handler.PluginDataHandler; +import org.apache.shenyu.plugin.maxkey.config.MaxkeyConfig; +import org.apache.shenyu.plugin.maxkey.service.MaxkeyService; + +import java.util.Map; +import java.util.Optional; + +public class MaxkeyPluginDataHandler implements PluginDataHandler { + + @Override + public void handlerPlugin(final PluginData pluginData) { + + // 获取配置参数 + Map configMap = GsonUtils.getInstance().toObjectMap(pluginData.getConfig(), String.class); + + final String clientId = Optional.ofNullable(configMap.get("clientId")).orElse(""); + final String clientSecret = Optional.ofNullable(configMap.get("clientSecret")).orElse(""); + final String authorizationEndpoint = Optional.ofNullable(configMap.get("authorizationEndpoint")).orElse(""); + final String scope = Optional.ofNullable(configMap.get("scope")).orElse(""); + final String responseType = Optional.ofNullable(configMap.get("responseType")).orElse(""); + final String redirectUrl = Optional.ofNullable(configMap.get("redirectUrl")).orElse(""); + final String realm = Optional.ofNullable(configMap.get("realm")).orElse(""); + final String grantType = Optional.ofNullable(configMap.get("grantType")).orElse(""); + final String tokenEndpoint = Optional.ofNullable(configMap.get("tokenEndpoint")).orElse(""); + final boolean bearerOnly = Optional.ofNullable(configMap.get("bearerOnly")).map(Boolean::parseBoolean).orElse(false); + final String introspectionEndpoint = Optional.ofNullable(configMap.get("introspectionEndpoint")).orElse(""); + final String introspectionEndpointAuthMethodsSupported = Optional.ofNullable(configMap.get("introspectionEndpointAuthMethodsSupported")).orElse(""); + final boolean setUserInfoHeader = Optional.ofNullable(configMap.get("setUserInfoHeader")).map(Boolean::parseBoolean).orElse(false); + final String userInfoEndpoint = Optional.ofNullable(configMap.get("userInfoEndpoint")).orElse(""); + final String discovery = Optional.ofNullable(configMap.get("discovery")).orElse(""); + + // 获取MaxkeyConfig + MaxkeyConfig maxkeyConfig = new MaxkeyConfig( + clientId, + clientSecret, + authorizationEndpoint, + scope, + responseType, + redirectUrl, + realm, + grantType, + tokenEndpoint, + bearerOnly, + introspectionEndpoint, + setUserInfoHeader, + userInfoEndpoint, + introspectionEndpointAuthMethodsSupported, + discovery); + + // 根据参数实例化 MaxkeyService 鉴权服务 + MaxkeyService maxkeyService = new MaxkeyService(maxkeyConfig); + Singleton.INST.single(MaxkeyService.class, maxkeyService); + } + + @Override + public String pluginNamed() { + return PluginEnum.MAXKEY.getName(); + } + +} diff --git a/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/service/Introspection.java b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/service/Introspection.java new file mode 100644 index 00000000..afe39677 --- /dev/null +++ b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/service/Introspection.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyu.plugin.maxkey.service; + +public class Introspection { + + private String token; + + private boolean active; + + private String sub; + + /** + * Gets token. + * + * @return the token + */ + public String getToken() { + return token; + } + + /** + * Sets token. + * + * @param token the token + */ + public void setToken(final String token) { + this.token = token; + } + + /** + * Gets active. + * + * @return is active + */ + public boolean isActive() { + return active; + } + + /** + * Sets active. + * + * @param active the active + */ + public void setActive(final boolean active) { + this.active = active; + } + + /** + * Gets sub. + * + * @return the sub + */ + public String getSub() { + return sub; + } + + /** + * Sets sub. + * + * @param sub the sub + */ + public void setSub(final String sub) { + this.sub = sub; + } +} diff --git a/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/service/MaxkeyService.java b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/service/MaxkeyService.java new file mode 100644 index 00000000..d036d8cf --- /dev/null +++ b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/service/MaxkeyService.java @@ -0,0 +1,180 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyu.plugin.maxkey.service; + +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import com.google.gson.JsonSyntaxException; +import org.apache.commons.lang3.StringUtils; +import org.apache.oltu.oauth2.client.OAuthClient; +import org.apache.oltu.oauth2.client.URLConnectionClient; +import org.apache.oltu.oauth2.client.request.OAuthClientRequest; +import org.apache.oltu.oauth2.client.response.OAuthJSONAccessTokenResponse; +import org.apache.oltu.oauth2.common.OAuth; +import org.apache.oltu.oauth2.common.exception.OAuthProblemException; +import org.apache.oltu.oauth2.common.exception.OAuthSystemException; +import org.apache.oltu.oauth2.common.message.types.GrantType; +import org.apache.shenyu.common.utils.GsonUtils; +import org.apache.shenyu.plugin.maxkey.config.MaxkeyConfig; +import org.springframework.http.HttpHeaders; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.publisher.Mono; + +public class MaxkeyService { + + private static final int REDIRECT_STATE_CODE = 302; + + private final MaxkeyConfig maxkeyConfig; + + public MaxkeyService(final MaxkeyConfig maxkeyConfig) { + this.maxkeyConfig = maxkeyConfig; + } + + /** + * redirect unauthenticated requests to the IdP service. + * + * @param exchange exchange + * @param state state + * @return void + */ + public Mono redirect(final ServerWebExchange exchange, final String state) { + ServerHttpResponse response = exchange.getResponse(); + String redirectUrl = UriComponentsBuilder.fromUriString(maxkeyConfig.getAuthorizationEndpoint()) + .queryParam("response_type", maxkeyConfig.getResponseType()) + .queryParam("client_id", maxkeyConfig.getClientId()) + .queryParam("redirect_uri", maxkeyConfig.getRedirectUrl()) + .queryParam("scope", maxkeyConfig.getScope()) + .queryParam("state", state) + .build() + .toUriString(); + + response.setRawStatusCode(REDIRECT_STATE_CODE); + response.getHeaders().add(HttpHeaders.LOCATION, redirectUrl); + return response.setComplete(); + } + + /** + * getAccessToken from maxkey service. + * + * @param code code + * @return String + */ + public String getOAuthToken(final String code) { + try { + OAuthClientRequest oAuthClientRequest = OAuthClientRequest + .tokenLocation(maxkeyConfig.getTokenEndpoint()) + .setGrantType(GrantType.AUTHORIZATION_CODE) + .setClientId(maxkeyConfig.getClientId()) + .setClientSecret(maxkeyConfig.getClientSecret()) + .setRedirectURI(String.format("%s", maxkeyConfig.getRedirectUrl())) + .setCode(code) + .buildQueryMessage(); + OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient()); + OAuthJSONAccessTokenResponse oAuthResponse = oAuthClient.accessToken(oAuthClientRequest, OAuth.HttpMethod.POST); + return oAuthResponse.getAccessToken(); + } catch (OAuthSystemException | OAuthProblemException e) { + throw new RuntimeException("Code error, cannot get OAuth token from maxkey server.", e); + } + } + + /** + * getOidcToken from maxkey service. + * + * @param code code + * @param state state + * @return OIDCToken + */ + public OIDCToken getOidcToken(final String code, final String state) { + String url = maxkeyConfig.getTokenEndpoint(); + String responseType = maxkeyConfig.getResponseType(); + String redirectUri = maxkeyConfig.getRedirectUrl(); + String scope = maxkeyConfig.getScope(); + String clientId = maxkeyConfig.getClientId(); + String clientSecret = maxkeyConfig.getClientSecret(); + String grantType = maxkeyConfig.getGrantType(); + + String requestUrl = String.format("%s?response_type=%s&code=%s&redirect_uri=%s&scope=%s&client_id=%s&client_secret=%s&grant_type=%s&state=%s", + url, responseType, code, redirectUri, scope, clientId, clientSecret, grantType, state); + HttpResponse response = HttpUtil.createGet(requestUrl).execute(); + OIDCToken oidcToken; + try { + oidcToken = GsonUtils.getInstance().fromJson(response.body(), OIDCToken.class); + } catch (JsonSyntaxException e) { + throw new RuntimeException("Code error, cannot get OAuth token from maxkey server.", e); + } + return oidcToken; + } + + /** + * introspect AccessToken via maxkey authentication server. + * + * @param token access token + * @return boolean + */ + public boolean introspectAccessToken(final String token) { + if (StringUtils.isBlank(token)) { + return false; + } + String url = maxkeyConfig.getIntrospectionEndpoint(); + String requestUrl = String.format("%s?access_token=%s", url, token); + HttpResponse response = HttpUtil + .createGet(requestUrl) + .execute(); + Introspection introspection = GsonUtils.getInstance().fromJson(response.body(), Introspection.class); + return introspection.isActive(); + } + + /** + * get maxkey user by access token. + * + * @param token access token + * @return MaxkeyUser + */ + public MaxkeyUser getMaxkeyUser(final String token) { + String fullUserInfoJson = getUserInfo(token); + return GsonUtils.getInstance().fromJson(fullUserInfoJson, MaxkeyUser.class); + } + + /** + * get user info by access token. + * + * @param token access token + * @return String + */ + public String getUserInfo(final String token) { + String url = maxkeyConfig.getUserInfoEndpoint(); + String requestUrl = String.format("%s?access_token=%s", url, token); + HttpResponse response = HttpUtil + .createGet(requestUrl) + .header("Content-Type", "application/x-www-form-urlencoded") + .execute(); + return response.body(); + } + + /** + * get maxkey config. + * + * @return MaxkeyConfig + */ + public MaxkeyConfig getMaxkeyConfig() { + return this.maxkeyConfig; + } +} + diff --git a/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/service/MaxkeyUser.java b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/service/MaxkeyUser.java new file mode 100644 index 00000000..4cd34510 --- /dev/null +++ b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/service/MaxkeyUser.java @@ -0,0 +1,376 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyu.plugin.maxkey.service; + +import com.google.gson.annotations.SerializedName; + +public class MaxkeyUser { + + private String userId; + + private String name; + + private String displayName; + + private String department; + + private String departmentId; + + private String gender; + + private String phoneNumber; + + private String email; + + private String region; + + private Address address; + + /** + * Gets userId. + * + * @return the userId + */ + public String getUserId() { + return userId; + } + + /** + * Sets userId. + * + * @param userId the userId + */ + public void setUserId(final String userId) { + this.userId = userId; + } + + /** + * Gets clientId. + * + * @return the clientId + */ + public String getName() { + return name; + } + + /** + * Sets name. + * + * @param name the name + */ + public void setName(final String name) { + this.name = name; + } + + /** + * Gets displayName. + * + * @return the displayName + */ + public String getDisplayName() { + return displayName; + } + + /** + * Sets displayName. + * + * @param displayName the displayName + */ + public void setDisplayName(final String displayName) { + this.displayName = displayName; + } + + /** + * Gets department. + * + * @return the department + */ + public String getDepartment() { + return department; + } + + /** + * Sets department. + * + * @param department the department + */ + public void setDepartment(final String department) { + this.department = department; + } + + /** + * Gets departmentId. + * + * @return the departmentId + */ + public String getDepartmentId() { + return departmentId; + } + + /** + * Sets departmentId. + * + * @param departmentId the departmentId + */ + public void setDepartmentId(final String departmentId) { + this.departmentId = departmentId; + } + + /** + * Gets gender. + * + * @return the gender + */ + public String getGender() { + return gender; + } + + /** + * Sets gender. + * + * @param gender the gender + */ + public void setGender(final String gender) { + this.gender = gender; + } + + /** + * Gets phoneNumber. + * + * @return the phoneNumber + */ + public String getPhoneNumber() { + return phoneNumber; + } + + /** + * Sets gender. + * + * @param phoneNumber the phoneNumber + */ + public void setPhoneNumber(final String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + /** + * Gets email. + * + * @return the email + */ + public String getEmail() { + return email; + } + + /** + * Sets email. + * + * @param email the email + */ + public void setEmail(final String email) { + this.email = email; + } + + /** + * Gets region. + * + * @return the region + */ + public String getRegion() { + return region; + } + + /** + * Sets region. + * + * @param region the region + */ + public void setRegion(final String region) { + this.region = region; + } + + /** + * Gets address. + * + * @return the address + */ + public Address getAddress() { + return address; + } + + /** + * Sets address. + * + * @param address the address + */ + public void setAddress(final Address address) { + this.address = address; + } + + @Override + public String toString() { + return "MaxkeyUser{" + + "userId='" + userId + '\'' + + ", name='" + name + '\'' + + ", displayName='" + displayName + '\'' + + ", department='" + department + '\'' + + ", departmentId='" + departmentId + '\'' + + ", gender='" + gender + '\'' + + ", phoneNumber='" + phoneNumber + '\'' + + ", email='" + email + '\'' + + ", region='" + region + '\'' + + ", address=" + address.toString() + + '}'; + } + + public static class Address { + + private String country; + + @SerializedName("street_address") + private String streetAddress; + + private String formatted; + + private String locality; + + private String region; + + @SerializedName("postal_code") + private String postalCode; + + /** + * Gets country. + * + * @return the country + */ + public String getCountry() { + return country; + } + + /** + * Sets streetAddress. + * + * @param country the streetAddress + */ + public void setCountry(final String country) { + this.country = country; + } + + /** + * Gets Street_address. + * + * @return the streetAddress + */ + public String getStreetAddress() { + return streetAddress; + } + + /** + * Sets streetAddress. + * + * @param streetAddress the streetAddress + */ + public void setStreetAddress(final String streetAddress) { + this.streetAddress = streetAddress; + } + + /** + * Gets formatted. + * + * @return the formatted + */ + public String getFormatted() { + return formatted; + } + + /** + * Sets formatted. + * + * @param formatted the formatted + */ + public void setFormatted(final String formatted) { + this.formatted = formatted; + } + + /** + * Gets formatted. + * + * @return the formatted + */ + public String getLocality() { + return locality; + } + + /** + * Sets locality. + * + * @param locality the locality + */ + public void setLocality(final String locality) { + this.locality = locality; + } + + /** + * Gets region. + * + * @return the region + */ + public String getRegion() { + return region; + } + + /** + * Sets region. + * + * @param region the region + */ + public void setRegion(final String region) { + this.region = region; + } + + /** + * Gets postalCode. + * + * @return the postalCode + */ + public String getPostalCode() { + return postalCode; + } + + /** + * Sets postalCode. + * + * @param postalCode the postalCode + */ + public void setPostalCode(final String postalCode) { + this.postalCode = postalCode; + } + + @Override + public String toString() { + return "Address{" + + "country='" + country + '\'' + + ", streetAddress='" + streetAddress + '\'' + + ", formatted='" + formatted + '\'' + + ", locality='" + locality + '\'' + + ", region='" + region + '\'' + + ", postalCode='" + postalCode + '\'' + + '}'; + } + } +} diff --git a/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/service/OIDCToken.java b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/service/OIDCToken.java new file mode 100644 index 00000000..5ebc9025 --- /dev/null +++ b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/service/OIDCToken.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyu.plugin.maxkey.service; + +import com.google.gson.annotations.SerializedName; + +public class OIDCToken { + + @SerializedName("access_token") + private String accessToken; + + @SerializedName("token_type") + private String tokenType; + + @SerializedName("refresh_token") + private String refreshToken; + + @SerializedName("expires_in") + private int expiresIn; + + @SerializedName("scope") + private String scope; + + @SerializedName("id_token") + private String idToken; + + public OIDCToken() { + } + + public OIDCToken(final String accessToken, + final String tokenType, + final String refreshToken, + final int expiresIn, + final String scope, + final String idToken) { + this.accessToken = accessToken; + this.tokenType = tokenType; + this.refreshToken = refreshToken; + this.expiresIn = expiresIn; + this.scope = scope; + this.idToken = idToken; + } + + /** + * Gets accessToken. + * + * @return the accessToken + */ + public String getAccessToken() { + return accessToken; + } + + /** + * Sets accessToken. + * + * @param accessToken the accessToken + */ + public void setAccessToken(final String accessToken) { + this.accessToken = accessToken; + } + + /** + * Gets tokenType. + * + * @return the tokenType + */ + public String getTokenType() { + return tokenType; + } + + /** + * Sets tokenType. + * + * @param tokenType the tokenType + */ + public void setTokenType(final String tokenType) { + this.tokenType = tokenType; + } + + /** + * Gets refreshToken. + * + * @return the refreshToken + */ + public String getRefreshToken() { + return refreshToken; + } + + /** + * Sets refreshToken. + * + * @param refreshToken the refreshToken + */ + public void setRefreshToken(final String refreshToken) { + this.refreshToken = refreshToken; + } + + /** + * Gets expiresIn. + * + * @return the expiresIn + */ + public int getExpiresIn() { + return expiresIn; + } + + /** + * Sets expiresIn. + * + * @param expiresIn the expiresIn + */ + public void setExpiresIn(final int expiresIn) { + this.expiresIn = expiresIn; + } + + /** + * Gets scope. + * + * @return the scope + */ + public String getScope() { + return scope; + } + + /** + * Sets scope. + * + * @param scope the scope + */ + public void setScope(final String scope) { + this.scope = scope; + } + + /** + * Gets idToken. + * + * @return the idToken + */ + public String getIdToken() { + return idToken; + } + + /** + * Sets idToken. + * + * @param idToken the idToken + */ + public void setIdToken(final String idToken) { + this.idToken = idToken; + } + + @Override + public String toString() { + return "OIDCToken{" + + "accessToken='" + accessToken + '\'' + + ", tokenType='" + tokenType + '\'' + + ", refreshToken='" + refreshToken + '\'' + + ", expiresIn=" + expiresIn + + ", scope='" + scope + '\'' + + ", idToken='" + idToken + '\'' + + '}'; + } +} diff --git a/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/test/java/org/apache/shenyu/plugin/maxkey/MaxkeyPluginTest.java b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/test/java/org/apache/shenyu/plugin/maxkey/MaxkeyPluginTest.java new file mode 100644 index 00000000..ce48bd9a --- /dev/null +++ b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/test/java/org/apache/shenyu/plugin/maxkey/MaxkeyPluginTest.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyu.plugin.maxkey; + +import org.apache.shenyu.common.dto.PluginData; +import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.common.dto.SelectorData; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.common.utils.Singleton; +import org.apache.shenyu.plugin.api.ShenyuPluginChain; +import org.apache.shenyu.plugin.api.result.DefaultShenyuResult; +import org.apache.shenyu.plugin.api.result.ShenyuResult; +import org.apache.shenyu.plugin.api.utils.SpringBeanUtils; +import org.apache.shenyu.plugin.maxkey.config.MaxkeyConfig; +import org.apache.shenyu.plugin.maxkey.handle.MaxkeyPluginDataHandler; +import org.apache.shenyu.plugin.maxkey.service.MaxkeyService; +import org.apache.shenyu.plugin.maxkey.service.MaxkeyUser; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.Mockito; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MaxkeyPluginTest { + + @Spy + private MaxKeyPlugin maxkeyPluginTest; + + @Spy + private MaxkeyPluginDataHandler maxkeyPluginDataHandlerTest; + + private ServerWebExchange exchange; + + @Mock + private ShenyuPluginChain chain; + + @Mock + private SelectorData selector; + + @Mock + private RuleData rule; + + @BeforeEach + void setup() { + ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class); + when(context.getBean(ShenyuResult.class)).thenReturn(new DefaultShenyuResult()); + SpringBeanUtils springBeanUtils = SpringBeanUtils.getInstance(); + springBeanUtils.setApplicationContext(context); + MockitoAnnotations.openMocks(this); + // 模拟请求 + exchange = MockServerWebExchange + .from(MockServerHttpRequest + .get("localhost") + .header(HttpHeaders.AUTHORIZATION, "25c8d6a6-ad3a-4767-8bfb-d80641b8dfdc") + .build()); + } + + @Test + void doExecute() { + final PluginData pluginData = new PluginData( + "pluginId", + "pluginName", + "{\n" + + "\"clientId\": \"ae20330a-ef0b-4dad-9f10-d5e3485ca2ad\",\n" + + "\"clientSecret\": \"KQY4MDUwNjIwMjAxNTE3NTM1OTEYty\",\n" + + "\"authorizationEndpoint\": \"http://192.168.1.16/sign/authz/oauth/v20/authorize\",\n" + + "\"scope\": \"openid\",\n" + + "\"responseType\": \"code\",\n" + + "\"redirectUrl\": \"http://192.168.1.5:9195/http/shenyu/client/hello\",\n" + + "\"realm\": \"1\",\n" + + "\"grantType\": \"authorization_code\",\n" + + "\"tokenEndpoint\": \"http://192.168.1.16/sign/authz/oauth/v20/token\",\n" + + "\"bearerOnly\": \"true\",\n" + + "\"introspectionEndpoint\": \"http://192.168.1.16/sign/authz/oauth/v20/introspect\",\n" + + "\"setUserInfoHeader\": \"true\",\n" + + "\"userInfoEndpoint\": \"http://192.168.1.16/sign/api/connect/v10/userinfo\",\n" + + "\"introspectionEndpointAuthMethodsSupported\": \"client_secret_basic\",\n" + + "\"discovery\": \"http://192.168.1.16/sign/authz/oauth/v20/1/.well-known/openid-configuration\"\n" + + "}", + "0", + false, + null); + // 测试数据同步 + maxkeyPluginDataHandlerTest.handlerPlugin(pluginData); + + // 模拟服务 + MaxkeyService maxkeyService = mock(MaxkeyService.class); + MaxkeyConfig maxkeyConfig = mock(MaxkeyConfig.class); + when(maxkeyService.getMaxkeyConfig()).thenReturn(maxkeyConfig); + + exchange = MockServerWebExchange.from(MockServerHttpRequest + .get("localhost") + .queryParam("state", "state") + .queryParam("code", "code") + .header(HttpHeaders.AUTHORIZATION, "token") + .build()); + + // 先测试bearerOnly模式和getUserInfo模式 + final String token = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION); + // 处理 MaxkeyUser + MaxkeyUser maxkeyUser = new MaxkeyUser(); + maxkeyUser.setAddress(new MaxkeyUser.Address()); + Mockito.when(maxkeyService.getMaxkeyUser(token)).thenReturn(maxkeyUser); + + // 模拟 Maxkey认证服务执行 + Singleton.INST.single(MaxkeyService.class, maxkeyService); + // 认证执行之后 返回一个异步任务 + when(this.chain.execute(any())).thenReturn(Mono.empty()); + Mono mono = maxkeyPluginTest.doExecute(exchange, chain, selector, rule); + StepVerifier.create(mono).expectSubscription().verifyComplete(); + + // 再测试code模式 + maxkeyService = Singleton.INST.get(MaxkeyService.class); + maxkeyConfig = maxkeyService.getMaxkeyConfig(); + maxkeyConfig.setBearerOnly(false); + + exchange = MockServerWebExchange.from(MockServerHttpRequest + .get("localhost") + .queryParam("state", "state") + .queryParam("code", "code") + .build()); + Mockito.when(maxkeyService.getOAuthToken("code")).thenReturn(token); + Singleton.INST.single(MaxkeyService.class, maxkeyService); + mono = maxkeyPluginTest.doExecute(exchange, chain, selector, rule); + StepVerifier.create(mono).expectSubscription().verifyComplete(); + } + + @Test + public void testNamed() { + final String result = maxkeyPluginTest.named(); + Assertions.assertEquals(PluginEnum.MAXKEY.getName(), result); + } + + @Test + public void testGetOrder() { + final int result = maxkeyPluginTest.getOrder(); + Assertions.assertEquals(PluginEnum.MAXKEY.getCode(), result); + } + + @Test + public void skipTest() { + Assumptions.assumeFalse(maxkeyPluginTest.skip(exchange)); + } + +} diff --git a/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/test/java/org/apache/shenyu/plugin/maxkey/config/MaxkeyConfigTest.java b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/test/java/org/apache/shenyu/plugin/maxkey/config/MaxkeyConfigTest.java new file mode 100644 index 00000000..cab558c0 --- /dev/null +++ b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/test/java/org/apache/shenyu/plugin/maxkey/config/MaxkeyConfigTest.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyu.plugin.maxkey.config; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class MaxkeyConfigTest { + + @Test + public void maxkeyConfig() { + + MaxkeyConfig maxkeyConfig = new MaxkeyConfig("a", "b", "c", "d", "e", "f", "g", "h", "i", false, "j", false, "k", "l", "m"); + assertEquals("a", maxkeyConfig.getClientId()); + assertEquals("b", maxkeyConfig.getClientSecret()); + assertEquals("c", maxkeyConfig.getAuthorizationEndpoint()); + assertEquals("d", maxkeyConfig.getScope()); + assertEquals("e", maxkeyConfig.getResponseType()); + assertEquals("f", maxkeyConfig.getRedirectUrl()); + assertEquals("g", maxkeyConfig.getRealm()); + assertEquals("h", maxkeyConfig.getGrantType()); + assertEquals("i", maxkeyConfig.getTokenEndpoint()); + assertFalse(maxkeyConfig.isBearerOnly()); + assertEquals("j", maxkeyConfig.getIntrospectionEndpoint()); + assertFalse(maxkeyConfig.isSetUserInfoHeader()); + assertEquals("k", maxkeyConfig.getUserInfoEndpoint()); + assertEquals("l", maxkeyConfig.getIntrospectionEndpointAuthMethodsSupported()); + assertEquals("m", maxkeyConfig.getDiscovery()); + + MaxkeyConfig maxkeyConfig1 = new MaxkeyConfig(); + maxkeyConfig1.setClientId("a"); + maxkeyConfig1.setClientSecret("b"); + maxkeyConfig1.setAuthorizationEndpoint("c"); + maxkeyConfig1.setScope("d"); + maxkeyConfig1.setResponseType("e"); + maxkeyConfig1.setRedirectUrl("f"); + maxkeyConfig1.setRealm("g"); + maxkeyConfig1.setGrantType("h"); + maxkeyConfig1.setTokenEndpoint("i"); + maxkeyConfig1.setBearerOnly(false); + maxkeyConfig1.setIntrospectionEndpoint("j"); + maxkeyConfig1.setSetUserInfoHeader(false); + maxkeyConfig1.setUserInfoEndpoint("k"); + maxkeyConfig1.setIntrospectionEndpointAuthMethodsSupported("l"); + maxkeyConfig1.setDiscovery("m"); + assertEquals("a", maxkeyConfig.getClientId()); + assertEquals("b", maxkeyConfig.getClientSecret()); + assertEquals("c", maxkeyConfig.getAuthorizationEndpoint()); + assertEquals("d", maxkeyConfig.getScope()); + assertEquals("e", maxkeyConfig.getResponseType()); + assertEquals("f", maxkeyConfig.getRedirectUrl()); + assertEquals("g", maxkeyConfig.getRealm()); + assertEquals("h", maxkeyConfig.getGrantType()); + assertEquals("i", maxkeyConfig.getTokenEndpoint()); + assertFalse(maxkeyConfig.isBearerOnly()); + assertEquals("j", maxkeyConfig.getIntrospectionEndpoint()); + assertFalse(maxkeyConfig.isSetUserInfoHeader()); + assertEquals("k", maxkeyConfig.getUserInfoEndpoint()); + assertEquals("l", maxkeyConfig.getIntrospectionEndpointAuthMethodsSupported()); + assertEquals("m", maxkeyConfig.getDiscovery()); + assertEquals( + "MaxkeyConfig{clientId='a', " + + "clientSecret='b', " + + "authorizationEndpoint='c', " + + "scope='d', " + + "responseType='e', " + + "redirectUrl='f', " + + "realm='g', " + + "grantType='h', " + + "tokenEndpoint='i', " + + "bearerOnly=false, " + + "introspectionEndpoint='j', " + + "setUserInfoHeader=false, " + + "userInfoEndpoint='k', " + + "introspectionEndpointAuthMethodsSupported='l', " + + "discovery='m'}", + maxkeyConfig1.toString()); + } +} diff --git a/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/test/java/org/apache/shenyu/plugin/maxkey/handle/MaxkeyPluginDataHandlerTest.java b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/test/java/org/apache/shenyu/plugin/maxkey/handle/MaxkeyPluginDataHandlerTest.java new file mode 100644 index 00000000..a32ad247 --- /dev/null +++ b/summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/test/java/org/apache/shenyu/plugin/maxkey/handle/MaxkeyPluginDataHandlerTest.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyu.plugin.maxkey.handle; + +import org.apache.shenyu.common.dto.PluginData; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.common.utils.Singleton; +import org.apache.shenyu.plugin.maxkey.service.MaxkeyService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class MaxkeyPluginDataHandlerTest { + + private MaxkeyPluginDataHandler maxkeyPluginDataHandlerTest; + + @BeforeEach + public void setup() { + maxkeyPluginDataHandlerTest = new MaxkeyPluginDataHandler(); + } + + @Test + public void handlerPlugin() { + final PluginData pluginData = new PluginData( + "pluginId", + "pluginName", + "{\n" + + "\t\"clientId\": \"ae20330a-ef0b-4dad-9f10-d5e3485ca2ad\",\n" + + "\t\"clientSecret\": \"KQY4MDUwNjIwMjAxNTE3NTM1OTEYty\",\n" + + "\t\"authorizationEndpoint\": \"http://192.168.1.16/sign/authz/oauth/v20/authorize\",\n" + + "\t\"scope\": \"openid\",\n" + + "\t\"responseType\": \"code\",\n" + + "\t\"redirectUrl\": \"http://192.168.1.5:9195/http/shenyu/client/hello\",\n" + + "\t\"realm\": \"1\",\n" + + "\t\"grantType\": \"authorization_code\",\n" + + "\t\"tokenEndpoint\": \"http://192.168.1.16/sign/authz/oauth/v20/token\",\n" + + "\t\"bearerOnly\": \"false\",\n" + + "\t\"introspectionEndpoint\": \"http://192.168.1.16/sign/authz/oauth/v20/introspect\",\n" + + "\t\"setUserInfoHeader\": \"false\",\n" + + "\t\"userInfoEndpoint\": \"http://192.168.1.16/sign/api/connect/v10/userinfo\",\n" + + "\t\"introspectionEndpointAuthMethodsSupported\": \"client_secret_basic\",\n" + + "\t\"discovery\": \"http://192.168.1.16/sign/authz/oauth/v20/1/.well-known/openid-configuration\"\n" + + "}\n", + "0", + false, + null); + maxkeyPluginDataHandlerTest.handlerPlugin(pluginData); + MaxkeyService maxkeyService = Singleton.INST.get(MaxkeyService.class); + } + + @Test + public void testPluginNamed() { + final String result = maxkeyPluginDataHandlerTest.pluginNamed(); + assertEquals(PluginEnum.MAXKEY.getName(), result); + } +} diff --git a/summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/pom.xml b/summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/pom.xml new file mode 100644 index 00000000..880faec4 --- /dev/null +++ b/summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/pom.xml @@ -0,0 +1,34 @@ + + + + shenyu-spring-boot-starter-plugin + org.apache.shenyu + 2.6.0-SNAPSHOT + + 4.0.0 + + shenyu-spring-boot-starter-plugin-maxkey + + + + org.springframework.boot + spring-boot-autoconfigure + + + + org.apache.shenyu + shenyu-plugin-maxkey + ${project.version} + + + + org.springframework.security + spring-security-oauth2-client + ${spring-security.version} + true + + + + \ No newline at end of file diff --git a/summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/src/main/java/org/apache/shenyu/springboot/starter/plugin/maxkey/MaxkeyPluginConfiguration.java b/summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/src/main/java/org/apache/shenyu/springboot/starter/plugin/maxkey/MaxkeyPluginConfiguration.java new file mode 100644 index 00000000..0725ae15 --- /dev/null +++ b/summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/src/main/java/org/apache/shenyu/springboot/starter/plugin/maxkey/MaxkeyPluginConfiguration.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.shenyu.springboot.starter.plugin.maxkey; + +import org.apache.shenyu.plugin.api.ShenyuPlugin; +import org.apache.shenyu.plugin.base.handler.PluginDataHandler; +import org.apache.shenyu.plugin.maxkey.MaxKeyPlugin; +import org.apache.shenyu.plugin.maxkey.config.MaxkeyConfig; +import org.apache.shenyu.plugin.maxkey.handle.MaxkeyPluginDataHandler; +import org.apache.shenyu.plugin.maxkey.service.MaxkeyService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty(value = {"shenyu.plugins.maxkey.enabled"}, havingValue = "true", matchIfMissing = true) +public class MaxkeyPluginConfiguration { + + /** + * the maxkey plugin. + * @return the shenyu plugin + */ + @Bean + public ShenyuPlugin maxkeyPlugin() { + return new MaxKeyPlugin(); + } + + /** + * Maxkey plugin data handler. + * + * @return the plugin data handler + */ + @Bean + public PluginDataHandler maxkeyPluginDataHandler() { + return new MaxkeyPluginDataHandler(); + } + + /** + * Maxkey plugin data handler. + * + * @return the plugin data handler + */ + @Bean + public MaxkeyService maxkeyService() { + return new MaxkeyService(new MaxkeyConfig()); + } +} diff --git a/summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/src/main/resources/META-INF/spring.factories b/summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..1aa8887d --- /dev/null +++ b/summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/src/main/resources/META-INF/spring.factories @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.apache.shenyu.springboot.starter.plugin.maxkey.MaxkeyPluginConfiguration \ No newline at end of file diff --git a/summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/src/main/resources/META-INF/spring.provides b/summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/src/main/resources/META-INF/spring.provides new file mode 100644 index 00000000..dfdd3009 --- /dev/null +++ b/summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/src/main/resources/META-INF/spring.provides @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +provides: shenyu-spring-boot-starter-plugin-maxkey