This is page 9 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-web/api-portal-admin/src/components/portal/ThirdPartyAuthManager.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import {useState} from 'react' 2 | import {Button, Form, Input, Select, Switch, Table, Modal, Space, message, Divider, Steps, Card, Tabs, Collapse, Radio} from 'antd' 3 | import {PlusOutlined, EditOutlined, DeleteOutlined, ExclamationCircleOutlined, MinusCircleOutlined, KeyOutlined, CheckCircleFilled, MinusCircleFilled} from '@ant-design/icons' 4 | import {ThirdPartyAuthConfig, AuthenticationType, GrantType, AuthCodeConfig, OAuth2Config, OidcConfig, PublicKeyFormat} from '@/types' 5 | 6 | interface ThirdPartyAuthManagerProps { 7 | configs: ThirdPartyAuthConfig[] 8 | onSave: (configs: ThirdPartyAuthConfig[]) => Promise<void> 9 | } 10 | 11 | export function ThirdPartyAuthManager({configs, onSave}: ThirdPartyAuthManagerProps) { 12 | const [form] = Form.useForm() 13 | const [modalVisible, setModalVisible] = useState(false) 14 | const [loading, setLoading] = useState(false) 15 | const [editingConfig, setEditingConfig] = useState<ThirdPartyAuthConfig | null>(null) 16 | const [currentStep, setCurrentStep] = useState(0) 17 | const [selectedType, setSelectedType] = useState<AuthenticationType | null>(null) 18 | 19 | 20 | // 添加新配置 21 | const handleAdd = () => { 22 | setEditingConfig(null) 23 | setSelectedType(null) 24 | setCurrentStep(0) 25 | setModalVisible(true) 26 | form.resetFields() 27 | } 28 | 29 | // 编辑配置 30 | const handleEdit = (config: ThirdPartyAuthConfig) => { 31 | setEditingConfig(config) 32 | setSelectedType(config.type) 33 | setCurrentStep(1) // 直接进入配置步骤 34 | setModalVisible(true) 35 | 36 | // 根据类型设置表单值 37 | if (config.type === AuthenticationType.OIDC) { 38 | // OIDC配置:直接使用OidcConfig的字段 39 | const oidcConfig = config as (OidcConfig & { type: AuthenticationType.OIDC }) 40 | 41 | // 检查是否是手动配置模式(有具体的端点地址) 42 | const hasManualEndpoints = !!(oidcConfig.authCodeConfig?.authorizationEndpoint && 43 | oidcConfig.authCodeConfig?.tokenEndpoint && 44 | oidcConfig.authCodeConfig?.userInfoEndpoint) 45 | 46 | form.setFieldsValue({ 47 | provider: oidcConfig.provider, 48 | name: oidcConfig.name, 49 | enabled: oidcConfig.enabled, 50 | type: oidcConfig.type, 51 | configMode: hasManualEndpoints ? 'manual' : 'auto', 52 | ...oidcConfig.authCodeConfig, 53 | // 身份映射字段可能在根级别或authCodeConfig中 54 | userIdField: oidcConfig.identityMapping?.userIdField || oidcConfig.authCodeConfig?.identityMapping?.userIdField, 55 | userNameField: oidcConfig.identityMapping?.userNameField || oidcConfig.authCodeConfig?.identityMapping?.userNameField, 56 | emailField: oidcConfig.identityMapping?.emailField || oidcConfig.authCodeConfig?.identityMapping?.emailField 57 | }) 58 | } else if (config.type === AuthenticationType.OAUTH2) { 59 | // OAuth2配置:直接使用OAuth2Config的字段 60 | const oauth2Config = config as (OAuth2Config & { type: AuthenticationType.OAUTH2 }) 61 | form.setFieldsValue({ 62 | provider: oauth2Config.provider, 63 | name: oauth2Config.name, 64 | enabled: oauth2Config.enabled, 65 | type: oauth2Config.type, 66 | grantType: oauth2Config.grantType || GrantType.JWT_BEARER, // 确保有默认值 67 | userIdField: oauth2Config.identityMapping?.userIdField, 68 | userNameField: oauth2Config.identityMapping?.userNameField, 69 | emailField: oauth2Config.identityMapping?.emailField, 70 | publicKeys: oauth2Config.jwtBearerConfig?.publicKeys || [] 71 | }) 72 | } 73 | } 74 | 75 | // 删除配置 76 | const handleDelete = async (provider: string, name: string) => { 77 | Modal.confirm({ 78 | title: '确认删除', 79 | icon: <ExclamationCircleOutlined/>, 80 | content: `确定要删除第三方认证配置 "${name}" 吗?此操作不可恢复。`, 81 | okText: '确认删除', 82 | okType: 'danger', 83 | cancelText: '取消', 84 | async onOk() { 85 | try { 86 | const updatedConfigs = configs.filter(config => config.provider !== provider) 87 | await onSave(updatedConfigs) 88 | message.success('第三方认证配置删除成功') 89 | } catch (error) { 90 | message.error('删除第三方认证配置失败') 91 | } 92 | }, 93 | }) 94 | } 95 | 96 | 97 | // 下一步 98 | const handleNext = async () => { 99 | if (currentStep === 0) { 100 | try { 101 | const values = await form.validateFields(['type']) 102 | setSelectedType(values.type) 103 | setCurrentStep(1) 104 | 105 | // 为不同类型设置默认值 106 | if (values.type === AuthenticationType.OAUTH2) { 107 | form.setFieldsValue({ 108 | grantType: GrantType.JWT_BEARER, 109 | enabled: true 110 | }) 111 | } else if (values.type === AuthenticationType.OIDC) { 112 | form.setFieldsValue({ 113 | enabled: true, 114 | configMode: 'auto' 115 | }) 116 | } 117 | } catch (error) { 118 | // 验证失败 119 | } 120 | } 121 | } 122 | 123 | // 上一步 124 | const handlePrevious = () => { 125 | setCurrentStep(0) 126 | } 127 | 128 | // 保存配置 129 | const handleSave = async () => { 130 | try { 131 | setLoading(true) 132 | 133 | const values = await form.validateFields() 134 | 135 | let newConfig: ThirdPartyAuthConfig 136 | 137 | if (selectedType === AuthenticationType.OIDC) { 138 | // OIDC配置:根据配置模式创建不同的authCodeConfig 139 | let authCodeConfig: AuthCodeConfig 140 | 141 | if (values.configMode === 'auto') { 142 | // 自动发现模式:只保存issuer,端点置空(后端会通过issuer自动发现) 143 | authCodeConfig = { 144 | clientId: values.clientId, 145 | clientSecret: values.clientSecret, 146 | scopes: values.scopes, 147 | issuer: values.issuer, 148 | authorizationEndpoint: '', // 自动发现模式下端点为空 149 | tokenEndpoint: '', 150 | userInfoEndpoint: '', 151 | jwkSetUri: '', 152 | // 可选的身份映射配置 153 | identityMapping: (values.userIdField || values.userNameField || values.emailField) ? { 154 | userIdField: values.userIdField || null, 155 | userNameField: values.userNameField || null, 156 | emailField: values.emailField || null 157 | } : undefined 158 | } 159 | } else { 160 | // 手动配置模式:保存具体的端点地址 161 | authCodeConfig = { 162 | clientId: values.clientId, 163 | clientSecret: values.clientSecret, 164 | scopes: values.scopes, 165 | issuer: values.issuer || '', // 手动配置模式下issuer可选 166 | authorizationEndpoint: values.authorizationEndpoint, 167 | tokenEndpoint: values.tokenEndpoint, 168 | userInfoEndpoint: values.userInfoEndpoint, 169 | jwkSetUri: values.jwkSetUri || '', 170 | // 可选的身份映射配置 171 | identityMapping: (values.userIdField || values.userNameField || values.emailField) ? { 172 | userIdField: values.userIdField || null, 173 | userNameField: values.userNameField || null, 174 | emailField: values.emailField || null 175 | } : undefined 176 | } 177 | } 178 | 179 | newConfig = { 180 | provider: values.provider, 181 | name: values.name, 182 | logoUrl: null, 183 | enabled: values.enabled ?? true, 184 | grantType: 'AUTHORIZATION_CODE' as const, 185 | authCodeConfig, 186 | // 根级别的身份映射(为兼容后端格式) 187 | identityMapping: authCodeConfig.identityMapping, 188 | type: AuthenticationType.OIDC 189 | } as (OidcConfig & { type: AuthenticationType.OIDC }) 190 | } else { 191 | // OAuth2配置:直接创建OAuth2Config格式 192 | const grantType = values.grantType || GrantType.JWT_BEARER // 确保有默认值 193 | newConfig = { 194 | provider: values.provider, 195 | name: values.name, 196 | enabled: values.enabled ?? true, 197 | grantType: grantType, 198 | jwtBearerConfig: grantType === GrantType.JWT_BEARER ? { 199 | publicKeys: values.publicKeys || [] 200 | } : undefined, 201 | identityMapping: { 202 | userIdField: values.userIdField || null, 203 | userNameField: values.userNameField || null, 204 | emailField: values.emailField || null 205 | }, 206 | type: AuthenticationType.OAUTH2 207 | } as (OAuth2Config & { type: AuthenticationType.OAUTH2 }) 208 | } 209 | 210 | let updatedConfigs 211 | if (editingConfig) { 212 | updatedConfigs = configs.map(config => 213 | config.provider === editingConfig.provider ? newConfig : config 214 | ) 215 | } else { 216 | updatedConfigs = [...configs, newConfig] 217 | } 218 | 219 | await onSave(updatedConfigs) 220 | 221 | message.success(editingConfig ? '第三方认证配置更新成功' : '第三方认证配置添加成功') 222 | setModalVisible(false) 223 | } catch (error) { 224 | message.error('保存第三方认证配置失败') 225 | } finally { 226 | setLoading(false) 227 | } 228 | } 229 | 230 | // 取消 231 | const handleCancel = () => { 232 | setModalVisible(false) 233 | setEditingConfig(null) 234 | setSelectedType(null) 235 | setCurrentStep(0) 236 | form.resetFields() 237 | } 238 | 239 | // OIDC表格列定义(不包含类型列) 240 | const oidcColumns = [ 241 | { 242 | title: '提供商', 243 | dataIndex: 'provider', 244 | key: 'provider', 245 | width: 120, 246 | render: (provider: string) => ( 247 | <span className="font-medium text-gray-700">{provider}</span> 248 | ) 249 | }, 250 | { 251 | title: '名称', 252 | dataIndex: 'name', 253 | key: 'name', 254 | width: 150, 255 | }, 256 | { 257 | title: '授权模式', 258 | key: 'grantType', 259 | width: 120, 260 | render: () => <span className="text-gray-600">授权码模式</span> 261 | }, 262 | { 263 | title: '状态', 264 | dataIndex: 'enabled', 265 | key: 'enabled', 266 | width: 80, 267 | render: (enabled: boolean) => ( 268 | <div className="flex items-center"> 269 | {enabled ? ( 270 | <CheckCircleFilled className="text-green-500 mr-2" style={{fontSize: '12px'}} /> 271 | ) : ( 272 | <MinusCircleFilled className="text-gray-500 mr-2" style={{fontSize: '12px'}} /> 273 | )} 274 | <span className="text-gray-700"> 275 | {enabled ? '已启用' : '已停用'} 276 | </span> 277 | </div> 278 | ) 279 | }, 280 | { 281 | title: '操作', 282 | key: 'action', 283 | width: 150, 284 | render: (_: any, record: ThirdPartyAuthConfig) => ( 285 | <Space> 286 | <Button 287 | type="link" 288 | icon={<EditOutlined/>} 289 | onClick={() => handleEdit(record)} 290 | > 291 | 编辑 292 | </Button> 293 | <Button 294 | type="link" 295 | danger 296 | icon={<DeleteOutlined/>} 297 | onClick={() => handleDelete(record.provider, record.name)} 298 | > 299 | 删除 300 | </Button> 301 | </Space> 302 | ) 303 | } 304 | ] 305 | 306 | // OAuth2表格列定义(不包含类型列) 307 | const oauth2Columns = [ 308 | { 309 | title: '提供商', 310 | dataIndex: 'provider', 311 | key: 'provider', 312 | width: 120, 313 | render: (provider: string) => ( 314 | <span className="font-medium text-gray-700">{provider}</span> 315 | ) 316 | }, 317 | { 318 | title: '名称', 319 | dataIndex: 'name', 320 | key: 'name', 321 | width: 150, 322 | }, 323 | { 324 | title: '授权模式', 325 | key: 'grantType', 326 | width: 120, 327 | render: (record: ThirdPartyAuthConfig) => { 328 | if (record.type === AuthenticationType.OAUTH2) { 329 | const oauth2Config = record as (OAuth2Config & { type: AuthenticationType.OAUTH2 }) 330 | return ( 331 | <span className="text-gray-600"> 332 | {oauth2Config.grantType === GrantType.JWT_BEARER ? 'JWT断言' : '授权码模式'} 333 | </span> 334 | ) 335 | } 336 | return <span className="text-gray-600">授权码模式</span> 337 | } 338 | }, 339 | { 340 | title: '状态', 341 | dataIndex: 'enabled', 342 | key: 'enabled', 343 | width: 80, 344 | render: (enabled: boolean) => ( 345 | <div className="flex items-center"> 346 | {enabled ? ( 347 | <CheckCircleFilled className="text-green-500 mr-2" style={{fontSize: '12px'}} /> 348 | ) : ( 349 | <MinusCircleFilled className="text-gray-500 mr-2" style={{fontSize: '12px'}} /> 350 | )} 351 | <span className="text-gray-700"> 352 | {enabled ? '已启用' : '已停用'} 353 | </span> 354 | </div> 355 | ) 356 | }, 357 | { 358 | title: '操作', 359 | key: 'action', 360 | width: 150, 361 | render: (_: any, record: ThirdPartyAuthConfig) => ( 362 | <Space> 363 | <Button 364 | type="link" 365 | icon={<EditOutlined/>} 366 | onClick={() => handleEdit(record)} 367 | > 368 | 编辑 369 | </Button> 370 | <Button 371 | type="link" 372 | danger 373 | icon={<DeleteOutlined/>} 374 | onClick={() => handleDelete(record.provider, record.name)} 375 | > 376 | 删除 377 | </Button> 378 | </Space> 379 | ) 380 | } 381 | ] 382 | 383 | // 渲染OIDC配置表单 384 | const renderOidcForm = () => ( 385 | <div className="space-y-6"> 386 | <Form.Item 387 | name="grantType" 388 | label="授权模式" 389 | initialValue="AUTHORIZATION_CODE" 390 | > 391 | <Select disabled> 392 | <Select.Option value="AUTHORIZATION_CODE">授权码模式</Select.Option> 393 | </Select> 394 | </Form.Item> 395 | 396 | <div className="grid grid-cols-2 gap-4"> 397 | <Form.Item 398 | name="clientId" 399 | label="Client ID" 400 | rules={[{required: true, message: '请输入 Client ID'}]} 401 | > 402 | <Input placeholder="Client ID"/> 403 | </Form.Item> 404 | <Form.Item 405 | name="clientSecret" 406 | label="Client Secret" 407 | rules={[{required: true, message: '请输入 Client Secret'}]} 408 | > 409 | <Input.Password placeholder="Client Secret"/> 410 | </Form.Item> 411 | </div> 412 | 413 | <div className="grid grid-cols-2 gap-4"> 414 | <Form.Item 415 | name="scopes" 416 | label="授权范围" 417 | rules={[{required: true, message: '请输入授权范围'}]} 418 | > 419 | <Input placeholder="如: openid profile email"/> 420 | </Form.Item> 421 | <div></div> 422 | </div> 423 | 424 | <Divider /> 425 | 426 | {/* 配置模式选择 */} 427 | <Form.Item 428 | name="configMode" 429 | label="端点配置" 430 | initialValue="auto" 431 | > 432 | <Radio.Group> 433 | <Radio value="auto">自动发现</Radio> 434 | <Radio value="manual">手动配置</Radio> 435 | </Radio.Group> 436 | </Form.Item> 437 | 438 | {/* 根据配置模式显示不同字段 */} 439 | <Form.Item 440 | noStyle 441 | shouldUpdate={(prevValues, curValues) => prevValues.configMode !== curValues.configMode} 442 | > 443 | {({ getFieldValue }) => { 444 | const configMode = getFieldValue('configMode') || 'auto' 445 | 446 | if (configMode === 'auto') { 447 | // 自动发现模式:只需要Issuer地址 448 | return ( 449 | <Form.Item 450 | name="issuer" 451 | label="Issuer" 452 | rules={[ 453 | { required: true, message: '请输入Issuer地址' }, 454 | { type: 'url', message: '请输入有效的URL' } 455 | ]} 456 | > 457 | <Input placeholder="如: https://accounts.google.com" /> 458 | </Form.Item> 459 | ) 460 | } else { 461 | // 手动配置模式:需要各个端点 462 | return ( 463 | <div className="space-y-4"> 464 | <div className="grid grid-cols-2 gap-4"> 465 | <Form.Item 466 | name="authorizationEndpoint" 467 | label="授权端点" 468 | rules={[{ required: true, message: '请输入授权端点' }]} 469 | > 470 | <Input placeholder="Authorization 授权端点"/> 471 | </Form.Item> 472 | <Form.Item 473 | name="tokenEndpoint" 474 | label="令牌端点" 475 | rules={[{ required: true, message: '请输入令牌端点' }]} 476 | > 477 | <Input placeholder="Token 令牌端点"/> 478 | </Form.Item> 479 | </div> 480 | <div className="grid grid-cols-2 gap-4"> 481 | <Form.Item 482 | name="userInfoEndpoint" 483 | label="用户信息端点" 484 | rules={[{ required: true, message: '请输入用户信息端点' }]} 485 | > 486 | <Input placeholder="UserInfo 端点"/> 487 | </Form.Item> 488 | <Form.Item 489 | name="jwkSetUri" 490 | label="公钥端点" 491 | > 492 | <Input placeholder="可选"/> 493 | </Form.Item> 494 | </div> 495 | </div> 496 | ) 497 | } 498 | }} 499 | </Form.Item> 500 | 501 | <div className="-ml-3"> 502 | <Collapse 503 | size="small" 504 | ghost 505 | expandIcon={({ isActive }) => ( 506 | <svg 507 | className={`w-4 h-4 transition-transform ${isActive ? 'rotate-90' : ''}`} 508 | fill="currentColor" 509 | viewBox="0 0 20 20" 510 | > 511 | <path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" /> 512 | </svg> 513 | )} 514 | items={[ 515 | { 516 | key: 'advanced', 517 | label: ( 518 | <div className="flex items-center text-gray-600"> 519 | <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> 520 | <path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947z" clipRule="evenodd" /> 521 | <path fillRule="evenodd" d="M10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" /> 522 | </svg> 523 | <span className="ml-2">高级配置</span> 524 | <span className="text-xs text-gray-400 ml-2">身份映射</span> 525 | </div> 526 | ), 527 | children: ( 528 | <div className="space-y-4 pt-2 ml-3"> 529 | <div className="grid grid-cols-3 gap-4"> 530 | <Form.Item 531 | name="userIdField" 532 | label="开发者ID" 533 | > 534 | <Input placeholder="默认: sub"/> 535 | </Form.Item> 536 | <Form.Item 537 | name="userNameField" 538 | label="开发者名称" 539 | > 540 | <Input placeholder="默认: name"/> 541 | </Form.Item> 542 | <Form.Item 543 | name="emailField" 544 | label="邮箱" 545 | > 546 | <Input placeholder="默认: email"/> 547 | </Form.Item> 548 | </div> 549 | 550 | <div className="bg-blue-50 p-3 rounded-lg"> 551 | <div className="flex items-start space-x-2"> 552 | <div className="text-blue-600 mt-0.5"> 553 | <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> 554 | <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" /> 555 | </svg> 556 | </div> 557 | <div> 558 | <h4 className="text-blue-800 font-medium text-sm">配置说明</h4> 559 | <p className="text-blue-700 text-xs mt-1"> 560 | 身份映射用于从OIDC令牌中提取用户信息。如果不填写,系统将使用OIDC标准字段。 561 | </p> 562 | </div> 563 | </div> 564 | </div> 565 | </div> 566 | ) 567 | } 568 | ]} 569 | /> 570 | </div> 571 | </div> 572 | ) 573 | 574 | // 渲染OAuth2配置表单 575 | const renderOAuth2Form = () => ( 576 | <div className="space-y-6"> 577 | <Form.Item 578 | name="grantType" 579 | label="授权模式" 580 | initialValue={GrantType.JWT_BEARER} 581 | rules={[{required: true}]} 582 | > 583 | <Select disabled> 584 | <Select.Option value={GrantType.JWT_BEARER}>JWT断言</Select.Option> 585 | </Select> 586 | </Form.Item> 587 | 588 | <Form.List name="publicKeys"> 589 | {(fields, { add, remove }) => ( 590 | <div className="space-y-4"> 591 | {fields.length > 0 && ( 592 | <Collapse 593 | size="small" 594 | items={fields.map(({ key, name, ...restField }) => ({ 595 | key: key, 596 | label: ( 597 | <div className="flex items-center"> 598 | <KeyOutlined className="mr-2" /> 599 | <span>公钥 {name + 1}</span> 600 | </div> 601 | ), 602 | extra: ( 603 | <Button 604 | type="link" 605 | danger 606 | size="small" 607 | icon={<MinusCircleOutlined />} 608 | onClick={(e) => { 609 | e.stopPropagation() 610 | remove(name) 611 | }} 612 | > 613 | 删除 614 | </Button> 615 | ), 616 | children: ( 617 | <div className="space-y-4 px-4"> 618 | <div className="grid grid-cols-3 gap-4"> 619 | <Form.Item 620 | {...restField} 621 | name={[name, 'kid']} 622 | label="Key ID" 623 | rules={[{ required: true, message: '请输入Key ID' }]} 624 | > 625 | <Input placeholder="公钥标识符" size="small" /> 626 | </Form.Item> 627 | <Form.Item 628 | {...restField} 629 | name={[name, 'algorithm']} 630 | label="签名算法" 631 | rules={[{ required: true, message: '请选择签名算法' }]} 632 | > 633 | <Select placeholder="选择签名算法" size="small"> 634 | <Select.Option value="RS256">RS256</Select.Option> 635 | <Select.Option value="RS384">RS384</Select.Option> 636 | <Select.Option value="RS512">RS512</Select.Option> 637 | <Select.Option value="ES256">ES256</Select.Option> 638 | <Select.Option value="ES384">ES384</Select.Option> 639 | <Select.Option value="ES512">ES512</Select.Option> 640 | </Select> 641 | </Form.Item> 642 | <Form.Item 643 | {...restField} 644 | name={[name, 'format']} 645 | label="公钥格式" 646 | rules={[{ required: true, message: '请选择公钥格式' }]} 647 | > 648 | <Select placeholder="选择公钥格式" size="small"> 649 | <Select.Option value={PublicKeyFormat.PEM}>PEM</Select.Option> 650 | <Select.Option value={PublicKeyFormat.JWK}>JWK</Select.Option> 651 | </Select> 652 | </Form.Item> 653 | </div> 654 | 655 | <Form.Item 656 | noStyle 657 | shouldUpdate={(prevValues, curValues) => { 658 | const prevFormat = prevValues?.publicKeys?.[name]?.format 659 | const curFormat = curValues?.publicKeys?.[name]?.format 660 | return prevFormat !== curFormat 661 | }} 662 | > 663 | {({ getFieldValue }) => { 664 | const format = getFieldValue(['publicKeys', name, 'format']) 665 | return ( 666 | <Form.Item 667 | {...restField} 668 | name={[name, 'value']} 669 | label="公钥内容" 670 | rules={[{ required: true, message: '请输入公钥内容' }]} 671 | > 672 | <Input.TextArea 673 | rows={6} 674 | placeholder={ 675 | format === PublicKeyFormat.JWK 676 | ? 'JWK格式公钥,例如:\n{\n "kty": "RSA",\n "kid": "key1",\n "n": "...",\n "e": "AQAB"\n}' 677 | : 'PEM格式公钥,例如:\n-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...\n-----END PUBLIC KEY-----' 678 | } 679 | style={{ fontFamily: 'monospace', fontSize: '12px' }} 680 | /> 681 | </Form.Item> 682 | ) 683 | }} 684 | </Form.Item> 685 | </div> 686 | ) 687 | }))} 688 | /> 689 | )} 690 | <Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />} size="small"> 691 | 添加公钥 692 | </Button> 693 | </div> 694 | )} 695 | </Form.List> 696 | 697 | <div className="-ml-3"> 698 | <Collapse 699 | size="small" 700 | ghost 701 | expandIcon={({ isActive }) => ( 702 | <svg 703 | className={`w-4 h-4 transition-transform ${isActive ? 'rotate-90' : ''}`} 704 | fill="currentColor" 705 | viewBox="0 0 20 20" 706 | > 707 | <path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" /> 708 | </svg> 709 | )} 710 | items={[ 711 | { 712 | key: 'advanced', 713 | label: ( 714 | <div className="flex items-center text-gray-600"> 715 | <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> 716 | <path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947z" clipRule="evenodd" /> 717 | <path fillRule="evenodd" d="M10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" /> 718 | </svg> 719 | <span className="ml-2">高级配置</span> 720 | <span className="text-xs text-gray-400 ml-2">身份映射</span> 721 | </div> 722 | ), 723 | children: ( 724 | <div className="space-y-4 pt-2 ml-3"> 725 | <div className="grid grid-cols-3 gap-4"> 726 | <Form.Item 727 | name="userIdField" 728 | label="开发者ID" 729 | > 730 | <Input placeholder="默认: userId"/> 731 | </Form.Item> 732 | <Form.Item 733 | name="userNameField" 734 | label="开发者名称" 735 | > 736 | <Input placeholder="默认: name"/> 737 | </Form.Item> 738 | <Form.Item 739 | name="emailField" 740 | label="邮箱" 741 | > 742 | <Input placeholder="默认: email"/> 743 | </Form.Item> 744 | </div> 745 | 746 | <div className="bg-blue-50 p-3 rounded-lg"> 747 | <div className="flex items-start space-x-2"> 748 | <div className="text-blue-600 mt-0.5"> 749 | <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> 750 | <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" /> 751 | </svg> 752 | </div> 753 | <div> 754 | <h4 className="text-blue-800 font-medium text-sm">配置说明</h4> 755 | <p className="text-blue-700 text-xs mt-1"> 756 | 身份映射用于从JWT载荷中提取用户信息。如果不填写,系统将使用默认字段名。 757 | </p> 758 | </div> 759 | </div> 760 | </div> 761 | </div> 762 | ) 763 | } 764 | ]} 765 | /> 766 | </div> 767 | </div> 768 | ) 769 | 770 | // 按类型分组配置 771 | const oidcConfigs = configs.filter(config => config.type === AuthenticationType.OIDC) 772 | const oauth2Configs = configs.filter(config => config.type === AuthenticationType.OAUTH2) 773 | 774 | return ( 775 | <div className="space-y-6"> 776 | <div className="flex justify-between items-center"> 777 | <div> 778 | <h3 className="text-lg font-medium">第三方认证</h3> 779 | <p className="text-sm text-gray-500">管理外部身份认证配置</p> 780 | </div> 781 | <Button 782 | type="primary" 783 | icon={<PlusOutlined/>} 784 | onClick={handleAdd} 785 | > 786 | 添加配置 787 | </Button> 788 | </div> 789 | 790 | <Tabs 791 | defaultActiveKey="oidc" 792 | items={[ 793 | { 794 | key: 'oidc', 795 | label: 'OIDC配置', 796 | children: ( 797 | <div className="bg-white rounded-lg"> 798 | <div className="py-4 border-b border-gray-200"> 799 | <h4 className="text-lg font-medium text-gray-900">OIDC配置</h4> 800 | <p className="text-sm text-gray-500 mt-1">支持OpenID Connect标准协议的身份提供商</p> 801 | </div> 802 | <Table 803 | columns={oidcColumns} 804 | dataSource={oidcConfigs} 805 | rowKey="provider" 806 | pagination={false} 807 | size="small" 808 | locale={{ 809 | emptyText: '暂无OIDC配置' 810 | }} 811 | /> 812 | </div> 813 | ), 814 | }, 815 | { 816 | key: 'oauth2', 817 | label: 'OAuth2配置', 818 | children: ( 819 | <div className="bg-white rounded-lg"> 820 | <div className="py-4 border-b border-gray-200"> 821 | <h4 className="text-lg font-medium text-gray-900">OAuth2配置</h4> 822 | <p className="text-sm text-gray-500 mt-1">支持OAuth 2.0标准协议的身份提供商</p> 823 | </div> 824 | <Table 825 | columns={oauth2Columns} 826 | dataSource={oauth2Configs} 827 | rowKey="provider" 828 | pagination={false} 829 | size="small" 830 | locale={{ 831 | emptyText: '暂无OAuth2配置' 832 | }} 833 | /> 834 | </div> 835 | ), 836 | }, 837 | ]} 838 | /> 839 | 840 | {/* 添加/编辑配置模态框 */} 841 | <Modal 842 | title={editingConfig ? '编辑第三方认证配置' : '添加第三方认证配置'} 843 | open={modalVisible} 844 | onCancel={handleCancel} 845 | width={800} 846 | footer={null} 847 | > 848 | <Steps 849 | current={currentStep} 850 | className="mb-6" 851 | items={[ 852 | { 853 | title: '选择类型', 854 | description: '选择认证协议类型' 855 | }, 856 | { 857 | title: '配置认证', 858 | description: '填写认证参数' 859 | } 860 | ]} 861 | /> 862 | 863 | <Form 864 | form={form} 865 | layout="vertical" 866 | > 867 | {currentStep === 0 ? ( 868 | // 第一步:选择类型 869 | <Card> 870 | <Form.Item 871 | name="type" 872 | label="认证类型" 873 | rules={[{required: true, message: '请选择认证类型'}]} 874 | > 875 | <Select placeholder="请选择认证方式" size="large"> 876 | <Select.Option value={AuthenticationType.OIDC}> 877 | <div className="py-2"> 878 | <div className="font-medium">OIDC(适用于支持OpenID Connect的身份提供商认证)</div> 879 | </div> 880 | </Select.Option> 881 | <Select.Option value={AuthenticationType.OAUTH2}> 882 | <div className="py-2"> 883 | <div className="font-medium">OAuth2(适用于服务间集成)</div> 884 | </div> 885 | </Select.Option> 886 | </Select> 887 | </Form.Item> 888 | 889 | <div className="flex justify-end"> 890 | <Button type="primary" onClick={handleNext}> 891 | 下一步 892 | </Button> 893 | </div> 894 | </Card> 895 | ) : ( 896 | // 第二步:配置详情 897 | <div> 898 | <div className="grid grid-cols-2 gap-4"> 899 | <Form.Item 900 | name="provider" 901 | label="提供商标识" 902 | rules={[ 903 | {required: true, message: '请输入提供商标识'}, 904 | { 905 | validator: (_, value) => { 906 | if (!value) return Promise.resolve() 907 | 908 | // 检查provider唯一性 909 | const isDuplicate = configs.some(config => 910 | config.provider === value && 911 | (!editingConfig || editingConfig.provider !== value) 912 | ) 913 | 914 | if (isDuplicate) { 915 | return Promise.reject(new Error('该提供商标识已存在,请使用不同的标识')) 916 | } 917 | 918 | return Promise.resolve() 919 | } 920 | } 921 | ]} 922 | > 923 | <Input 924 | placeholder="如: google, company-sso" 925 | disabled={editingConfig !== null} 926 | /> 927 | </Form.Item> 928 | <Form.Item 929 | name="name" 930 | label="显示名称" 931 | rules={[{required: true, message: '请输入显示名称'}]} 932 | > 933 | <Input placeholder="如: Google登录、公司SSO"/> 934 | </Form.Item> 935 | </div> 936 | 937 | <Form.Item 938 | name="enabled" 939 | label="启用状态" 940 | valuePropName="checked" 941 | > 942 | <Switch/> 943 | </Form.Item> 944 | 945 | <Divider /> 946 | 947 | {/* 根据类型显示不同的配置表单 */} 948 | {selectedType === AuthenticationType.OIDC ? renderOidcForm() : renderOAuth2Form()} 949 | 950 | <div className="flex justify-between mt-6"> 951 | <Button onClick={handlePrevious}> 952 | 上一步 953 | </Button> 954 | <Space> 955 | <Button onClick={handleCancel}> 956 | 取消 957 | </Button> 958 | <Button type="primary" loading={loading} onClick={handleSave}> 959 | {editingConfig ? '更新' : '添加'} 960 | </Button> 961 | </Space> 962 | </div> 963 | </div> 964 | )} 965 | </Form> 966 | </Modal> 967 | 968 | </div> 969 | ) 970 | } 971 | ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/service/gateway/AdpAIGatewayOperator.java: -------------------------------------------------------------------------------- ```java 1 | package com.alibaba.apiopenplatform.service.gateway; 2 | 3 | import com.alibaba.apiopenplatform.core.exception.BusinessException; 4 | import com.alibaba.apiopenplatform.core.exception.ErrorCode; 5 | import com.alibaba.apiopenplatform.dto.params.gateway.QueryAdpAIGatewayParam; 6 | import com.alibaba.apiopenplatform.dto.result.*; 7 | import com.alibaba.apiopenplatform.dto.result.AdpGatewayInstanceResult; 8 | import com.alibaba.apiopenplatform.entity.Consumer; 9 | import com.alibaba.apiopenplatform.entity.ConsumerCredential; 10 | import com.alibaba.apiopenplatform.entity.Gateway; 11 | import com.alibaba.apiopenplatform.support.consumer.AdpAIAuthConfig; 12 | import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig; 13 | import com.alibaba.apiopenplatform.support.enums.GatewayType; 14 | import com.alibaba.apiopenplatform.support.gateway.AdpAIGatewayConfig; 15 | import com.alibaba.apiopenplatform.service.gateway.client.AdpAIGatewayClient; 16 | import com.alibaba.apiopenplatform.support.gateway.GatewayConfig; 17 | import com.alibaba.apiopenplatform.support.product.APIGRefConfig; 18 | import com.alibaba.apiopenplatform.dto.result.MCPConfigResult; 19 | import cn.hutool.json.JSONUtil; 20 | import lombok.Data; 21 | import lombok.extern.slf4j.Slf4j; 22 | import org.springframework.http.HttpEntity; 23 | import org.springframework.http.HttpMethod; 24 | import org.springframework.http.ResponseEntity; 25 | import org.springframework.stereotype.Service; 26 | 27 | import java.time.LocalDateTime; 28 | import java.time.format.DateTimeFormatter; 29 | import java.util.ArrayList; 30 | import java.util.Collections; 31 | import java.util.List; 32 | import java.util.stream.Collectors; 33 | 34 | /** 35 | * ADP AI网关操作器 36 | */ 37 | @Service 38 | @Slf4j 39 | public class AdpAIGatewayOperator extends GatewayOperator { 40 | 41 | @Override 42 | public PageResult<APIResult> fetchHTTPAPIs(Gateway gateway, int page, int size) { 43 | return null; 44 | } 45 | 46 | @Override 47 | public PageResult<APIResult> fetchRESTAPIs(Gateway gateway, int page, int size) { 48 | return null; 49 | } 50 | 51 | @Override 52 | public PageResult<? extends GatewayMCPServerResult> fetchMcpServers(Gateway gateway, int page, int size) { 53 | AdpAIGatewayConfig config = gateway.getAdpAIGatewayConfig(); 54 | if (config == null) { 55 | throw new BusinessException(ErrorCode.INVALID_PARAMETER, "ADP AI Gateway 配置缺失"); 56 | } 57 | 58 | AdpAIGatewayClient client = new AdpAIGatewayClient(config); 59 | try { 60 | String url = client.getFullUrl("/mcpServer/listMcpServers"); 61 | // 修复:添加必需的 gwInstanceId 参数 62 | String requestBody = String.format( 63 | "{\"current\": %d, \"size\": %d, \"gwInstanceId\": \"%s\"}", 64 | page, 65 | size, 66 | gateway.getGatewayId() 67 | ); 68 | HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); 69 | 70 | ResponseEntity<AdpMcpServerListResult> response = client.getRestTemplate().exchange( 71 | url, HttpMethod.POST, requestEntity, AdpMcpServerListResult.class); 72 | 73 | if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { 74 | AdpMcpServerListResult result = response.getBody(); 75 | if (result.getCode() != null && result.getCode() == 200 && result.getData() != null) { 76 | List<GatewayMCPServerResult> items = new ArrayList<>(); 77 | if (result.getData().getRecords() != null) { 78 | items.addAll(result.getData().getRecords()); 79 | } 80 | int total = result.getData().getTotal() != null ? result.getData().getTotal() : 0; 81 | return PageResult.of(items, page, size, total); 82 | } 83 | String msg = result.getMessage() != null ? result.getMessage() : result.getMsg(); 84 | throw new BusinessException(ErrorCode.GATEWAY_ERROR, msg); 85 | } 86 | throw new BusinessException(ErrorCode.GATEWAY_ERROR, "调用 ADP /mcpServer/listMcpServers 失败"); 87 | } catch (Exception e) { 88 | log.error("Error fetching ADP MCP servers", e); 89 | throw new BusinessException(ErrorCode.INTERNAL_ERROR, e.getMessage()); 90 | } finally { 91 | client.close(); 92 | } 93 | } 94 | 95 | @Override 96 | public String fetchAPIConfig(Gateway gateway, Object config) { 97 | return ""; 98 | } 99 | 100 | @Override 101 | public String fetchMcpConfig(Gateway gateway, Object conf) { 102 | AdpAIGatewayConfig config = gateway.getAdpAIGatewayConfig(); 103 | if (config == null) { 104 | throw new BusinessException(ErrorCode.INVALID_PARAMETER, "ADP AI Gateway 配置缺失"); 105 | } 106 | 107 | // 从 conf 参数中获取 APIGRefConfig 108 | APIGRefConfig apigRefConfig = (APIGRefConfig) conf; 109 | if (apigRefConfig == null || apigRefConfig.getMcpServerName() == null) { 110 | throw new BusinessException(ErrorCode.INVALID_PARAMETER, "MCP Server 名称缺失"); 111 | } 112 | 113 | AdpAIGatewayClient client = new AdpAIGatewayClient(config); 114 | try { 115 | String url = client.getFullUrl("/mcpServer/getMcpServer"); 116 | 117 | // 构建请求体,包含 gwInstanceId 和 mcpServerName 118 | String requestBody = String.format( 119 | "{\"gwInstanceId\": \"%s\", \"mcpServerName\": \"%s\"}", 120 | gateway.getGatewayId(), 121 | apigRefConfig.getMcpServerName() 122 | ); 123 | 124 | HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); 125 | 126 | ResponseEntity<AdpMcpServerDetailResult> response = client.getRestTemplate().exchange( 127 | url, HttpMethod.POST, requestEntity, AdpMcpServerDetailResult.class); 128 | 129 | if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { 130 | AdpMcpServerDetailResult result = response.getBody(); 131 | if (result.getCode() != null && result.getCode() == 200 && result.getData() != null) { 132 | return convertToMCPConfig(result.getData(), config); 133 | } 134 | String msg = result.getMessage() != null ? result.getMessage() : result.getMsg(); 135 | throw new BusinessException(ErrorCode.GATEWAY_ERROR, msg); 136 | } 137 | throw new BusinessException(ErrorCode.GATEWAY_ERROR, "调用 ADP /mcpServer/getMcpServer 失败"); 138 | } catch (Exception e) { 139 | log.error("Error fetching ADP MCP config for server: {}", apigRefConfig.getMcpServerName(), e); 140 | throw new BusinessException(ErrorCode.INTERNAL_ERROR, e.getMessage()); 141 | } finally { 142 | client.close(); 143 | } 144 | } 145 | 146 | /** 147 | * 将 ADP MCP Server 详情转换为 MCPConfigResult 格式 148 | */ 149 | private String convertToMCPConfig(AdpMcpServerDetailResult.AdpMcpServerDetail data, AdpAIGatewayConfig config) { 150 | MCPConfigResult mcpConfig = new MCPConfigResult(); 151 | mcpConfig.setMcpServerName(data.getName()); 152 | 153 | // 设置 MCP Server 配置 154 | MCPConfigResult.MCPServerConfig serverConfig = new MCPConfigResult.MCPServerConfig(); 155 | serverConfig.setPath("/" + data.getName()); 156 | 157 | // 获取网关实例访问信息并设置域名信息 158 | List<MCPConfigResult.Domain> domains = getGatewayAccessDomains(data.getGwInstanceId(), config); 159 | if (domains != null && !domains.isEmpty()) { 160 | serverConfig.setDomains(domains); 161 | } else { 162 | // 如果无法获取网关访问信息,则使用原有的services信息作为备选 163 | if (data.getServices() != null && !data.getServices().isEmpty()) { 164 | List<MCPConfigResult.Domain> fallbackDomains = data.getServices().stream() 165 | .map(domain -> MCPConfigResult.Domain.builder() 166 | .domain(domain.getName() + ":" + domain.getPort()) 167 | .protocol("http") 168 | .build()) 169 | .collect(Collectors.toList()); 170 | serverConfig.setDomains(fallbackDomains); 171 | } 172 | } 173 | 174 | mcpConfig.setMcpServerConfig(serverConfig); 175 | 176 | // 设置工具配置 177 | mcpConfig.setTools(data.getRawConfigurations()); 178 | 179 | // 设置元数据 180 | MCPConfigResult.McpMetadata meta = new MCPConfigResult.McpMetadata(); 181 | meta.setSource(GatewayType.ADP_AI_GATEWAY.name()); 182 | meta.setCreateFromType(data.getType()); 183 | mcpConfig.setMeta(meta); 184 | 185 | return JSONUtil.toJsonStr(mcpConfig); 186 | } 187 | 188 | /** 189 | * 获取网关实例的访问信息并构建域名列表 190 | */ 191 | private List<MCPConfigResult.Domain> getGatewayAccessDomains(String gwInstanceId, AdpAIGatewayConfig config) { 192 | AdpAIGatewayClient client = new AdpAIGatewayClient(config); 193 | try { 194 | String url = client.getFullUrl("/gatewayInstance/getInstanceInfo"); 195 | String requestBody = String.format("{\"gwInstanceId\": \"%s\"}", gwInstanceId); 196 | HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); 197 | 198 | // 注意:getInstanceInfo 返回的 data 是单个实例对象(无 records 字段),直接从 data.accessMode 读取 199 | ResponseEntity<String> response = client.getRestTemplate().exchange( 200 | url, HttpMethod.POST, requestEntity, String.class); 201 | 202 | if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { 203 | cn.hutool.json.JSONObject root = JSONUtil.parseObj(response.getBody()); 204 | Integer code = root.getInt("code"); 205 | if (code != null && code == 200 && root.containsKey("data")) { 206 | cn.hutool.json.JSONObject dataObj = root.getJSONObject("data"); 207 | if (dataObj != null && dataObj.containsKey("accessMode")) { 208 | cn.hutool.json.JSONArray arr = dataObj.getJSONArray("accessMode"); 209 | List<AdpGatewayInstanceResult.AccessMode> accessModes = JSONUtil.toList(arr, AdpGatewayInstanceResult.AccessMode.class); 210 | return buildDomainsFromAccessModes(accessModes); 211 | } 212 | log.warn("Gateway instance has no accessMode, instanceId={}", gwInstanceId); 213 | return null; 214 | } 215 | String message = root.getStr("message", root.getStr("msg")); 216 | log.warn("Failed to get gateway instance access info: {}", message); 217 | return null; 218 | } 219 | log.warn("Failed to call gateway instance access API"); 220 | return null; 221 | } catch (Exception e) { 222 | log.error("Error fetching gateway access info for instance: {}", gwInstanceId, e); 223 | return null; 224 | } finally { 225 | client.close(); 226 | } 227 | } 228 | 229 | /** 230 | * 根据网关实例访问信息构建域名列表 231 | */ 232 | private List<MCPConfigResult.Domain> buildDomainsFromAccessInfo(AdpGatewayInstanceResult.AdpGatewayInstanceData data) { 233 | // 兼容 listInstances 调用:取第一条记录的 accessMode 234 | if (data != null && data.getRecords() != null && !data.getRecords().isEmpty()) { 235 | AdpGatewayInstanceResult.AdpGatewayInstance instance = data.getRecords().get(0); 236 | if (instance.getAccessMode() != null) { 237 | return buildDomainsFromAccessModes(instance.getAccessMode()); 238 | } 239 | } 240 | return new ArrayList<>(); 241 | } 242 | 243 | private List<MCPConfigResult.Domain> buildDomainsFromAccessModes(List<AdpGatewayInstanceResult.AccessMode> accessModes) { 244 | List<MCPConfigResult.Domain> domains = new ArrayList<>(); 245 | if (accessModes == null || accessModes.isEmpty()) { return domains; } 246 | AdpGatewayInstanceResult.AccessMode accessMode = accessModes.get(0); 247 | 248 | // 1) LoadBalancer: externalIps:80 249 | if ("LoadBalancer".equalsIgnoreCase(accessMode.getAccessModeType())) { 250 | if (accessMode.getExternalIps() != null && !accessMode.getExternalIps().isEmpty()) { 251 | for (String externalIp : accessMode.getExternalIps()) { 252 | if (externalIp == null || externalIp.isEmpty()) { continue; } 253 | MCPConfigResult.Domain domain = MCPConfigResult.Domain.builder() 254 | .domain(externalIp + ":80") 255 | .protocol("http") 256 | .build(); 257 | domains.add(domain); 258 | } 259 | } 260 | } 261 | 262 | // 2) NodePort: ips + ports → ip:nodePort 263 | if (domains.isEmpty() && "NodePort".equalsIgnoreCase(accessMode.getAccessModeType())) { 264 | List<String> ips = accessMode.getIps(); 265 | List<String> ports = accessMode.getPorts(); 266 | if (ips != null && !ips.isEmpty() && ports != null && !ports.isEmpty()) { 267 | for (String ip : ips) { 268 | if (ip == null || ip.isEmpty()) { continue; } 269 | for (String portMapping : ports) { 270 | if (portMapping == null || portMapping.isEmpty()) { continue; } 271 | String[] parts = portMapping.split(":"); 272 | if (parts.length >= 2) { 273 | String nodePort = parts[1].split("/")[0]; 274 | MCPConfigResult.Domain domain = MCPConfigResult.Domain.builder() 275 | .domain(ip + ":" + nodePort) 276 | .protocol("http") 277 | .build(); 278 | domains.add(domain); 279 | } 280 | } 281 | } 282 | } 283 | } 284 | 285 | // 3) fallback: only externalIps → :80 286 | if (domains.isEmpty() && accessMode.getExternalIps() != null && !accessMode.getExternalIps().isEmpty()) { 287 | for (String externalIp : accessMode.getExternalIps()) { 288 | if (externalIp == null || externalIp.isEmpty()) { continue; } 289 | MCPConfigResult.Domain domain = MCPConfigResult.Domain.builder() 290 | .domain(externalIp + ":80") 291 | .protocol("http") 292 | .build(); 293 | domains.add(domain); 294 | } 295 | } 296 | 297 | return domains; 298 | } 299 | 300 | @Override 301 | public String createConsumer(Consumer consumer, ConsumerCredential credential, GatewayConfig config) { 302 | AdpAIGatewayConfig adpConfig = config.getAdpAIGatewayConfig(); 303 | if (adpConfig == null) { 304 | throw new BusinessException(ErrorCode.INVALID_PARAMETER, "ADP AI Gateway配置缺失"); 305 | } 306 | 307 | AdpAIGatewayClient client = new AdpAIGatewayClient(adpConfig); 308 | try { 309 | // 构建请求参数 310 | cn.hutool.json.JSONObject requestData = JSONUtil.createObj(); 311 | requestData.set("authType", 5); 312 | 313 | // 从凭证中获取key 314 | if (credential.getApiKeyConfig() != null && 315 | credential.getApiKeyConfig().getCredentials() != null && 316 | !credential.getApiKeyConfig().getCredentials().isEmpty()) { 317 | String key = credential.getApiKeyConfig().getCredentials().get(0).getApiKey(); 318 | requestData.set("key", key); 319 | } 320 | 321 | requestData.set("appName", consumer.getName()); 322 | 323 | // 从 GatewayConfig 中获取 Gateway 实体,与 fetchMcpConfig 方法保持一致 324 | Gateway gateway = config.getGateway(); 325 | if (gateway == null || gateway.getGatewayId() == null) { 326 | throw new BusinessException(ErrorCode.INVALID_PARAMETER, "网关实例ID缺失"); 327 | } 328 | requestData.set("gwInstanceId", gateway.getGatewayId()); 329 | 330 | String url = client.getFullUrl("/application/createApp"); 331 | String requestBody = requestData.toString(); 332 | HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); 333 | 334 | log.info("Creating consumer in ADP gateway: url={}, requestBody={}", url, requestBody); 335 | 336 | ResponseEntity<String> response = client.getRestTemplate().exchange( 337 | url, HttpMethod.POST, requestEntity, String.class); 338 | 339 | if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { 340 | log.info("ADP gateway response: {}", response.getBody()); 341 | // 对于ADP AI网关,返回的data就是appName,可以直接用于后续的MCP授权 342 | return extractConsumerIdFromResponse(response.getBody(), consumer.getName()); 343 | } 344 | throw new BusinessException(ErrorCode.GATEWAY_ERROR, "Failed to create consumer in ADP gateway"); 345 | } catch (BusinessException e) { 346 | log.error("Business error creating consumer in ADP gateway", e); 347 | throw e; 348 | } catch (Exception e) { 349 | log.error("Error creating consumer in ADP gateway", e); 350 | throw new BusinessException(ErrorCode.INTERNAL_ERROR, 351 | "Error creating consumer in ADP gateway: " + e.getMessage()); 352 | } finally { 353 | client.close(); 354 | } 355 | } 356 | 357 | /** 358 | * 从响应中提取消费者ID 359 | * 对于ADP AI网关,/application/createApp接口返回的data就是appName(应用名称) 360 | * 我们直接返回appName,这样在授权时可以直接使用 361 | */ 362 | private String extractConsumerIdFromResponse(String responseBody, String defaultConsumerId) { 363 | try { 364 | cn.hutool.json.JSONObject responseJson = JSONUtil.parseObj(responseBody); 365 | // ADP AI网关的/application/createApp接口,成功时返回格式: {"code": 200, "data": "appName"} 366 | if (responseJson.getInt("code", 0) == 200 && responseJson.containsKey("data")) { 367 | Object dataObj = responseJson.get("data"); 368 | if (dataObj != null) { 369 | // data字段就是appName,直接返回 370 | if (dataObj instanceof String) { 371 | return (String) dataObj; 372 | } 373 | // 如果data是对象类型,则按原逻辑处理(兼容性考虑) 374 | if (dataObj instanceof cn.hutool.json.JSONObject) { 375 | cn.hutool.json.JSONObject data = (cn.hutool.json.JSONObject) dataObj; 376 | if (data.containsKey("applicationId")) { 377 | return data.getStr("applicationId"); 378 | } 379 | // 如果没有applicationId字段,将整个data对象转为字符串 380 | return data.toString(); 381 | } 382 | // 其他类型直接转为字符串 383 | return dataObj.toString(); 384 | } 385 | } 386 | // 如果无法解析,使用应用名称作为fallback 387 | return defaultConsumerId; // 这里传入的是consumer.getName() 388 | } catch (Exception e) { 389 | log.warn("Failed to parse response body, using consumer name as fallback", e); 390 | return defaultConsumerId; 391 | } 392 | } 393 | 394 | @Override 395 | public void updateConsumer(String consumerId, ConsumerCredential credential, GatewayConfig config) { 396 | AdpAIGatewayConfig adpConfig = config.getAdpAIGatewayConfig(); 397 | if (adpConfig == null) { 398 | throw new BusinessException(ErrorCode.INVALID_PARAMETER, "ADP AI Gateway配置缺失"); 399 | } 400 | 401 | Gateway gateway = config.getGateway(); 402 | if (gateway == null || gateway.getGatewayId() == null) { 403 | throw new BusinessException(ErrorCode.INVALID_PARAMETER, "网关实例ID缺失"); 404 | } 405 | 406 | AdpAIGatewayClient client = new AdpAIGatewayClient(adpConfig); 407 | try { 408 | 409 | // 从凭据中提取API Key 410 | String apiKey = null; 411 | if (credential != null 412 | && credential.getApiKeyConfig() != null 413 | && credential.getApiKeyConfig().getCredentials() != null 414 | && !credential.getApiKeyConfig().getCredentials().isEmpty()) { 415 | apiKey = credential.getApiKeyConfig().getCredentials().get(0).getApiKey(); 416 | } 417 | 418 | String url = client.getFullUrl("/application/modifyApp"); 419 | 420 | // 构建请求体 421 | cn.hutool.json.JSONObject requestData = JSONUtil.createObj(); 422 | requestData.set("appId", consumerId); 423 | requestData.set("appName", consumerId); 424 | requestData.set("authType", 5); // 固定参数 425 | requestData.set("authTypeName", "API_KEY"); 426 | requestData.set("description", consumerId); 427 | requestData.set("enable", true); // 固定参数 428 | if (apiKey != null) { 429 | requestData.set("key", apiKey); 430 | } 431 | requestData.set("groups", Collections.singletonList("true")); // 固定参数 432 | requestData.set("gwInstanceId", gateway.getGatewayId()); 433 | 434 | String requestBody = requestData.toString(); 435 | HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); 436 | 437 | log.info("Updating consumer in ADP gateway: url={}, requestBody={}", url, requestBody); 438 | 439 | ResponseEntity<String> response = client.getRestTemplate().exchange( 440 | url, HttpMethod.POST, requestEntity, String.class); 441 | 442 | if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { 443 | cn.hutool.json.JSONObject responseJson = JSONUtil.parseObj(response.getBody()); 444 | Integer code = responseJson.getInt("code", 0); 445 | if (code != null && code == 200) { 446 | log.info("Successfully updated consumer {} in ADP gateway instance {}", consumerId, gateway.getGatewayId()); 447 | return; 448 | } 449 | String message = responseJson.getStr("message", responseJson.getStr("msg", "Unknown error")); 450 | throw new BusinessException(ErrorCode.GATEWAY_ERROR, "更新ADP网关消费者失败: " + message); 451 | } 452 | throw new BusinessException(ErrorCode.GATEWAY_ERROR, "调用 ADP /application/modifyApp 失败"); 453 | } catch (BusinessException e) { 454 | throw e; 455 | } catch (Exception e) { 456 | log.error("Error updating consumer {} in ADP gateway instance {}", consumerId, 457 | gateway != null ? gateway.getGatewayId() : "unknown", e); 458 | throw new BusinessException(ErrorCode.INTERNAL_ERROR, "更新ADP网关消费者异常: " + e.getMessage()); 459 | } finally { 460 | client.close(); 461 | } 462 | } 463 | 464 | @Override 465 | public void deleteConsumer(String consumerId, GatewayConfig config) { 466 | AdpAIGatewayConfig adpConfig = config.getAdpAIGatewayConfig(); 467 | if (adpConfig == null) { 468 | throw new BusinessException(ErrorCode.INVALID_PARAMETER, "ADP AI Gateway配置缺失"); 469 | } 470 | 471 | Gateway gateway = config.getGateway(); 472 | if (gateway == null || gateway.getGatewayId() == null) { 473 | throw new BusinessException(ErrorCode.INVALID_PARAMETER, "网关实例ID缺失"); 474 | } 475 | 476 | AdpAIGatewayClient client = new AdpAIGatewayClient(adpConfig); 477 | try { 478 | 479 | String url = client.getFullUrl("/application/deleteApp"); 480 | String requestBody = String.format( 481 | "{\"appId\": \"%s\", \"gwInstanceId\": \"%s\"}", 482 | consumerId, gateway.getGatewayId() 483 | ); 484 | HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); 485 | 486 | log.info("Deleting consumer in ADP gateway: url={}, requestBody={}", url, requestBody); 487 | 488 | ResponseEntity<String> response = client.getRestTemplate().exchange( 489 | url, HttpMethod.POST, requestEntity, String.class); 490 | 491 | if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { 492 | cn.hutool.json.JSONObject responseJson = JSONUtil.parseObj(response.getBody()); 493 | Integer code = responseJson.getInt("code", 0); 494 | if (code != null && code == 200) { 495 | log.info("Successfully deleted consumer {} from ADP gateway instance {}", 496 | consumerId, gateway.getGatewayId()); 497 | return; 498 | } 499 | String message = responseJson.getStr("message", responseJson.getStr("msg", "Unknown error")); 500 | throw new BusinessException(ErrorCode.GATEWAY_ERROR, "删除ADP网关消费者失败: " + message); 501 | } 502 | throw new BusinessException(ErrorCode.GATEWAY_ERROR, "调用 ADP /application/deleteApp 失败"); 503 | } catch (BusinessException e) { 504 | throw e; 505 | } catch (Exception e) { 506 | log.error("Error deleting consumer {} from ADP gateway instance {}", 507 | consumerId, gateway != null ? gateway.getGatewayId() : "unknown", e); 508 | throw new BusinessException(ErrorCode.INTERNAL_ERROR, "删除ADP网关消费者异常: " + e.getMessage()); 509 | } finally { 510 | client.close(); 511 | } 512 | } 513 | 514 | @Override 515 | public boolean isConsumerExists(String consumerId, GatewayConfig config) { 516 | AdpAIGatewayConfig adpConfig = config.getAdpAIGatewayConfig(); 517 | if (adpConfig == null) { 518 | log.warn("ADP AI Gateway配置缺失,无法检查消费者存在性"); 519 | return false; 520 | } 521 | 522 | AdpAIGatewayClient client = new AdpAIGatewayClient(adpConfig); 523 | try { 524 | // 从 GatewayConfig 中获取 Gateway 实体 525 | Gateway gateway = config.getGateway(); 526 | if (gateway == null || gateway.getGatewayId() == null) { 527 | log.warn("网关实例ID缺失,无法检查消费者存在性"); 528 | return false; 529 | } 530 | 531 | String url = client.getFullUrl("/application/getApp"); 532 | String requestBody = String.format( 533 | "{\"%s\": \"%s\", \"%s\": \"%s\"}", 534 | "gwInstanceId", gateway.getGatewayId(), 535 | "appId", consumerId 536 | ); 537 | HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); 538 | 539 | ResponseEntity<String> response = client.getRestTemplate().exchange( 540 | url, HttpMethod.POST, requestEntity, String.class); 541 | 542 | if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { 543 | cn.hutool.json.JSONObject responseJson = JSONUtil.parseObj(response.getBody()); 544 | Integer code = responseJson.getInt("code", 0); 545 | // 如果返回200且有data,说明消费者存在 546 | return code == 200 && responseJson.containsKey("data") && responseJson.get("data") != null; 547 | } 548 | return false; 549 | } catch (Exception e) { 550 | log.warn("检查ADP网关消费者存在性失败: consumerId={}", consumerId, e); 551 | return false; 552 | } finally { 553 | client.close(); 554 | } 555 | } 556 | 557 | @Override 558 | public ConsumerAuthConfig authorizeConsumer(Gateway gateway, String consumerId, Object refConfig) { 559 | AdpAIGatewayConfig adpConfig = gateway.getAdpAIGatewayConfig(); 560 | if (adpConfig == null) { 561 | throw new BusinessException(ErrorCode.INVALID_PARAMETER, "ADP AI Gateway配置缺失"); 562 | } 563 | 564 | // 解析MCP Server配置 565 | APIGRefConfig apigRefConfig = (APIGRefConfig) refConfig; 566 | if (apigRefConfig == null || apigRefConfig.getMcpServerName() == null) { 567 | throw new BusinessException(ErrorCode.INVALID_PARAMETER, "MCP Server名称缺失"); 568 | } 569 | 570 | AdpAIGatewayClient client = new AdpAIGatewayClient(adpConfig); 571 | try { 572 | // 构建授权请求参数 573 | // 由于createConsumer返回的就是appName,所以consumerId就是应用名称 574 | cn.hutool.json.JSONObject requestData = JSONUtil.createObj(); 575 | requestData.set("mcpServerName", apigRefConfig.getMcpServerName()); 576 | requestData.set("consumers", Collections.singletonList(consumerId)); // consumerId就是appName 577 | requestData.set("gwInstanceId", gateway.getGatewayId()); 578 | 579 | String url = client.getFullUrl("/mcpServer/addMcpServerConsumers"); 580 | String requestBody = requestData.toString(); 581 | HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); 582 | 583 | log.info("Authorizing consumer to MCP server: url={}, requestBody={}", url, requestBody); 584 | 585 | ResponseEntity<String> response = client.getRestTemplate().exchange( 586 | url, HttpMethod.POST, requestEntity, String.class); 587 | 588 | if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { 589 | cn.hutool.json.JSONObject responseJson = JSONUtil.parseObj(response.getBody()); 590 | Integer code = responseJson.getInt("code", 0); 591 | 592 | if (code == 200) { 593 | log.info("Successfully authorized consumer {} to MCP server {}", 594 | consumerId, apigRefConfig.getMcpServerName()); 595 | 596 | // 构建授权配置返回结果 597 | AdpAIAuthConfig authConfig = AdpAIAuthConfig.builder() 598 | .mcpServerName(apigRefConfig.getMcpServerName()) 599 | .consumerId(consumerId) 600 | .gwInstanceId(gateway.getGatewayId()) 601 | .build(); 602 | 603 | return ConsumerAuthConfig.builder() 604 | .adpAIAuthConfig(authConfig) 605 | .build(); 606 | } else { 607 | String message = responseJson.getStr("message", responseJson.getStr("msg", "Unknown error")); 608 | throw new BusinessException(ErrorCode.GATEWAY_ERROR, 609 | "Failed to authorize consumer to MCP server: " + message); 610 | } 611 | } 612 | throw new BusinessException(ErrorCode.GATEWAY_ERROR, "Failed to authorize consumer to MCP server"); 613 | } catch (BusinessException e) { 614 | log.error("Business error authorizing consumer to MCP server", e); 615 | throw e; 616 | } catch (Exception e) { 617 | log.error("Error authorizing consumer {} to MCP server {}", 618 | consumerId, apigRefConfig.getMcpServerName(), e); 619 | throw new BusinessException(ErrorCode.INTERNAL_ERROR, 620 | "Error authorizing consumer to MCP server: " + e.getMessage()); 621 | } finally { 622 | client.close(); 623 | } 624 | } 625 | 626 | @Override 627 | public void revokeConsumerAuthorization(Gateway gateway, String consumerId, ConsumerAuthConfig authConfig) { 628 | AdpAIAuthConfig adpAIAuthConfig = authConfig.getAdpAIAuthConfig(); 629 | if (adpAIAuthConfig == null) { 630 | log.warn("ADP AI 授权配置为空,无法撤销授权"); 631 | return; 632 | } 633 | 634 | AdpAIGatewayConfig adpConfig = gateway.getAdpAIGatewayConfig(); 635 | if (adpConfig == null) { 636 | throw new BusinessException(ErrorCode.INVALID_PARAMETER, "ADP AI Gateway配置缺失"); 637 | } 638 | 639 | AdpAIGatewayClient client = new AdpAIGatewayClient(adpConfig); 640 | try { 641 | // 构建撤销授权请求参数 642 | // 由于createConsumer返回的就是appName,所以consumerId就是应用名称 643 | cn.hutool.json.JSONObject requestData = JSONUtil.createObj(); 644 | requestData.set("mcpServerName", adpAIAuthConfig.getMcpServerName()); 645 | requestData.set("consumers", Collections.singletonList(consumerId)); // consumerId就是appName 646 | requestData.set("gwInstanceId", gateway.getGatewayId()); 647 | 648 | String url = client.getFullUrl("/mcpServer/deleteMcpServerConsumers"); 649 | String requestBody = requestData.toString(); 650 | HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); 651 | 652 | log.info("Revoking consumer authorization from MCP server: url={}, requestBody={}", url, requestBody); 653 | 654 | ResponseEntity<String> response = client.getRestTemplate().exchange( 655 | url, HttpMethod.POST, requestEntity, String.class); 656 | 657 | if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { 658 | cn.hutool.json.JSONObject responseJson = JSONUtil.parseObj(response.getBody()); 659 | Integer code = responseJson.getInt("code", 0); 660 | 661 | if (code == 200) { 662 | log.info("Successfully revoked consumer {} authorization from MCP server {}", 663 | consumerId, adpAIAuthConfig.getMcpServerName()); 664 | } else { 665 | String message = responseJson.getStr("message", responseJson.getStr("msg", "Unknown error")); 666 | log.warn("Failed to revoke consumer authorization from MCP server: {}", message); 667 | // 撤销授权失败不抛异常,只记录日志 668 | } 669 | } else { 670 | log.warn("Failed to revoke consumer authorization from MCP server, HTTP status: {}", 671 | response.getStatusCode()); 672 | } 673 | } catch (Exception e) { 674 | log.error("Error revoking consumer {} authorization from MCP server {}", 675 | consumerId, adpAIAuthConfig.getMcpServerName(), e); 676 | // 撤销授权失败不抛异常,只记录日志 677 | } finally { 678 | client.close(); 679 | } 680 | } 681 | 682 | @Override 683 | public APIResult fetchAPI(Gateway gateway, String apiId) { 684 | return null; 685 | } 686 | 687 | @Override 688 | public GatewayType getGatewayType() { 689 | return GatewayType.ADP_AI_GATEWAY; 690 | } 691 | 692 | @Override 693 | public String getDashboard(Gateway gateway,String type) { 694 | return null; 695 | } 696 | 697 | @Override 698 | public PageResult<GatewayResult> fetchGateways(Object param, int page, int size) { 699 | if (!(param instanceof QueryAdpAIGatewayParam)) { 700 | throw new BusinessException(ErrorCode.INVALID_PARAMETER, "param"); 701 | } 702 | return fetchGateways((QueryAdpAIGatewayParam) param, page, size); 703 | } 704 | 705 | public PageResult<GatewayResult> fetchGateways(QueryAdpAIGatewayParam param, int page, int size) { 706 | AdpAIGatewayConfig config = new AdpAIGatewayConfig(); 707 | config.setBaseUrl(param.getBaseUrl()); 708 | config.setPort(param.getPort()); 709 | 710 | // 根据认证类型设置不同的认证信息 711 | if ("Seed".equals(param.getAuthType())) { 712 | if (param.getAuthSeed() == null || param.getAuthSeed().trim().isEmpty()) { 713 | throw new BusinessException(ErrorCode.INVALID_PARAMETER, "Seed认证方式下authSeed不能为空"); 714 | } 715 | config.setAuthSeed(param.getAuthSeed()); 716 | } else if ("Header".equals(param.getAuthType())) { 717 | if (param.getAuthHeaders() == null || param.getAuthHeaders().isEmpty()) { 718 | throw new BusinessException(ErrorCode.INVALID_PARAMETER, "Header认证方式下authHeaders不能为空"); 719 | } 720 | // 将authHeaders转换为配置 721 | List<AdpAIGatewayConfig.AuthHeader> configHeaders = new ArrayList<>(); 722 | for (QueryAdpAIGatewayParam.AuthHeader paramHeader : param.getAuthHeaders()) { 723 | AdpAIGatewayConfig.AuthHeader configHeader = new AdpAIGatewayConfig.AuthHeader(); 724 | configHeader.setKey(paramHeader.getKey()); 725 | configHeader.setValue(paramHeader.getValue()); 726 | configHeaders.add(configHeader); 727 | } 728 | config.setAuthHeaders(configHeaders); 729 | } else { 730 | throw new BusinessException(ErrorCode.INVALID_PARAMETER, "不支持的认证类型: " + param.getAuthType()); 731 | } 732 | 733 | AdpAIGatewayClient client = new AdpAIGatewayClient(config); 734 | try { 735 | String url = client.getFullUrl("/gatewayInstance/listInstances"); 736 | String requestBody = String.format("{\"current\": %d, \"size\": %d}", page, size); 737 | HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); 738 | 739 | ResponseEntity<AdpGatewayInstanceResult> response = client.getRestTemplate().exchange( 740 | url, HttpMethod.POST, requestEntity, AdpGatewayInstanceResult.class); 741 | 742 | if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { 743 | AdpGatewayInstanceResult result = response.getBody(); 744 | if (result.getCode() == 200 && result.getData() != null) { 745 | return convertToGatewayResult(result.getData(), page, size); 746 | } 747 | String msg = result.getMessage() != null ? result.getMessage() : result.getMsg(); 748 | throw new BusinessException(ErrorCode.GATEWAY_ERROR, msg); 749 | } 750 | throw new BusinessException(ErrorCode.GATEWAY_ERROR, "Failed to call ADP gateway API"); 751 | } catch (Exception e) { 752 | log.error("Error fetching ADP gateways", e); 753 | throw new BusinessException(ErrorCode.INTERNAL_ERROR, e.getMessage()); 754 | } finally { 755 | client.close(); 756 | } 757 | } 758 | 759 | private PageResult<GatewayResult> convertToGatewayResult(AdpGatewayInstanceResult.AdpGatewayInstanceData data, int page, int size) { 760 | List<GatewayResult> gateways = new ArrayList<>(); 761 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); 762 | if (data.getRecords() != null) { 763 | for (AdpGatewayInstanceResult.AdpGatewayInstance instance : data.getRecords()) { 764 | LocalDateTime createTime = null; 765 | try { 766 | if (instance.getCreateTime() != null) { 767 | createTime = LocalDateTime.parse(instance.getCreateTime(), formatter); 768 | } 769 | } catch (Exception e) { 770 | log.warn("Failed to parse create time: {}", instance.getCreateTime(), e); 771 | } 772 | GatewayResult gateway = GatewayResult.builder() 773 | .gatewayId(instance.getGwInstanceId()) 774 | .gatewayName(instance.getName()) 775 | .gatewayType(GatewayType.ADP_AI_GATEWAY) 776 | .createAt(createTime) 777 | .build(); 778 | gateways.add(gateway); 779 | } 780 | } 781 | return PageResult.of(gateways, page, size, data.getTotal() != null ? data.getTotal() : 0); 782 | } 783 | 784 | @Data 785 | public static class AdpMcpServerDetailResult { 786 | private Integer code; 787 | private String msg; 788 | private String message; 789 | private AdpMcpServerDetail data; 790 | 791 | @Data 792 | public static class AdpMcpServerDetail { 793 | private String gwInstanceId; 794 | private String name; 795 | private String description; 796 | private List<String> domains; 797 | private List<Service> services; 798 | private ConsumerAuthInfo consumerAuthInfo; 799 | private String rawConfigurations; 800 | private String type; 801 | private String dsn; 802 | private String dbType; 803 | private String upstreamPathPrefix; 804 | 805 | @Data 806 | public static class Service { 807 | private String name; 808 | private Integer port; 809 | private String version; 810 | private Integer weight; 811 | } 812 | 813 | @Data 814 | public static class ConsumerAuthInfo { 815 | private String type; 816 | private Boolean enable; 817 | private List<String> allowedConsumers; 818 | } 819 | } 820 | } 821 | } 822 | ```