Bootstrap

系统URL整合系列【不改hosts文件版】-- 代码1(springcloud-gateway动态路由)

需求

      实现URL web资源整合,实现使用一个web地址访问多个web资源

方案

      本方案使用SpringCloud Gateway实现,不需要在hosts文件加添加域名映射(也不需要定义一系列域名),通过url路径来将请求转发到不同的Web资源

      如:http://${proxyIP}:${proxyPORT}/nacos1 

             转发到

             http://${nacos1IP}:${nacos1PORT}              

1、优点

  • 不需要用户端修改hosts文件(或是域名服务支持)

2、缺点

  • session信息可能会生产覆盖,存在session失效和横向越权风险

环境

spring-cloud 2024.0.0

spring-cloud gateway 4.2.0

实现代码

pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <!-- 3.4.0  3.3.6  3.1.5  3.0.12  -->
        <version>3.4.0</version>
        <!--  lookup parent from repository  -->
        <relativePath/>
    </parent>

    <groupId>person.daizd</groupId>
    <artifactId>gwproxy</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 统一管理jar包版本 -->
    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>

        <!--   3.4.0   3.3.6  -->
        <spring-boot.version>3.4.0</spring-boot.version>
        <!-- 2024.0.0   2023.0.4    -->
        <spring-cloud.version>2024.0.0</spring-cloud.version>
        <!--  2023.0.3.2   2023.0.1.3    2022.0.0.0    2021.1   2021.0.6.1   2021.0.5.0   -->
        <spring-cloud-alibaba.version>2023.0.3.2</spring-cloud-alibaba.version>

        <!--  springcloud 2020.02 以后需要单独引入     4.1.4   4.0.5  3.1.9  3.0.6  -->
        <spring-cloud-starter-bootstrap.version>4.1.4</spring-cloud-starter-bootstrap.version>
        <!-- 0.3.0-RC 适配springboot3    0.2.12 适配springboot2    0.1.10 适配springboot1   -->
        <nacos-config-spring-boot-starter.version>0.3.0-RC</nacos-config-spring-boot-starter.version>

        <!--        <junit.version>5.8.2</junit.version>-->
        <log4j.version>1.2.17</log4j.version>
        <lombok.version>1.18.28</lombok.version>

        <commons-lang3.version>3.14.0</commons-lang3.version>
        <io.micrometer.version>1.9.17</io.micrometer.version>

        <!--  pmd-core 7.0.0:  3.25.0  3.24.0  3.23.0  3.22.0
              pmd-core 6.55.0:  3.21.2    3.13.0  3.11.0 copote  -->
        <maven-pmd-plugin.version>3.26.0</maven-pmd-plugin.version>
        <maven-clean-plugin.version>3.26.0</maven-clean-plugin.version>
        <fastjson2.version>2.0.53</fastjson2.version>
    </properties>

    <dependencies>
        <!--gateway的依赖 springcloud开发-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
            <version>${spring-cloud-starter-bootstrap.version}</version>
        </dependency>

        <!--   之前工程即没有引入 SpringCloud,也没有引入 SpringCloudAlibaba  -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${commons-lang3.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
        </dependency>

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-resolver-dns-native-macos</artifactId>
            <version>4.1.115.Final</version>
            <classifier>osx-aarch_64</classifier>
        </dependency>


        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.22</version>
<!--            <scope>test</scope>-->
        </dependency>

        <!-- 测试相关 这个包含junit5  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
                <optional>true</optional>
            </dependency>

            <dependency>
                <groupId>io.micrometer</groupId>
                <artifactId>micrometer-registry-prometheus</artifactId>
                <version>${io.micrometer.version}</version>
                <scope>compile</scope>
            </dependency>

        </dependencies>

    </dependencyManagement>


    <build>

        <plugins>
            <!-- 用于支持热部署 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <!-- 表示它将创建(fork)一个新的JVM来运行编译器。 spring boot plugin 3.0.0没有这个属性了
                    <fork>true</fork> -->
                    <addResources>true</addResources>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.3.1</version>

                <executions>
                    <execution>
                        <id>copy-resource</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${basedir}/target</outputDirectory>
                            <resources>
                                <resource>
                                    <!-- 文件地址 -->
                                    <directory>${basedir}/src/main/resources</directory>
                                    <includes>
                                        <include>config.ini</include>
                                    </includes>
                                </resource>
                                <resource>
                                    <!-- 文件地址 -->
                                    <directory>${basedir}/src/main/resources</directory>
                                    <includes>
                                        <include>cookie.png</include>
                                    </includes>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <target>${maven.compiler.target}</target>
                    <source>${maven.compiler.source}</source>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <version>3.3.1</version>
                <executions>
                    <execution>
                        <phase>verify</phase>
                        <goals>
                            <goal>jar-no-fork</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-clean-plugin</artifactId>
                <version>3.3.2</version>
            </plugin>
            <!--  PMD toolkit  -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-pmd-plugin</artifactId>
                <version>3.26.0</version>
            </plugin>

            <!--- 支持单元测试 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.5.2</version>
            </plugin>
            <!--- 代码覆盖率工具 -->
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.12</version>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.7.1</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.4.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>lib/</classpathPrefix>
                            <mainClass>person.daizd.GatewayApplication</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

配置

# 这里有很多组件:eureka、apollo、nacos、skywalking等,且每一类组件都有多套(如:有的各省一套)

# id与uri的映射
gwproxy.routes.cteureka_local=http://eureka:[email protected]:8200
gwproxy.routes.cmdb_local=http://127.0.0.1:8071

# eureka
gwproxy.routes.cteureka_sh_test=http://eureka:[email protected].***.157:8200
gwproxy.routes.sxeureka_sh_test=http://eureka:[email protected].***.157:8206
gwproxy.routes.fjeureka_sh_test=http://eureka:[email protected].***.157:8207

gwproxy.routes.zjeureka_gcgz_nmb3_prod=http://10.141.***.240:20222

# apollo
gwproxy.routes.apollo_sh_test=http://10.130.***.157:28003
gwproxy.routes.apollo_sh_sit=http://10.130.155.112:30001

gwproxy.routes.ctapollo__nmb3_prod=http://10.141.***.240:20400
gwproxy.routes.lnapollo_idc01_nmb3_prod=http://10.141.***.240:20403

# sentinel
gwproxy.routes.lnsentinel_idc01_nmb3_prod=http://10.141.***.240:23903
gwproxy.routes.jssentinel_idc01_nmb3_prod=http://10.141.***.240:23915
gwproxy.routes.ahsentinel_idc01_nmb3_prod=http://10.141.***.240:23923

# apisix
# http://${gwproxy.root_uri}/apisix/index.html?app=apisix_sh_test
gwproxy.routes.apisix_sh_test=http://10.130.***.157:9000

gwproxy.routes.ctapisix_gz_gzb1_prod=http://10.***.5.131:32065

gwproxy.routes.ctapisix_idc01_nmb3_prod=http://10.141.***.240:31600
gwproxy.routes.lnapisix_idc01_nmb3_prod=http://10.141.***.240:31603


# prometheus 
# http://${gwproxy.root_uri}/prometheus_sh_test/graph
gwproxy.routes.prometheus_sh_test=http://10.130.***.136:9090
gwproxy.routes.prometheus_idc01_gzb1c1_prod=http://10.***.32.245:30006
gwproxy.routes.prometheus_idc01_gzb1c2_prod=http://10.***.32.246:30006
gwproxy.routes.prometheus_idc01_gzb1c3_prod=http://10.***.32.247:30006

gwproxy.routes.prometheus_idc01_nmb3c1_prod=http://10.141.***.240:30006
gwproxy.routes.prometheus_idc01_nmb3c2_prod=http://10.141.***.241:30006
gwproxy.routes.prometheus_idc01_nmb3c3_prod=http://10.141.***.242:30006

gwproxy.routes.prometheus_sh_crm120=http://10.130.***.120:9090
gwproxy.routes.prometheus_sh_crm121=http://10.130.***.121:9090
gwproxy.routes.prometheus_gz_saas190=http://10.***.31.190:30900

# nacos
# http://${gwproxy.root_uri}/nacos_sh_test/nacos/#/login
gwproxy.routes.nacos_sh_test=http://10.130.***.92:18848

# gateway
gwproxy.routes.ahgateway_idc01_nmb3_prod=http://10.141.***.242:23266
gwproxy.routes.jsgateway_idc01_nmb3_prod=http://10.141.***.216:32015
gwproxy.routes.bjgateway_idc01_nmb3_prod=http://10.141.***.209:32008
gwproxy.routes.lngateway_idc01_nmb3_prod=http://10.141.***.204:30003

# elasticsearch
gwproxy.routes.elasticsearch_gz_247=http://10.***.6.247:9200

# kibana     待优化KibanaRoute1 用于同时支持kibana 直接访问和代理访问,不过官方说只能2选一,优化难度大
# /kibana_local/login?next=/kibana_local/
gwproxy.routes.kibana_local=http://esserver:5601

# /kibana_sh_test/login?next=/kibana_sh_test/
gwproxy.routes.kibana_sh_cs134_test=http://10.130.***.134:15601
gwproxy.routes.kibana_sh_jg92_test=http://10.130.***.92:5602

# /gzkibana_5601/login?next=/gzkibana_5601/
# /gzkibana_5601/login?msg=LOGGED_OUT
gwproxy.routes.kibana_gz_5601_prod=http://10.***.6.247:5601
gwproxy.routes.kibana_gz_5602_prod=http://10.***.6.247:5602
gwproxy.routes.kibana_gz_5603_prod=http://10.***.6.247:5603

# /nmkibana_5601/login?next=%2F
gwproxy.routes.kibana_nm_5601_prod=http://10.141.137.***:5601
gwproxy.routes.kibana_nm_5602_prod=http://10.141.137.***:5601
gwproxy.routes.kibana_nm_5603_prod=http://10.141.137.***:5601

# timetask  当前可参考kibana 通过改根路径解决(这里直接改应用包名)
# http://${gwproxy.root_uri}/timetask/toLogin
# http://${gwproxy.root_uri}/cttimetask_idc01_nmb3_prod/timetask/toLogin
# 因静态资源命名冲突,所以两种(改工程名与直接支持)配置不能同时存在
#
gwproxy.routes.timetask=http://10.141.***.240:22400
#gwproxy.routes.timetask-xxl-job=http://10.141.***.240:22400
gwproxy.routes.cttimetask_idc01_nmb3_prod=http://10.141.***.240:22400
gwproxy.routes.cttimetask_idc01_gzb1_prod=http://10.***.5.131:22400

# Grafana
# http://${gwproxy.root_uri}/grafana_sh_prod/login
gwproxy.routes.grafana_sh_prod=http://10.130.***.121:50001

# Ambari (Hadoop - Hbase )   
# http://${gwproxy.root_uri}/ambari_sh_prod/#/login
gwproxy.routes.ambari_sh_prod=http://10.***.6.231:8080

# kafka-map   
gwproxy.routes.kafkamap_sh_jg92_test=http://10.130.***.92:8083
gwproxy.routes.kafkamap_sh_prod=http://10.130.***.121:8093

# cmak
# /cmak_sh_jg93_test
gwproxy.routes.cmak_sh_jg93_test=http://10.130.***.93:8094
# 没启或没安装
gwproxy.routes.cmak_sh_prod=http://10.130.***.121:8094

# skywalking
gwproxy.routes.skywalking_sh_jg131_test=http://10.130.***.131:30198
# 
gwproxy.routes.skywalking_xw_nm_prod=http://10.141.138.41:31051
gwproxy.routes.skywalking_xw_gz_prod=hhttp://10.***.6.204:31051
gwproxy.routes.skywalking_idc01_nmb3_prod=hhttp://10.141.***.240:31051

# pinpoint
gwproxy.routes.pinpoint_idc01_gzb1_prod=http://10.***.5.131:20900
gwproxy.routes.pinpoint_idc01_nmb3_prod=http://10.141.137.95:20900

# redmine
gwproxy.routes.redmine_idc01_nmb3_prod=http://10.142.252.247:8080

# harbor
gwproxy.routes.harbor_idc01_gzb1_prod=http://10.***.100.18:8021

# jenkins
gwproxy.routes.jenkins_sh_cs124_prod=http://10.130.***.124:1906

# nexus
gwproxy.routes.nexus_sh_jg122_prod=http://10.130.***.122:18081

# gitlab
gwproxy.routes.gitlab_sh_jg122_prod=http://10.130.***.122:8082


# kubesphere
gwproxy.routes.kubesphere_sh_cs131_prod=http://10.130.***.131:30880


# nightingale 
# nightingale_sh_test/login   
# nightingale_sh_test/metric/explorer
# 10.130.***.120 nightingale.sh.test
# 10.130.***.120 nightingale.sh.prod
gwproxy.routes.nightingale_sh_test=http://10.130.***.136:17000
gwproxy.routes.nightingale_sh_prod=http://10.130.***.121:17000

# ~~~~~~~~~~~~~~~~   业务应用 begin   ~~~~~~~~~~~~~~~~~~~
# http://${gwproxy.root_uri}/portal1_sh_prod/login
gwproxy.routes.portal1_sh_prod=http://10.130.***.121:8082


gwproxy.routes.web2_sh_test=http://10.130.***.124:8201
# http://${gwproxy.root_uri}/web2_idc01_nmb3_prod/web2/
gwproxy.routes.web2_idc01_nmb3_prod=http://10.***.228.140:32100

配置类

/**
 * @Description: 从nacos获取配置
 * @Author: dand
 * @CreateDate: 2024/12/14 5:43 PM
 * @Version: 1.0
 */

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
@ConfigurationProperties(prefix = "gwproxy")
@Getter
@Setter
@RefreshScope
public class NacosMappingConfig {

    private Map<String, String> routes;

}

动态路由实现【重点】

# 这里涉及很多组件,本文先上eureka的代码(其他组件支持代码读者可自行注释掉),后续更新文章其他组件相关动态路由实现代码会陆续上架

package person.daizd.config;

import com.alibaba.nacos.api.exception.NacosException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import person.daizd.config.route.*;
import person.daizd.filters.factory.*;

import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/***
 * @Author dand   QQ:413881461
 * @Slogan 致敬大师,致敬未来的你
 */
@Slf4j
@Configuration
public class GatewayConfig {

    @Value("${gwproxy.root_uri}")
    private String rootUri;

    @Autowired
    private NacosMappingConfig nacosMappingConfig;

    @Autowired
    private CmdbFilterFactory1 cmdbFilterFactory1;
    @Autowired
    private CmdbFilterFactory2 cmdbFilterFactory2;

    @Autowired
    private ApisixFilterFactory1 apisixFilterFactory1;
    @Autowired
    private ApisixFilterFactory2 apisixFilterFactory2;

    @Autowired
    private EurekaFilterFactory1 eurekaFilterFactory1;
    @Autowired
    private EurekaFilterFactory2 eurekaFilterFactory2;

    @Autowired
    private N9eFilterFactory1 n9eFilterFactory1;
    @Autowired
    private N9eFilterFactory2 n9eFilterFactory2;

//    @Autowired
//    private KibanaFilterFactory1 kibanaFilterFactory1;
//    @Autowired
//    private KibanaFilterFactory2 kibanaFilterFactory2;

    @Autowired
    private TimeTaskFilterFactory1 timeTaskFilterFactory1;
    @Autowired
    private TimeTaskFilterFactory2 timeTaskFilterFactory2;

    @Bean
//    @RefreshScope
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) throws NacosException {

        log.debug("nacosMappingConfig:{}",nacosMappingConfig.getRoutes());

        // 需要换成配置中取
//        Map<String,String> map = new HashMap<String,String>();
        Map<String,String> map = nacosMappingConfig.getRoutes();
//        map.put("cteureka_local","http://eureka:[email protected]:8200");
//        map.put("cmdb_local","http://127.0.0.1:8071");

        RouteLocatorBuilder.Builder innerBuilder = builder.routes();

        Set<Map.Entry<String, String>> set = map.entrySet();
        Iterator<Map.Entry<String, String>> it = set.iterator();

        /**
         *  特别注意:
         *  1、路径名称不能与已有资源名重命
         *  2、相互之间不能是包含(子集)关系
         *
         *
         */
        // 先统一处理所有eureka
        while (it.hasNext()) {
            Map.Entry<String, String> entry = it.next();

            if(entry.getKey().contains("eureka")){
                innerBuilder = EurekaRoute.route(eurekaFilterFactory1, eurekaFilterFactory2,
                          innerBuilder,    entry,  rootUri);
            }
            // 防止不重复处理 用 else if
            else if(entry.getKey().contains("cmdb")){
                innerBuilder = CmdbRoute.route(cmdbFilterFactory1, cmdbFilterFactory2,
                        innerBuilder,    entry,  rootUri);
            }
            else if(entry.getKey().contains("sentinel")){
                innerBuilder = CommonRoute.route(
                        innerBuilder,    entry,  rootUri);
            }
            else if(entry.getKey().contains("apisix")){
                innerBuilder = ApisixRoute2.route(apisixFilterFactory1,apisixFilterFactory2,
                        innerBuilder,    entry,  rootUri);
            }
            else if(entry.getKey().contains("ddal")){
                innerBuilder = CmdbRoute.route(cmdbFilterFactory1, cmdbFilterFactory2,
                        innerBuilder,    entry,  rootUri);
            }
            /* 待优化KibanaRoute1 用于同时支持kibana 直接访问和代理访问,不过官方说只能2选一,优化难度大
               另外也可开通SSO能力后研究其他方案
               前端使用了react,采用nodejs部署
             */
            else if(entry.getKey().contains("kibana")){
                innerBuilder = KibanaRoute2.route(
                        innerBuilder,    entry,  rootUri);
            }
            else if(entry.getKey().contains("elasticsearch")){
                innerBuilder = CommonRoute.route(
                        innerBuilder,    entry,  rootUri);
            }
            else if(entry.getKey().contains("nacos")){
                innerBuilder = CmdbRoute.route(cmdbFilterFactory1, cmdbFilterFactory2,
                        innerBuilder,    entry,  rootUri);
            }
            else if(entry.getKey().contains("prometheus")){
                innerBuilder = CmdbRoute.route( cmdbFilterFactory1, cmdbFilterFactory2,
                        innerBuilder,    entry,  rootUri);
            }
            // @todo  n9e 前端使用了React和D3.js, 等官方支持修改根路径(改为与配置名一致)或 二次n9e前端工程(https://github.com/n9e/fe)
            else if(entry.getKey().contains("nightingale")){
                innerBuilder = N9eRoute.route(    n9eFilterFactory1, n9eFilterFactory2,
                        innerBuilder,    entry,  rootUri);
            }
            /*
             * 方案一: 改timetask 上下文(应用名/根路径)-- 推荐(kibana 方案)
             * 方案二: 直接支持(不调整应用) -- 目前与n9e一样存在类似(菜单路径)问题
             */
            else if(entry.getKey().contains("timetask")){
                innerBuilder = KibanaRoute2.route(
                        innerBuilder,    entry,  rootUri);

//                innerBuilder = TimeTaskRoute.route( timeTaskFilterFactory1, timeTaskFilterFactory2,
//                        innerBuilder,    entry,  rootUri);
            }
            // @todo
            else if(entry.getKey().contains("grafana")){  // /grafana_sh_prod/login
                innerBuilder = CommonRoute.route(
                        innerBuilder,    entry,  rootUri);
            }
            else if(entry.getKey().contains("ambari")){  // /ambari_sh_prod/#/login
                innerBuilder = CommonRoute.route(
                        innerBuilder,    entry,  rootUri);
            }
            // eureka √
            else if(entry.getKey().contains("kafkamap")){  // /kafkamap_sh_jg92_test/#/login
                innerBuilder = CommonRoute.route(
                        innerBuilder,    entry,  rootUri);
            }
            // cmak 存在类似n9e的菜单路径问题
            else if(entry.getKey().contains("cmak")){  // /cmak_sh_jg93_test
                innerBuilder = CommonRoute.route(
                        innerBuilder,    entry,  rootUri);
            }
            // 补充了一个js待验证
            else if(entry.getKey().contains("skywalking")){  // /skywalking_sh_jg131_test/General-Service/Services
                innerBuilder = CommonRoute.route(
                        innerBuilder,    entry,  rootUri);
            }
            // 初步让为 √
            else if(entry.getKey().contains("pinpoint")){  // /pinpoint_gcgz_gzb1_prod/#/main
                innerBuilder = CommonRoute.route(
                        innerBuilder,    entry,  rootUri);
            }
            // 120网络不通
            else if(entry.getKey().contains("redmine")){  // /redmine_gcgz_nmb3_prod/login
                innerBuilder = CommonRoute.route(
                        innerBuilder,    entry,  rootUri);
            }
            // 初步让为harbor上下文路径可以修改
            else if(entry.getKey().contains("harbor")){  // /harbor_gcgz_gzb1_prod/harbor/sign-in
                innerBuilder = CommonRoute.route(
                        innerBuilder,    entry,  rootUri);
            }
            // 能登陆,但主页面显示不正常
            else if(entry.getKey().contains("jenkins")){  // /jenkins_sh_cs124_prod/jenkins/login
                innerBuilder = CommonRoute.route(
                        innerBuilder,    entry,  rootUri);
            }
            // 登陆有问题
            else if(entry.getKey().contains("nexus")){  // /nexus_sh_jg122_prod/nexus/#welcome
                innerBuilder = CommonRoute.route(
                        innerBuilder,    entry,  rootUri);
            }
            // 静态资源访问有问题
            else if(entry.getKey().contains("gitlab")){  // /gitlab_sh_jg122_prod/users/sign_in
                innerBuilder = CommonRoute.route(
                        innerBuilder,    entry,  rootUri);
            }
            // 未知错误
            else if(entry.getKey().contains("kubesphere")){  // /kubesphere_sh_cs131_prod/login
                innerBuilder = CommonRoute.route(
                        innerBuilder,    entry,  rootUri);
            }
            // 可以登陆,静态资源加载有问题
            else if(entry.getKey().contains("opsportal")){  // /opsportal_sh_prod/login
                innerBuilder = CommonRoute.route(
                        innerBuilder,    entry,  rootUri);
            }
        };

        return innerBuilder.build();
    }

}

