Browse Source

OSPP-2023_shenyu

Saiph 1 year ago
parent
commit
1cc3ffa7cf
15 changed files with 1922 additions and 0 deletions
  1. 53 0
      summer-ospp/2023/shenyu/shenyu-plugin-maxkey/pom.xml
  2. 119 0
      summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/MaxKeyPlugin.java
  3. 377 0
      summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/config/MaxkeyConfig.java
  4. 83 0
      summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/handle/MaxkeyPluginDataHandler.java
  5. 81 0
      summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/service/Introspection.java
  6. 180 0
      summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/service/MaxkeyService.java
  7. 376 0
      summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/service/MaxkeyUser.java
  8. 178 0
      summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/main/java/org/apache/shenyu/plugin/maxkey/service/OIDCToken.java
  9. 175 0
      summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/test/java/org/apache/shenyu/plugin/maxkey/MaxkeyPluginTest.java
  10. 95 0
      summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/test/java/org/apache/shenyu/plugin/maxkey/config/MaxkeyConfigTest.java
  11. 72 0
      summer-ospp/2023/shenyu/shenyu-plugin-maxkey/src/test/java/org/apache/shenyu/plugin/maxkey/handle/MaxkeyPluginDataHandlerTest.java
  12. 34 0
      summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/pom.xml
  13. 62 0
      summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/src/main/java/org/apache/shenyu/springboot/starter/plugin/maxkey/MaxkeyPluginConfiguration.java
  14. 19 0
      summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/src/main/resources/META-INF/spring.factories
  15. 18 0
      summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/src/main/resources/META-INF/spring.provides

+ 53 - 0
summer-ospp/2023/shenyu/shenyu-plugin-maxkey/pom.xml

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>shenyu-plugin-security</artifactId>
+        <groupId>org.apache.shenyu</groupId>
+        <version>2.6.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>shenyu-plugin-maxkey</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-plugin-base</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.oltu.oauth2</groupId>
+            <artifactId>org.apache.oltu.oauth2.client</artifactId>
+            <version>1.0.2</version>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>5.8.16</version>
+        </dependency>
+
+        <!-- Spring Security Test -->
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>io.projectreactor</groupId>
+            <artifactId>reactor-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 119 - 0
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<Void> 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();
+    }
+
+}

+ 377 - 0
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 + '\''
+                + '}';
+    }
+}

+ 83 - 0
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<String, String> 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();
+    }
+
+}

+ 81 - 0
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;
+    }
+}

+ 180 - 0
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<Void> 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;
+    }
+}
+

+ 376 - 0
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 + '\''
+                    + '}';
+        }
+    }
+}

+ 178 - 0
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 + '\''
+                + '}';
+    }
+}

+ 175 - 0
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<Void> 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));
+    }
+
+}

+ 95 - 0
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());
+    }
+}

+ 72 - 0
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);
+    }
+}

+ 34 - 0
summer-ospp/2023/shenyu/shenyu-spring-boot-starter-plugin-maxkey/pom.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>shenyu-spring-boot-starter-plugin</artifactId>
+        <groupId>org.apache.shenyu</groupId>
+        <version>2.6.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>shenyu-spring-boot-starter-plugin-maxkey</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-autoconfigure</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-plugin-maxkey</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-oauth2-client</artifactId>
+            <version>${spring-security.version}</version>
+            <optional>true</optional>
+        </dependency>
+    </dependencies>
+
+</project>

+ 62 - 0
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());
+    }
+}

+ 19 - 0
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

+ 18 - 0
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