#
tokens: 49832/50000 14/349 files (page 6/9)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 6 of 9. Use http://codebase.md/higress-group/himarket?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .cursor
│   └── rules
│       ├── api-style.mdc
│       └── project-architecture.mdc
├── .gitignore
├── build.sh
├── deploy
│   ├── docker
│   │   ├── docker-compose.yml
│   │   └── Docker部署说明.md
│   └── helm
│       ├── Chart.yaml
│       ├── Helm部署说明.md
│       ├── templates
│       │   ├── _helpers.tpl
│       │   ├── himarket-admin-cm.yaml
│       │   ├── himarket-admin-deployment.yaml
│       │   ├── himarket-admin-service.yaml
│       │   ├── himarket-frontend-cm.yaml
│       │   ├── himarket-frontend-deployment.yaml
│       │   ├── himarket-frontend-service.yaml
│       │   ├── himarket-server-cm.yaml
│       │   ├── himarket-server-deployment.yaml
│       │   ├── himarket-server-service.yaml
│       │   ├── mysql.yaml
│       │   └── serviceaccount.yaml
│       └── values.yaml
├── LICENSE
├── NOTICE
├── pom.xml
├── portal-bootstrap
│   ├── Dockerfile
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── com
│       │   │       └── alibaba
│       │   │           └── apiopenplatform
│       │   │               ├── config
│       │   │               │   ├── AsyncConfig.java
│       │   │               │   ├── FilterConfig.java
│       │   │               │   ├── PageConfig.java
│       │   │               │   ├── RestTemplateConfig.java
│       │   │               │   ├── SecurityConfig.java
│       │   │               │   └── SwaggerConfig.java
│       │   │               ├── filter
│       │   │               │   └── PortalResolvingFilter.java
│       │   │               └── PortalApplication.java
│       │   └── resources
│       │       └── application.yaml
│       └── test
│           └── java
│               └── com
│                   └── alibaba
│                       └── apiopenplatform
│                           └── integration
│                               └── AdministratorAuthIntegrationTest.java
├── portal-dal
│   ├── pom.xml
│   └── src
│       └── main
│           └── java
│               └── com
│                   └── alibaba
│                       └── apiopenplatform
│                           ├── converter
│                           │   ├── AdpAIGatewayConfigConverter.java
│                           │   ├── APIGConfigConverter.java
│                           │   ├── APIGRefConfigConverter.java
│                           │   ├── ApiKeyConfigConverter.java
│                           │   ├── ConsumerAuthConfigConverter.java
│                           │   ├── GatewayConfigConverter.java
│                           │   ├── HigressConfigConverter.java
│                           │   ├── HigressRefConfigConverter.java
│                           │   ├── HmacConfigConverter.java
│                           │   ├── JsonConverter.java
│                           │   ├── JwtConfigConverter.java
│                           │   ├── NacosRefConfigConverter.java
│                           │   ├── PortalSettingConfigConverter.java
│                           │   ├── PortalUiConfigConverter.java
│                           │   └── ProductIconConverter.java
│                           ├── entity
│                           │   ├── Administrator.java
│                           │   ├── BaseEntity.java
│                           │   ├── Consumer.java
│                           │   ├── ConsumerCredential.java
│                           │   ├── ConsumerRef.java
│                           │   ├── Developer.java
│                           │   ├── DeveloperExternalIdentity.java
│                           │   ├── Gateway.java
│                           │   ├── NacosInstance.java
│                           │   ├── Portal.java
│                           │   ├── PortalDomain.java
│                           │   ├── Product.java
│                           │   ├── ProductPublication.java
│                           │   ├── ProductRef.java
│                           │   └── ProductSubscription.java
│                           ├── repository
│                           │   ├── AdministratorRepository.java
│                           │   ├── BaseRepository.java
│                           │   ├── ConsumerCredentialRepository.java
│                           │   ├── ConsumerRefRepository.java
│                           │   ├── ConsumerRepository.java
│                           │   ├── DeveloperExternalIdentityRepository.java
│                           │   ├── DeveloperRepository.java
│                           │   ├── GatewayRepository.java
│                           │   ├── NacosInstanceRepository.java
│                           │   ├── PortalDomainRepository.java
│                           │   ├── PortalRepository.java
│                           │   ├── ProductPublicationRepository.java
│                           │   ├── ProductRefRepository.java
│                           │   ├── ProductRepository.java
│                           │   └── SubscriptionRepository.java
│                           └── support
│                               ├── common
│                               │   ├── Encrypted.java
│                               │   ├── Encryptor.java
│                               │   └── User.java
│                               ├── consumer
│                               │   ├── AdpAIAuthConfig.java
│                               │   ├── APIGAuthConfig.java
│                               │   ├── ApiKeyConfig.java
│                               │   ├── ConsumerAuthConfig.java
│                               │   ├── HigressAuthConfig.java
│                               │   ├── HmacConfig.java
│                               │   └── JwtConfig.java
│                               ├── enums
│                               │   ├── APIGAPIType.java
│                               │   ├── ConsumerAuthType.java
│                               │   ├── ConsumerStatus.java
│                               │   ├── CredentialMode.java
│                               │   ├── DeveloperAuthType.java
│                               │   ├── DeveloperStatus.java
│                               │   ├── DomainType.java
│                               │   ├── GatewayType.java
│                               │   ├── GrantType.java
│                               │   ├── HigressAPIType.java
│                               │   ├── JwtAlgorithm.java
│                               │   ├── ProductIconType.java
│                               │   ├── ProductStatus.java
│                               │   ├── ProductType.java
│                               │   ├── ProtocolType.java
│                               │   ├── PublicKeyFormat.java
│                               │   ├── SourceType.java
│                               │   ├── SubscriptionStatus.java
│                               │   └── UserType.java
│                               ├── gateway
│                               │   ├── AdpAIGatewayConfig.java
│                               │   ├── APIGConfig.java
│                               │   ├── GatewayConfig.java
│                               │   └── HigressConfig.java
│                               ├── portal
│                               │   ├── AuthCodeConfig.java
│                               │   ├── IdentityMapping.java
│                               │   ├── JwtBearerConfig.java
│                               │   ├── OAuth2Config.java
│                               │   ├── OidcConfig.java
│                               │   ├── PortalSettingConfig.java
│                               │   ├── PortalUiConfig.java
│                               │   └── PublicKeyConfig.java
│                               └── product
│                                   ├── APIGRefConfig.java
│                                   ├── HigressRefConfig.java
│                                   ├── NacosRefConfig.java
│                                   └── ProductIcon.java
├── portal-server
│   ├── pom.xml
│   └── src
│       └── main
│           └── java
│               └── com
│                   └── alibaba
│                       └── apiopenplatform
│                           ├── controller
│                           │   ├── AdministratorController.java
│                           │   ├── ConsumerController.java
│                           │   ├── DeveloperController.java
│                           │   ├── GatewayController.java
│                           │   ├── NacosController.java
│                           │   ├── OAuth2Controller.java
│                           │   ├── OidcController.java
│                           │   ├── PortalController.java
│                           │   └── ProductController.java
│                           ├── core
│                           │   ├── advice
│                           │   │   ├── ExceptionAdvice.java
│                           │   │   └── ResponseAdvice.java
│                           │   ├── annotation
│                           │   │   ├── AdminAuth.java
│                           │   │   ├── AdminOrDeveloperAuth.java
│                           │   │   └── DeveloperAuth.java
│                           │   ├── constant
│                           │   │   ├── CommonConstants.java
│                           │   │   ├── IdpConstants.java
│                           │   │   ├── JwtConstants.java
│                           │   │   └── Resources.java
│                           │   ├── event
│                           │   │   ├── DeveloperDeletingEvent.java
│                           │   │   ├── PortalDeletingEvent.java
│                           │   │   └── ProductDeletingEvent.java
│                           │   ├── exception
│                           │   │   ├── BusinessException.java
│                           │   │   └── ErrorCode.java
│                           │   ├── response
│                           │   │   └── Response.java
│                           │   ├── security
│                           │   │   ├── ContextHolder.java
│                           │   │   ├── DeveloperAuthenticationProvider.java
│                           │   │   └── JwtAuthenticationFilter.java
│                           │   └── utils
│                           │       ├── IdGenerator.java
│                           │       ├── PasswordHasher.java
│                           │       └── TokenUtil.java
│                           ├── dto
│                           │   ├── converter
│                           │   │   ├── InputConverter.java
│                           │   │   ├── NacosToGatewayToolsConverter.java
│                           │   │   └── OutputConverter.java
│                           │   ├── params
│                           │   │   ├── admin
│                           │   │   │   ├── AdminCreateParam.java
│                           │   │   │   ├── AdminLoginParam.java
│                           │   │   │   └── ResetPasswordParam.java
│                           │   │   ├── consumer
│                           │   │   │   ├── CreateConsumerParam.java
│                           │   │   │   ├── CreateCredentialParam.java
│                           │   │   │   ├── CreateSubscriptionParam.java
│                           │   │   │   ├── QueryConsumerParam.java
│                           │   │   │   ├── QuerySubscriptionParam.java
│                           │   │   │   └── UpdateCredentialParam.java
│                           │   │   ├── developer
│                           │   │   │   ├── CreateDeveloperParam.java
│                           │   │   │   ├── CreateExternalDeveloperParam.java
│                           │   │   │   ├── DeveloperLoginParam.java
│                           │   │   │   ├── QueryDeveloperParam.java
│                           │   │   │   ├── UnbindExternalIdentityParam.java
│                           │   │   │   ├── UpdateDeveloperParam.java
│                           │   │   │   └── UpdateDeveloperStatusParam.java
│                           │   │   ├── gateway
│                           │   │   │   ├── ImportGatewayParam.java
│                           │   │   │   ├── QueryAdpAIGatewayParam.java
│                           │   │   │   ├── QueryAPIGParam.java
│                           │   │   │   └── QueryGatewayParam.java
│                           │   │   ├── nacos
│                           │   │   │   ├── CreateNacosParam.java
│                           │   │   │   ├── QueryNacosNamespaceParam.java
│                           │   │   │   ├── QueryNacosParam.java
│                           │   │   │   └── UpdateNacosParam.java
│                           │   │   ├── portal
│                           │   │   │   ├── BindDomainParam.java
│                           │   │   │   ├── CreatePortalParam.java
│                           │   │   │   └── UpdatePortalParam.java
│                           │   │   └── product
│                           │   │       ├── CreateProductParam.java
│                           │   │       ├── CreateProductRefParam.java
│                           │   │       ├── PublishProductParam.java
│                           │   │       ├── QueryProductParam.java
│                           │   │       ├── QueryProductSubscriptionParam.java
│                           │   │       ├── UnPublishProductParam.java
│                           │   │       └── UpdateProductParam.java
│                           │   └── result
│                           │       ├── AdminResult.java
│                           │       ├── AdpGatewayInstanceResult.java
│                           │       ├── AdpMcpServerListResult.java
│                           │       ├── AdpMCPServerResult.java
│                           │       ├── APIConfigResult.java
│                           │       ├── APIGMCPServerResult.java
│                           │       ├── APIResult.java
│                           │       ├── AuthResult.java
│                           │       ├── ConsumerCredentialResult.java
│                           │       ├── ConsumerResult.java
│                           │       ├── DeveloperResult.java
│                           │       ├── GatewayMCPServerResult.java
│                           │       ├── GatewayResult.java
│                           │       ├── HigressMCPServerResult.java
│                           │       ├── IdpResult.java
│                           │       ├── IdpState.java
│                           │       ├── IdpTokenResult.java
│                           │       ├── MCPConfigResult.java
│                           │       ├── MCPServerResult.java
│                           │       ├── MseNacosResult.java
│                           │       ├── NacosMCPServerResult.java
│                           │       ├── NacosNamespaceResult.java
│                           │       ├── NacosResult.java
│                           │       ├── PageResult.java
│                           │       ├── PortalResult.java
│                           │       ├── ProductPublicationResult.java
│                           │       ├── ProductRefResult.java
│                           │       ├── ProductResult.java
│                           │       └── SubscriptionResult.java
│                           └── service
│                               ├── AdministratorService.java
│                               ├── AdpAIGatewayService.java
│                               ├── ConsumerService.java
│                               ├── DeveloperService.java
│                               ├── gateway
│                               │   ├── AdpAIGatewayOperator.java
│                               │   ├── AIGatewayOperator.java
│                               │   ├── APIGOperator.java
│                               │   ├── client
│                               │   │   ├── AdpAIGatewayClient.java
│                               │   │   ├── APIGClient.java
│                               │   │   ├── GatewayClient.java
│                               │   │   ├── HigressClient.java
│                               │   │   ├── PopGatewayClient.java
│                               │   │   └── SLSClient.java
│                               │   ├── factory
│                               │   │   └── HTTPClientFactory.java
│                               │   ├── GatewayOperator.java
│                               │   └── HigressOperator.java
│                               ├── GatewayService.java
│                               ├── IdpService.java
│                               ├── impl
│                               │   ├── AdministratorServiceImpl.java
│                               │   ├── ConsumerServiceImpl.java
│                               │   ├── DeveloperServiceImpl.java
│                               │   ├── GatewayServiceImpl.java
│                               │   ├── IdpServiceImpl.java
│                               │   ├── NacosServiceImpl.java
│                               │   ├── OAuth2ServiceImpl.java
│                               │   ├── OidcServiceImpl.java
│                               │   ├── PortalServiceImpl.java
│                               │   └── ProductServiceImpl.java
│                               ├── NacosService.java
│                               ├── OAuth2Service.java
│                               ├── OidcService.java
│                               ├── PortalService.java
│                               └── ProductService.java
├── portal-web
│   ├── api-portal-admin
│   │   ├── .env
│   │   ├── .gitignore
│   │   ├── bin
│   │   │   ├── replace_var.py
│   │   │   └── start.sh
│   │   ├── Dockerfile
│   │   ├── eslint.config.js
│   │   ├── index.html
│   │   ├── nginx.conf
│   │   ├── package.json
│   │   ├── postcss.config.js
│   │   ├── proxy.conf
│   │   ├── public
│   │   │   ├── logo.png
│   │   │   └── vite.svg
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── aliyunThemeToken.ts
│   │   │   ├── App.css
│   │   │   ├── App.tsx
│   │   │   ├── assets
│   │   │   │   └── react.svg
│   │   │   ├── components
│   │   │   │   ├── api-product
│   │   │   │   │   ├── ApiProductApiDocs.tsx
│   │   │   │   │   ├── ApiProductDashboard.tsx
│   │   │   │   │   ├── ApiProductFormModal.tsx
│   │   │   │   │   ├── ApiProductLinkApi.tsx
│   │   │   │   │   ├── ApiProductOverview.tsx
│   │   │   │   │   ├── ApiProductPolicy.tsx
│   │   │   │   │   ├── ApiProductPortal.tsx
│   │   │   │   │   ├── ApiProductUsageGuide.tsx
│   │   │   │   │   ├── SwaggerUIWrapper.css
│   │   │   │   │   └── SwaggerUIWrapper.tsx
│   │   │   │   ├── common
│   │   │   │   │   ├── AdvancedSearch.tsx
│   │   │   │   │   └── index.ts
│   │   │   │   ├── console
│   │   │   │   │   ├── GatewayTypeSelector.tsx
│   │   │   │   │   ├── ImportGatewayModal.tsx
│   │   │   │   │   ├── ImportHigressModal.tsx
│   │   │   │   │   ├── ImportMseNacosModal.tsx
│   │   │   │   │   └── NacosTypeSelector.tsx
│   │   │   │   ├── icons
│   │   │   │   │   └── McpServerIcon.tsx
│   │   │   │   ├── Layout.tsx
│   │   │   │   ├── LayoutWrapper.tsx
│   │   │   │   ├── portal
│   │   │   │   │   ├── PortalConsumers.tsx
│   │   │   │   │   ├── PortalDashboard.tsx
│   │   │   │   │   ├── PortalDevelopers.tsx
│   │   │   │   │   ├── PortalDomain.tsx
│   │   │   │   │   ├── PortalFormModal.tsx
│   │   │   │   │   ├── PortalOverview.tsx
│   │   │   │   │   ├── PortalPublishedApis.tsx
│   │   │   │   │   ├── PortalSecurity.tsx
│   │   │   │   │   ├── PortalSettings.tsx
│   │   │   │   │   ├── PublicKeyManager.tsx
│   │   │   │   │   └── ThirdPartyAuthManager.tsx
│   │   │   │   └── subscription
│   │   │   │       └── SubscriptionListModal.tsx
│   │   │   ├── contexts
│   │   │   │   └── LoadingContext.tsx
│   │   │   ├── index.css
│   │   │   ├── lib
│   │   │   │   ├── api.ts
│   │   │   │   ├── constant.ts
│   │   │   │   └── utils.ts
│   │   │   ├── main.tsx
│   │   │   ├── pages
│   │   │   │   ├── ApiProductDetail.tsx
│   │   │   │   ├── ApiProducts.tsx
│   │   │   │   ├── Dashboard.tsx
│   │   │   │   ├── GatewayConsoles.tsx
│   │   │   │   ├── Login.tsx
│   │   │   │   ├── NacosConsoles.tsx
│   │   │   │   ├── PortalDetail.tsx
│   │   │   │   ├── Portals.tsx
│   │   │   │   └── Register.tsx
│   │   │   ├── routes
│   │   │   │   └── index.tsx
│   │   │   ├── types
│   │   │   │   ├── api-product.ts
│   │   │   │   ├── consumer.ts
│   │   │   │   ├── gateway.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── portal.ts
│   │   │   │   ├── shims-js-yaml.d.ts
│   │   │   │   └── subscription.ts
│   │   │   └── vite-env.d.ts
│   │   ├── tailwind.config.js
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   └── api-portal-frontend
│       ├── .env
│       ├── .gitignore
│       ├── .husky
│       │   └── pre-commit
│       ├── bin
│       │   ├── replace_var.py
│       │   └── start.sh
│       ├── Dockerfile
│       ├── eslint.config.js
│       ├── index.html
│       ├── nginx.conf
│       ├── package.json
│       ├── postcss.config.js
│       ├── proxy.conf
│       ├── public
│       │   ├── favicon.ico
│       │   ├── logo.png
│       │   ├── logo.svg
│       │   ├── MCP.png
│       │   ├── MCP.svg
│       │   └── vite.svg
│       ├── README.md
│       ├── src
│       │   ├── aliyunThemeToken.ts
│       │   ├── App.css
│       │   ├── App.tsx
│       │   ├── assets
│       │   │   ├── aliyun.png
│       │   │   ├── github.png
│       │   │   ├── google.png
│       │   │   └── react.svg
│       │   ├── components
│       │   │   ├── consumer
│       │   │   │   ├── ConsumerBasicInfo.tsx
│       │   │   │   ├── CredentialManager.tsx
│       │   │   │   ├── index.ts
│       │   │   │   └── SubscriptionManager.tsx
│       │   │   ├── Layout.tsx
│       │   │   ├── Navigation.tsx
│       │   │   ├── ProductHeader.tsx
│       │   │   ├── SwaggerUIWrapper.css
│       │   │   ├── SwaggerUIWrapper.tsx
│       │   │   └── UserInfo.tsx
│       │   ├── index.css
│       │   ├── lib
│       │   │   ├── api.ts
│       │   │   ├── statusUtils.ts
│       │   │   └── utils.ts
│       │   ├── main.tsx
│       │   ├── pages
│       │   │   ├── ApiDetail.tsx
│       │   │   ├── Apis.tsx
│       │   │   ├── Callback.tsx
│       │   │   ├── ConsumerDetail.tsx
│       │   │   ├── Consumers.tsx
│       │   │   ├── GettingStarted.tsx
│       │   │   ├── Home.tsx
│       │   │   ├── Login.tsx
│       │   │   ├── Mcp.tsx
│       │   │   ├── McpDetail.tsx
│       │   │   ├── OidcCallback.tsx
│       │   │   ├── Profile.tsx
│       │   │   ├── Register.tsx
│       │   │   └── Test.css
│       │   ├── router.tsx
│       │   ├── types
│       │   │   ├── consumer.ts
│       │   │   └── index.ts
│       │   └── vite-env.d.ts
│       ├── tailwind.config.js
│       ├── tsconfig.app.json
│       ├── tsconfig.json
│       ├── tsconfig.node.json
│       └── vite.config.ts
└── README.md
```

# Files

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/OAuth2ServiceImpl.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.service.impl;
 21 | 
 22 | import cn.hutool.core.collection.CollUtil;
 23 | import cn.hutool.core.convert.Convert;
 24 | import cn.hutool.core.util.EnumUtil;
 25 | import cn.hutool.core.util.StrUtil;
 26 | import cn.hutool.jwt.JWT;
 27 | import cn.hutool.jwt.JWTUtil;
 28 | import cn.hutool.jwt.signers.JWTSigner;
 29 | import cn.hutool.jwt.signers.JWTSignerUtil;
 30 | import com.alibaba.apiopenplatform.core.constant.JwtConstants;
 31 | import com.alibaba.apiopenplatform.core.constant.Resources;
 32 | import com.alibaba.apiopenplatform.core.exception.BusinessException;
 33 | import com.alibaba.apiopenplatform.core.exception.ErrorCode;
 34 | import com.alibaba.apiopenplatform.core.utils.TokenUtil;
 35 | import com.alibaba.apiopenplatform.dto.params.developer.CreateExternalDeveloperParam;
 36 | import com.alibaba.apiopenplatform.dto.result.AuthResult;
 37 | import com.alibaba.apiopenplatform.dto.result.DeveloperResult;
 38 | import com.alibaba.apiopenplatform.dto.result.PortalResult;
 39 | import com.alibaba.apiopenplatform.service.DeveloperService;
 40 | import com.alibaba.apiopenplatform.service.IdpService;
 41 | import com.alibaba.apiopenplatform.service.OAuth2Service;
 42 | import com.alibaba.apiopenplatform.service.PortalService;
 43 | import com.alibaba.apiopenplatform.support.enums.DeveloperAuthType;
 44 | import com.alibaba.apiopenplatform.support.enums.GrantType;
 45 | import com.alibaba.apiopenplatform.support.enums.JwtAlgorithm;
 46 | import com.alibaba.apiopenplatform.support.portal.*;
 47 | import lombok.RequiredArgsConstructor;
 48 | import lombok.extern.slf4j.Slf4j;
 49 | import org.springframework.stereotype.Service;
 50 | 
 51 | import java.security.PublicKey;
 52 | import java.util.*;
 53 | 
 54 | /**
 55 |  * @author zh
 56 |  */
 57 | @Service
 58 | @RequiredArgsConstructor
 59 | @Slf4j
 60 | public class OAuth2ServiceImpl implements OAuth2Service {
 61 | 
 62 |     private final PortalService portalService;
 63 | 
 64 |     private final DeveloperService developerService;
 65 | 
 66 |     private final IdpService idpService;
 67 | 
 68 |     @Override
 69 |     public AuthResult authenticate(String grantType, String jwtToken) {
 70 |         if (!GrantType.JWT_BEARER.getType().equals(grantType)) {
 71 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "不支持的授权模式");
 72 |         }
 73 | 
 74 |         // 解析JWT
 75 |         JWT jwt = JWTUtil.parseToken(jwtToken);
 76 |         String kid = (String) jwt.getHeader(JwtConstants.HEADER_KID);
 77 |         if (StrUtil.isBlank(kid)) {
 78 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "JWT header缺少字段kid");
 79 |         }
 80 |         String provider = (String) jwt.getPayload(JwtConstants.PAYLOAD_PROVIDER);
 81 |         if (StrUtil.isBlank(provider)) {
 82 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "JWT payload缺少字段provider");
 83 |         }
 84 | 
 85 |         String portalId = (String) jwt.getPayload(JwtConstants.PAYLOAD_PORTAL);
 86 |         if (StrUtil.isBlank(portalId)) {
 87 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "JWT payload缺少字段portal");
 88 |         }
 89 | 
 90 |         // 根据provider确定OAuth2配置
 91 |         PortalResult portal = portalService.getPortal(portalId);
 92 |         List<OAuth2Config> oauth2Configs = Optional.ofNullable(portal.getPortalSettingConfig())
 93 |                 .map(PortalSettingConfig::getOauth2Configs)
 94 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.OAUTH2_CONFIG, portalId));
 95 | 
 96 |         OAuth2Config oAuth2Config = oauth2Configs.stream()
 97 |                 // JWT Bearer模式
 98 |                 .filter(config -> config.getGrantType() == GrantType.JWT_BEARER)
 99 |                 .filter(config -> config.getJwtBearerConfig() != null
100 |                         && CollUtil.isNotEmpty(config.getJwtBearerConfig().getPublicKeys()))
101 |                 // provider标识
102 |                 .filter(config -> config.getProvider().equals(provider))
103 |                 .findFirst()
104 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.OAUTH2_CONFIG, provider));
105 | 
106 |         // 根据kid找到对应公钥
107 |         JwtBearerConfig jwtConfig = oAuth2Config.getJwtBearerConfig();
108 |         PublicKeyConfig publicKeyConfig = jwtConfig.getPublicKeys().stream()
109 |                 .filter(key -> kid.equals(key.getKid()))
110 |                 .findFirst()
111 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PUBLIC_KEY, kid));
112 | 
113 |         // 验签
114 |         if (!verifySignature(jwt, publicKeyConfig)) {
115 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "JWT签名验证失败");
116 |         }
117 | 
118 |         // 验证Claims
119 |         validateJwtClaims(jwt);
120 | 
121 |         // Developer
122 |         String developerId = createOrGetDeveloper(jwt, oAuth2Config);
123 | 
124 |         // 生成Access Token
125 |         String accessToken = TokenUtil.generateDeveloperToken(developerId);
126 |         log.info("JWT Bearer认证成功,provider: {}, developer: {}", oAuth2Config.getProvider(), developerId);
127 |         return AuthResult.of(accessToken, TokenUtil.getTokenExpiresIn());
128 |     }
129 | 
130 |     private boolean verifySignature(JWT jwt, PublicKeyConfig keyConfig) {
131 |         // 加载公钥
132 |         PublicKey publicKey = idpService.loadPublicKey(keyConfig.getFormat(), keyConfig.getValue());
133 | 
134 |         // 验证JWT
135 |         JWTSigner signer = createJWTSigner(keyConfig.getAlgorithm(), publicKey);
136 |         return jwt.setSigner(signer).verify();
137 |     }
138 | 
139 |     private JWTSigner createJWTSigner(String algorithm, PublicKey publicKey) {
140 |         JwtAlgorithm alg = EnumUtil.fromString(JwtAlgorithm.class, algorithm.toUpperCase());
141 | 
142 |         switch (alg) {
143 |             case RS256:
144 |                 return JWTSignerUtil.rs256(publicKey);
145 |             case RS384:
146 |                 return JWTSignerUtil.rs384(publicKey);
147 |             case RS512:
148 |                 return JWTSignerUtil.rs512(publicKey);
149 |             case ES256:
150 |                 return JWTSignerUtil.es256(publicKey);
151 |             case ES384:
152 |                 return JWTSignerUtil.es384(publicKey);
153 |             case ES512:
154 |                 return JWTSignerUtil.es512(publicKey);
155 |             default:
156 |                 throw new BusinessException(ErrorCode.INVALID_PARAMETER, "不支持的JWT签名算法");
157 |         }
158 |     }
159 | 
160 |     private void validateJwtClaims(JWT jwt) {
161 |         // 过期时间
162 |         Object expObj = jwt.getPayload(JwtConstants.PAYLOAD_EXP);
163 |         Long exp = Convert.toLong(expObj);
164 |         // 签发时间
165 |         Object iatObj = jwt.getPayload(JwtConstants.PAYLOAD_IAT);
166 |         Long iat = Convert.toLong(iatObj);
167 | 
168 |         if (iat == null || exp == null || iat > exp) {
169 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "JWT payload中exp或iat不合法");
170 |         }
171 | 
172 |         long currentTime = System.currentTimeMillis() / 1000;
173 |         if (exp <= currentTime) {
174 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "JWT已过期");
175 |         }
176 |     }
177 | 
178 |     private String createOrGetDeveloper(JWT jwt, OAuth2Config config) {
179 |         IdentityMapping identityMapping = config.getIdentityMapping();
180 |         // userId & userName
181 |         String userIdField = StrUtil.isBlank(identityMapping.getUserIdField()) ?
182 |                 JwtConstants.PAYLOAD_USER_ID : identityMapping.getUserIdField();
183 |         String userNameField = StrUtil.isBlank(identityMapping.getUserNameField()) ?
184 |                 JwtConstants.PAYLOAD_USER_NAME : identityMapping.getUserNameField();
185 |         Object userIdObj = jwt.getPayload(userIdField);
186 |         Object userNameObj = jwt.getPayload(userNameField);
187 | 
188 |         String userId = Convert.toStr(userIdObj);
189 |         String userName = Convert.toStr(userNameObj);
190 |         if (StrUtil.isBlank(userId) || StrUtil.isBlank(userName)) {
191 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "JWT payload中缺少用户ID字段或用户名称");
192 |         }
193 | 
194 |         // 复用已有的Developer,否则创建
195 |         return Optional.ofNullable(developerService.getExternalDeveloper(config.getProvider(), userId))
196 |                 .map(DeveloperResult::getDeveloperId)
197 |                 .orElseGet(() -> {
198 |                     CreateExternalDeveloperParam param = CreateExternalDeveloperParam.builder()
199 |                             .provider(config.getProvider())
200 |                             .subject(userId)
201 |                             .displayName(userName)
202 |                             .authType(DeveloperAuthType.OAUTH2)
203 |                             .build();
204 | 
205 |                     return developerService.createExternalDeveloper(param).getDeveloperId();
206 |                 });
207 |     }
208 | 
209 | }
210 | 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/IdpServiceImpl.java:
--------------------------------------------------------------------------------

```java
  1 | package com.alibaba.apiopenplatform.service.impl;
  2 | 
  3 | import cn.hutool.core.collection.CollUtil;
  4 | import cn.hutool.core.util.StrUtil;
  5 | import cn.hutool.json.JSONObject;
  6 | import cn.hutool.json.JSONUtil;
  7 | import com.alibaba.apiopenplatform.core.constant.IdpConstants;
  8 | import com.alibaba.apiopenplatform.core.exception.BusinessException;
  9 | import com.alibaba.apiopenplatform.core.exception.ErrorCode;
 10 | import com.alibaba.apiopenplatform.service.IdpService;
 11 | import com.alibaba.apiopenplatform.support.enums.GrantType;
 12 | import com.alibaba.apiopenplatform.support.enums.PublicKeyFormat;
 13 | import com.alibaba.apiopenplatform.support.portal.*;
 14 | import lombok.RequiredArgsConstructor;
 15 | import lombok.extern.slf4j.Slf4j;
 16 | import org.springframework.http.HttpMethod;
 17 | import org.springframework.stereotype.Service;
 18 | import org.springframework.web.client.RestTemplate;
 19 | 
 20 | import java.math.BigInteger;
 21 | import java.security.KeyFactory;
 22 | import java.security.PublicKey;
 23 | import java.security.spec.RSAPublicKeySpec;
 24 | import java.security.spec.X509EncodedKeySpec;
 25 | import java.util.*;
 26 | import java.util.stream.Collectors;
 27 | 
 28 | /**
 29 |  * @author zh
 30 |  */
 31 | @Service
 32 | @RequiredArgsConstructor
 33 | @Slf4j
 34 | public class IdpServiceImpl implements IdpService {
 35 | 
 36 |     private final RestTemplate restTemplate;
 37 | 
 38 |     @Override
 39 |     public void validateOidcConfigs(List<OidcConfig> oidcConfigs) {
 40 |         if (CollUtil.isEmpty(oidcConfigs)) {
 41 |             return;
 42 |         }
 43 | 
 44 |         // provider唯一
 45 |         Set<String> providers = oidcConfigs.stream()
 46 |                 .map(OidcConfig::getProvider)
 47 |                 .filter(StrUtil::isNotBlank)
 48 |                 .collect(Collectors.toSet());
 49 |         if (providers.size() != oidcConfigs.size()) {
 50 |             throw new BusinessException(ErrorCode.CONFLICT, "OIDC配置中存在空或重复的provider");
 51 |         }
 52 | 
 53 |         oidcConfigs.forEach(config -> {
 54 |             AuthCodeConfig authConfig = Optional.ofNullable(config.getAuthCodeConfig())
 55 |                     .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_PARAMETER,
 56 |                             StrUtil.format("OIDC配置{}缺少授权码配置", config.getProvider())));
 57 |             // 基础参数
 58 |             if (StrUtil.isBlank(authConfig.getClientId()) ||
 59 |                     StrUtil.isBlank(authConfig.getClientSecret()) ||
 60 |                     StrUtil.isBlank(authConfig.getScopes())) {
 61 |                 throw new BusinessException(ErrorCode.INVALID_PARAMETER,
 62 |                         StrUtil.format("OIDC配置{}缺少必要参数: Client ID, Client Secret 或 Scopes", config.getProvider()));
 63 |             }
 64 | 
 65 |             // 端点配置
 66 |             if (StrUtil.isNotBlank(authConfig.getIssuer())) {
 67 |                 discoverAndSetEndpoints(config.getProvider(), authConfig);
 68 |             } else {
 69 |                 if (StrUtil.isBlank(authConfig.getAuthorizationEndpoint()) ||
 70 |                         StrUtil.isBlank(authConfig.getTokenEndpoint()) ||
 71 |                         StrUtil.isBlank(authConfig.getUserInfoEndpoint())) {
 72 |                     throw new BusinessException(ErrorCode.INVALID_PARAMETER,
 73 |                             StrUtil.format("OIDC配置{}缺少必要端点配置", config.getProvider()));
 74 |                 }
 75 |             }
 76 |         });
 77 |     }
 78 | 
 79 |     @SuppressWarnings("unchecked")
 80 |     private void discoverAndSetEndpoints(String provider, AuthCodeConfig config) {
 81 |         String discoveryUrl = config.getIssuer().replaceAll("/$", "") + "/.well-known/openid-configuration";
 82 |         try {
 83 |             Map<String, Object> discovery = restTemplate.exchange(
 84 |                             discoveryUrl,
 85 |                             HttpMethod.GET,
 86 |                             null,
 87 |                             Map.class)
 88 |                     .getBody();
 89 | 
 90 |             // 验证并设置端点
 91 |             String authEndpoint = getRequiredEndpoint(discovery, IdpConstants.AUTHORIZATION_ENDPOINT);
 92 |             String tokenEndpoint = getRequiredEndpoint(discovery, IdpConstants.TOKEN_ENDPOINT);
 93 |             String userInfoEndpoint = getRequiredEndpoint(discovery, IdpConstants.USERINFO_ENDPOINT);
 94 | 
 95 |             config.setAuthorizationEndpoint(authEndpoint);
 96 |             config.setTokenEndpoint(tokenEndpoint);
 97 |             config.setUserInfoEndpoint(userInfoEndpoint);
 98 |         } catch (Exception e) {
 99 |             log.error("Failed to discover OIDC endpoints from discovery URL: {}", discoveryUrl, e);
100 |             throw new BusinessException(ErrorCode.INVALID_PARAMETER, StrUtil.format("OIDC配置{}的Issuer无效或无法访问", provider));
101 |         }
102 |     }
103 | 
104 |     private String getRequiredEndpoint(Map<String, Object> discovery, String name) {
105 |         return Optional.ofNullable(discovery.get(name))
106 |                 .map(Object::toString)
107 |                 .filter(StrUtil::isNotBlank)
108 |                 .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_PARAMETER,
109 |                         "OIDC Discovery配置中缺少端点: " + name));
110 |     }
111 | 
112 |     @Override
113 |     public void validateOAuth2Configs(List<OAuth2Config> oauth2Configs) {
114 |         if (CollUtil.isEmpty(oauth2Configs)) {
115 |             return;
116 |         }
117 | 
118 |         // provider唯一
119 |         Set<String> providers = oauth2Configs.stream()
120 |                 .map(OAuth2Config::getProvider)
121 |                 .filter(StrUtil::isNotBlank)
122 |                 .collect(Collectors.toSet());
123 |         if (providers.size() != oauth2Configs.size()) {
124 |             throw new BusinessException(ErrorCode.CONFLICT, "OAuth2配置中存在空或重复的provider");
125 |         }
126 | 
127 |         oauth2Configs.forEach(config -> {
128 |             if (GrantType.JWT_BEARER.equals(config.getGrantType())) {
129 |                 validateJwtBearerConfig(config);
130 |             }
131 |         });
132 |     }
133 | 
134 |     private void validateJwtBearerConfig(OAuth2Config config) {
135 |         JwtBearerConfig jwtBearerConfig = config.getJwtBearerConfig();
136 |         if (jwtBearerConfig == null) {
137 |             throw new BusinessException(ErrorCode.INVALID_PARAMETER,
138 |                     StrUtil.format("OAuth2配置{}使用JWT断言模式但缺少JWT断言配置", config.getProvider()));
139 |         }
140 | 
141 |         List<PublicKeyConfig> publicKeys = jwtBearerConfig.getPublicKeys();
142 |         if (CollUtil.isEmpty(publicKeys)) {
143 |             throw new BusinessException(ErrorCode.INVALID_PARAMETER,
144 |                     StrUtil.format("OAuth2配置{}缺少公钥配置", config.getProvider()));
145 |         }
146 | 
147 |         if (publicKeys.stream()
148 |                 .map(key -> {
149 |                     // 加载公钥验证有效性
150 |                     loadPublicKey(key.getFormat(), key.getValue());
151 |                     return key.getKid();
152 |                 })
153 |                 .collect(Collectors.toSet()).size() != publicKeys.size()) {
154 |             throw new BusinessException(ErrorCode.CONFLICT,
155 |                     StrUtil.format("OAuth2配置{}的公钥ID存在重复", config.getProvider()));
156 |         }
157 |     }
158 | 
159 | 
160 |     @Override
161 |     public PublicKey loadPublicKey(PublicKeyFormat format, String publicKey) {
162 |         switch (format) {
163 |             case PEM:
164 |                 return loadPublicKeyFromPem(publicKey);
165 |             case JWK:
166 |                 return loadPublicKeyFromJwk(publicKey);
167 |             default:
168 |                 throw new BusinessException(ErrorCode.INVALID_PARAMETER, "公钥格式不支持");
169 |         }
170 |     }
171 | 
172 |     private PublicKey loadPublicKeyFromPem(String pemContent) {
173 |         // 清理PEM格式标记和空白字符
174 |         String publicKeyPEM = pemContent
175 |                 .replace("-----BEGIN PUBLIC KEY-----", "")
176 |                 .replace("-----END PUBLIC KEY-----", "")
177 |                 .replace("-----BEGIN RSA PUBLIC KEY-----", "")
178 |                 .replace("-----END RSA PUBLIC KEY-----", "")
179 |                 .replaceAll("\\s", "");
180 | 
181 |         if (StrUtil.isBlank(publicKeyPEM)) {
182 |             throw new IllegalArgumentException("PEM内容为空");
183 |         }
184 | 
185 |         try {
186 |             // Base64解码
187 |             byte[] decoded = Base64.getDecoder().decode(publicKeyPEM);
188 | 
189 |             // 公钥对象
190 |             X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
191 |             KeyFactory keyFactory = KeyFactory.getInstance("RSA");
192 |             return keyFactory.generatePublic(spec);
193 |         } catch (Exception e) {
194 |             log.error("PEM公钥解析失败", e);
195 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "PEM公钥解析失败: " + e.getMessage());
196 |         }
197 |     }
198 | 
199 |     private PublicKey loadPublicKeyFromJwk(String jwkContent) {
200 |         JSONObject jwk = JSONUtil.parseObj(jwkContent);
201 | 
202 |         // 验证必需字段
203 |         String kty = getRequiredField(jwk, "kty");
204 |         if (!"RSA".equals(kty)) {
205 |             throw new IllegalArgumentException("当前仅支持RSA类型的JWK");
206 |         }
207 | 
208 |         return loadRSAPublicKeyFromJwk(jwk);
209 |     }
210 | 
211 |     private PublicKey loadRSAPublicKeyFromJwk(JSONObject jwk) {
212 |         // 获取必需的RSA参数
213 |         String nStr = getRequiredField(jwk, "n");
214 |         String eStr = getRequiredField(jwk, "e");
215 | 
216 |         try {
217 |             // Base64解码参数
218 |             byte[] nBytes = Base64.getUrlDecoder().decode(nStr);
219 |             byte[] eBytes = Base64.getUrlDecoder().decode(eStr);
220 | 
221 |             // 构建RSA公钥
222 |             BigInteger modulus = new BigInteger(1, nBytes);
223 |             BigInteger exponent = new BigInteger(1, eBytes);
224 | 
225 |             RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
226 |             KeyFactory keyFactory = KeyFactory.getInstance("RSA");
227 |             return keyFactory.generatePublic(spec);
228 |         } catch (Exception e) {
229 |             log.error("JWK RSA参数解析失败", e);
230 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "JWK RSA参数解析失败: " + e.getMessage());
231 |         }
232 |     }
233 | 
234 |     private String getRequiredField(JSONObject jwk, String fieldName) {
235 |         String value = jwk.getStr(fieldName);
236 |         if (StrUtil.isBlank(value)) {
237 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "JWK中缺少字段: " + fieldName);
238 |         }
239 |         return value;
240 |     }
241 | }
242 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/portal/PublicKeyManager.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import {useState} from 'react'
  2 | import {Button, Form, Input, Select, Table, Modal, Space, Tag, message, Card, Row, Col} from 'antd'
  3 | import {PlusOutlined, EditOutlined, DeleteOutlined, ExclamationCircleOutlined} from '@ant-design/icons'
  4 | import {PublicKeyConfig, PublicKeyFormat} from '@/types'
  5 | 
  6 | interface PublicKeyManagerProps {
  7 |   provider?: string | null
  8 |   publicKeys: PublicKeyConfig[]
  9 |   onSave: (publicKeys: PublicKeyConfig[]) => void
 10 | }
 11 | 
 12 | interface PublicKeyFormData {
 13 |   kid: string
 14 |   format: PublicKeyFormat
 15 |   algorithm: string
 16 |   value: string
 17 | }
 18 | 
 19 | export function PublicKeyManager({provider, publicKeys, onSave}: PublicKeyManagerProps) {
 20 |   const [form] = Form.useForm<PublicKeyFormData>()
 21 |   const [modalVisible, setModalVisible] = useState(false)
 22 |   const [editingIndex, setEditingIndex] = useState<number | null>(null)
 23 |   const [localPublicKeys, setLocalPublicKeys] = useState<PublicKeyConfig[]>(publicKeys)
 24 |   const [selectedFormat, setSelectedFormat] = useState<PublicKeyFormat>(PublicKeyFormat.PEM)
 25 | 
 26 |   const handleAdd = () => {
 27 |     setEditingIndex(null)
 28 |     setModalVisible(true)
 29 |     form.resetFields()
 30 |     setSelectedFormat(PublicKeyFormat.PEM)
 31 |     form.setFieldsValue({
 32 |       format: PublicKeyFormat.PEM,
 33 |       algorithm: 'RS256'
 34 |     })
 35 |   }
 36 | 
 37 |   const handleEdit = (index: number) => {
 38 |     setEditingIndex(index)
 39 |     setModalVisible(true)
 40 |     const publicKey = localPublicKeys[index]
 41 |     setSelectedFormat(publicKey.format)
 42 |     form.setFieldsValue(publicKey)
 43 |   }
 44 | 
 45 |   const handleDelete = (index: number) => {
 46 |     Modal.confirm({
 47 |       title: '确认删除',
 48 |       icon: <ExclamationCircleOutlined/>,
 49 |       content: '确定要删除这个公钥配置吗?',
 50 |       okText: '确认删除',
 51 |       okType: 'danger',
 52 |       cancelText: '取消',
 53 |       onOk() {
 54 |         const updatedKeys = localPublicKeys.filter((_, i) => i !== index)
 55 |         setLocalPublicKeys(updatedKeys)
 56 |         onSave(updatedKeys)
 57 |         message.success('公钥删除成功')
 58 |       },
 59 |     })
 60 |   }
 61 | 
 62 |   const handleModalOk = async () => {
 63 |     try {
 64 |       const values = await form.validateFields()
 65 |       
 66 |       // 验证Kid的唯一性
 67 |       const isKidExists = localPublicKeys.some((key, index) => 
 68 |         key.kid === values.kid && index !== editingIndex
 69 |       )
 70 |       
 71 |       if (isKidExists) {
 72 |         message.error('公钥ID已存在,请使用不同的ID')
 73 |         return
 74 |       }
 75 | 
 76 |       let updatedKeys
 77 |       if (editingIndex !== null) {
 78 |         // 编辑模式
 79 |         updatedKeys = localPublicKeys.map((key, index) => 
 80 |           index === editingIndex ? values as PublicKeyConfig : key
 81 |         )
 82 |       } else {
 83 |         // 新增模式
 84 |         updatedKeys = [...localPublicKeys, values as PublicKeyConfig]
 85 |       }
 86 | 
 87 |       setLocalPublicKeys(updatedKeys)
 88 |       onSave(updatedKeys)
 89 |       setModalVisible(false)
 90 |       message.success(editingIndex !== null ? '公钥更新成功' : '公钥添加成功')
 91 |     } catch (error) {
 92 |       message.error('保存公钥失败')
 93 |     }
 94 |   }
 95 | 
 96 |   const handleModalCancel = () => {
 97 |     setModalVisible(false)
 98 |     setEditingIndex(null)
 99 |     setSelectedFormat(PublicKeyFormat.PEM)
100 |     form.resetFields()
101 |   }
102 | 
103 |   // 验证公钥内容格式
104 |   const validatePublicKey = (_: any, value: string) => {
105 |     if (!value) {
106 |       return Promise.reject(new Error('请输入公钥内容'))
107 |     }
108 |     
109 |     if (selectedFormat === PublicKeyFormat.PEM) {
110 |       // 简单的PEM格式验证
111 |       if (!value.includes('-----BEGIN') || !value.includes('-----END')) {
112 |         return Promise.reject(new Error('PEM格式公钥应包含BEGIN和END标记'))
113 |       }
114 |     } else if (selectedFormat === PublicKeyFormat.JWK) {
115 |       // 简单的JWK格式验证
116 |       try {
117 |         const jwk = JSON.parse(value)
118 |         if (!jwk.kty || !jwk.n || !jwk.e) {
119 |           return Promise.reject(new Error('JWK格式应包含kty、n、e字段'))
120 |         }
121 |       } catch {
122 |         return Promise.reject(new Error('JWK格式应为有效的JSON'))
123 |       }
124 |     }
125 |     
126 |     return Promise.resolve()
127 |   }
128 | 
129 |   const columns = [
130 |     {
131 |       title: '公钥ID (kid)',
132 |       dataIndex: 'kid',
133 |       key: 'kid',
134 |       render: (kid: string) => (
135 |         <Tag color="blue">{kid}</Tag>
136 |       )
137 |     },
138 |     {
139 |       title: '格式',
140 |       dataIndex: 'format',
141 |       key: 'format',
142 |       render: (format: PublicKeyFormat) => (
143 |         <Tag color={format === PublicKeyFormat.PEM ? 'green' : 'orange'}>
144 |           {format}
145 |         </Tag>
146 |       )
147 |     },
148 |     {
149 |       title: '算法',
150 |       dataIndex: 'algorithm',
151 |       key: 'algorithm',
152 |       render: (algorithm: string) => (
153 |         <Tag color="purple">{algorithm}</Tag>
154 |       )
155 |     },
156 |     {
157 |       title: '公钥内容',
158 |       key: 'value',
159 |       render: (record: PublicKeyConfig) => (
160 |         <span className="font-mono text-xs text-gray-600">
161 |           {record.format === PublicKeyFormat.PEM 
162 |             ? record.value.substring(0, 50) + '...'
163 |             : JSON.stringify(JSON.parse(record.value || '{}')).substring(0, 50) + '...'
164 |           }
165 |         </span>
166 |       )
167 |     },
168 |     {
169 |       title: '操作',
170 |       key: 'action',
171 |       render: (_: any, _record: PublicKeyConfig, index: number) => (
172 |         <Space>
173 |           <Button
174 |             type="link"
175 |             size="small"
176 |             icon={<EditOutlined/>}
177 |             onClick={() => handleEdit(index)}
178 |           >
179 |             编辑
180 |           </Button>
181 |           <Button
182 |             type="link"
183 |             danger
184 |             size="small"
185 |             icon={<DeleteOutlined/>}
186 |             onClick={() => handleDelete(index)}
187 |           >
188 |             删除
189 |           </Button>
190 |         </Space>
191 |       )
192 |     }
193 |   ]
194 | 
195 |   return (
196 |     <div>
197 |       <div className="flex justify-between items-center mb-4">
198 |         <div>
199 |           <h4 className="text-lg font-medium">
200 |             {provider && `${provider} - `}JWT签名公钥管理
201 |           </h4>
202 |           <p className="text-sm text-gray-500">
203 |             管理用于验证JWT签名的公钥,支持PEM和JWK格式
204 |           </p>
205 |         </div>
206 |         <Button
207 |           type="primary"
208 |           icon={<PlusOutlined/>}
209 |           onClick={handleAdd}
210 |         >
211 |           添加公钥
212 |         </Button>
213 |       </div>
214 | 
215 |       <Table
216 |         columns={columns}
217 |         dataSource={localPublicKeys}
218 |         rowKey="kid"
219 |         pagination={false}
220 |         size="small"
221 |         locale={{
222 |           emptyText: '暂无公钥配置'
223 |         }}
224 |       />
225 | 
226 |       {/* 公钥配置说明 */}
227 |       <Card size="small" className="mt-4 bg-blue-50">
228 |         <Row gutter={16}>
229 |           <Col span={12}>
230 |             <div className="text-sm">
231 |               <h5 className="font-medium mb-2 text-blue-800">PEM格式示例:</h5>
232 |               <div className="bg-white p-2 rounded font-mono text-xs border">
233 |                 -----BEGIN PUBLIC KEY-----<br/>
234 |                 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...<br/>
235 |                 -----END PUBLIC KEY-----
236 |               </div>
237 |             </div>
238 |           </Col>
239 |           <Col span={12}>
240 |             <div className="text-sm">
241 |               <h5 className="font-medium mb-2 text-blue-800">JWK格式示例:</h5>
242 |               <div className="bg-white p-2 rounded font-mono text-xs border">
243 |                 {`{
244 |   "kty": "RSA",
245 |   "kid": "key1",
246 |   "n": "...",
247 |   "e": "AQAB"
248 | }`}
249 |               </div>
250 |             </div>
251 |           </Col>
252 |         </Row>
253 |       </Card>
254 | 
255 |       {/* 公钥配置模态框 */}
256 |       <Modal
257 |         title={editingIndex !== null ? '编辑公钥' : '添加公钥'}
258 |         open={modalVisible}
259 |         onOk={handleModalOk}
260 |         onCancel={handleModalCancel}
261 |         width={700}
262 |         okText={editingIndex !== null ? '更新' : '添加'}
263 |         cancelText="取消"
264 |       >
265 |         <Form
266 |           form={form}
267 |           layout="vertical"
268 |         >
269 |           <div className="grid grid-cols-2 gap-4">
270 |             <Form.Item
271 |               name="kid"
272 |               label="公钥ID (kid)"
273 |               rules={[
274 |                 {required: true, message: '请输入公钥ID'},
275 |                 {pattern: /^[a-zA-Z0-9_-]+$/, message: '公钥ID只能包含字母、数字、下划线和连字符'}
276 |               ]}
277 |             >
278 |               <Input placeholder="如: key1, auth-key-2024"/>
279 |             </Form.Item>
280 |             <Form.Item
281 |               name="algorithm"
282 |               label="签名算法"
283 |               rules={[{required: true, message: '请选择签名算法'}]}
284 |             >
285 |               <Select placeholder="选择签名算法">
286 |                 <Select.Option value="RS256">RS256</Select.Option>
287 |                 <Select.Option value="RS384">RS384</Select.Option>
288 |                 <Select.Option value="RS512">RS512</Select.Option>
289 |                 <Select.Option value="ES256">ES256</Select.Option>
290 |                 <Select.Option value="ES384">ES384</Select.Option>
291 |                 <Select.Option value="ES512">ES512</Select.Option>
292 |               </Select>
293 |             </Form.Item>
294 |           </div>
295 | 
296 |           <Form.Item
297 |             name="format"
298 |             label="公钥格式"
299 |             rules={[{required: true, message: '请选择公钥格式'}]}
300 |           >
301 |             <Select 
302 |               placeholder="选择公钥格式"
303 |               onChange={(value) => setSelectedFormat(value as PublicKeyFormat)}
304 |             >
305 |               <Select.Option value={PublicKeyFormat.PEM}>PEM格式</Select.Option>
306 |               <Select.Option value={PublicKeyFormat.JWK}>JWK格式</Select.Option>
307 |             </Select>
308 |           </Form.Item>
309 | 
310 |           <Form.Item
311 |             name="value"
312 |             label="公钥内容"
313 |             rules={[
314 |               {required: true, message: '请输入公钥内容'},
315 |               {validator: validatePublicKey}
316 |             ]}
317 |           >
318 |             <Input.TextArea
319 |               rows={8}
320 |               placeholder={
321 |                 selectedFormat === PublicKeyFormat.JWK
322 |                   ? '请输入JWK格式的公钥,例如:\n{\n  "kty": "RSA",\n  "kid": "key1",\n  "n": "...",\n  "e": "AQAB"\n}'
323 |                   : '请输入PEM格式的公钥,例如:\n-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...\n-----END PUBLIC KEY-----'
324 |               }
325 |               style={{fontFamily: 'monospace'}}
326 |             />
327 |           </Form.Item>
328 |         </Form>
329 |       </Modal>
330 |     </div>
331 |   )
332 | }
333 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/api-product/ApiProductPortal.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useNavigate } from 'react-router-dom'
  2 | import { Card, Button, Table, Tag, Space, Switch, Modal, Form, Input, Select, message } from 'antd'
  3 | import { PlusOutlined, EyeOutlined, EditOutlined, DeleteOutlined, ExclamationCircleOutlined, GlobalOutlined, CheckCircleFilled, MinusCircleFilled } from '@ant-design/icons'
  4 | import { useState, useEffect } from 'react'
  5 | import type { ApiProduct } from '@/types/api-product';
  6 | import { apiProductApi, portalApi } from '@/lib/api';
  7 | 
  8 | interface ApiProductPortalProps {
  9 |   apiProduct: ApiProduct
 10 | }
 11 | 
 12 | interface Portal {
 13 |   portalId: string
 14 |   portalName: string
 15 |   autoApproveSubscription: boolean
 16 |   createdAt: string
 17 | }
 18 | 
 19 | export function ApiProductPortal({ apiProduct }: ApiProductPortalProps) {
 20 |   const [publishedPortals, setPublishedPortals] = useState<Portal[]>([])
 21 |   const [allPortals, setAllPortals] = useState<Portal[]>([])
 22 |   const [isModalVisible, setIsModalVisible] = useState(false)
 23 |   const [selectedPortalIds, setSelectedPortalIds] = useState<string[]>([])
 24 |   const [form] = Form.useForm()
 25 |   const [loading, setLoading] = useState(false)
 26 |   const [portalLoading, setPortalLoading] = useState(false)
 27 |   const [modalLoading, setModalLoading] = useState(false)
 28 | 
 29 |   // 分页状态
 30 |   const [currentPage, setCurrentPage] = useState(1)
 31 |   const [pageSize, setPageSize] = useState(10)
 32 |   const [total, setTotal] = useState(0)
 33 | 
 34 |   const navigate = useNavigate()
 35 | 
 36 |   // 获取已发布的门户列表
 37 |   useEffect(() => {
 38 |     if (apiProduct.productId) {
 39 |       fetchPublishedPortals()
 40 |     }
 41 |   }, [apiProduct.productId, currentPage, pageSize])
 42 | 
 43 |   // 获取所有门户列表
 44 |   useEffect(() => {
 45 |     fetchAllPortals()
 46 |   }, [])
 47 | 
 48 |   const fetchPublishedPortals = async () => {
 49 |     setLoading(true)
 50 |     try {
 51 |       const res = await apiProductApi.getApiProductPublications(apiProduct.productId, {
 52 |         page: currentPage,
 53 |         size: pageSize
 54 |       })
 55 |       setPublishedPortals(res.data.content?.map((item: any) => ({
 56 |         ...item,
 57 |         autoApproveSubscription: item.autoApproveSubscriptions || false,
 58 |       })) || [])
 59 |       setTotal(res.data.totalElements || 0)
 60 |     } catch (error) {
 61 |       console.error('获取已发布门户失败:', error)
 62 |       // message.error('获取已发布门户失败')
 63 |     } finally {
 64 |       setLoading(false)
 65 |     }
 66 |   }
 67 | 
 68 |   const fetchAllPortals = async () => {
 69 |     setPortalLoading(true)
 70 |     try {
 71 |       const res = await portalApi.getPortals({
 72 |         page: 1,
 73 |         size: 500 // 获取所有门户
 74 |       })
 75 |       setAllPortals(res.data.content?.map((item: any) => ({
 76 |         ...item,
 77 |         portalName: item.name,
 78 |         autoApproveSubscription: item.portalSettingConfig?.autoApproveSubscriptions || false,
 79 |       })) || [])
 80 |     } catch (error) {
 81 |       console.error('获取门户列表失败:', error)
 82 |       // message.error('获取门户列表失败')
 83 |     } finally {
 84 |       setPortalLoading(false)
 85 |     }
 86 |   }
 87 | 
 88 |   const handlePageChange = (page: number, size?: number) => {
 89 |     setCurrentPage(page)
 90 |     if (size) {
 91 |       setPageSize(size)
 92 |     }
 93 |   }
 94 | 
 95 |   const columns = [
 96 |     {
 97 |       title: '门户信息',
 98 |       key: 'portalInfo',
 99 |       width: 400,
100 |       render: (_: any, record: Portal) => (
101 |         <div>
102 |           <div className="text-sm font-medium text-gray-900 truncate">
103 |             {record.portalName}
104 |           </div>
105 |           <div className="text-xs text-gray-500 truncate">
106 |             {record.portalId}
107 |           </div>
108 |         </div>
109 |       ),
110 |     },
111 |     {
112 |       title: '订阅自动审批',
113 |       key: 'autoApprove',
114 |       width: 160,
115 |       render: (_: any, record: Portal) => (
116 |         <div className="flex items-center">
117 |           {record.autoApproveSubscription ? (
118 |             <>
119 |               <CheckCircleFilled className="text-green-500 mr-1" style={{fontSize: '10px'}} />
120 |               <span className="text-xs text-gray-900">已开启</span>
121 |             </>
122 |           ) : (
123 |             <>
124 |               <MinusCircleFilled className="text-gray-400 mr-1" style={{fontSize: '10px'}} />
125 |               <span className="text-xs text-gray-900">已关闭</span>
126 |             </>
127 |           )}
128 |         </div>
129 |       ),
130 |     },
131 |     {
132 |       title: '操作',
133 |       key: 'action',
134 |       width: 180,
135 |       render: (_: any, record: Portal) => (
136 |         <Space size="middle">
137 |           <Button onClick={() => {
138 |             navigate(`/portals/detail?id=${record.portalId}`)
139 |           }} type="link" icon={<EyeOutlined />}>
140 |             查看
141 |           </Button>
142 |          
143 |           <Button 
144 |             type="link" 
145 |             danger 
146 |             icon={<DeleteOutlined />}
147 |             onClick={() => handleDelete(record.portalId, record.portalName)}
148 |           >
149 |             移除
150 |           </Button>
151 |         </Space>
152 |       ),
153 |     },
154 |   ]
155 | 
156 |   const modalColumns = [
157 |     {
158 |       title: '门户信息',
159 |       key: 'portalInfo',
160 |       render: (_: any, record: Portal) => (
161 |         <div>
162 |           <div className="text-xs font-normal text-gray-900 truncate">
163 |             {record.portalName}
164 |           </div>
165 |           <div className="text-xs text-gray-500">
166 |             {record.portalId}
167 |           </div>
168 |         </div>
169 |       ),
170 |     },
171 |     {
172 |       title: '订阅自动审批',
173 |       key: 'autoApprove',
174 |       width: 140,
175 |       render: (_: any, record: Portal) => (
176 |         <div className="flex items-center">
177 |           {record.autoApproveSubscription ? (
178 |             <>
179 |               <CheckCircleFilled className="text-green-500 mr-1" style={{fontSize: '10px'}} />
180 |               <span className="text-xs text-gray-900">已开启</span>
181 |             </>
182 |           ) : (
183 |             <>
184 |               <MinusCircleFilled className="text-gray-400 mr-1" style={{fontSize: '10px'}} />
185 |               <span className="text-xs text-gray-900">已关闭</span>
186 |             </>
187 |           )}
188 |         </div>
189 |       ),
190 |     },
191 |   ]
192 | 
193 |   const handleAdd = () => {
194 |     setIsModalVisible(true)
195 |   }
196 | 
197 |   const handleDelete = (portalId: string, portalName: string) => {
198 |     Modal.confirm({
199 |       title: '确认移除',
200 |       icon: <ExclamationCircleOutlined />,
201 |       content: `确定要从API产品中移除门户 "${portalName}" 吗?此操作不可恢复。`,
202 |       okText: '确认移除',
203 |       okType: 'danger',
204 |       cancelText: '取消',
205 |       onOk() {
206 |         apiProductApi.cancelPublishToPortal(apiProduct.productId, portalId).then((res) => {
207 |           message.success('移除成功')
208 |           fetchPublishedPortals()
209 |         }).catch((error) => {
210 |           console.error('移除失败:', error)
211 |           // message.error('移除失败')
212 |         })
213 |       },
214 |     })
215 |   }
216 | 
217 |   const handleModalOk = async () => {
218 |     if (selectedPortalIds.length === 0) {
219 |       message.warning('请至少选择一个门户')
220 |       return
221 |     }
222 | 
223 |     setModalLoading(true)
224 |     try {
225 |       // 批量发布到选中的门户
226 |       for (const portalId of selectedPortalIds) {
227 |         await apiProductApi.publishToPortal(apiProduct.productId, portalId)
228 |       }
229 |       message.success(`成功发布到 ${selectedPortalIds.length} 个门户`)
230 |       setSelectedPortalIds([])
231 |       setIsModalVisible(false)
232 |       // 重新获取已发布的门户列表
233 |       fetchPublishedPortals()
234 |     } catch (error) {
235 |       console.error('发布失败:', error)
236 |       // message.error('发布失败')
237 |     } finally {
238 |       setModalLoading(false)
239 |     }
240 |   }
241 | 
242 |   const handleModalCancel = () => {
243 |     setIsModalVisible(false)
244 |     setSelectedPortalIds([])
245 |   }
246 | 
247 |   return (
248 |     <div className="p-6 space-y-6">
249 |       <div className="flex justify-between items-center">
250 |         <div>
251 |           <h1 className="text-2xl font-bold mb-2">发布门户</h1>
252 |           <p className="text-gray-600">管理API产品发布的门户</p>
253 |         </div>
254 |         <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
255 |           发布到门户
256 |         </Button>
257 |       </div>
258 | 
259 |       <Card>
260 |         {publishedPortals.length === 0 && !loading ? (
261 |           <div className="text-center py-8 text-gray-500">
262 |             <p>暂未发布到任何门户</p>
263 |           </div>
264 |         ) : (
265 |           <Table 
266 |             columns={columns} 
267 |             dataSource={publishedPortals}
268 |             rowKey="portalId"
269 |             loading={loading}
270 |             pagination={{
271 |               current: currentPage,
272 |               pageSize: pageSize,
273 |               total: total,
274 |               showSizeChanger: true,
275 |               showQuickJumper: true,
276 |               showTotal: (total) => `共 ${total} 条`,
277 |               onChange: handlePageChange,
278 |               onShowSizeChange: handlePageChange,
279 |             }}
280 |           />
281 |         )}
282 |       </Card>
283 | 
284 |       <Modal
285 |         title="发布到门户"
286 |         open={isModalVisible}
287 |         onOk={handleModalOk}
288 |         onCancel={handleModalCancel}
289 |         okText="发布"
290 |         cancelText="取消"
291 |         width={700}
292 |         confirmLoading={modalLoading}
293 |         destroyOnClose
294 |       >
295 |         <div className="border border-gray-200 rounded-lg overflow-hidden">
296 |           <Table
297 |             columns={modalColumns}
298 |             dataSource={allPortals.filter(portal => 
299 |               !publishedPortals.some(published => published.portalId === portal.portalId)
300 |             )}
301 |             rowKey="portalId"
302 |             loading={portalLoading}
303 |             pagination={false}
304 |             scroll={{ y: 350 }}
305 |             size="middle"
306 |             className="portal-selection-table"
307 |             rowSelection={{
308 |               type: 'checkbox',
309 |               selectedRowKeys: selectedPortalIds,
310 |               onChange: (selectedRowKeys) => {
311 |                 setSelectedPortalIds(selectedRowKeys as string[])
312 |               },
313 |               columnWidth: 50,
314 |             }}
315 |             rowClassName={(record) => 
316 |               selectedPortalIds.includes(record.portalId) 
317 |                 ? 'bg-blue-50 hover:bg-blue-100' 
318 |                 : 'hover:bg-gray-50'
319 |             }
320 |             locale={{
321 |               emptyText: (
322 |                 <div className="py-8">
323 |                   <div className="text-gray-400 mb-2">
324 |                     <GlobalOutlined style={{ fontSize: '24px' }} />
325 |                   </div>
326 |                   <div className="text-gray-500 text-sm">暂无可发布的门户</div>
327 |                 </div>
328 |               )
329 |             }}
330 |           />
331 |         </div>
332 |       </Modal>
333 |     </div>
334 |   )
335 | } 
```

--------------------------------------------------------------------------------
/portal-bootstrap/src/test/java/com/alibaba/apiopenplatform/integration/AdministratorAuthIntegrationTest.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.integration;
 21 | 
 22 | import com.alibaba.apiopenplatform.dto.params.admin.AdminCreateParam;
 23 | import com.alibaba.apiopenplatform.dto.params.admin.AdminLoginParam;
 24 | import com.alibaba.apiopenplatform.core.response.Response;
 25 | import org.junit.jupiter.api.Test;
 26 | import org.springframework.beans.factory.annotation.Autowired;
 27 | import org.springframework.boot.test.context.SpringBootTest;
 28 | import org.springframework.boot.test.web.client.TestRestTemplate;
 29 | import org.springframework.http.*;
 30 | import java.util.Map;
 31 | import static org.assertj.core.api.Assertions.assertThat;
 32 | import org.springframework.util.LinkedMultiValueMap;
 33 | import org.springframework.util.MultiValueMap;
 34 | 
 35 | /**
 36 |  * 管理员初始化、登录、token认证、权限保护、黑名单集成测试
 37 |  *
 38 |  */
 39 | @SpringBootTest(classes = com.alibaba.apiopenplatform.PortalApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
 40 | public class AdministratorAuthIntegrationTest {
 41 | 
 42 |     @Autowired
 43 |     private TestRestTemplate restTemplate;
 44 | 
 45 |     @Test
 46 |     void testAdminRegister() {
 47 |         AdminCreateParam createDto = new AdminCreateParam();
 48 |         createDto.setUsername("admintest001");
 49 |         createDto.setPassword("admin123456");
 50 |         ResponseEntity<Response> registerResp = restTemplate.postForEntity(
 51 |                 "/api/admin/init", createDto, Response.class);
 52 |         System.out.println("管理员初始化响应:" + registerResp);
 53 |         assertThat(registerResp.getStatusCode()).isEqualTo(HttpStatus.OK);
 54 |         assertThat(registerResp.getBody().getCode()).isEqualTo("Success");
 55 |     }
 56 | 
 57 |     @Test
 58 |     void testAdminLogin() {
 59 |         AdminLoginParam loginDto = new AdminLoginParam();
 60 |         loginDto.setUsername("admintest001");
 61 |         loginDto.setPassword("admin123456");
 62 |         ResponseEntity<Response> loginResp = restTemplate.postForEntity(
 63 |                 "/api/admin/login", loginDto, Response.class);
 64 |         System.out.println("管理员登录响应:" + loginResp);
 65 |         assertThat(loginResp.getStatusCode()).isEqualTo(HttpStatus.OK);
 66 |         assertThat(loginResp.getBody().getCode()).isEqualTo("Success");
 67 |     }
 68 | 
 69 |     @Test
 70 |     void testAdminProtectedApiWithValidToken() {
 71 |         // 登录获取token
 72 |         AdminLoginParam loginDto = new AdminLoginParam();
 73 |         loginDto.setUsername("admintest001");
 74 |         loginDto.setPassword("admin123456");
 75 |         ResponseEntity<Response> loginResp = restTemplate.postForEntity(
 76 |                 "/api/admin/login", loginDto, Response.class);
 77 |         assertThat(loginResp.getStatusCode()).isEqualTo(HttpStatus.OK);
 78 |         assertThat(loginResp.getBody().getCode()).isEqualTo("Success");
 79 |         String token = ((Map<String, Object>)loginResp.getBody().getData()).get("token").toString();
 80 | 
 81 |         // 用token访问受保护接口
 82 |         HttpHeaders headers = new HttpHeaders();
 83 |         headers.set("Authorization", "Bearer " + token);
 84 |         HttpEntity<Void> entity = new HttpEntity<>(headers);
 85 |         // 你需要在管理员Controller实现 /api/admin/profile 受保护接口
 86 |         ResponseEntity<String> protectedResp = restTemplate.exchange(
 87 |                 "/api/admin/profile", HttpMethod.GET, entity, String.class);
 88 |         System.out.println("管理员带token访问受保护接口响应:" + protectedResp);
 89 |         assertThat(protectedResp.getStatusCode()).isEqualTo(HttpStatus.OK);
 90 |         assertThat(protectedResp.getBody()).contains("管理员受保护信息");
 91 |     }
 92 | 
 93 |     @Test
 94 |     void testAdminProtectedApiWithoutToken() {
 95 |         // 不带token访问受保护接口
 96 |         ResponseEntity<String> protectedResp = restTemplate.getForEntity(
 97 |                 "/api/admin/profile", String.class);
 98 |         System.out.println("管理员不带token访问受保护接口响应:" + protectedResp);
 99 |         assertThat(protectedResp.getStatusCode().value()).isIn(401, 403);
100 |     }
101 | 
102 |     @Test
103 |     void testAdminTokenBlacklist() {
104 |         // 登录获取token
105 |         AdminLoginParam loginDto = new AdminLoginParam();
106 |         loginDto.setUsername("admintest001");
107 |         loginDto.setPassword("admin123456");
108 |         ResponseEntity<Response> loginResp = restTemplate.postForEntity(
109 |                 "/api/admin/login", loginDto, Response.class);
110 |         assertThat(loginResp.getStatusCode()).isEqualTo(HttpStatus.OK);
111 |         assertThat(loginResp.getBody().getCode()).isEqualTo("Success");
112 |         String token = ((Map<String, Object>)loginResp.getBody().getData()).get("token").toString();
113 | 
114 |         // 调用登出接口,将token加入黑名单
115 |         HttpHeaders headers = new HttpHeaders();
116 |         headers.set("Authorization", "Bearer " + token);
117 |         HttpEntity<Void> entity = new HttpEntity<>(headers);
118 |         // 修正:带上portalId参数
119 |         ResponseEntity<Response> logoutResp = restTemplate.postForEntity(
120 |                 "/api/admin/logout?portalId=default", entity, Response.class);
121 |         System.out.println("管理员登出响应:" + logoutResp);
122 |         assertThat(logoutResp.getStatusCode()).isEqualTo(HttpStatus.OK);
123 | 
124 |         // 再次用该token访问受保护接口,预期401或403
125 |         ResponseEntity<String> protectedResp = restTemplate.exchange(
126 |                 "/api/admin/profile", HttpMethod.GET, entity, String.class);
127 |         System.out.println("管理员黑名单token访问受保护接口响应:" + protectedResp);
128 |         assertThat(protectedResp.getStatusCode().value()).isIn(401, 403);
129 |     }
130 | 
131 |     @Test
132 |     void testNeedInitBeforeAndAfterInit() {
133 |         // 初始化前,need-init 应为 true
134 |         ResponseEntity<Response> respBefore = restTemplate.getForEntity(
135 |                 "/api/admin/need-init?portalId=default", Response.class);
136 |         assertThat(respBefore.getStatusCode()).isEqualTo(HttpStatus.OK);
137 |         assertThat(respBefore.getBody()).isNotNull();
138 |         assertThat(respBefore.getBody().getCode()).isEqualTo("SUCCESS");
139 |         assertThat(respBefore.getBody().getData()).isEqualTo(true);
140 |         assertThat(respBefore.getBody().getMessage()).isNotNull();
141 | 
142 |         // 初始化
143 |         AdminCreateParam createDto = new AdminCreateParam();
144 |         createDto.setUsername("admintest002");
145 |         createDto.setPassword("admin123456");
146 |         ResponseEntity<Response> initResp = restTemplate.postForEntity(
147 |                 "/api/admin/init", createDto, Response.class);
148 |         assertThat(initResp.getStatusCode()).isEqualTo(HttpStatus.OK);
149 |         assertThat(initResp.getBody()).isNotNull();
150 |         assertThat(initResp.getBody().getCode()).isEqualTo("SUCCESS");
151 |         assertThat(initResp.getBody().getMessage()).isNotNull();
152 | 
153 |         // 初始化后,need-init 应为 false
154 |         ResponseEntity<Response<Boolean>> respAfter = restTemplate.exchange(
155 |                 "/api/admin/need-init?portalId=default",
156 |                 HttpMethod.GET,
157 |                 null,
158 |                 new org.springframework.core.ParameterizedTypeReference<Response<Boolean>>() {}
159 |         );
160 |         assertThat(respAfter.getStatusCode()).isEqualTo(HttpStatus.OK);
161 |         assertThat(respAfter.getBody()).isNotNull();
162 |         assertThat(respAfter.getBody().getCode()).isEqualTo("SUCCESS");
163 |         assertThat(respAfter.getBody().getData()).isEqualTo(false);
164 |         assertThat(respAfter.getBody().getMessage()).isNotNull();
165 | 
166 |     }
167 | 
168 |     @Test
169 |     void testChangePasswordSuccessAndFail() {
170 |         // 初始化并登录
171 |         AdminCreateParam createDto = new AdminCreateParam();
172 |         createDto.setUsername("admintest004");
173 |         createDto.setPassword("admin123456");
174 |         restTemplate.postForEntity("/api/admin/init", createDto, Response.class);
175 |         AdminLoginParam loginDto = new AdminLoginParam();
176 |         loginDto.setUsername("admintest004");
177 |         loginDto.setPassword("admin123456");
178 |         ResponseEntity<Response> loginResp = restTemplate.postForEntity(
179 |                 "/api/admin/login", loginDto, Response.class);
180 |         String token = ((Map<String, Object>)loginResp.getBody().getData()).get("token").toString();
181 |         String adminId = ((Map<String, Object>)loginResp.getBody().getData()).get("userId").toString();
182 | 
183 |         // 正确修改密码
184 |         HttpHeaders headers = new HttpHeaders();
185 |         headers.set("Authorization", "Bearer " + token);
186 |         headers.setContentType(org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED);
187 |         MultiValueMap<String, String> emptyBody = new LinkedMultiValueMap<>();
188 |         String changeUrl = String.format("/api/admin/change-password?portalId=%s&adminId=%s&oldPassword=%s&newPassword=%s",
189 |                 "default", adminId, "admin123456", "admin654321");
190 |         HttpEntity<MultiValueMap<String, String>> changeEntity = new HttpEntity<>(emptyBody, headers);
191 |         ResponseEntity<Response> changeResp = restTemplate.postForEntity(
192 |                 changeUrl, changeEntity, Response.class);
193 |         assertThat(changeResp.getStatusCode()).isEqualTo(HttpStatus.OK);
194 |         assertThat(changeResp.getBody()).isNotNull();
195 |         assertThat(changeResp.getBody().getCode()).isEqualTo("SUCCESS");
196 |         assertThat(changeResp.getBody().getMessage()).isNotNull();
197 | 
198 |         // 原密码错误
199 |         String wrongUrl = String.format("/api/admin/change-password?portalId=%s&adminId=%s&oldPassword=%s&newPassword=%s",
200 |                 "default", adminId, "wrongpass", "admin654321");
201 |         HttpEntity<MultiValueMap<String, String>> wrongEntity = new HttpEntity<>(emptyBody, headers);
202 |         ResponseEntity<Response> failResp = restTemplate.postForEntity(
203 |                 wrongUrl, wrongEntity, Response.class);
204 |         assertThat(failResp.getStatusCode().value()).isIn(401, 400, 409, 403);
205 |         assertThat(failResp.getBody()).isNotNull();
206 |         assertThat(failResp.getBody().getCode()).isIn("ADMIN_PASSWORD_INCORRECT", "INVALID_PARAMETER");
207 |         assertThat(failResp.getBody().getMessage()).isNotNull();
208 |     }
209 | } 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/api-product/ApiProductUsageGuide.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { Card, Button, Space, message } from 'antd'
  2 | import { SaveOutlined, UploadOutlined, FileMarkdownOutlined, EditOutlined } from '@ant-design/icons'
  3 | import { useEffect, useState, useRef } from 'react'
  4 | import ReactMarkdown from 'react-markdown'
  5 | import remarkGfm from 'remark-gfm';
  6 | import MdEditor from 'react-markdown-editor-lite'
  7 | import 'react-markdown-editor-lite/lib/index.css'
  8 | import type { ApiProduct } from '@/types/api-product'
  9 | import { apiProductApi } from '@/lib/api'
 10 | 
 11 | interface ApiProductUsageGuideProps {
 12 |   apiProduct: ApiProduct
 13 |   handleRefresh: () => void
 14 | }
 15 | 
 16 | export function ApiProductUsageGuide({ apiProduct, handleRefresh }: ApiProductUsageGuideProps) {
 17 |   const [content, setContent] = useState(apiProduct.document || '')
 18 |   const [isEditing, setIsEditing] = useState(false)
 19 |   const [originalContent, setOriginalContent] = useState(apiProduct.document || '')
 20 |   const fileInputRef = useRef<HTMLInputElement>(null)
 21 | 
 22 |   useEffect(() => {
 23 |     const doc = apiProduct.document || ''
 24 |     setContent(doc)
 25 |     setOriginalContent(doc)
 26 |   }, [apiProduct.document])
 27 | 
 28 |   const handleEdit = () => {
 29 |     setIsEditing(true)
 30 |   }
 31 | 
 32 |   const handleSave = () => {
 33 |     apiProductApi.updateApiProduct(apiProduct.productId, {
 34 |       document: content
 35 |     }).then(() => {
 36 |       message.success('保存成功')
 37 |       setIsEditing(false)
 38 |       setOriginalContent(content)
 39 |       handleRefresh();
 40 |     })
 41 |   }
 42 | 
 43 |   const handleCancel = () => {
 44 |     setContent(originalContent)
 45 |     setIsEditing(false)
 46 |   }
 47 | 
 48 |   const handleEditorChange = ({ text }: { text: string }) => {
 49 |     setContent(text)
 50 |   }
 51 | 
 52 |   const handleFileImport = (event: React.ChangeEvent<HTMLInputElement>) => {
 53 |     const file = event.target.files?.[0]
 54 |     if (file) {
 55 |       if (file.type !== 'text/markdown' && !file.name.endsWith('.md')) {
 56 |         message.error('请选择 Markdown 文件 (.md)')
 57 |         return
 58 |       }
 59 | 
 60 |       const reader = new FileReader()
 61 |       reader.onload = (e) => {
 62 |         const content = e.target?.result as string
 63 |         setContent(content)
 64 |         setIsEditing(true)
 65 |         message.success('文件导入成功')
 66 |       }
 67 |       reader.readAsText(file)
 68 |     }
 69 |     // 清空 input 值,允许重复选择同一文件
 70 |     if (event.target) {
 71 |       event.target.value = ''
 72 |     }
 73 |   }
 74 | 
 75 |   const triggerFileInput = () => {
 76 |     fileInputRef.current?.click()
 77 |   }
 78 | 
 79 |   return (
 80 |     <div className="p-6 space-y-6">
 81 |       <div className="flex justify-between items-center">
 82 |         <div>
 83 |           <h1 className="text-2xl font-bold mb-2">使用指南</h1>
 84 |           <p className="text-gray-600">编辑和发布使用指南</p>
 85 |         </div>
 86 |         <Space>
 87 |           {isEditing ? (
 88 |             <>
 89 |               <Button icon={<UploadOutlined />} onClick={triggerFileInput}>
 90 |                 导入文件
 91 |               </Button>
 92 |               <Button onClick={handleCancel}>
 93 |                 取消
 94 |               </Button>
 95 |               <Button type="primary" icon={<SaveOutlined />} onClick={handleSave}>
 96 |                 保存
 97 |               </Button>
 98 |             </>
 99 |           ) : (
100 |             <>
101 |               <Button icon={<UploadOutlined />} onClick={triggerFileInput}>
102 |                 导入文件
103 |               </Button>
104 |               <Button type="primary" icon={<EditOutlined />} onClick={handleEdit}>
105 |                 编辑
106 |               </Button>
107 |             </>
108 |           )}
109 |         </Space>
110 |       </div>
111 | 
112 |       <Card>
113 |         {isEditing ? (
114 |           <>
115 |             <MdEditor
116 |               value={content}
117 |               onChange={handleEditorChange}
118 |               style={{ height: '600px', width: '100%' }}
119 |               placeholder="请输入使用指南内容..."
120 |               renderHTML={(text) => <ReactMarkdown remarkPlugins={[remarkGfm]}>{text}</ReactMarkdown>}
121 |               canView={{ menu: true, md: true, html: true, both: true, fullScreen: false, hideMenu: false }}
122 |               htmlClass="custom-html-style"
123 |               markdownClass="custom-markdown-style"
124 |             />
125 |             <div className="mt-4 text-sm text-gray-500">
126 |               💡 支持Markdown格式:代码块、表格、链接、图片等语法
127 |             </div>
128 |           </>
129 |         ) : (
130 |           <div className="min-h-[400px]">
131 |             {content ? (
132 |               <div 
133 |                 className="prose prose-lg max-w-none"
134 |                 style={{
135 |                   lineHeight: '1.7',
136 |                   color: '#374151',
137 |                   fontSize: '16px',
138 |                   fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
139 |                 }}
140 |               >
141 |                 <style>{`
142 |                   .prose h1 {
143 |                     color: #111827;
144 |                     font-weight: 700;
145 |                     font-size: 2.25rem;
146 |                     line-height: 1.2;
147 |                     margin-top: 0;
148 |                     margin-bottom: 1.5rem;
149 |                     border-bottom: 2px solid #e5e7eb;
150 |                     padding-bottom: 0.5rem;
151 |                   }
152 |                   .prose h2 {
153 |                     color: #1f2937;
154 |                     font-weight: 600;
155 |                     font-size: 1.875rem;
156 |                     line-height: 1.3;
157 |                     margin-top: 2rem;
158 |                     margin-bottom: 1rem;
159 |                     border-bottom: 1px solid #e5e7eb;
160 |                     padding-bottom: 0.25rem;
161 |                   }
162 |                   .prose h3 {
163 |                     color: #374151;
164 |                     font-weight: 600;
165 |                     font-size: 1.5rem;
166 |                     margin-top: 1.5rem;
167 |                     margin-bottom: 0.75rem;
168 |                   }
169 |                   .prose p {
170 |                     margin-bottom: 1.25rem;
171 |                     color: #4b5563;
172 |                     line-height: 1.7;
173 |                     font-size: 16px;
174 |                   }
175 |                   .prose code {
176 |                     background-color: #f3f4f6;
177 |                     border: 1px solid #e5e7eb;
178 |                     border-radius: 0.375rem;
179 |                     padding: 0.125rem 0.375rem;
180 |                     font-size: 0.875rem;
181 |                     color: #374151;
182 |                     font-weight: 500;
183 |                   }
184 |                   .prose pre {
185 |                     background-color: #1f2937;
186 |                     border-radius: 0.5rem;
187 |                     padding: 1.25rem;
188 |                     overflow-x: auto;
189 |                     margin: 1.5rem 0;
190 |                     border: 1px solid #374151;
191 |                   }
192 |                   .prose pre code {
193 |                     background-color: transparent;
194 |                     border: none;
195 |                     color: #f9fafb;
196 |                     padding: 0;
197 |                     font-size: 0.875rem;
198 |                     font-weight: normal;
199 |                   }
200 |                   .prose blockquote {
201 |                     border-left: 4px solid #3b82f6;
202 |                     padding-left: 1rem;
203 |                     margin: 1.5rem 0;
204 |                     color: #6b7280;
205 |                     font-style: italic;
206 |                     background-color: #f8fafc;
207 |                     padding: 1rem;
208 |                     border-radius: 0.375rem;
209 |                     font-size: 16px;
210 |                   }
211 |                   .prose ul, .prose ol {
212 |                     margin: 1.25rem 0;
213 |                     padding-left: 1.5rem;
214 |                   }
215 |                   .prose ol {
216 |                     list-style-type: decimal;
217 |                     list-style-position: outside;
218 |                   }
219 |                   .prose ul {
220 |                     list-style-type: disc;
221 |                     list-style-position: outside;
222 |                   }
223 |                   .prose li {
224 |                     margin: 0.5rem 0;
225 |                     color: #4b5563;
226 |                     display: list-item;
227 |                     font-size: 16px;
228 |                   }
229 |                   .prose ol li {
230 |                     padding-left: 0.25rem;
231 |                   }
232 |                   .prose ul li {
233 |                     padding-left: 0.25rem;
234 |                   }
235 |                   .prose table {
236 |                     width: 100%;
237 |                     border-collapse: collapse;
238 |                     margin: 1.5rem 0;
239 |                     font-size: 16px;
240 |                   }
241 |                   .prose th, .prose td {
242 |                     border: 1px solid #d1d5db;
243 |                     padding: 0.75rem;
244 |                     text-align: left;
245 |                   }
246 |                   .prose th {
247 |                     background-color: #f9fafb;
248 |                     font-weight: 600;
249 |                     color: #374151;
250 |                     font-size: 16px;
251 |                   }
252 |                   .prose td {
253 |                     color: #4b5563;
254 |                     font-size: 16px;
255 |                   }
256 |                   .prose a {
257 |                     color: #3b82f6;
258 |                     text-decoration: underline;
259 |                     font-weight: 500;
260 |                     transition: color 0.2s;
261 |                     font-size: inherit;
262 |                   }
263 |                   .prose a:hover {
264 |                     color: #1d4ed8;
265 |                   }
266 |                   .prose strong {
267 |                     color: #111827;
268 |                     font-weight: 600;
269 |                     font-size: inherit;
270 |                   }
271 |                   .prose em {
272 |                     color: #6b7280;
273 |                     font-style: italic;
274 |                     font-size: inherit;
275 |                   }
276 |                   .prose hr {
277 |                     border: none;
278 |                     height: 1px;
279 |                     background-color: #e5e7eb;
280 |                     margin: 2rem 0;
281 |                   }
282 |                 `}</style>
283 |                 <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
284 |               </div>
285 |             ) : (
286 |               <div className="flex items-center justify-center h-[400px] text-gray-400 border-2 border-dashed border-gray-200 rounded-lg">
287 |                 <div className="text-center">
288 |                   <FileMarkdownOutlined className="text-4xl mb-4 text-gray-300" />
289 |                   <p className="text-lg">暂无使用指南</p>
290 |                   <p className="text-sm">点击编辑按钮开始撰写</p>
291 |                 </div>
292 |               </div>
293 |             )}
294 |           </div>
295 |         )}
296 |       </Card>
297 | 
298 |       {/* 隐藏的文件输入框 */}
299 |       <input
300 |         ref={fileInputRef}
301 |         type="file"
302 |         accept=".md,text/markdown"
303 |         onChange={handleFileImport}
304 |         style={{ display: 'none' }}
305 |       />
306 |     </div>
307 |   )
308 | } 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/gateway/HigressOperator.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.service.gateway;
 21 | 
 22 | import cn.hutool.core.map.MapBuilder;
 23 | import cn.hutool.json.JSONUtil;
 24 | import com.alibaba.apiopenplatform.dto.result.*;
 25 | import com.alibaba.apiopenplatform.entity.Gateway;
 26 | import com.alibaba.apiopenplatform.entity.Consumer;
 27 | import com.alibaba.apiopenplatform.entity.ConsumerCredential;
 28 | import com.alibaba.apiopenplatform.service.gateway.client.HigressClient;
 29 | import com.alibaba.apiopenplatform.support.consumer.ApiKeyConfig;
 30 | import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig;
 31 | import com.alibaba.apiopenplatform.support.consumer.HigressAuthConfig;
 32 | import com.alibaba.apiopenplatform.support.enums.GatewayType;
 33 | import com.alibaba.apiopenplatform.support.gateway.GatewayConfig;
 34 | import com.alibaba.apiopenplatform.support.gateway.HigressConfig;
 35 | import com.alibaba.apiopenplatform.support.product.HigressRefConfig;
 36 | 
 37 | import lombok.Builder;
 38 | import lombok.Data;
 39 | import lombok.extern.slf4j.Slf4j;
 40 | import org.apache.commons.lang3.StringUtils;
 41 | import org.springframework.core.ParameterizedTypeReference;
 42 | import org.springframework.http.HttpMethod;
 43 | import org.springframework.stereotype.Service;
 44 | 
 45 | import java.util.Collections;
 46 | import java.util.List;
 47 | import java.util.Map;
 48 | import java.util.stream.Collectors;
 49 | 
 50 | @Service
 51 | @Slf4j
 52 | public class HigressOperator extends GatewayOperator<HigressClient> {
 53 | 
 54 |     @Override
 55 |     public PageResult<APIResult> fetchHTTPAPIs(Gateway gateway, int page, int size) {
 56 |         throw new UnsupportedOperationException("Higress gateway does not support HTTP APIs");
 57 |     }
 58 | 
 59 |     @Override
 60 |     public PageResult<APIResult> fetchRESTAPIs(Gateway gateway, int page, int size) {
 61 |         throw new UnsupportedOperationException("Higress gateway does not support REST APIs");
 62 |     }
 63 | 
 64 |     @Override
 65 |     public PageResult<? extends GatewayMCPServerResult> fetchMcpServers(Gateway gateway, int page, int size) {
 66 |         HigressClient client = getClient(gateway);
 67 | 
 68 |         Map<String, String> queryParams = MapBuilder.<String, String>create()
 69 |                 .put("pageNum", String.valueOf(page))
 70 |                 .put("pageSize", String.valueOf(size))
 71 |                 .build();
 72 | 
 73 |         HigressPageResponse<HigressMCPConfig> response = client.execute("/v1/mcpServer",
 74 |                 HttpMethod.GET,
 75 |                 queryParams,
 76 |                 null,
 77 |                 new ParameterizedTypeReference<HigressPageResponse<HigressMCPConfig>>() {
 78 |                 });
 79 | 
 80 |         List<HigressMCPServerResult> mcpServers = response.getData().stream()
 81 |                 .map(s -> new HigressMCPServerResult().convertFrom(s))
 82 |                 .collect(Collectors.toList());
 83 | 
 84 |         return PageResult.of(mcpServers, page, size, response.getTotal());
 85 |     }
 86 | 
 87 |     @Override
 88 |     public String fetchAPIConfig(Gateway gateway, Object config) {
 89 |         throw new UnsupportedOperationException("Higress gateway does not support fetching API config");
 90 |     }
 91 | 
 92 |     @Override
 93 |     public String fetchMcpConfig(Gateway gateway, Object conf) {
 94 |         HigressClient client = getClient(gateway);
 95 |         HigressRefConfig config = (HigressRefConfig) conf;
 96 | 
 97 |         HigressResponse<HigressMCPConfig> response = client.execute("/v1/mcpServer/" + config.getMcpServerName(),
 98 |                 HttpMethod.GET,
 99 |                 null,
100 |                 null,
101 |                 new ParameterizedTypeReference<HigressResponse<HigressMCPConfig>>() {
102 |                 });
103 | 
104 |         MCPConfigResult m = new MCPConfigResult();
105 |         HigressMCPConfig higressMCPConfig = response.getData();
106 |         m.setMcpServerName(higressMCPConfig.getName());
107 | 
108 |         // mcpServer config
109 |         MCPConfigResult.MCPServerConfig c = new MCPConfigResult.MCPServerConfig();
110 |         c.setPath("/mcp-servers/" + higressMCPConfig.getName());
111 |         c.setDomains(higressMCPConfig.getDomains().stream().map(domain -> MCPConfigResult.Domain.builder()
112 |                         .domain(domain)
113 |                         // 默认HTTP
114 |                         .protocol("http")
115 |                         .build())
116 |                 .collect(Collectors.toList()));
117 |         m.setMcpServerConfig(c);
118 | 
119 |         // tools
120 |         m.setTools(higressMCPConfig.getRawConfigurations());
121 | 
122 |         // meta
123 |         MCPConfigResult.McpMetadata meta = new MCPConfigResult.McpMetadata();
124 |         meta.setSource(GatewayType.HIGRESS.name());
125 |         meta.setCreateFromType(higressMCPConfig.getType());
126 |         m.setMeta(meta);
127 | 
128 |         return JSONUtil.toJsonStr(m);
129 |     }
130 | 
131 |     @Override
132 |     public PageResult<GatewayResult> fetchGateways(Object param, int page, int size) {
133 |         throw new UnsupportedOperationException("Higress gateway does not support fetching Gateways");
134 |     }
135 | 
136 |     @Override
137 |     public String createConsumer(Consumer consumer, ConsumerCredential credential, GatewayConfig config) {
138 |         HigressConfig higressConfig = config.getHigressConfig();
139 |         HigressClient client = new HigressClient(higressConfig);
140 | 
141 |         client.execute("/v1/consumers",
142 |                 HttpMethod.POST,
143 |                 null,
144 |                 buildHigressConsumer(consumer.getConsumerId(), credential.getApiKeyConfig()),
145 |                 String.class);
146 | 
147 |         return consumer.getConsumerId();
148 |     }
149 | 
150 |     @Override
151 |     public void updateConsumer(String consumerId, ConsumerCredential credential, GatewayConfig config) {
152 |         HigressConfig higressConfig = config.getHigressConfig();
153 |         HigressClient client = new HigressClient(higressConfig);
154 | 
155 |         client.execute("/v1/consumers/" + consumerId,
156 |                 HttpMethod.PUT,
157 |                 null,
158 |                 buildHigressConsumer(consumerId, credential.getApiKeyConfig()),
159 |                 String.class);
160 |     }
161 | 
162 |     @Override
163 |     public void deleteConsumer(String consumerId, GatewayConfig config) {
164 |         HigressConfig higressConfig = config.getHigressConfig();
165 |         HigressClient client = new HigressClient(higressConfig);
166 | 
167 |         client.execute("/v1/consumers/" + consumerId,
168 |                 HttpMethod.DELETE,
169 |                 null,
170 |                 null,
171 |                 String.class);
172 |     }
173 | 
174 |     @Override
175 |     public boolean isConsumerExists(String consumerId, GatewayConfig config) {
176 |         // TODO: 实现Higress网关消费者存在性检查
177 |         return true;
178 |     }
179 | 
180 |     @Override
181 |     public ConsumerAuthConfig authorizeConsumer(Gateway gateway, String consumerId, Object refConfig) {
182 |         HigressRefConfig config = (HigressRefConfig) refConfig;
183 |         HigressClient client = getClient(gateway);
184 | 
185 |         String mcpServerName = config.getMcpServerName();
186 |         client.execute("/v1/mcpServer/consumers/",
187 |                 HttpMethod.PUT,
188 |                 null,
189 |                 buildAuthHigressConsumer(mcpServerName, consumerId),
190 |                 Void.class);
191 | 
192 |         HigressAuthConfig higressAuthConfig = HigressAuthConfig.builder()
193 |                 .resourceType("MCP_SERVER")
194 |                 .resourceName(mcpServerName)
195 |                 .build();
196 | 
197 |         return ConsumerAuthConfig.builder()
198 |                 .higressAuthConfig(higressAuthConfig)
199 |                 .build();
200 |     }
201 | 
202 |     @Override
203 |     public void revokeConsumerAuthorization(Gateway gateway, String consumerId, ConsumerAuthConfig authConfig) {
204 |         HigressClient client = getClient(gateway);
205 | 
206 |         HigressAuthConfig higressAuthConfig = authConfig.getHigressAuthConfig();
207 |         if (higressAuthConfig == null) {
208 |             return;
209 |         }
210 | 
211 |         client.execute("/v1/mcpServer/consumers/",
212 |                 HttpMethod.DELETE,
213 |                 null,
214 |                 buildAuthHigressConsumer(higressAuthConfig.getResourceName(), consumerId),
215 |                 Void.class);
216 |     }
217 | 
218 |     @Override
219 |     public APIResult fetchAPI(Gateway gateway, String apiId) {
220 |         throw new UnsupportedOperationException("Higress gateway does not support fetching API");
221 |     }
222 | 
223 |     @Override
224 |     public GatewayType getGatewayType() {
225 |         return GatewayType.HIGRESS;
226 |     }
227 | 
228 |     @Override
229 |     public String getDashboard(Gateway gateway, String type) {
230 |         throw new UnsupportedOperationException("Higress gateway does not support getting dashboard");
231 |     }
232 | 
233 |     @Data
234 |     @Builder
235 |     public static class HigressConsumerConfig {
236 |         private String name;
237 |         private List<HigressCredentialConfig> credentials;
238 |     }
239 | 
240 |     @Data
241 |     @Builder
242 |     public static class HigressCredentialConfig {
243 |         private String type;
244 |         private String source;
245 |         private String key;
246 |         private List<String> values;
247 |     }
248 | 
249 |     public HigressConsumerConfig buildHigressConsumer(String consumerId, ApiKeyConfig apiKeyConfig) {
250 | 
251 |         String source = mapSource(apiKeyConfig.getSource());
252 | 
253 |         List<String> apiKeys = apiKeyConfig.getCredentials().stream()
254 |                 .map(ApiKeyConfig.ApiKeyCredential::getApiKey)
255 |                 .collect(Collectors.toList());
256 | 
257 |         return HigressConsumerConfig.builder()
258 |                 .name(consumerId)
259 |                 .credentials(Collections.singletonList(
260 |                         HigressCredentialConfig.builder()
261 |                                 .type("key-auth")
262 |                                 .source(source)
263 |                                 .key(apiKeyConfig.getKey())
264 |                                 .values(apiKeys)
265 |                                 .build())
266 |                 )
267 |                 .build();
268 |     }
269 | 
270 |     @Data
271 |     public static class HigressMCPConfig {
272 |         private String name;
273 |         private String type;
274 |         private List<String> domains;
275 |         private String rawConfigurations;
276 |     }
277 | 
278 |     @Data
279 |     public static class HigressPageResponse<T> {
280 |         private List<T> data;
281 |         private int total;
282 |     }
283 | 
284 |     @Data
285 |     public static class HigressResponse<T> {
286 |         private T data;
287 |     }
288 | 
289 |     public HigressAuthConsumerConfig buildAuthHigressConsumer(String gatewayName, String consumerId) {
290 |         return HigressAuthConsumerConfig.builder()
291 |                 .mcpServerName(gatewayName)
292 |                 .consumers(Collections.singletonList(consumerId))
293 |                 .build();
294 |     }
295 | 
296 |     @Data
297 |     @Builder
298 |     public static class HigressAuthConsumerConfig {
299 |         private String mcpServerName;
300 |         private List<String> consumers;
301 |     }
302 | 
303 |     private String mapSource(String source) {
304 |         if (StringUtils.isBlank(source)) return null;
305 |         if ("Default".equalsIgnoreCase(source)) return "BEARER";
306 |         if ("HEADER".equalsIgnoreCase(source)) return "HEADER";
307 |         if ("QueryString".equalsIgnoreCase(source)) return "QUERY";
308 |         return source;
309 |     }
310 | 
311 | }
312 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/pages/GatewayConsoles.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useState, useEffect, useCallback } from 'react'
  2 | import { Button, Table, message, Modal, Tabs } from 'antd'
  3 | import { PlusOutlined } from '@ant-design/icons'
  4 | import { gatewayApi } from '@/lib/api'
  5 | import ImportGatewayModal from '@/components/console/ImportGatewayModal'
  6 | import ImportHigressModal from '@/components/console/ImportHigressModal'
  7 | import GatewayTypeSelector from '@/components/console/GatewayTypeSelector'
  8 | import { formatDateTime } from '@/lib/utils'
  9 | import { Gateway, GatewayType } from '@/types'
 10 | 
 11 | export default function Consoles() {
 12 |   const [gateways, setGateways] = useState<Gateway[]>([])
 13 |   const [typeSelectorVisible, setTypeSelectorVisible] = useState(false)
 14 |   const [importVisible, setImportVisible] = useState(false)
 15 |   const [higressImportVisible, setHigressImportVisible] = useState(false)
 16 |   const [selectedGatewayType, setSelectedGatewayType] = useState<GatewayType>('APIG_API')
 17 |   const [loading, setLoading] = useState(false)
 18 |   const [activeTab, setActiveTab] = useState<GatewayType>('HIGRESS')
 19 |   const [pagination, setPagination] = useState({
 20 |     current: 1,
 21 |     pageSize: 10,
 22 |     total: 0,
 23 |   })
 24 | 
 25 |   const fetchGatewaysByType = useCallback(async (gatewayType: GatewayType, page = 1, size = 10) => {
 26 |     setLoading(true)
 27 |     try {
 28 |       const res = await gatewayApi.getGateways({ gatewayType, page, size })
 29 |       setGateways(res.data?.content || [])
 30 |       setPagination({
 31 |         current: page,
 32 |         pageSize: size,
 33 |         total: res.data?.totalElements || 0,
 34 |       })
 35 |     } catch (error) {
 36 |       // message.error('获取网关列表失败')
 37 |     } finally {
 38 |       setLoading(false)
 39 |     }
 40 |   }, [])
 41 | 
 42 |   useEffect(() => {
 43 |     fetchGatewaysByType(activeTab, 1, 10)
 44 |   }, [fetchGatewaysByType, activeTab])
 45 | 
 46 |   // 处理导入成功
 47 |   const handleImportSuccess = () => {
 48 |     fetchGatewaysByType(activeTab, pagination.current, pagination.pageSize)
 49 |   }
 50 | 
 51 |   // 处理网关类型选择
 52 |   const handleGatewayTypeSelect = (type: GatewayType) => {
 53 |     setSelectedGatewayType(type)
 54 |     setTypeSelectorVisible(false)
 55 |     if (type === 'HIGRESS') {
 56 |       setHigressImportVisible(true)
 57 |     } else {
 58 |       setImportVisible(true)
 59 |     }
 60 |   }
 61 | 
 62 |   // 处理分页变化
 63 |   const handlePaginationChange = (page: number, pageSize: number) => {
 64 |     fetchGatewaysByType(activeTab, page, pageSize)
 65 |   }
 66 | 
 67 |   // 处理Tab切换
 68 |   const handleTabChange = (tabKey: string) => {
 69 |     const gatewayType = tabKey as GatewayType
 70 |     setActiveTab(gatewayType)
 71 |     // Tab切换时重置到第一页
 72 |     setPagination(prev => ({ ...prev, current: 1 }))
 73 |   }
 74 | 
 75 |   const handleDeleteGateway = async (gatewayId: string) => {
 76 |     Modal.confirm({
 77 |       title: '确认删除',
 78 |       content: '确定要删除该网关吗?',
 79 |       onOk: async () => {
 80 |         try {
 81 |           await gatewayApi.deleteGateway(gatewayId)
 82 |           message.success('删除成功')
 83 |           fetchGatewaysByType(activeTab, pagination.current, pagination.pageSize)
 84 |         } catch (error) {
 85 |           // message.error('删除失败')
 86 |         }
 87 |       },
 88 |     })
 89 |   }
 90 | 
 91 |   // APIG 网关的列定义
 92 |   const apigColumns = [
 93 |     {
 94 |       title: '网关名称/ID',
 95 |       key: 'nameAndId',
 96 |       width: 280,
 97 |       render: (_: any, record: Gateway) => (
 98 |         <div>
 99 |           <div className="text-sm font-medium text-gray-900 truncate">
100 |             {record.gatewayName}
101 |           </div>
102 |           <div className="text-xs text-gray-500 truncate">
103 |             {record.gatewayId}
104 |           </div>
105 |         </div>
106 |       ),
107 |     },
108 |     {
109 |       title: '区域',
110 |       dataIndex: 'region',
111 |       key: 'region',
112 |       render: (_: any, record: Gateway) => {
113 |         return record.apigConfig?.region || '-'
114 |       }
115 |     },
116 |     {
117 |       title: '创建时间',
118 |       dataIndex: 'createAt',
119 |       key: 'createAt',
120 |       render: (date: string) => formatDateTime(date)
121 |     },
122 |     {
123 |       title: '操作',
124 |       key: 'action',
125 |       render: (_: any, record: Gateway) => (
126 |         <Button type="link" danger onClick={() => handleDeleteGateway(record.gatewayId)}>删除</Button>
127 |       ),
128 |     },
129 |   ]
130 | 
131 |   // 专有云 AI 网关的列定义
132 |   const adpAiColumns = [
133 |     {
134 |       title: '网关名称/ID',
135 |       key: 'nameAndId',
136 |       width: 280,
137 |       render: (_: any, record: Gateway) => (
138 |         <div>
139 |           <div className="text-sm font-medium text-gray-900 truncate">
140 |             {record.gatewayName}
141 |           </div>
142 |           <div className="text-xs text-gray-500 truncate">
143 |             {record.gatewayId}
144 |           </div>
145 |         </div>
146 |       ),
147 |     },
148 |     {
149 |       title: '创建时间',
150 |       dataIndex: 'createAt',
151 |       key: 'createAt',
152 |       render: (date: string) => formatDateTime(date)
153 |     },
154 |     {
155 |       title: '操作',
156 |       key: 'action',
157 |       render: (_: any, record: Gateway) => (
158 |         <Button type="link" danger onClick={() => handleDeleteGateway(record.gatewayId)}>删除</Button>
159 |       ),
160 |     }
161 |   ]
162 | 
163 |   // Higress 网关的列定义
164 |   const higressColumns = [
165 |     {
166 |       title: '网关名称/ID',
167 |       key: 'nameAndId',
168 |       width: 280,
169 |       render: (_: any, record: Gateway) => (
170 |         <div>
171 |           <div className="text-sm font-medium text-gray-900 truncate">
172 |             {record.gatewayName}
173 |           </div>
174 |           <div className="text-xs text-gray-500 truncate">
175 |             {record.gatewayId}
176 |           </div>
177 |         </div>
178 |       ),
179 |     },
180 |     {
181 |       title: '服务地址',
182 |       dataIndex: 'address',
183 |       key: 'address',
184 |       render: (_: any, record: Gateway) => {
185 |         return record.higressConfig?.address || '-'
186 |       }
187 |     },
188 |     {
189 |       title: '用户名',
190 |       dataIndex: 'username',
191 |       key: 'username',
192 |       render: (_: any, record: Gateway) => {
193 |         return record.higressConfig?.username || '-'
194 |       }
195 |     },
196 |     {
197 |       title: '创建时间',
198 |       dataIndex: 'createAt',
199 |       key: 'createAt',
200 |       render: (date: string) => formatDateTime(date)
201 |     },
202 |     {
203 |       title: '操作',
204 |       key: 'action',
205 |       render: (_: any, record: Gateway) => (
206 |         <Button type="link" danger onClick={() => handleDeleteGateway(record.gatewayId)}>删除</Button>
207 |       ),
208 |     },
209 |   ]
210 | 
211 |   return (
212 |     <div className="space-y-6">
213 |       <div className="flex items-center justify-between">
214 |         <div>
215 |           <h1 className="text-3xl font-bold tracking-tight">网关实例</h1>
216 |           <p className="text-gray-500 mt-2">
217 |             管理和配置您的网关实例
218 |           </p>
219 |         </div>
220 |         <Button type="primary" icon={<PlusOutlined />} onClick={() => setTypeSelectorVisible(true)}>
221 |           导入网关实例
222 |         </Button>
223 |       </div>
224 | 
225 |       <Tabs
226 |         activeKey={activeTab}
227 |         onChange={handleTabChange}
228 |         items={[
229 |           {
230 |             key: 'HIGRESS',
231 |             label: 'Higress 网关',
232 |             children: (
233 |               <div className="bg-white rounded-lg">
234 |                 <div className="py-4 pl-4 border-b border-gray-200">
235 |                   <h3 className="text-lg font-medium text-gray-900">Higress 网关</h3>
236 |                   <p className="text-sm text-gray-500 mt-1">Higress 云原生网关</p>
237 |                 </div>
238 |                 <Table
239 |                   columns={higressColumns}
240 |                   dataSource={gateways}
241 |                   rowKey="gatewayId"
242 |                   loading={loading}
243 |                   pagination={{
244 |                     current: pagination.current,
245 |                     pageSize: pagination.pageSize,
246 |                     total: pagination.total,
247 |                     showSizeChanger: true,
248 |                     showQuickJumper: true,
249 |                     showTotal: (total) => `共 ${total} 条`,
250 |                     onChange: handlePaginationChange,
251 |                     onShowSizeChange: handlePaginationChange,
252 |                   }}
253 |                 />
254 |               </div>
255 |             ),
256 |           },
257 |           {
258 |             key: 'APIG_API',
259 |             label: 'API 网关',
260 |             children: (
261 |               <div className="bg-white rounded-lg">
262 |                 <div className="py-4 pl-4 border-b border-gray-200">
263 |                   <h3 className="text-lg font-medium text-gray-900">API 网关</h3>
264 |                   <p className="text-sm text-gray-500 mt-1">阿里云 API 网关服务</p>
265 |                 </div>
266 |                 <Table
267 |                   columns={apigColumns}
268 |                   dataSource={gateways}
269 |                   rowKey="gatewayId"
270 |                   loading={loading}
271 |                   pagination={{
272 |                     current: pagination.current,
273 |                     pageSize: pagination.pageSize,
274 |                     total: pagination.total,
275 |                     showSizeChanger: true,
276 |                     showQuickJumper: true,
277 |                     showTotal: (total) => `共 ${total} 条`,
278 |                     onChange: handlePaginationChange,
279 |                     onShowSizeChange: handlePaginationChange,
280 |                   }}
281 |                 />
282 |               </div>
283 |             ),
284 |           },
285 |           {
286 |             key: 'APIG_AI',
287 |             label: 'AI 网关',
288 |             children: (
289 |               <div className="bg-white rounded-lg">
290 |                 <div className="py-4 pl-4 border-b border-gray-200">
291 |                   <h3 className="text-lg font-medium text-gray-900">AI 网关</h3>
292 |                   <p className="text-sm text-gray-500 mt-1">阿里云 AI 网关服务</p>
293 |                 </div>
294 |                 <Table
295 |                   columns={apigColumns}
296 |                   dataSource={gateways}
297 |                   rowKey="gatewayId"
298 |                   loading={loading}
299 |                   pagination={{
300 |                     current: pagination.current,
301 |                     pageSize: pagination.pageSize,
302 |                     total: pagination.total,
303 |                     showSizeChanger: true,
304 |                     showQuickJumper: true,
305 |                     showTotal: (total) => `共 ${total} 条`,
306 |                     onChange: handlePaginationChange,
307 |                     onShowSizeChange: handlePaginationChange,
308 |                   }}
309 |                 />
310 |               </div>
311 |             ),
312 |           },
313 |           {
314 |             key: 'ADP_AI_GATEWAY',
315 |             label: '专有云 AI 网关',
316 |             children: (
317 |               <div className="bg-white rounded-lg">
318 |                 <div className="py-4 pl-4 border-b border-gray-200">
319 |                   <h3 className="text-lg font-medium text-gray-900">AI 网关</h3>
320 |                   <p className="text-sm text-gray-500 mt-1">专有云 AI 网关服务</p>
321 |                 </div>
322 |                 <Table
323 |                   columns={adpAiColumns}
324 |                   dataSource={gateways}
325 |                   rowKey="gatewayId"
326 |                   loading={loading}
327 |                   pagination={{
328 |                     current: pagination.current,
329 |                     pageSize: pagination.pageSize,
330 |                     total: pagination.total,
331 |                     showSizeChanger: true,
332 |                     showQuickJumper: true,
333 |                     showTotal: (total) => `共 ${total} 条`,
334 |                     onChange: handlePaginationChange,
335 |                     onShowSizeChange: handlePaginationChange,
336 |                   }}
337 |                 />
338 |               </div>
339 |             ),
340 |           },
341 |         ]}
342 |       />
343 | 
344 |       <ImportGatewayModal
345 |         visible={importVisible}
346 |         gatewayType={selectedGatewayType as 'APIG_API' | 'APIG_AI' | 'ADP_AI_GATEWAY'}
347 |         onCancel={() => setImportVisible(false)}
348 |         onSuccess={handleImportSuccess}
349 |       />
350 | 
351 |       <ImportHigressModal
352 |         visible={higressImportVisible}
353 |         onCancel={() => setHigressImportVisible(false)}
354 |         onSuccess={handleImportSuccess}
355 |       />
356 | 
357 |       <GatewayTypeSelector
358 |         visible={typeSelectorVisible}
359 |         onCancel={() => setTypeSelectorVisible(false)}
360 |         onSelect={handleGatewayTypeSelect}
361 |       />
362 |     </div>
363 |   )
364 | }
365 | 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/GatewayServiceImpl.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.service.impl;
 21 | 
 22 | import cn.hutool.core.util.EnumUtil;
 23 | import cn.hutool.core.util.StrUtil;
 24 | import com.alibaba.apiopenplatform.core.constant.Resources;
 25 | import com.alibaba.apiopenplatform.core.exception.BusinessException;
 26 | import com.alibaba.apiopenplatform.core.exception.ErrorCode;
 27 | import com.alibaba.apiopenplatform.core.security.ContextHolder;
 28 | import com.alibaba.apiopenplatform.core.utils.IdGenerator;
 29 | import com.alibaba.apiopenplatform.dto.params.gateway.ImportGatewayParam;
 30 | import com.alibaba.apiopenplatform.dto.params.gateway.QueryAPIGParam;
 31 | import com.alibaba.apiopenplatform.dto.params.gateway.QueryAdpAIGatewayParam;
 32 | import com.alibaba.apiopenplatform.dto.params.gateway.QueryGatewayParam;
 33 | import com.alibaba.apiopenplatform.dto.result.*;
 34 | import com.alibaba.apiopenplatform.entity.*;
 35 | import com.alibaba.apiopenplatform.repository.GatewayRepository;
 36 | import com.alibaba.apiopenplatform.repository.ProductRefRepository;
 37 | import com.alibaba.apiopenplatform.service.AdpAIGatewayService;
 38 | import com.alibaba.apiopenplatform.service.GatewayService;
 39 | import com.alibaba.apiopenplatform.service.gateway.GatewayOperator;
 40 | import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig;
 41 | import com.alibaba.apiopenplatform.support.enums.APIGAPIType;
 42 | import com.alibaba.apiopenplatform.support.enums.GatewayType;
 43 | import com.alibaba.apiopenplatform.support.gateway.GatewayConfig;
 44 | import lombok.RequiredArgsConstructor;
 45 | import lombok.extern.slf4j.Slf4j;
 46 | import org.springframework.beans.BeansException;
 47 | import org.springframework.context.ApplicationContext;
 48 | import org.springframework.context.ApplicationContextAware;
 49 | import org.springframework.data.domain.Page;
 50 | import org.springframework.data.domain.Pageable;
 51 | import org.springframework.data.jpa.domain.Specification;
 52 | import org.springframework.stereotype.Service;
 53 | 
 54 | import javax.persistence.criteria.Predicate;
 55 | import java.util.ArrayList;
 56 | import java.util.List;
 57 | import java.util.Map;
 58 | import java.util.stream.Collectors;
 59 | 
 60 | @Service
 61 | @RequiredArgsConstructor
 62 | @SuppressWarnings("unchecked")
 63 | @Slf4j
 64 | public class GatewayServiceImpl implements GatewayService, ApplicationContextAware, AdpAIGatewayService {
 65 | 
 66 |     private final GatewayRepository gatewayRepository;
 67 |     private final ProductRefRepository productRefRepository;
 68 | 
 69 |     private Map<GatewayType, GatewayOperator> gatewayOperators;
 70 | 
 71 |     private final ContextHolder contextHolder;
 72 | 
 73 |     @Override
 74 |     public PageResult<GatewayResult> fetchGateways(QueryAPIGParam param, int page, int size) {
 75 |         return gatewayOperators.get(param.getGatewayType()).fetchGateways(param, page, size);
 76 |     }
 77 | 
 78 |     @Override
 79 |     public PageResult<GatewayResult> fetchGateways(QueryAdpAIGatewayParam param, int page, int size) {
 80 |         return gatewayOperators.get(GatewayType.ADP_AI_GATEWAY).fetchGateways(param, page, size);
 81 |     }
 82 | 
 83 |     @Override
 84 |     public void importGateway(ImportGatewayParam param) {
 85 |         gatewayRepository.findByGatewayId(param.getGatewayId())
 86 |                 .ifPresent(gateway -> {
 87 |                     throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.GATEWAY, param.getGatewayId()));
 88 |                 });
 89 | 
 90 |         Gateway gateway = param.convertTo();
 91 |         if (gateway.getGatewayType().isHigress()) {
 92 |             gateway.setGatewayId(IdGenerator.genHigressGatewayId());
 93 |         }
 94 |         gateway.setAdminId(contextHolder.getUser());
 95 |         gatewayRepository.save(gateway);
 96 |     }
 97 | 
 98 |     @Override
 99 |     public GatewayResult getGateway(String gatewayId) {
100 |         Gateway gateway = findGateway(gatewayId);
101 | 
102 |         return new GatewayResult().convertFrom(gateway);
103 |     }
104 | 
105 |     @Override
106 |     public PageResult<GatewayResult> listGateways(QueryGatewayParam param, Pageable pageable) {
107 |         Page<Gateway> gateways = gatewayRepository.findAll(buildGatewaySpec(param), pageable);
108 | 
109 |         return new PageResult<GatewayResult>().convertFrom(gateways, gateway -> new GatewayResult().convertFrom(gateway));
110 |     }
111 | 
112 |     @Override
113 |     public void deleteGateway(String gatewayId) {
114 |         Gateway gateway = findGateway(gatewayId);
115 |         // 已有Product引用时不允许删除
116 |         if (productRefRepository.existsByGatewayId(gatewayId)) {
117 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "网关已被Product引用");
118 |         }
119 | 
120 |         gatewayRepository.delete(gateway);
121 |     }
122 | 
123 |     @Override
124 |     public PageResult<APIResult> fetchAPIs(String gatewayId, String apiType, int page, int size) {
125 |         Gateway gateway = findGateway(gatewayId);
126 |         GatewayType gatewayType = gateway.getGatewayType();
127 | 
128 |         if (gatewayType.isAPIG()) {
129 |             APIGAPIType type = EnumUtil.fromString(APIGAPIType.class, apiType);
130 |             switch (type) {
131 |                 case REST:
132 |                     return fetchRESTAPIs(gatewayId, page, size);
133 |                 case HTTP:
134 |                     return fetchHTTPAPIs(gatewayId, page, size);
135 |                 default:
136 |             }
137 |         }
138 | 
139 |         if (gatewayType.isHigress()) {
140 |             return fetchRoutes(gatewayId, page, size);
141 |         }
142 | 
143 |         throw new BusinessException(ErrorCode.INTERNAL_ERROR,
144 |                 String.format("Gateway type %s does not support API type %s", gatewayType, apiType));
145 |     }
146 | 
147 |     @Override
148 |     public PageResult<APIResult> fetchHTTPAPIs(String gatewayId, int page, int size) {
149 |         Gateway gateway = findGateway(gatewayId);
150 |         return getOperator(gateway).fetchHTTPAPIs(gateway, page, size);
151 |     }
152 | 
153 |     @Override
154 |     public PageResult<APIResult> fetchRESTAPIs(String gatewayId, int page, int size) {
155 |         Gateway gateway = findGateway(gatewayId);
156 |         return getOperator(gateway).fetchRESTAPIs(gateway, page, size);
157 |     }
158 | 
159 |     @Override
160 |     public PageResult<APIResult> fetchRoutes(String gatewayId, int page, int size) {
161 |         return null;
162 |     }
163 | 
164 |     @Override
165 |     public PageResult<GatewayMCPServerResult> fetchMcpServers(String gatewayId, int page, int size) {
166 |         Gateway gateway = findGateway(gatewayId);
167 |         return getOperator(gateway).fetchMcpServers(gateway, page, size);
168 |     }
169 | 
170 |     @Override
171 |     public String fetchAPIConfig(String gatewayId, Object config) {
172 |         Gateway gateway = findGateway(gatewayId);
173 |         return getOperator(gateway).fetchAPIConfig(gateway, config);
174 |     }
175 | 
176 |     @Override
177 |     public String fetchMcpConfig(String gatewayId, Object conf) {
178 |         Gateway gateway = findGateway(gatewayId);
179 |         return getOperator(gateway).fetchMcpConfig(gateway, conf);
180 |     }
181 | 
182 |     @Override
183 |     public String createConsumer(Consumer consumer, ConsumerCredential credential, GatewayConfig config) {
184 |         return gatewayOperators.get(config.getGatewayType()).createConsumer(consumer, credential, config);
185 |     }
186 | 
187 |     @Override
188 |     public void updateConsumer(String gwConsumerId, ConsumerCredential credential, GatewayConfig config) {
189 |         gatewayOperators.get(config.getGatewayType()).updateConsumer(gwConsumerId, credential, config);
190 |     }
191 | 
192 |     @Override
193 |     public void deleteConsumer(String gwConsumerId, GatewayConfig config) {
194 |         gatewayOperators.get(config.getGatewayType()).deleteConsumer(gwConsumerId, config);
195 |     }
196 | 
197 |     @Override
198 |     public boolean isConsumerExists(String gwConsumerId, GatewayConfig config) {
199 |         return gatewayOperators.get(config.getGatewayType()).isConsumerExists(gwConsumerId, config);
200 |     }
201 | 
202 |     @Override
203 |     public ConsumerAuthConfig authorizeConsumer(String gatewayId, String gwConsumerId, ProductRefResult productRef) {
204 |         Gateway gateway = findGateway(gatewayId);
205 |         Object refConfig;
206 |         if (gateway.getGatewayType().isHigress()) {
207 |             refConfig = productRef.getHigressRefConfig();
208 |         } else if (gateway.getGatewayType().isAdpAIGateway()) {
209 |             refConfig = productRef.getAdpAIGatewayRefConfig();
210 |         } else {
211 |             refConfig = productRef.getApigRefConfig();
212 |         }
213 |         return getOperator(gateway).authorizeConsumer(gateway, gwConsumerId, refConfig);
214 |     }
215 | 
216 |     @Override
217 |     public void revokeConsumerAuthorization(String gatewayId, String gwConsumerId, ConsumerAuthConfig config) {
218 |         Gateway gateway = findGateway(gatewayId);
219 | 
220 |         getOperator(gateway).revokeConsumerAuthorization(gateway, gwConsumerId, config);
221 |     }
222 | 
223 |     @Override
224 |     public GatewayConfig getGatewayConfig(String gatewayId) {
225 |         Gateway gateway = findGateway(gatewayId);
226 | 
227 |         return GatewayConfig.builder()
228 |                 .gatewayType(gateway.getGatewayType())
229 |                 .apigConfig(gateway.getApigConfig())
230 |                 .higressConfig(gateway.getHigressConfig())
231 |                 .adpAIGatewayConfig(gateway.getAdpAIGatewayConfig())
232 |                 .gateway(gateway)  // 添加Gateway实体引用
233 |                 .build();
234 |     }
235 | 
236 |     @Override
237 |     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
238 |         Map<String, GatewayOperator> operators = applicationContext.getBeansOfType(GatewayOperator.class);
239 | 
240 |         gatewayOperators = operators.values().stream()
241 |                 .collect(Collectors.toMap(
242 |                         operator -> operator.getGatewayType(),
243 |                         operator -> operator,
244 |                         (existing, replacement) -> existing));
245 |     }
246 | 
247 |     private Gateway findGateway(String gatewayId) {
248 |         return gatewayRepository.findByGatewayId(gatewayId)
249 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.GATEWAY, gatewayId));
250 |     }
251 | 
252 |     private GatewayOperator getOperator(Gateway gateway) {
253 |         GatewayOperator gatewayOperator = gatewayOperators.get(gateway.getGatewayType());
254 |         if (gatewayOperator == null) {
255 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR,
256 |                     "No gateway operator found for gateway type: " + gateway.getGatewayType());
257 |         }
258 |         return gatewayOperator;
259 |     }
260 | 
261 |     @Override
262 |     public String getDashboard(String gatewayId,String type) {
263 |         Gateway gateway = findGateway(gatewayId);
264 |         return getOperator(gateway).getDashboard(gateway,type); //type: Portal,MCP,API
265 |     }
266 | 
267 |     private Specification<Gateway> buildGatewaySpec(QueryGatewayParam param) {
268 |         return (root, query, cb) -> {
269 |             List<Predicate> predicates = new ArrayList<>();
270 | 
271 |             if (param != null && param.getGatewayType() != null) {
272 |                 predicates.add(cb.equal(root.get("gatewayType"), param.getGatewayType()));
273 |             }
274 | 
275 |             String adminId = contextHolder.getUser();
276 |             if (StrUtil.isNotBlank(adminId)) {
277 |                 predicates.add(cb.equal(root.get("adminId"), adminId));
278 |             }
279 | 
280 |             return cb.and(predicates.toArray(new Predicate[0]));
281 |         };
282 |     }
283 | }
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/console/ImportGatewayModal.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useState } from 'react'
  2 | import { Button, Table, Modal, Form, Input, message, Select } from 'antd'
  3 | import { PlusOutlined } from '@ant-design/icons'
  4 | import { gatewayApi } from '@/lib/api'
  5 | import { Gateway, ApigConfig } from '@/types'
  6 | import { getGatewayTypeLabel } from '@/lib/constant'
  7 | 
  8 | interface ImportGatewayModalProps {
  9 |   visible: boolean
 10 |   gatewayType: 'APIG_API' | 'APIG_AI' | 'ADP_AI_GATEWAY'
 11 |   onCancel: () => void
 12 |   onSuccess: () => void
 13 | }
 14 | 
 15 | export default function ImportGatewayModal({ visible, gatewayType, onCancel, onSuccess }: ImportGatewayModalProps) {
 16 |   const [importForm] = Form.useForm()
 17 | 
 18 |   const [gatewayLoading, setGatewayLoading] = useState(false)
 19 |   const [gatewayList, setGatewayList] = useState<Gateway[]>([])
 20 |   const [selectedGateway, setSelectedGateway] = useState<Gateway | null>(null)
 21 | 
 22 |   const [apigConfig, setApigConfig] = useState<ApigConfig>({
 23 |     region: '',
 24 |     accessKey: '',
 25 |     secretKey: '',
 26 |   })
 27 | 
 28 |   const [gatewayPagination, setGatewayPagination] = useState({
 29 |     current: 1,
 30 |     pageSize: 10,
 31 |     total: 0,
 32 |   })
 33 | 
 34 |   // 监听表单中的认证方式,确保切换时联动渲染
 35 |   const authType = Form.useWatch('authType', importForm)
 36 | 
 37 |   // 获取网关列表
 38 |   const fetchGateways = async (values: any, page = 1, size = 10) => {
 39 |     setGatewayLoading(true)
 40 |     try {
 41 |       const res = await gatewayApi.getApigGateway({
 42 |         ...values,
 43 |         page,
 44 |         size,
 45 |       })
 46 |       
 47 |       setGatewayList(res.data?.content || [])
 48 |       setGatewayPagination({
 49 |         current: page,
 50 |         pageSize: size,
 51 |         total: res.data?.totalElements || 0,
 52 |       })
 53 |     } catch {
 54 |       // message.error('获取网关列表失败')
 55 |     } finally {
 56 |       setGatewayLoading(false)
 57 |     }
 58 |   }
 59 | 
 60 |   const fetchAdpGateways = async (values: any, page = 1, size = 50) => {
 61 |     setGatewayLoading(true)
 62 |     try {
 63 |       const res = await gatewayApi.getAdpGateways({...values, page, size})
 64 |       setGatewayList(res.data?.content || [])
 65 |       setGatewayPagination({
 66 |         current: page,
 67 |         pageSize: size,
 68 |         total: res.data?.totalElements || 0,
 69 |       })
 70 |     } catch {
 71 |       // message.error('获取网关列表失败')
 72 |     } finally {
 73 |       setGatewayLoading(false)
 74 |     }
 75 |   }
 76 | 
 77 |   // 处理网关选择
 78 |   const handleGatewaySelect = (gateway: Gateway) => {
 79 |     setSelectedGateway(gateway)
 80 |   }
 81 | 
 82 |   // 处理网关分页变化
 83 |   const handleGatewayPaginationChange = (page: number, pageSize: number) => {
 84 |     const values = importForm.getFieldsValue()
 85 |     const data = JSON.parse(sessionStorage.getItem('importFormConfig') || '');
 86 |     if (JSON.stringify(values) === '{}') {
 87 |       fetchGateways({...data, gatewayType: gatewayType}, page, pageSize)
 88 |     } else {
 89 |       fetchGateways({...values, gatewayType: gatewayType}, page, pageSize)
 90 |     }
 91 |   }
 92 | 
 93 |   // 处理导入
 94 |   const handleImport = () => {
 95 |     if (!selectedGateway) {
 96 |       message.warning('请选择一个Gateway')
 97 |       return
 98 |     }
 99 |     const payload: any = {
100 |       ...selectedGateway,
101 |       gatewayType: gatewayType,
102 |     }
103 |     if (gatewayType === 'ADP_AI_GATEWAY') {
104 |       payload.adpAIGatewayConfig = apigConfig
105 |     } else {
106 |       payload.apigConfig = apigConfig
107 |     }
108 |     gatewayApi.importGateway(payload).then(() => {
109 |       message.success('导入成功!')
110 |       handleCancel()
111 |       onSuccess()
112 |     }).catch(() => {
113 |       // message.error(error.response?.data?.message || '导入失败!')
114 |     })
115 |   }
116 | 
117 |   // 重置弹窗状态
118 |   const handleCancel = () => {
119 |     setSelectedGateway(null)
120 |     setGatewayList([])
121 |     setGatewayPagination({ current: 1, pageSize: 10, total: 0 })
122 |     importForm.resetFields()
123 |     onCancel()
124 |   }
125 | 
126 |   return (
127 |     <Modal
128 |       title="导入网关实例"
129 |       open={visible}
130 |       onCancel={handleCancel}
131 |       footer={null}
132 |       width={800}
133 |     >
134 |       <Form form={importForm} layout="vertical" preserve={false}>
135 |         {gatewayList.length === 0 && ['APIG_API', 'APIG_AI'].includes(gatewayType) && (
136 |           <div className="mb-4">
137 |             <h3 className="text-lg font-medium mb-3">认证信息</h3>
138 |             <Form.Item label="Region" name="region" rules={[{ required: true, message: '请输入region' }]}>
139 |               <Input />
140 |             </Form.Item>
141 |             <Form.Item label="Access Key" name="accessKey" rules={[{ required: true, message: '请输入accessKey' }]}>
142 |               <Input />
143 |             </Form.Item>
144 |             <Form.Item label="Secret Key" name="secretKey" rules={[{ required: true, message: '请输入secretKey' }]}>
145 |               <Input.Password />
146 |             </Form.Item>
147 |             <Button 
148 |               type="primary" 
149 |               onClick={() => {
150 |                 importForm.validateFields().then((values) => {
151 |                   setApigConfig(values)
152 |                   sessionStorage.setItem('importFormConfig', JSON.stringify(values))
153 |                   fetchGateways({...values, gatewayType: gatewayType})
154 |                 })
155 |               }}
156 |               loading={gatewayLoading}
157 |             >
158 |               获取网关列表
159 |             </Button>
160 |           </div>
161 |         )}
162 | 
163 |         {['ADP_AI_GATEWAY'].includes(gatewayType) && gatewayList.length === 0 && (
164 |           <div className="mb-4">
165 |             <h3 className="text-lg font-medium mb-3">认证信息</h3>
166 |             <Form.Item label="服务地址" name="baseUrl" rules={[{ required: true, message: '请输入服务地址' }, { pattern: /^https?:\/\//i, message: '必须以 http:// 或 https:// 开头' }]}> 
167 |               <Input placeholder="如:http://apigateway.example.com 或者 http://10.236.6.144" />
168 |             </Form.Item>
169 |             <Form.Item 
170 |               label="端口" 
171 |               name="port" 
172 |               initialValue={80} 
173 |               rules={[
174 |                 { required: true, message: '请输入端口号' }, 
175 |                 { 
176 |                   validator: (_, value) => {
177 |                     if (value === undefined || value === null || value === '') return Promise.resolve()
178 |                     const n = Number(value)
179 |                     return n >= 1 && n <= 65535 ? Promise.resolve() : Promise.reject(new Error('端口范围需在 1-65535'))
180 |                   }
181 |                 }
182 |               ]}
183 |             > 
184 |               <Input type="text" placeholder="如:8080" />
185 |             </Form.Item>
186 |             <Form.Item
187 |               label="认证方式"
188 |               name="authType"
189 |               initialValue="Seed"
190 |               rules={[{ required: true, message: '请选择认证方式' }]}
191 |             >
192 |               <Select>
193 |                 <Select.Option value="Seed">Seed</Select.Option>
194 |                 <Select.Option value="Header">固定Header</Select.Option>
195 |               </Select>
196 |             </Form.Item>
197 |             {authType === 'Seed' && (
198 |               <Form.Item label="Seed" name="authSeed" rules={[{ required: true, message: '请输入Seed' }]}>
199 |                 <Input placeholder="通过configmap获取" />
200 |               </Form.Item>
201 |             )}
202 |             {authType === 'Header' && (
203 |               <Form.Item label="Headers">
204 |                 <Form.List name="authHeaders" initialValue={[{ key: '', value: '' }]}>
205 |                   {(fields, { add, remove }) => (
206 |                     <>
207 |                       {fields.map(({ key, name, ...restField }) => (
208 |                         <div key={key} style={{ display: 'flex', marginBottom: 8, alignItems: 'center' }}>
209 |                           <Form.Item
210 |                             {...restField}
211 |                             name={[name, 'key']}
212 |                             rules={[{ required: true, message: '请输入Header名称' }]}
213 |                             style={{ flex: 1, marginRight: 8, marginBottom: 0 }}
214 |                           >
215 |                             <Input placeholder="Header名称,如:X-Auth-Token" />
216 |                           </Form.Item>
217 |                           <Form.Item
218 |                             {...restField}
219 |                             name={[name, 'value']}
220 |                             rules={[{ required: true, message: '请输入Header值' }]}
221 |                             style={{ flex: 1, marginRight: 8, marginBottom: 0 }}
222 |                           >
223 |                             <Input placeholder="Header值" />
224 |                           </Form.Item>
225 |                           {fields.length > 1 && (
226 |                             <Button 
227 |                               type="text" 
228 |                               danger 
229 |                               onClick={() => remove(name)}
230 |                               style={{ marginBottom: 0 }}
231 |                             >
232 |                               删除
233 |                             </Button>
234 |                           )}
235 |                         </div>
236 |                       ))}
237 |                       <Form.Item style={{ marginBottom: 0 }}>
238 |                         <Button 
239 |                           type="dashed" 
240 |                           onClick={() => add({ key: '', value: '' })} 
241 |                           block 
242 |                           icon={<PlusOutlined />}
243 |                         >
244 |                           添加Header
245 |                         </Button>
246 |                       </Form.Item>
247 |                     </>
248 |                   )}
249 |                 </Form.List>
250 |               </Form.Item>
251 |             )}
252 |             <Button 
253 |               type="primary" 
254 |               onClick={() => {
255 |                 importForm.validateFields().then((values) => {
256 |                   // 处理认证参数
257 |                   const processedValues = { ...values };
258 |                   
259 |                   // 根据认证方式设置相应的参数
260 |                   if (values.authType === 'Seed') {
261 |                     processedValues.authSeed = values.authSeed;
262 |                     delete processedValues.authHeaders;
263 |                   } else if (values.authType === 'Header') {
264 |                     processedValues.authHeaders = values.authHeaders;
265 |                     delete processedValues.authSeed;
266 |                   }
267 |                   
268 |                   setApigConfig(processedValues)
269 |                   sessionStorage.setItem('importFormConfig', JSON.stringify(processedValues))
270 |                   fetchAdpGateways({...processedValues, gatewayType: gatewayType})
271 |                 })
272 |               }}
273 |               loading={gatewayLoading}
274 |             >
275 |               获取网关列表
276 |             </Button>
277 |           </div>
278 |         )}
279 | 
280 |         {gatewayList.length > 0 && (
281 |           <div className="mb-4">
282 |             <h3 className="text-lg font-medium mb-3">选择网关实例</h3>
283 |             <Table
284 |               rowKey="gatewayId"
285 |               columns={[
286 |                 { title: 'ID', dataIndex: 'gatewayId' },
287 |                 { 
288 |                   title: '类型', 
289 |                   dataIndex: 'gatewayType',
290 |                   render: (gatewayType: string) => getGatewayTypeLabel(gatewayType as any)
291 |                 },
292 |                 { title: '名称', dataIndex: 'gatewayName' },
293 |               ]}
294 |               dataSource={gatewayList}
295 |               rowSelection={{
296 |                 type: 'radio',
297 |                 selectedRowKeys: selectedGateway ? [selectedGateway.gatewayId] : [],
298 |                 onChange: (_selectedRowKeys, selectedRows) => {
299 |                   handleGatewaySelect(selectedRows[0])
300 |                 },
301 |               }}
302 |               pagination={{
303 |                 current: gatewayPagination.current,
304 |                 pageSize: gatewayPagination.pageSize,
305 |                 total: gatewayPagination.total,
306 |                 onChange: handleGatewayPaginationChange,
307 |                 showSizeChanger: true,
308 |                 showQuickJumper: true,
309 |                 showTotal: (total) => `共 ${total} 条`,
310 |               }}
311 |               size="small"
312 |             />
313 |           </div>
314 |         )}
315 | 
316 |         {selectedGateway && (
317 |           <div className="text-right">
318 |             <Button type="primary" onClick={handleImport}>
319 |               完成导入
320 |             </Button>
321 |           </div>
322 |         )}
323 |       </Form>
324 |     </Modal>
325 |   )
326 | } 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/PortalServiceImpl.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.service.impl;
 21 | 
 22 | import cn.hutool.core.collection.CollUtil;
 23 | import cn.hutool.core.util.BooleanUtil;
 24 | import cn.hutool.core.util.StrUtil;
 25 | import com.alibaba.apiopenplatform.core.constant.Resources;
 26 | import com.alibaba.apiopenplatform.core.event.PortalDeletingEvent;
 27 | import com.alibaba.apiopenplatform.core.exception.BusinessException;
 28 | import com.alibaba.apiopenplatform.core.exception.ErrorCode;
 29 | import com.alibaba.apiopenplatform.core.security.ContextHolder;
 30 | import com.alibaba.apiopenplatform.core.utils.IdGenerator;
 31 | import com.alibaba.apiopenplatform.dto.params.consumer.QuerySubscriptionParam;
 32 | import com.alibaba.apiopenplatform.dto.params.portal.*;
 33 | import com.alibaba.apiopenplatform.dto.result.PageResult;
 34 | import com.alibaba.apiopenplatform.dto.result.PortalResult;
 35 | import com.alibaba.apiopenplatform.dto.result.SubscriptionResult;
 36 | import com.alibaba.apiopenplatform.entity.Portal;
 37 | import com.alibaba.apiopenplatform.entity.PortalDomain;
 38 | import com.alibaba.apiopenplatform.entity.ProductSubscription;
 39 | import com.alibaba.apiopenplatform.repository.PortalDomainRepository;
 40 | import com.alibaba.apiopenplatform.repository.PortalRepository;
 41 | import com.alibaba.apiopenplatform.repository.SubscriptionRepository;
 42 | import com.alibaba.apiopenplatform.repository.ProductPublicationRepository;
 43 | import com.alibaba.apiopenplatform.repository.ProductRefRepository;
 44 | import com.alibaba.apiopenplatform.service.GatewayService;
 45 | import com.alibaba.apiopenplatform.entity.ProductPublication;
 46 | import com.alibaba.apiopenplatform.entity.ProductRef;
 47 | import org.springframework.data.domain.PageRequest;
 48 | import com.alibaba.apiopenplatform.service.IdpService;
 49 | import com.alibaba.apiopenplatform.service.PortalService;
 50 | import com.alibaba.apiopenplatform.support.enums.DomainType;
 51 | import com.alibaba.apiopenplatform.support.portal.OidcConfig;
 52 | import com.alibaba.apiopenplatform.support.portal.PortalSettingConfig;
 53 | import com.alibaba.apiopenplatform.support.portal.PortalUiConfig;
 54 | import lombok.RequiredArgsConstructor;
 55 | import lombok.extern.slf4j.Slf4j;
 56 | import org.springframework.context.ApplicationEventPublisher;
 57 | import org.springframework.data.domain.Page;
 58 | import org.springframework.data.domain.Pageable;
 59 | import org.springframework.data.jpa.domain.Specification;
 60 | import org.springframework.stereotype.Service;
 61 | 
 62 | import javax.persistence.criteria.Predicate;
 63 | import javax.transaction.Transactional;
 64 | import java.util.ArrayList;
 65 | import java.util.List;
 66 | import java.util.Map;
 67 | import java.util.Optional;
 68 | import java.util.stream.Collectors;
 69 | 
 70 | @Service
 71 | @Slf4j
 72 | @RequiredArgsConstructor
 73 | @Transactional
 74 | public class PortalServiceImpl implements PortalService {
 75 | 
 76 |     private final PortalRepository portalRepository;
 77 | 
 78 |     private final PortalDomainRepository portalDomainRepository;
 79 | 
 80 |     private final ApplicationEventPublisher eventPublisher;
 81 | 
 82 |     private final SubscriptionRepository subscriptionRepository;
 83 | 
 84 |     private final ContextHolder contextHolder;
 85 | 
 86 |     private final IdpService idpService;
 87 | 
 88 |     private final String domainFormat = "%s.api.portal.local";
 89 | 
 90 |     private final ProductPublicationRepository publicationRepository;
 91 | 
 92 |     private final ProductRefRepository productRefRepository;
 93 | 
 94 |     private final GatewayService gatewayService;
 95 | 
 96 |     public PortalResult createPortal(CreatePortalParam param) {
 97 |         portalRepository.findByName(param.getName())
 98 |                 .ifPresent(portal -> {
 99 |                     throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.PORTAL, portal.getName()));
100 |                 });
101 | 
102 |         String portalId = IdGenerator.genPortalId();
103 |         Portal portal = param.convertTo();
104 |         portal.setPortalId(portalId);
105 |         portal.setAdminId(contextHolder.getUser());
106 | 
107 |         // Setting & Ui
108 |         portal.setPortalSettingConfig(new PortalSettingConfig());
109 |         portal.setPortalUiConfig(new PortalUiConfig());
110 | 
111 |         // Domain
112 |         PortalDomain portalDomain = new PortalDomain();
113 |         portalDomain.setDomain(String.format(domainFormat, portalId));
114 |         portalDomain.setPortalId(portalId);
115 | 
116 |         portalDomainRepository.save(portalDomain);
117 |         portalRepository.save(portal);
118 | 
119 |         return getPortal(portalId);
120 |     }
121 | 
122 |     @Override
123 |     public PortalResult getPortal(String portalId) {
124 |         Portal portal = findPortal(portalId);
125 |         List<PortalDomain> domains = portalDomainRepository.findAllByPortalId(portalId);
126 |         portal.setPortalDomains(domains);
127 | 
128 |         return new PortalResult().convertFrom(portal);
129 |     }
130 | 
131 |     @Override
132 |     public void existsPortal(String portalId) {
133 |         portalRepository.findByPortalId(portalId)
134 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PORTAL, portalId));
135 |     }
136 | 
137 |     @Override
138 |     public PageResult<PortalResult> listPortals(Pageable pageable) {
139 |         Page<Portal> portals = portalRepository.findAll(pageable);
140 | 
141 |         // 填充Domain
142 |         if (portals.hasContent()) {
143 |             List<String> portalIds = portals.getContent().stream()
144 |                     .map(Portal::getPortalId)
145 |                     .collect(Collectors.toList());
146 | 
147 |             List<PortalDomain> allDomains = portalDomainRepository.findAllByPortalIdIn(portalIds);
148 |             Map<String, List<PortalDomain>> portalDomains = allDomains.stream()
149 |                     .collect(Collectors.groupingBy(PortalDomain::getPortalId));
150 | 
151 |             portals.getContent().forEach(portal -> {
152 |                 List<PortalDomain> domains = portalDomains.getOrDefault(portal.getPortalId(), new ArrayList<>());
153 |                 portal.setPortalDomains(domains);
154 |             });
155 |         }
156 | 
157 |         return new PageResult<PortalResult>().convertFrom(portals, portal -> new PortalResult().convertFrom(portal));
158 |     }
159 | 
160 |     @Override
161 |     public PortalResult updatePortal(String portalId, UpdatePortalParam param) {
162 |         Portal portal = findPortal(portalId);
163 | 
164 |         Optional.ofNullable(param.getName())
165 |                 .filter(name -> !name.equals(portal.getName()))
166 |                 .flatMap(portalRepository::findByName)
167 |                 .ifPresent(p -> {
168 |                     throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.PORTAL, portal.getName()));
169 |                 });
170 | 
171 |         param.update(portal);
172 |         // 验证OIDC配置
173 |         PortalSettingConfig setting = portal.getPortalSettingConfig();
174 |         if (CollUtil.isNotEmpty(setting.getOidcConfigs())) {
175 |             idpService.validateOidcConfigs(setting.getOidcConfigs());
176 |         }
177 | 
178 |         if (CollUtil.isNotEmpty(setting.getOauth2Configs())) {
179 |             idpService.validateOAuth2Configs(setting.getOauth2Configs());
180 |         }
181 | 
182 |         // 至少保留一种认证方式
183 |         if (BooleanUtil.isFalse(setting.getBuiltinAuthEnabled())) {
184 |             boolean enabledOidc = Optional.ofNullable(setting.getOidcConfigs())
185 |                     .filter(CollUtil::isNotEmpty)
186 |                     .map(configs -> configs.stream().anyMatch(OidcConfig::isEnabled))
187 |                     .orElse(false);
188 | 
189 |             if (!enabledOidc) {
190 |                 throw new BusinessException(ErrorCode.INVALID_REQUEST, "至少配置一种认证方式");
191 |             }
192 |         }
193 |         portalRepository.saveAndFlush(portal);
194 | 
195 |         return getPortal(portal.getPortalId());
196 |     }
197 | 
198 |     @Override
199 |     public void deletePortal(String portalId) {
200 |         Portal portal = findPortal(portalId);
201 | 
202 |         // 清理Domain
203 |         portalDomainRepository.deleteAllByPortalId(portalId);
204 | 
205 |         // 异步清理门户资源
206 |         eventPublisher.publishEvent(new PortalDeletingEvent(portalId));
207 |         portalRepository.delete(portal);
208 |     }
209 | 
210 |     @Override
211 |     public String resolvePortal(String domain) {
212 |         return portalDomainRepository.findByDomain(domain)
213 |                 .map(PortalDomain::getPortalId)
214 |                 .orElse(null);
215 |     }
216 | 
217 |     @Override
218 |     public PortalResult bindDomain(String portalId, BindDomainParam param) {
219 |         existsPortal(portalId);
220 |         portalDomainRepository.findByPortalIdAndDomain(portalId, param.getDomain())
221 |                 .ifPresent(portalDomain -> {
222 |                     throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.PORTAL_DOMAIN, portalDomain.getDomain()));
223 |                 });
224 | 
225 |         PortalDomain portalDomain = param.convertTo();
226 |         portalDomain.setPortalId(portalId);
227 | 
228 |         portalDomainRepository.save(portalDomain);
229 |         return getPortal(portalId);
230 |     }
231 | 
232 |     @Override
233 |     public PortalResult unbindDomain(String portalId, String domain) {
234 |         portalDomainRepository.findByPortalIdAndDomain(portalId, domain)
235 |                 .ifPresent(portalDomain -> {
236 |                     // 默认域名不允许解绑
237 |                     if (portalDomain.getType() == DomainType.DEFAULT) {
238 |                         throw new BusinessException(ErrorCode.INVALID_REQUEST, "默认域名不允许解绑");
239 |                     }
240 |                     portalDomainRepository.delete(portalDomain);
241 |                 });
242 |         return getPortal(portalId);
243 |     }
244 | 
245 |     @Override
246 |     public PageResult<SubscriptionResult> listSubscriptions(String portalId, QuerySubscriptionParam param, Pageable pageable) {
247 |         // Ensure portal exists
248 |         existsPortal(portalId);
249 | 
250 |         Specification<ProductSubscription> spec = (root, query, cb) -> {
251 |             List<Predicate> predicates = new ArrayList<>();
252 |             predicates.add(cb.equal(root.get("portalId"), portalId));
253 |             if (param != null && param.getStatus() != null) {
254 |                 predicates.add(cb.equal(root.get("status"), param.getStatus()));
255 |             }
256 |             return cb.and(predicates.toArray(new Predicate[0]));
257 |         };
258 | 
259 |         Page<ProductSubscription> page = subscriptionRepository.findAll(spec, pageable);
260 |         return new PageResult<SubscriptionResult>().convertFrom(page, s -> new SubscriptionResult().convertFrom(s));
261 |     }
262 | 
263 |     @Override
264 |     public String getDefaultPortal() {
265 |         Portal portal = portalRepository.findFirstByOrderByIdAsc().orElse(null);
266 |         if (portal == null) {
267 |             return null;
268 |         }
269 |         return portal.getPortalId();
270 |     }
271 | 
272 |     @Override
273 |     public String getDashboard(String portalId) {
274 |         existsPortal(portalId);
275 | 
276 |         // 找到该门户下任一已发布产品(取第一页第一条)
277 |         ProductPublication pub = publicationRepository.findByPortalId(portalId, PageRequest.of(0, 1))
278 |                 .stream()
279 |                 .findFirst()
280 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PORTAL, portalId));
281 | 
282 |         // 取产品的网关引用
283 |         ProductRef productRef = productRefRepository.findFirstByProductId(pub.getProductId())
284 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PRODUCT, pub.getProductId()));
285 | 
286 |         if (productRef.getGatewayId() == null) {
287 |             throw new BusinessException(ErrorCode.NOT_FOUND, "网关", "该门户下的产品尚未关联网关服务");
288 |         }
289 | 
290 |         // 复用网关的Dashboard能力
291 |         return gatewayService.getDashboard(productRef.getGatewayId(),"Portal");
292 |     }
293 | 
294 |     private Portal findPortal(String portalId) {
295 |         return portalRepository.findByPortalId(portalId)
296 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PORTAL, portalId));
297 |     }
298 | }
299 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/portal/PortalDevelopers.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import {Card, Table, Badge, Button, Space, message, Modal} from 'antd'
  2 | import {
  3 |     EditOutlined,
  4 |     DeleteOutlined,
  5 |     ExclamationCircleOutlined,
  6 |     EyeOutlined,
  7 |     UnorderedListOutlined,
  8 |     CheckCircleFilled,
  9 |     ClockCircleOutlined
 10 | } from '@ant-design/icons'
 11 | import {useEffect, useState} from 'react'
 12 | import {Portal, Developer, Consumer} from '@/types'
 13 | import {portalApi} from '@/lib/api'
 14 | import {formatDateTime} from '@/lib/utils'
 15 | import {SubscriptionListModal} from '@/components/subscription/SubscriptionListModal'
 16 | 
 17 | interface PortalDevelopersProps {
 18 |     portal: Portal
 19 | }
 20 | 
 21 | export function PortalDevelopers({portal}: PortalDevelopersProps) {
 22 |     const [developers, setDevelopers] = useState<Developer[]>([])
 23 |     const [pagination, setPagination] = useState({
 24 |         current: 1,
 25 |         pageSize: 10,
 26 |         total: 0,
 27 |         showSizeChanger: true,
 28 |         showQuickJumper: true,
 29 |         showTotal: (total: number, range: [number, number]) =>
 30 |             `共 ${total} 条`
 31 |     })
 32 | 
 33 |     // Consumer相关状态
 34 |     const [consumers, setConsumers] = useState<Consumer[]>([])
 35 |     const [consumerModalVisible, setConsumerModalVisible] = useState(false)
 36 |     const [currentDeveloper, setCurrentDeveloper] = useState<Developer | null>(null)
 37 |     const [consumerPagination, setConsumerPagination] = useState({
 38 |         current: 1,
 39 |         pageSize: 10,
 40 |         total: 0,
 41 |         showSizeChanger: true,
 42 |         showQuickJumper: true,
 43 |         showTotal: (total: number, range: [number, number]) =>
 44 |             `共 ${total} 条`
 45 |     })
 46 | 
 47 |     // 订阅列表相关状态
 48 |     const [subscriptionModalVisible, setSubscriptionModalVisible] = useState(false)
 49 |     const [currentConsumer, setCurrentConsumer] = useState<Consumer | null>(null)
 50 | 
 51 |     useEffect(() => {
 52 |         fetchDevelopers()
 53 |     }, [portal.portalId, pagination.current, pagination.pageSize])
 54 | 
 55 |     const fetchDevelopers = () => {
 56 |         portalApi.getDeveloperList(portal.portalId, {
 57 |             page: pagination.current, // 后端从0开始
 58 |             size: pagination.pageSize
 59 |         }).then((res) => {
 60 |             setDevelopers(res.data.content)
 61 |             setPagination(prev => ({
 62 |                 ...prev,
 63 |                 total: res.data.totalElements || 0
 64 |             }))
 65 |         })
 66 |     }
 67 | 
 68 |     const handleUpdateDeveloperStatus = (developerId: string, status: string) => {
 69 |         portalApi.updateDeveloperStatus(portal.portalId, developerId, status).then(() => {
 70 |             if (status === 'PENDING') {
 71 |                 message.success('取消授权成功')
 72 |             } else {
 73 |                 message.success('审批成功')
 74 |             }
 75 |             fetchDevelopers()
 76 |         }).catch(() => {
 77 |             message.error('审批失败')
 78 |         })
 79 |     }
 80 | 
 81 |     const handleTableChange = (paginationInfo: any) => {
 82 |         setPagination(prev => ({
 83 |             ...prev,
 84 |             current: paginationInfo.current,
 85 |             pageSize: paginationInfo.pageSize
 86 |         }))
 87 |     }
 88 | 
 89 |     const handleDeleteDeveloper = (developerId: string, username: string) => {
 90 |         Modal.confirm({
 91 |             title: '确认删除',
 92 |             icon: <ExclamationCircleOutlined/>,
 93 |             content: `确定要删除开发者 "${username}" 吗?此操作不可恢复。`,
 94 |             okText: '确认删除',
 95 |             okType: 'danger',
 96 |             cancelText: '取消',
 97 |             onOk() {
 98 |                 portalApi.deleteDeveloper(developerId).then(() => {
 99 |                     message.success('删除成功')
100 |                     fetchDevelopers()
101 |                 }).catch(() => {
102 |                     message.error('删除失败')
103 |                 })
104 |             },
105 |         })
106 |     }
107 | 
108 |     // Consumer相关函数
109 |     const handleViewConsumers = (developer: Developer) => {
110 |         setCurrentDeveloper(developer)
111 |         setConsumerModalVisible(true)
112 |         setConsumerPagination(prev => ({...prev, current: 1}))
113 |         fetchConsumers(developer.developerId, 1, consumerPagination.pageSize)
114 |     }
115 | 
116 |     const fetchConsumers = (developerId: string, page: number, size: number) => {
117 |         portalApi.getConsumerList(portal.portalId, developerId, {page: page, size}).then((res) => {
118 |             setConsumers(res.data.content || [])
119 |             setConsumerPagination(prev => ({
120 |                 ...prev,
121 |                 total: res.data.totalElements || 0
122 |             }))
123 |         }).then((res: any) => {
124 |             setConsumers(res.data.content || [])
125 |             setConsumerPagination(prev => ({
126 |                 ...prev,
127 |                 total: res.data.totalElements || 0
128 |             }))
129 |         })
130 |     }
131 | 
132 |     const handleConsumerTableChange = (paginationInfo: any) => {
133 |         if (currentDeveloper) {
134 |             setConsumerPagination(prev => ({
135 |                 ...prev,
136 |                 current: paginationInfo.current,
137 |                 pageSize: paginationInfo.pageSize
138 |             }))
139 |             fetchConsumers(currentDeveloper.developerId, paginationInfo.current, paginationInfo.pageSize)
140 |         }
141 |     }
142 | 
143 |     const handleConsumerStatusUpdate = (consumerId: string) => {
144 |         if (currentDeveloper) {
145 |             portalApi.approveConsumer(consumerId).then((res) => {
146 |                 message.success('审批成功')
147 |                 fetchConsumers(currentDeveloper.developerId, consumerPagination.current, consumerPagination.pageSize)
148 |             }).catch((err) => {
149 |                 // message.error('审批失败')
150 |             })
151 |         }
152 |     }
153 | 
154 |     // 查看订阅列表
155 |     const handleViewSubscriptions = (consumer: Consumer) => {
156 |         setCurrentConsumer(consumer)
157 |         setSubscriptionModalVisible(true)
158 |     }
159 | 
160 |     // 关闭订阅列表模态框
161 |     const handleSubscriptionModalCancel = () => {
162 |         setSubscriptionModalVisible(false)
163 |         setCurrentConsumer(null)
164 |     }
165 | 
166 | 
167 |     const columns = [
168 |         {
169 |             title: '开发者名称/ID',
170 |             dataIndex: 'username',
171 |             key: 'username',
172 |             fixed: 'left' as const,
173 |             width: 280,
174 |             render: (username: string, record: Developer) => (
175 |                 <div className="ml-2">
176 |                     <div className="font-medium">{username}</div>
177 |                     <div className="text-sm text-gray-500">{record.developerId}</div>
178 |                 </div>
179 |             ),
180 |         },
181 |         {
182 |             title: '状态',
183 |             dataIndex: 'status',
184 |             key: 'status',
185 |             width: 120,
186 |             render: (status: string) => (
187 |                 <div className="flex items-center">
188 |                     {status === 'APPROVED' ? (
189 |                         <>
190 |                             <CheckCircleFilled className="text-green-500 mr-2" style={{fontSize: '10px'}} />
191 |                             <span className="text-xs text-gray-900">可用</span>
192 |                         </>
193 |                     ) : (
194 |                         <>
195 |                             <ClockCircleOutlined className="text-orange-500 mr-2" style={{fontSize: '10px'}} />
196 |                             <span className="text-xs text-gray-900">待审核</span>
197 |                         </>
198 |                     )}
199 |                 </div>
200 |             )
201 |         },
202 | 
203 |         {
204 |             title: '创建时间',
205 |             dataIndex: 'createAt',
206 |             key: 'createAt',
207 |             width: 160,
208 |             render: (date: string) => formatDateTime(date)
209 |         },
210 | 
211 |         {
212 |             title: '操作',
213 |             key: 'action',
214 |             fixed: 'right' as const,
215 |             width: 250,
216 |             render: (_: any, record: Developer) => (
217 |                 <Space size="middle">
218 |                     <Button onClick={() => handleViewConsumers(record)} type="link" icon={<EyeOutlined/>}>
219 |                         查看Consumer
220 |                     </Button>
221 |                     {
222 |                         !portal.portalSettingConfig.autoApproveDevelopers && (
223 |                             record.status === 'APPROVED' ? (
224 |                                 <Button onClick={() => handleUpdateDeveloperStatus(record.developerId, 'PENDING')}
225 |                                         type="link" icon={<EditOutlined/>}>
226 |                                     取消授权
227 |                                 </Button>
228 |                             ) : (
229 |                                 <Button onClick={() => handleUpdateDeveloperStatus(record.developerId, 'APPROVED')}
230 |                                         type="link" icon={<EditOutlined/>}>
231 |                                     审批通过
232 |                                 </Button>
233 |                             )
234 |                         )
235 |                     }
236 |                     <Button onClick={() => handleDeleteDeveloper(record.developerId, record.username)} type="link"
237 |                             danger icon={<DeleteOutlined/>}>
238 |                         删除
239 |                     </Button>
240 |                 </Space>
241 |             ),
242 |         },
243 |     ]
244 | 
245 |     // Consumer表格列定义
246 |     const consumerColumns = [
247 |         {
248 |             title: 'Consumer名称',
249 |             dataIndex: 'name',
250 |             key: 'name',
251 |             width: 200,
252 |         },
253 |         {
254 |             title: 'Consumer ID',
255 |             dataIndex: 'consumerId',
256 |             key: 'consumerId',
257 |             width: 200,
258 |         },
259 |         {
260 |             title: '描述',
261 |             dataIndex: 'description',
262 |             key: 'description',
263 |             ellipsis: true,
264 |             width: 200,
265 |         },
266 |         // {
267 |         //   title: '状态',
268 |         //   dataIndex: 'status',
269 |         //   key: 'status',
270 |         //   width: 120,
271 |         //   render: (status: string) => (
272 |         //     <Badge status={status === 'APPROVED' ? 'success' : 'default'} text={status === 'APPROVED' ? '可用' : '待审核'} />
273 |         //   )
274 |         // },
275 |         {
276 |             title: '创建时间',
277 |             dataIndex: 'createAt',
278 |             key: 'createAt',
279 |             width: 150,
280 |             render: (date: string) => formatDateTime(date)
281 |         },
282 |         {
283 |             title: '操作',
284 |             key: 'action',
285 |             width: 120,
286 |             render: (_: any, record: Consumer) => (
287 |                 <Button
288 |                     onClick={() => handleViewSubscriptions(record)}
289 |                     type="link"
290 |                     icon={<UnorderedListOutlined/>}
291 |                 >
292 |                     订阅列表
293 |                 </Button>
294 |             ),
295 |         },
296 |     ]
297 | 
298 |     return (
299 |         <div className="p-6 space-y-6">
300 |             <div className="flex justify-between items-center">
301 |                 <div>
302 |                     <h1 className="text-2xl font-bold mb-2">开发者</h1>
303 |                     <p className="text-gray-600">管理Portal的开发者用户</p>
304 |                 </div>
305 |             </div>
306 | 
307 |             <Card>
308 |                 {/* <div className="mb-4">
309 |           <Input
310 |             placeholder="搜索开发者..."
311 |             prefix={<SearchOutlined />}
312 |             value={searchText}
313 |             onChange={(e) => setSearchText(e.target.value)}
314 |             style={{ width: 300 }}
315 |           />
316 |         </div> */}
317 |                 <Table
318 |                     columns={columns}
319 |                     dataSource={developers}
320 |                     rowKey="developerId"
321 |                     pagination={pagination}
322 |                     onChange={handleTableChange}
323 |                     scroll={{
324 |                         y: 'calc(100vh - 400px)',
325 |                         x: 'max-content'
326 |                     }}
327 |                 />
328 |             </Card>
329 | 
330 |             {/* Consumer弹窗 */}
331 |             <Modal
332 |                 title={`查看Consumer - ${currentDeveloper?.username || ''}`}
333 |                 open={consumerModalVisible}
334 |                 onCancel={() => setConsumerModalVisible(false)}
335 |                 footer={null}
336 |                 width={1000}
337 |                 destroyOnClose
338 |             >
339 |                 <Table
340 |                     columns={consumerColumns}
341 |                     dataSource={consumers}
342 |                     rowKey="consumerId"
343 |                     pagination={consumerPagination}
344 |                     onChange={handleConsumerTableChange}
345 |                     scroll={{y: 'calc(100vh - 400px)'}}
346 |                 />
347 |             </Modal>
348 | 
349 |             {/* 订阅列表弹窗 */}
350 |             {currentConsumer && (
351 |                 <SubscriptionListModal
352 |                     visible={subscriptionModalVisible}
353 |                     consumerId={currentConsumer.consumerId}
354 |                     consumerName={currentConsumer.name}
355 |                     onCancel={handleSubscriptionModalCancel}
356 |                 />
357 |             )}
358 | 
359 |         </div>
360 |     )
361 | } 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/api-product/ApiProductFormModal.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useState, useEffect } from "react";
  2 | import {
  3 |   Modal,
  4 |   Form,
  5 |   Input,
  6 |   Select,
  7 |   Image,
  8 |   message,
  9 |   UploadFile,
 10 |   Switch,
 11 |   Radio,
 12 |   Space,
 13 | } from "antd";
 14 | import { CameraOutlined } from "@ant-design/icons";
 15 | import { apiProductApi } from "@/lib/api";
 16 | import type { ApiProduct } from "@/types/api-product";
 17 | 
 18 | interface ApiProductFormModalProps {
 19 |   visible: boolean;
 20 |   onCancel: () => void;
 21 |   onSuccess: () => void;
 22 |   productId?: string;
 23 |   initialData?: Partial<ApiProduct>;
 24 | }
 25 | 
 26 | export default function ApiProductFormModal({
 27 |   visible,
 28 |   onCancel,
 29 |   onSuccess,
 30 |   productId,
 31 |   initialData,
 32 | }: ApiProductFormModalProps) {
 33 |   const [form] = Form.useForm();
 34 |   const [loading, setLoading] = useState(false);
 35 |   const [previewOpen, setPreviewOpen] = useState(false);
 36 |   const [previewImage, setPreviewImage] = useState("");
 37 |   const [fileList, setFileList] = useState<UploadFile[]>([]);
 38 |   const [iconMode, setIconMode] = useState<'BASE64' | 'URL'>('URL');
 39 |   const isEditMode = !!productId;
 40 | 
 41 |   // 初始化时加载已有数据
 42 |   useEffect(() => {
 43 |     if (visible && isEditMode && initialData && initialData.name) {
 44 |       setTimeout(() => {
 45 |         // 1. 先设置所有字段
 46 |         form.setFieldsValue({
 47 |           name: initialData.name,
 48 |           description: initialData.description,
 49 |           type: initialData.type,
 50 |           autoApprove: initialData.autoApprove,
 51 |         });
 52 |       }, 100);
 53 | 
 54 |       // 2. 处理 icon 字段
 55 |       if (initialData.icon) {
 56 |         if (typeof initialData.icon === 'object' && initialData.icon.type && initialData.icon.value) {
 57 |           // 新格式:{ type: 'BASE64' | 'URL', value: string }
 58 |           const iconType = initialData.icon.type as 'BASE64' | 'URL';
 59 |           const iconValue = initialData.icon.value;
 60 |           
 61 |           setIconMode(iconType);
 62 |           
 63 |           if (iconType === 'BASE64') {
 64 |             setFileList([
 65 |               {
 66 |                 uid: "-1",
 67 |                 name: "头像.png",
 68 |                 status: "done",
 69 |                 url: iconValue,
 70 |               },
 71 |             ]);
 72 |             form.setFieldsValue({ icon: iconValue });
 73 |           } else {
 74 |             form.setFieldsValue({ iconUrl: iconValue });
 75 |           }
 76 |         } else {
 77 |           // 兼容旧格式(字符串格式)
 78 |           const iconStr = initialData.icon as unknown as string;
 79 |           if (iconStr && typeof iconStr === 'string' && iconStr.includes("value=")) {
 80 |             const startIndex = iconStr.indexOf("value=") + 6;
 81 |             const endIndex = iconStr.length - 1;
 82 |             const base64Data = iconStr.substring(startIndex, endIndex).trim();
 83 |             
 84 |             setIconMode('BASE64');
 85 |             setFileList([
 86 |               {
 87 |                 uid: "-1",
 88 |                 name: "头像.png",
 89 |                 status: "done",
 90 |                 url: base64Data,
 91 |               },
 92 |             ]);
 93 |             form.setFieldsValue({ icon: base64Data });
 94 |           }
 95 |         }
 96 |       }
 97 |     } else if (visible && !isEditMode) {
 98 |       // 新建模式下清空表单
 99 |       form.resetFields();
100 |       setFileList([]);
101 |       setIconMode('URL');
102 |     }
103 |   }, [visible, isEditMode, initialData, form]);
104 | 
105 |   // 将文件转为 Base64
106 |   const getBase64 = (file: File): Promise<string> =>
107 |     new Promise((resolve, reject) => {
108 |       const reader = new FileReader();
109 |       reader.readAsDataURL(file);
110 |       reader.onload = () => resolve(reader.result as string);
111 |       reader.onerror = (error) => reject(error);
112 |     });
113 | 
114 | 
115 |   const uploadButton = (
116 |     <div style={{ 
117 |       display: 'flex', 
118 |       flexDirection: 'column', 
119 |       alignItems: 'center', 
120 |       justifyContent: 'center',
121 |       color: '#999'
122 |     }}>
123 |       <CameraOutlined style={{ fontSize: '16px', marginBottom: '6px' }} />
124 |       <span style={{ fontSize: '12px', color: '#999' }}>上传图片</span>
125 |     </div>
126 |   );
127 | 
128 |   // 处理Icon模式切换
129 |   const handleIconModeChange = (mode: 'BASE64' | 'URL') => {
130 |     setIconMode(mode);
131 |     // 清空相关字段
132 |     if (mode === 'URL') {
133 |       form.setFieldsValue({ icon: undefined });
134 |       setFileList([]);
135 |     } else {
136 |       form.setFieldsValue({ iconUrl: undefined });
137 |     }
138 |   };
139 | 
140 |   const resetForm = () => {
141 |     form.resetFields();
142 |     setFileList([]);
143 |     setPreviewImage("");
144 |     setPreviewOpen(false);
145 |     setIconMode('URL');
146 |   };
147 | 
148 |   const handleCancel = () => {
149 |     resetForm();
150 |     onCancel();
151 |   };
152 | 
153 |   const handleSubmit = async () => {
154 |     try {
155 |       const values = await form.validateFields();
156 |       setLoading(true);
157 | 
158 |       const { icon, iconUrl, ...otherValues } = values;
159 | 
160 |       if (isEditMode) {
161 |         let params = { ...otherValues };
162 |         
163 |         // 处理icon字段
164 |         if (iconMode === 'BASE64' && icon) {
165 |           params.icon = {
166 |             type: "BASE64",
167 |             value: icon,
168 |           };
169 |         } else if (iconMode === 'URL' && iconUrl) {
170 |           params.icon = {
171 |             type: "URL",
172 |             value: iconUrl,
173 |           };
174 |         } else if (!icon && !iconUrl) {
175 |           // 如果两种模式都没有提供icon,保持原有icon不变
176 |           delete params.icon;
177 |         }
178 |         
179 |         await apiProductApi.updateApiProduct(productId!, params);
180 |         message.success("API Product 更新成功");
181 |       } else {
182 |         let params = { ...otherValues };
183 |         
184 |         // 处理icon字段
185 |         if (iconMode === 'BASE64' && icon) {
186 |           params.icon = {
187 |             type: "BASE64",
188 |             value: icon,
189 |           };
190 |         } else if (iconMode === 'URL' && iconUrl) {
191 |           params.icon = {
192 |             type: "URL",
193 |             value: iconUrl,
194 |           };
195 |         }
196 |         
197 |         await apiProductApi.createApiProduct(params);
198 |         message.success("API Product 创建成功");
199 |       }
200 | 
201 |       resetForm();
202 |       onSuccess();
203 |     } catch (error: any) {
204 |       if (error?.errorFields) return;
205 |       message.error("操作失败");
206 |     } finally {
207 |       setLoading(false);
208 |     }
209 |   };
210 | 
211 |   return (
212 |     <Modal
213 |       title={isEditMode ? "编辑 API Product" : "创建 API Product"}
214 |       open={visible}
215 |       onOk={handleSubmit}
216 |       onCancel={handleCancel}
217 |       confirmLoading={loading}
218 |       width={600}
219 |     >
220 |       <Form form={form} layout="vertical" preserve={false}>
221 |         <Form.Item
222 |           label="名称"
223 |           name="name"
224 |           rules={[{ required: true, message: "请输入API Product名称" }]}
225 |         >
226 |           <Input placeholder="请输入API Product名称" />
227 |         </Form.Item>
228 | 
229 |         <Form.Item
230 |           label="描述"
231 |           name="description"
232 |           rules={[{ required: true, message: "请输入描述" }]}
233 |         >
234 |           <Input.TextArea placeholder="请输入描述" rows={3} />
235 |         </Form.Item>
236 | 
237 |         <Form.Item
238 |           label="类型"
239 |           name="type"
240 |           rules={[{ required: true, message: "请选择类型" }]}
241 |         >
242 |           <Select placeholder="请选择类型">
243 |             <Select.Option value="REST_API">REST API</Select.Option>
244 |             <Select.Option value="MCP_SERVER">MCP Server</Select.Option>
245 |           </Select>
246 |         </Form.Item>
247 | 
248 |         <Form.Item
249 |           label="自动审批订阅"
250 |           name="autoApprove"
251 |           tooltip={{
252 |             title: (
253 |               <div style={{ 
254 |                 color: '#000000', 
255 |                 backgroundColor: '#ffffff',
256 |                 fontSize: '13px',
257 |                 lineHeight: '1.4',
258 |                 padding: '4px 0'
259 |               }}>
260 |                 启用后,该产品的订阅申请将自动审批通过,否则使用Portal的消费者订阅审批设置。
261 |               </div>
262 |             ),
263 |             placement: "topLeft",
264 |             overlayInnerStyle: {
265 |               backgroundColor: '#ffffff',
266 |               color: '#000000',
267 |               border: '1px solid #d9d9d9',
268 |               borderRadius: '6px',
269 |               boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
270 |             },
271 |             overlayStyle: {
272 |               maxWidth: '300px'
273 |             }
274 |           }}
275 |           valuePropName="checked"
276 |         >
277 |           <Switch />
278 |         </Form.Item>
279 | 
280 |         <Form.Item label="Icon设置" style={{ marginBottom: '16px' }}>
281 |           <Space direction="vertical" style={{ width: '100%' }}>
282 |             <Radio.Group 
283 |               value={iconMode} 
284 |               onChange={(e) => handleIconModeChange(e.target.value)}
285 |             >
286 |               <Radio value="URL">图片链接</Radio>
287 |               <Radio value="BASE64">本地上传</Radio>
288 |             </Radio.Group>
289 |             
290 |             {iconMode === 'URL' ? (
291 |               <Form.Item 
292 |                 name="iconUrl" 
293 |                 style={{ marginBottom: 0 }}
294 |                 rules={[
295 |                   { 
296 |                     type: 'url', 
297 |                     message: '请输入有效的图片链接' 
298 |                   }
299 |                 ]}
300 |               >
301 |                 <Input placeholder="请输入图片链接地址" />
302 |               </Form.Item>
303 |             ) : (
304 |               <Form.Item name="icon" style={{ marginBottom: 0 }}>
305 |                 <div 
306 |                   style={{ 
307 |                     width: '80px', 
308 |                     height: '80px',
309 |                     border: '1px dashed #d9d9d9',
310 |                     borderRadius: '8px',
311 |                     display: 'flex',
312 |                     alignItems: 'center',
313 |                     justifyContent: 'center',
314 |                     cursor: 'pointer',
315 |                     transition: 'border-color 0.3s',
316 |                     position: 'relative'
317 |                   }}
318 |                   onClick={() => {
319 |                     // 触发文件选择
320 |                     const input = document.createElement('input');
321 |                     input.type = 'file';
322 |                     input.accept = 'image/*';
323 |                     input.onchange = (e) => {
324 |                       const file = (e.target as HTMLInputElement).files?.[0];
325 |                       if (file) {
326 |                         // 验证文件大小,限制为16KB
327 |                         const maxSize = 16 * 1024; // 16KB
328 |                         if (file.size > maxSize) {
329 |                           message.error(`图片大小不能超过 16KB,当前图片大小为 ${Math.round(file.size / 1024)}KB`);
330 |                           return;
331 |                         }
332 |                         
333 |                         const newFileList: UploadFile[] = [{
334 |                           uid: Date.now().toString(),
335 |                           name: file.name,
336 |                           status: 'done' as const,
337 |                           url: URL.createObjectURL(file)
338 |                         }];
339 |                         setFileList(newFileList);
340 |                         getBase64(file).then((base64) => {
341 |                           form.setFieldsValue({ icon: base64 });
342 |                         });
343 |                       }
344 |                     };
345 |                     input.click();
346 |                   }}
347 |                   onMouseEnter={(e) => {
348 |                     e.currentTarget.style.borderColor = '#1890ff';
349 |                   }}
350 |                   onMouseLeave={(e) => {
351 |                     e.currentTarget.style.borderColor = '#d9d9d9';
352 |                   }}
353 |                 >
354 |                   {fileList.length >= 1 ? (
355 |                     <img 
356 |                       src={fileList[0].url} 
357 |                       alt="uploaded" 
358 |                       style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: '6px' }}
359 |                       onClick={(e) => {
360 |                         e.stopPropagation();
361 |                         // 预览图片
362 |                         setPreviewImage(fileList[0].url || '');
363 |                         setPreviewOpen(true);
364 |                       }}
365 |                     />
366 |                   ) : (
367 |                     uploadButton
368 |                   )}
369 |                   {fileList.length >= 1 && (
370 |                     <div 
371 |                       style={{ 
372 |                         position: 'absolute', 
373 |                         top: '4px', 
374 |                         right: '4px', 
375 |                         background: 'rgba(0, 0, 0, 0.5)', 
376 |                         borderRadius: '50%', 
377 |                         width: '16px', 
378 |                         height: '16px', 
379 |                         display: 'flex', 
380 |                         alignItems: 'center', 
381 |                         justifyContent: 'center',
382 |                         cursor: 'pointer',
383 |                         color: 'white',
384 |                         fontSize: '10px'
385 |                       }}
386 |                       onClick={(e) => {
387 |                         e.stopPropagation();
388 |                         setFileList([]);
389 |                         form.setFieldsValue({ icon: null });
390 |                       }}
391 |                     >
392 |                       ×
393 |                     </div>
394 |                   )}
395 |                 </div>
396 |               </Form.Item>
397 |             )}
398 |           </Space>
399 |         </Form.Item>
400 | 
401 |         {/* 图片预览弹窗 */}
402 |         {previewImage && (
403 |           <Image
404 |             wrapperStyle={{ display: "none" }}
405 |             preview={{
406 |               visible: previewOpen,
407 |               onVisibleChange: (visible) => setPreviewOpen(visible),
408 |               afterOpenChange: (visible) => {
409 |                 if (!visible) setPreviewImage("");
410 |               },
411 |             }}
412 |             src={previewImage}
413 |           />
414 |         )}
415 |       </Form>
416 |     </Modal>
417 |   );
418 | }
419 | 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/OidcServiceImpl.java:
--------------------------------------------------------------------------------

```java
  1 | package com.alibaba.apiopenplatform.service.impl;
  2 | 
  3 | import cn.hutool.core.codec.Base64;
  4 | import cn.hutool.core.convert.Convert;
  5 | import cn.hutool.core.map.MapUtil;
  6 | import cn.hutool.core.util.IdUtil;
  7 | import cn.hutool.core.util.StrUtil;
  8 | import cn.hutool.json.JSONUtil;
  9 | import cn.hutool.jwt.JWT;
 10 | import cn.hutool.jwt.JWTUtil;
 11 | import com.alibaba.apiopenplatform.core.constant.CommonConstants;
 12 | import com.alibaba.apiopenplatform.core.constant.IdpConstants;
 13 | import com.alibaba.apiopenplatform.core.constant.Resources;
 14 | import com.alibaba.apiopenplatform.core.exception.BusinessException;
 15 | import com.alibaba.apiopenplatform.core.exception.ErrorCode;
 16 | import com.alibaba.apiopenplatform.core.security.ContextHolder;
 17 | import com.alibaba.apiopenplatform.core.utils.TokenUtil;
 18 | import com.alibaba.apiopenplatform.dto.params.developer.CreateExternalDeveloperParam;
 19 | import com.alibaba.apiopenplatform.dto.result.*;
 20 | import com.alibaba.apiopenplatform.service.OidcService;
 21 | import com.alibaba.apiopenplatform.service.DeveloperService;
 22 | import com.alibaba.apiopenplatform.service.PortalService;
 23 | import com.alibaba.apiopenplatform.support.enums.DeveloperAuthType;
 24 | import com.alibaba.apiopenplatform.support.enums.GrantType;
 25 | import com.alibaba.apiopenplatform.support.portal.AuthCodeConfig;
 26 | import com.alibaba.apiopenplatform.support.portal.IdentityMapping;
 27 | import com.alibaba.apiopenplatform.support.portal.OidcConfig;
 28 | import lombok.RequiredArgsConstructor;
 29 | import lombok.extern.slf4j.Slf4j;
 30 | import org.springframework.http.HttpEntity;
 31 | import org.springframework.http.HttpHeaders;
 32 | import org.springframework.http.HttpMethod;
 33 | import org.springframework.http.ResponseEntity;
 34 | import org.springframework.stereotype.Service;
 35 | import org.springframework.util.LinkedMultiValueMap;
 36 | import org.springframework.util.MultiValueMap;
 37 | import org.springframework.web.client.RestTemplate;
 38 | import org.springframework.web.util.UriComponentsBuilder;
 39 | 
 40 | import javax.servlet.http.HttpServletRequest;
 41 | import javax.servlet.http.HttpServletResponse;
 42 | import java.util.*;
 43 | import java.util.stream.Collectors;
 44 | 
 45 | @Slf4j
 46 | @Service
 47 | @RequiredArgsConstructor
 48 | public class OidcServiceImpl implements OidcService {
 49 | 
 50 |     private final PortalService portalService;
 51 | 
 52 |     private final DeveloperService developerService;
 53 | 
 54 |     private final RestTemplate restTemplate;
 55 | 
 56 |     private final ContextHolder contextHolder;
 57 | 
 58 |     @Override
 59 |     public String buildAuthorizationUrl(String provider, String apiPrefix, HttpServletRequest request) {
 60 |         OidcConfig oidcConfig = findOidcConfig(provider);
 61 |         AuthCodeConfig authCodeConfig = oidcConfig.getAuthCodeConfig();
 62 | 
 63 |         // state保存上下文信息
 64 |         String state = buildState(provider, apiPrefix);
 65 |         String redirectUri = buildRedirectUri(request);
 66 | 
 67 |         // 重定向URL
 68 |         String authUrl = UriComponentsBuilder
 69 |                 .fromUriString(authCodeConfig.getAuthorizationEndpoint())
 70 |                 // 授权码模式
 71 |                 .queryParam(IdpConstants.RESPONSE_TYPE, IdpConstants.CODE)
 72 |                 .queryParam(IdpConstants.CLIENT_ID, authCodeConfig.getClientId())
 73 |                 .queryParam(IdpConstants.REDIRECT_URI, redirectUri)
 74 |                 .queryParam(IdpConstants.SCOPE, authCodeConfig.getScopes())
 75 |                 .queryParam(IdpConstants.STATE, state)
 76 |                 .build()
 77 |                 .toUriString();
 78 | 
 79 |         log.info("Generated OIDC authorization URL: {}", authUrl);
 80 |         return authUrl;
 81 |     }
 82 | 
 83 |     @Override
 84 |     public AuthResult handleCallback(String code, String state, HttpServletRequest request, HttpServletResponse response) {
 85 |         log.info("Processing OIDC callback with code: {}, state: {}", code, state);
 86 | 
 87 |         // 解析state获取provider信息
 88 |         IdpState idpState = parseState(state);
 89 |         String provider = idpState.getProvider();
 90 | 
 91 |         if (StrUtil.isBlank(provider)) {
 92 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "缺少OIDC provider");
 93 |         }
 94 | 
 95 |         OidcConfig oidcConfig = findOidcConfig(provider);
 96 | 
 97 |         // 使用授权码获取Token
 98 |         IdpTokenResult tokenResult = requestToken(code, oidcConfig, request);
 99 | 
100 |         // 获取用户信息,优先使用ID Token,降级到UserInfo端点
101 |         Map<String, Object> userInfo = getUserInfo(tokenResult, oidcConfig);
102 |         log.info("Get OIDC user info: {}", userInfo);
103 | 
104 |         // 处理用户认证逻辑
105 |         String developerId = createOrGetDeveloper(userInfo, oidcConfig);
106 |         String accessToken = TokenUtil.generateDeveloperToken(developerId);
107 | 
108 |         return AuthResult.of(accessToken, TokenUtil.getTokenExpiresIn());
109 |     }
110 | 
111 |     @Override
112 |     public List<IdpResult> getAvailableProviders() {
113 |         return Optional.ofNullable(portalService.getPortal(contextHolder.getPortal()))
114 |                 .filter(portal -> portal.getPortalSettingConfig() != null)
115 |                 .filter(portal -> portal.getPortalSettingConfig().getOidcConfigs() != null)
116 |                 .map(portal -> portal.getPortalSettingConfig().getOidcConfigs())
117 |                 // 确定当前Portal下启用的OIDC配置,返回Idp信息
118 |                 .map(configs -> configs.stream()
119 |                         .filter(OidcConfig::isEnabled)
120 |                         .map(config -> IdpResult.builder()
121 |                                 .provider(config.getProvider())
122 |                                 .displayName(config.getName())
123 |                                 .build())
124 |                         .collect(Collectors.toList()))
125 |                 .orElse(Collections.emptyList());
126 |     }
127 | 
128 |     private String buildRedirectUri(HttpServletRequest request) {
129 |         String scheme = request.getScheme();
130 | //        String serverName = "localhost";
131 | //        int serverPort = 5173;
132 |         String serverName = request.getServerName();
133 |         int serverPort = request.getServerPort();
134 | 
135 |         String baseUrl = scheme + "://" + serverName;
136 |         if (serverPort != CommonConstants.HTTP_PORT && serverPort != CommonConstants.HTTPS_PORT) {
137 |             baseUrl += ":" + serverPort;
138 |         }
139 | 
140 |         // 重定向到前端的Callback接口
141 |         return baseUrl + "/oidc/callback";
142 |     }
143 | 
144 |     private OidcConfig findOidcConfig(String provider) {
145 |         return Optional.ofNullable(portalService.getPortal(contextHolder.getPortal()))
146 |                 .filter(portal -> portal.getPortalSettingConfig() != null)
147 |                 .filter(portal -> portal.getPortalSettingConfig().getOidcConfigs() != null)
148 |                 // 根据provider字段过滤
149 |                 .flatMap(portal -> portal.getPortalSettingConfig()
150 |                         .getOidcConfigs()
151 |                         .stream()
152 |                         .filter(config -> provider.equals(config.getProvider()) && config.isEnabled())
153 |                         .findFirst())
154 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.OIDC_CONFIG, provider));
155 |     }
156 | 
157 |     private String buildState(String provider, String apiPrefix) {
158 |         IdpState state = IdpState.builder()
159 |                 .provider(provider)
160 |                 .timestamp(System.currentTimeMillis())
161 |                 .nonce(IdUtil.fastSimpleUUID())
162 |                 .apiPrefix(apiPrefix)
163 |                 .build();
164 |         return Base64.encode(JSONUtil.toJsonStr(state));
165 |     }
166 | 
167 |     private IdpState parseState(String encodedState) {
168 |         String stateJson = Base64.decodeStr(encodedState);
169 |         IdpState idpState = JSONUtil.toBean(stateJson, IdpState.class);
170 | 
171 |         // 验证时间戳,10分钟有效期
172 |         if (idpState.getTimestamp() != null) {
173 |             long currentTime = System.currentTimeMillis();
174 |             if (currentTime - idpState.getTimestamp() > 10 * 60 * 1000) {
175 |                 throw new BusinessException(ErrorCode.INVALID_REQUEST, "请求已过期");
176 |             }
177 |         }
178 | 
179 |         return idpState;
180 |     }
181 | 
182 |     private IdpTokenResult requestToken(String code, OidcConfig oidcConfig, HttpServletRequest request) {
183 |         AuthCodeConfig authCodeConfig = oidcConfig.getAuthCodeConfig();
184 |         String redirectUri = buildRedirectUri(request);
185 | 
186 |         MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
187 |         params.add(IdpConstants.GRANT_TYPE, GrantType.AUTHORIZATION_CODE.getType());
188 |         params.add(IdpConstants.CODE, code);
189 |         params.add(IdpConstants.REDIRECT_URI, redirectUri);
190 |         params.add(IdpConstants.CLIENT_ID, authCodeConfig.getClientId());
191 |         params.add(IdpConstants.CLIENT_SECRET, authCodeConfig.getClientSecret());
192 | 
193 |         log.info("Request tokens at: {}, params: {}", authCodeConfig.getTokenEndpoint(), params);
194 |         return executeRequest(authCodeConfig.getTokenEndpoint(), HttpMethod.POST, null, params, IdpTokenResult.class);
195 |     }
196 | 
197 |     private Map<String, Object> getUserInfo(IdpTokenResult tokenResult, OidcConfig oidcConfig) {
198 |         // 优先使用ID Token
199 |         if (StrUtil.isNotBlank(tokenResult.getIdToken())) {
200 |             log.info("Get user info form id token: {}", tokenResult.getIdToken());
201 |             return parseUserInfo(tokenResult.getIdToken(), oidcConfig);
202 |         }
203 | 
204 |         // 降级策略:使用UserInfo端点
205 |         log.warn("ID Token not available, falling back to UserInfo endpoint");
206 |         if (StrUtil.isBlank(tokenResult.getAccessToken())) {
207 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "OIDC获取用户信息失败");
208 |         }
209 | 
210 |         AuthCodeConfig authCodeConfig = oidcConfig.getAuthCodeConfig();
211 |         if (StrUtil.isBlank(authCodeConfig.getUserInfoEndpoint())) {
212 |             throw new BusinessException(ErrorCode.INVALID_PARAMETER, "OIDC配置缺少用户信息端点");
213 |         }
214 | 
215 |         return requestUserInfo(tokenResult.getAccessToken(), authCodeConfig, oidcConfig);
216 |     }
217 | 
218 |     private Map<String, Object> parseUserInfo(String idToken, OidcConfig oidcConfig) {
219 |         JWT jwt = JWTUtil.parseToken(idToken);
220 | 
221 |         // 验证过期时间
222 |         Object exp = jwt.getPayload("exp");
223 |         if (exp != null) {
224 |             long expTime = Convert.toLong(exp);
225 |             long currentTime = System.currentTimeMillis() / 1000;
226 |             if (expTime <= currentTime) {
227 |                 throw new BusinessException(ErrorCode.INVALID_REQUEST, "ID Token已过期");
228 |             }
229 |         }
230 |         // TODO 验签
231 | 
232 |         Map<String, Object> userInfo = jwt.getPayload().getClaimsJson();
233 | 
234 |         log.info("Successfully extracted user info from ID Token, sub: {}", userInfo);
235 |         return userInfo;
236 |     }
237 | 
238 |     @SuppressWarnings("unchecked")
239 |     private Map<String, Object> requestUserInfo(String accessToken, AuthCodeConfig authCodeConfig, OidcConfig oidcConfig) {
240 |         try {
241 |             HttpHeaders headers = new HttpHeaders();
242 |             headers.setBearerAuth(accessToken);
243 | 
244 |             log.info("Fetching user info from endpoint: {}", authCodeConfig.getUserInfoEndpoint());
245 |             Map<String, Object> userInfo = executeRequest(authCodeConfig.getUserInfoEndpoint(), HttpMethod.GET, headers, null, Map.class);
246 | 
247 |             log.info("Successfully fetched user info from endpoint, sub: {}", userInfo);
248 |             return userInfo;
249 |         } catch (Exception e) {
250 |             log.error("Failed to fetch user info from endpoint: {}", authCodeConfig.getUserInfoEndpoint(), e);
251 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "获取用户信息失败");
252 |         }
253 |     }
254 | 
255 |     private String createOrGetDeveloper(Map<String, Object> userInfo, OidcConfig config) {
256 |         IdentityMapping identityMapping = config.getIdentityMapping();
257 |         // userId & userName & email
258 |         String userIdField = StrUtil.isBlank(identityMapping.getUserIdField()) ?
259 |                 IdpConstants.SUBJECT : identityMapping.getUserIdField();
260 |         String userNameField = StrUtil.isBlank(identityMapping.getUserNameField()) ?
261 |                 IdpConstants.NAME : identityMapping.getUserNameField();
262 |         String emailField = StrUtil.isBlank(identityMapping.getEmailField()) ?
263 |                 IdpConstants.EMAIL : identityMapping.getEmailField();
264 | 
265 |         Object userIdObj = userInfo.get(userIdField);
266 |         Object userNameObj = userInfo.get(userNameField);
267 |         Object emailObj = userInfo.get(emailField);
268 | 
269 |         String userId = Convert.toStr(userIdObj);
270 |         String userName = Convert.toStr(userNameObj);
271 |         String email = Convert.toStr(emailObj);
272 |         if (StrUtil.isBlank(userId) || StrUtil.isBlank(userName)) {
273 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "Id Token中缺少用户ID字段或用户名称");
274 |         }
275 | 
276 |         // 复用已有的Developer,否则创建
277 |         return Optional.ofNullable(developerService.getExternalDeveloper(config.getProvider(), userId))
278 |                 .map(DeveloperResult::getDeveloperId)
279 |                 .orElseGet(() -> {
280 |                     CreateExternalDeveloperParam param = CreateExternalDeveloperParam.builder()
281 |                             .provider(config.getProvider())
282 |                             .subject(userId)
283 |                             .displayName(userName)
284 |                             .email(email)
285 |                             .authType(DeveloperAuthType.OIDC)
286 |                             .build();
287 | 
288 |                     return developerService.createExternalDeveloper(param).getDeveloperId();
289 |                 });
290 |     }
291 | 
292 |     private <T> T executeRequest(String url, HttpMethod method, HttpHeaders headers, Object body, Class<T> responseType) {
293 |         HttpEntity<?> requestEntity = new HttpEntity<>(body, headers);
294 |         log.info("Executing HTTP request to: {}", url);
295 |         ResponseEntity<String> response = restTemplate.exchange(
296 |                 url,
297 |                 method,
298 |                 requestEntity,
299 |                 String.class
300 |         );
301 | 
302 |         log.info("Received HTTP response from: {}, status: {}, body: {}", url, response.getStatusCode(), response.getBody());
303 | 
304 |         return JSONUtil.toBean(response.getBody(), responseType);
305 |     }
306 | }
307 | 
```
Page 6/9FirstPrevNextLast