eureka动态路由实现类

package person.daizd.config.route;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.cloud.gateway.route.builder.GatewayFilterSpec;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.cloud.gateway.route.builder.UriSpec;
import person.daizd.filters.factory.*;

import java.util.Map;
import java.util.function.Function;

/**
 * 路由
 *
 * @Author: daizd
 * @CreateDate: 2024/12/13 9:28 PM
 * @Version: 1.0
 */
@Slf4j
public class EurekaRoute {
    public static RouteLocatorBuilder.Builder route(EurekaFilterFactory1 filterFactory1,
                                                    EurekaFilterFactory2 filterFactory2,
                                                    RouteLocatorBuilder.Builder innerBuilder,
                                                    Map.Entry<String, String> entry,
                                                    String rootUri
    ){
        // 每个eureka要配两个路由, 第一个
        innerBuilder = innerBuilder.route( entry.getKey()+"_1", // id
                r -> r.path("/"+entry.getKey()+"/**")       //   --   /cteureka/**
                        .filters( f -> f.stripPrefix(1)
                                .preserveHostHeader()
                                .saveSession()  )

                        .uri( entry.getValue() ));


        log.debug("Referer:"+rootUri+"/"+entry.getKey()+"[^\\s]*" );
        log.debug("uri:"+entry.getValue() );
        // 每个eureka要配两个路由,第二个  modifyRedirectFilterFactory
        innerBuilder.route( entry.getKey()+"_2", // id
                r -> r.header("Referer",rootUri+"/"+entry.getKey()+"[^\\s]*" )
//                        .filters( f -> f.stripPrefix(0).preserveHostHeader() )
                        .filters(new Function<GatewayFilterSpec, UriSpec>() {
                            @Override
                            public UriSpec apply(GatewayFilterSpec gatewayFilterSpec) {
                                return gatewayFilterSpec.filter(
                                        filterFactory2.apply(applyFilter("app",entry.getKey() ))
                                );
                            }
                        })
                        .uri( entry.getValue() ));
        return innerBuilder;
    }

    /**
     * 构造config
     *
     * @param headerName
     * @param headerValue
     * @return NameValueConfig
     */
    private static AbstractNameValueGatewayFilterFactory.NameValueConfig applyFilter(String headerName, String headerValue) {
        AbstractNameValueGatewayFilterFactory.NameValueConfig config = new AbstractNameValueGatewayFilterFactory.NameValueConfig();
        config.setName(headerName);
        config.setValue(headerValue);
        return config;
    }
}

GatewayFilterFactory1【保留】

package person.daizd.filters.factory;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

/**
 * 用在路由1上
 *
 * @Author: dand
 * @CreateDate: 2024/12/8 7:49 PM
 * @Version: 1.0
 *
 *  没有用到
 */
@Component
@Slf4j
public class EurekaFilterFactory1 extends AbstractNameValueGatewayFilterFactory  {

    @Value("${gwproxy.root_uri}")
    private String rootUri;

    /**
     * 1、如果是重定向则定向到配置参数(应用名)对应的路径
     * 2、如果不是重定向,且第一级路径与应用名不一致,则修改为一致
     *
     * @param config
     * @return
     */
    @Override
    public GatewayFilter apply( NameValueConfig config) {
        return (exchange, chain) ->
                chain.filter(exchange).then(Mono.fromRunnable(() -> {
                    ServerHttpRequest request = exchange.getRequest();
                    ServerHttpResponse response = exchange.getResponse();
                    HttpStatusCode statusCode = response.getStatusCode();

                    String name = config.getName();
                    String value = config.getValue();
                    String rootUrl = rootUri+"/"+config.getValue();
                    if (statusCode == HttpStatus.SEE_OTHER || statusCode == HttpStatus.FOUND) {
                        String location = response.getHeaders().getFirst(HttpHeaders.LOCATION);
                        // 修改location的逻辑
                        if(response.getHeaders().containsKey("Set-Cookie")
//                                || request.getHeaders().containsKey("Cookie")
                        ){
                            // 有 Set-Cookie  和 cookie都认为已登陆
                            response.getHeaders().set(HttpHeaders.LOCATION, rootUrl );
                            log.info("response.getHeaders().containsKey(\"Set-Cookie\") , to rootUrl:{} ",rootUrl);
                        }else{
                            response.getHeaders().set(HttpHeaders.LOCATION, rootUrl+"/signin" );
                            log.info(" else : {}/signin ",rootUrl);
                        }
                    }
                }));
    }

    @Override
    public Class<NameValueConfig> getConfigClass() {
        // 如果你有配置属性,返回配置属性的类;如果没有,返回null
        return NameValueConfig.class; // 或者 return YourConfigProperties.class;
    }
}

GatewayFilterFactory2

package person.daizd.filters.factory;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.List;

/**
 * 用在路由2上
 *
 * @Author: dand
 * @CreateDate: 2024/12/8 7:49 PM
 * @Version: 1.0
 */
@Component
@Slf4j
public class EurekaFilterFactory2 extends AbstractNameValueGatewayFilterFactory  {

    @Value("${gwproxy.root_uri}")
    private String rootUri;

    /**
     * 1、如果是重定向则定向到配置参数(应用名)对应的路径
     * 2、如果不是重定向,且第一级路径与应用名不一致,则修改为一致
     *
     * @param config
     * @return
     */
    @Override
    public GatewayFilter apply( NameValueConfig config) {
        return (exchange, chain) ->
                chain.filter(exchange).then(Mono.fromRunnable(() -> {
                    ServerHttpResponse response = exchange.getResponse();
                    HttpStatusCode statusCode = response.getStatusCode();

                    String remoteIp = exchange.getRequest().getRemoteAddress().getHostName();
                    log.info("remoteIp:"+remoteIp);

                    // "http://127.0.0.1:8089/oldcmdb";
                    String rootUrl = rootUri+"/"+config.getValue();
                    System.out.println("statusCode:"+statusCode);
//                    FluxOnAssembly a;
                    if (statusCode != HttpStatus.SEE_OTHER && statusCode != HttpStatus.FOUND) {

                        ServerHttpRequest request = exchange.getRequest();
                        URI uri = request.getURI() ;
                        String path = request.getPath().value();

                        HttpHeaders headers = request.getHeaders();
                        List<String> referer = headers.get("referer");
                        log.info("path:{}", uri.getPath() );
                        if( referer!=null
                                && referer.size()>0
                                && referer.get(0).indexOf( rootUrl ) != -1 // referer 包含:http://127.0.0.1:8089/oldcmdb
                                && uri.getPath().indexOf( "/"+config.getValue() ) == -1  // uri 不包含 /oldcmdb
                                && (uri.getPath().equalsIgnoreCase("/")
                                   || uri.getPath().equalsIgnoreCase("/lastn")   )
                                  ) //  是html页面
                        {
                            // 重定向到 RewritePath=/(?<segment>.*), /oldcmdb/$\{segment}
                            exchange.getResponse().setStatusCode(HttpStatus.FOUND);
                            exchange.getResponse().getHeaders().setLocation( URI.create( "/"+config.getValue()+ path) );
                            log.info("uri.getPath().indexOf( \".html\") ");
                        }
                    }
                }));
    }

    @Override
    public Class<NameValueConfig> getConfigClass() {
        // 如果你有配置属性,返回配置属性的类;如果没有,返回null
        return NameValueConfig.class; // 或者 return YourConfigProperties.class;
    }
}

效果说明

为了让读者更好的读懂上面的代码,下面给出静态配置实现(上面代码动态实现了该效果且更灵活)

server:
  port: 8089

gwproxy:
  root_uri: ${root_uri:http://127.0.0.1:8089}

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      httpclient:
        response-timeout: PT60S
        connect-timeout: 2000
        # 链接池配置
        pool:
          # 最大连接数
          max-connections: 10000
          # 仅对于FIXED类型,等待获取线程的最长时间(毫秒)
          acquire-timeout: 1000
          # 最大空闲时间
          max-idle-time: PT10S
          # 设置固定链接池
          type: fixed
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true


      #路由规则
      routes:

        #  ~~~~~~~~  eureka  ~~~~~~~~~~~~~~~~
        - id: cteureka_route_1 #  8100000    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: http://eureka:[email protected]:8200          #匹配后提供服务的路由地址
          predicates:
          - Path=/cteureka/**         # 断言,路径相匹配的进行路由
          filters:
          - StripPrefix=1  # 转发之前去掉第一层路径

        - id: cteureka_route_2
          uri: http://eureka:[email protected]:8200          #匹配后提供服务的路由地址
          predicates:
            - Header=Referer,${gwproxy.root_uri}/cteureka


management:
  info: # 显示任意的应用信息,默认关闭  springBoot版本:2.7.15  GA如果是<2.7.5 的版本默认是开启的
    env:
      enabled: true
  endpoints:
    web:
      exposure:
        include: "*"
      base-path: /whoami
info:
  app:
    name: gateway
    encoding: "@project.build.sourceEncoding@"
    java:
      source: "@java.version@"
      target: "@java.version@"
  company:
    name: www.asiainfo.com

debug: true

附件一:整合界面地址效果截图

附件二:springcloud gateway官方文档

Spring Cloud Gateway 帮助文档

Spring Cloud Gateway 官方首页

;