SpringCloud Gateway 入门到精通

一、概念快速理解
1. 什么是微服务网关

微服务网关是整个微服务API请求的入口,可以实现日志拦截、权限控制、解决跨域问题、
限流、熔断、负载均衡、黑名单与白名单拦截、授权等。

  • 1.日志拦截(统一请求/响应入口日志拦截,非业务模块请求日志)
  • 2.权限控制(请求拦截动态路由统一身份认证中心鉴权)
  • 3.解决跨域问题(统一跨域处理)
  • 4.限流/熔断(保护内部真实业务系统)
  • 5.负载均衡(类似于nginx配置权重,通过nacos获取服务地址列表进行负载均衡)
  • 6.黑名单与白名单拦截、授权
  • 7.解决代码冗余性的问题

微服务网关与nginx非常相似的。
nginx网关通过Nginx+lua实现

具备的功能:

  • 1.反向代理(动态配置)
  • 2.对api接口限流
  • 3.负载均衡策略。
2. 为什么需要使用微服务网关

微服务网关的基本概念:客户端所有的请求统一都先到达我们
微服务网关,微服务网关在转发到真实服务IP。
如果微服务网关宕机 导致整个微服务的api接口是无法访问的。

在这里插入图片描述

3. Nginx与微服务网关的区别

相同点:都能做- 权限控制

  • 解决跨域问题
  • 限流/熔断
  • 负载均衡
  • 黑名单与白名单拦截、授权

不同点:
1.Nginx—负载均衡 使用C语言编写,使用nginx实现我网关需要额外学习lua脚本增加语言的成本;
而微服务网关gateway是用-java编写的。可以根据提供的api定制化我们的微服务网关

4. 过滤器与网关的区别

1.过滤器用于拦截单个服务 Servlet
2.网关拦截整个的微服务 解决每个服务中 写过滤器重复代码

5. Zuul与Gateway有那些区别

1.Zuul网关属于Netflix公司开源的产品属于第一代微服务网关
2.Gateway属于SpringCloud自研发的第二代微服务网关
3.相比来说SpringCloudGateway性能比Zuul性能要好:
4.注意:Zuul基于Servlet实现的,阻塞式的Api, 不支持长连接。
5.SpringCloudGateway基于Spring5构建WebFlux,底层基于netty实现的,能够实现响应式非阻塞式的Api,支持长连接,能够更好的整合Spring体系的产品。

Web—基于 servlet实现
Gateway底层基于netty实现

6. Gateway中几个重要的概念

1.Route(路由):
路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由,目标URI会被访问。

2.Predicate(断言):
这是一个java 8的Predicate,可以使用它来匹配来自HTTP请求的任何内容,如:请求头和请求参数。断言的输入类型是一个ServerWebExchange。

3.Filter(过滤器):
指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者后对请求进行修改。

简单理解:
web 请求,通过一些匹配条件,定位到真正的服务节点,并在这个转发过程的前后,进行一些精细化控制

predicate 就是我们的匹配条件

filter:就可以理解为一个无所不能的拦截器,有了这两个元素,再加上目标的uri,就可以实现一个具体的路由了。

在这里插入图片描述

二、简单gateway网关
2.1. Maven依赖
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
        <version>2.0.0.RELEASE</version>
    </dependency>
</dependencies>
2.2. application配置
server:
  port: 80
####服务网关名称
spring:
  application:
    name: gblfy-gateway
  cloud:
    gateway:
      discovery:
        locator:
          ####开启以服务id去注册中心上获取转发地址
          enabled: true
        ###路由策略
      routes:
        ###路由id
        - id: gblfy-http
          ####转发http://www.gblfy.com/
          uri: http://www.gblfy.com/
          ###匹配规则
          predicates:
            - Path=/gblfy-http/**

2.3. 启动网关
package com.gblfy;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GblfyGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GblfyGatewayApplication.class, args);
    }

}

如果报错:网关依赖中 不能够引入:

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

控制台异常信息提示

Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway at this time. Please remove spring-boot-starter-web dependency.
三、Gateway整合Nacos实现服务转发
3.1. Maven依赖
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
        <version>2.0.0.RELEASE</version>
    </dependency>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        <version>0.2.2.RELEASE</version>
    </dependency>
</dependencies>
3.2. application配置
server:
  port: 80
####服务网关名称
spring:
  application:
    name: gblfy-gateway
  cloud:
    gateway:
      discovery:
        locator:
          ####开启以服务id去注册中心上获取转发地址
          enabled: true
        ###路由策略
      routes:
        ###路由id
        - id: gblfy-http
          ####转发http://www.gblfy.com/
          uri: http://www.gblfy.com/
          ###匹配规则
          predicates:
            - Path=/gblfy-http/**
              ###路由id
        - id: member
      #### 基于lb负载均衡形式转发
        uri: lb://gblfy-member
        filters:
          - StripPrefix=1
      ###匹配规则
        predicates:
          - Path=/gblfy-member/**
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
3.3. 参数说明

StripPrefix=1
127.0.0.1/gblfy-member/getMember
127.0.0.1:8081/getMember
StripPrefix参数表示在将请求发送到下游之前从请求中剥离的路径个数。

路由类型:
1.走转发http协议
2.Lb 走微服务内部根据服务名称 获取接口地址

四、Gateway如何设计动态网关(思路+源码分析)
4.1. 实现动态网关思路

1.动态网关路由配置数据源—mysql中 Redis或者nacos
2.直接Gateway Api接口 可以直接动态对网关配置做增删改查。
3.private RouteDefinitionWriter routeDefinitionWriter;

4.2. 源码分析

gateway.config配置都是给这个配置类填充配置信息

org.springframework.cloud.gateway.config.GatewayProperties

在这里插入图片描述
查看org.springframework.cloud.gateway.route.RouteDefinition引用可以定位到org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint这个类
在这里插入图片描述
查看类中定义的方法,已经实现了对路由配置信息的增删改查刷新等方法
在这里插入图片描述
具体操作路由配置信息增删改查刷新的类是通过注入RouteDefinitionWriter实现的
在这里插入图片描述
在这里插入图片描述

4.3. 网关路由配置如何读取到 内存中

1.当我们springboot启动成功时:读取数据库db中数据源路由策略
刷新到内存中。
2.Gateway 需要提供 对路由配置 做增删改查API接口。----权限管理。

4.4. 网关全局异常捕获

当访问网关出现异常时友好提示,需要整合 网关全局捕获异常

4.5. 网关集群如何刷新路由策略

分析:网关将配置信息加载到内存中,如果网关是集群的话,会有以下二种场景:
第一种场景:网关节点项目启动
每个网关节点的内存中的配置信息初始化加载时可以通过项目启动,回调方法实现配置信息加载到内存

第二种场景:网关节点已启动
如果配置信息也就是网关路由信息如果我们想修改发生改变了呢?如果想实现每个网关节点的加载的路由信息是相同的呢?

问题:如何是网关节点集群环境 如何刷新路由策略呢?

实现思路:
因为我们的网关都是注册nacos上 服务的名称都是相同的。

1.编写代码 根据服务名称从nacos上获取网关的接口地址
2.循环动态刷新 每个网关接口即可

也就是当路由信息发生变化时,先从nacos服务注册中心根据网关服务名称获取网关服务名称对应的网关节点列表信息,再循环遍历每一个网关节点信息,依次发送rest请求调用网关更新路由信息接口从而实现更新网关路由信息

五、Gateway如何设计动态网关(实战)
5.1. Mysql 表数据结构
CREATE TABLE `gblfy_gateway` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `route_id` varchar(100) DEFAULT NULL,
  `route_name` varchar(255) DEFAULT NULL,
  `route_pattern` varchar(255) DEFAULT NULL,
  `route_type` varchar(255) DEFAULT NULL,
  `route_url` varchar(255) DEFAULT NULL,
  `is_delete` int(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

INSERT INTO `gblfy_gateway` VALUES ('1', 'gblfy-member', 'gblfy-member', '/gblfy-member/**', 'lb', 'lb://gblfy-member', '0');
INSERT INTO `gblfy_gateway` VALUES ('2', 'gblfy-http', 'gblfy-http', '/gblfy-http/**', 'http', 'http://www.gblfy.com', '0');

5.2. 数据库层相关代码
package com.gblfy.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gblfy.entity.GateWayEntity;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import java.util.List;

public interface GblfyGatewayMapper extends BaseMapper<GateWayEntity> {

    @Select("SELECT ID AS ID, route_id as routeid, route_name as routeName,route_pattern as routePattern\n" +
            ",route_type as routeType,route_url as routeUrl\n" +
            " ,is_delete as isDelete  FROM gblfy_gateway where is_delete='0'")
    List<GateWayEntity> gateWayAll();


    @Update("update gblfy_gateway set route_url=#{routeUrl} where route_id=#{routeId} and is_delete='0';")
    Integer updateGateWay(@Param("routeId") String routeId,
                          @Param("routeUrl") String routeUrl);
}

5.3. 路由配置类
package com.gblfy.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

/**
 * 路由配置信息
 *
 * @author gblfy
 * @date 2023-04-15
 */
@Data
@TableName("gblfy_gateway")
public class GateWayEntity {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    private String routeId;
    private String routeName;
    private String routePattern;
    private String routeType;
    private String routeUrl;
    @TableLogic
    private int isDelete;
}

5.4. 动态网关刷新代码

Springboot启动加载网关配置,将此配置加载到内存中

package com.gblfy.start;

import com.gblfy.service.GblfyGatewayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Component;

/**
 * Springboot启动加载网关配置
 *
 * @author gblfy
 * @date 2023-04-15
 */
@Slf4j
@Component
public class GblfyApplicationEventPublisherAware implements ApplicationEventPublisherAware {
    @Autowired
    private GblfyGatewayService gblfyGatewayService;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        log.info("<网关配置开始读取到内存中...>");
        gblfyGatewayService.init();
        log.info("<网关配置结束读取到内存中...>");
    }
}

package com.gblfy.service;

import com.gblfy.constant.MayiktConstant;
import com.gblfy.entity.GateWayEntity;
import com.gblfy.mapper.GblfyGatewayMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Springboot启动加载网关配置
 *
 * @author gblfy
 * @date 2023-04-15
 */
@Slf4j
@Component
public class GblfyGatewayService implements ApplicationEventPublisherAware {
    @Autowired
    private GblfyGatewayMapper gblfyGatewayMapper;
    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;
    private ApplicationEventPublisher publisher;

    /**
     * 初始化 网关配置
     */
    public void init() {
        // 1.读取数据库中网关路由配置
        List<GateWayEntity> gateWayEntities = gblfyGatewayMapper.gateWayAll();
        // 2.加载网关配置内存中
        gateWayEntities.forEach((gateWayEntity -> {
            // 加载网关内存配置中。
            addConfig(gateWayEntity);
        }));
    }

    /**
     * 新增配置
     */
    public void addConfig(GateWayEntity gateWayEntity) {
        RouteDefinition definition = new RouteDefinition();
        Map<String, String> predicateParams = new HashMap<>(8);
        PredicateDefinition predicate = new PredicateDefinition();
        FilterDefinition filterDefinition = new FilterDefinition();
        Map<String, String> filterParams = new HashMap<>(8);
        URI uri = null;
        if (MayiktConstant.ROUTE_TYPE_LB.equals(gateWayEntity.getRouteType())) {
            uri = UriComponentsBuilder.fromUriString(gateWayEntity.getRouteUrl()).build().toUri();
        } else {
            uri = UriComponentsBuilder.fromHttpUrl(gateWayEntity.getRouteUrl()).build().toUri();
        }
        // 定义的路由唯一的id
        definition.setId(gateWayEntity.getRouteId());
        predicate.setName("Path");
        //  定义拦截匹配规则
        predicateParams.put("pattern", gateWayEntity.getRoutePattern());
        predicate.setArgs(predicateParams);
        // 名称是固定的, 路径去前缀
        filterDefinition.setName("StripPrefix");
        filterParams.put("_genkey_0", "1");
        filterDefinition.setArgs(filterParams);
        definition.setPredicates(Arrays.asList(predicate));
        definition.setFilters(Arrays.asList(filterDefinition));
        definition.setUri(uri);
        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        // 刷新网关路由配置 生效---
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
        log.info("新增成功:" + gateWayEntity.toString());
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }

/**
     * 根据路由ID删除路由信息
     *
     * @param routeId
     */
    public void deleteConfig(String routeId) {
        this.routeDefinitionWriter.delete(Mono.just(routeId))
                .then(Mono.defer(() -> Mono.just(ResponseEntity.ok().build())))
                .onErrorResume(t -> t instanceof NotFoundException, t -> Mono.just(ResponseEntity.notFound().build())).subscribe();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
    }
}

5.5. 提供API接口
package com.gblfy.service;

import com.gblfy.entity.GateWayEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 新增/删除路由配置信息api
 *
 * @author gblfy
 * @date 2023-04-15
 */
@RestController
public class ApiGatewayService {

    @Autowired
    private GblfyGatewayService gblfyGatewayService;

    @RequestMapping("/deleteGateway")
    public String deleteGateway(String routeId) {
        gblfyGatewayService.deleteConfig(routeId);
        return "ok";
    }

    @RequestMapping("/addGateway")
    public String addGateway(@RequestBody GateWayEntity gateWayEntity) {
        gblfyGatewayService.addConfig(gateWayEntity);
        return "ok";
    }
}

5.6. 测试API接口
{
"routeId":"gblfy-member",
"routeName":"gblfy-member",
"routePattern":"/gblfy-member/**",
"routeType":"lb",
"routeUrl":"lb://gblfy-member"
}
5.7. 自定义微服务网关全局捕获异常
  • 网关全局异常配置类
package com.gblfy.error;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;

import java.util.Collections;
import java.util.List;

/**
 * 网关全局异常配置类
 *
 * @author gblfy
 * @date 2023-04-15
 */
@Configuration
@EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class})
public class ErrorHandlerConfiguration {

    private final ServerProperties serverProperties;

    private final ApplicationContext applicationContext;

    private final ResourceProperties resourceProperties;

    private final List<ViewResolver> viewResolvers;

    private final ServerCodecConfigurer serverCodecConfigurer;

    public ErrorHandlerConfiguration(ServerProperties serverProperties,
                                     ResourceProperties resourceProperties,
                                     ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                     ServerCodecConfigurer serverCodecConfigurer,
                                     ApplicationContext applicationContext) {
        this.serverProperties = serverProperties;
        this.applicationContext = applicationContext;
        this.resourceProperties = resourceProperties;
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
        JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(
                errorAttributes,
                this.resourceProperties,
                this.serverProperties.getError(),
                this.applicationContext);
        exceptionHandler.setViewResolvers(this.viewResolvers);
        exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
        exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
        return exceptionHandler;
    }

}

  • 自定义网关全局异常处理
package com.gblfy.error;

import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.server.*;

import java.util.HashMap;
import java.util.Map;

/**
 * 自定义网关全局异常处理
 *
 * @author gblfy
 * @date 2023-04-15
 */
public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {

    public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
                                ErrorProperties errorProperties, ApplicationContext applicationContext) {
        super(errorAttributes, resourceProperties, errorProperties, applicationContext);
    }

    /**
     * 获取异常属性
     */
    @Override
    protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
        int code = 500;
        Throwable error = super.getError(request);
        if (error instanceof org.springframework.cloud.gateway.support.NotFoundException) {
            code = 404;
        }
        return response(code, "网关已经关闭该路由策略或者请求地址错误!");
    }

    /**
     * 指定响应处理方法为JSON处理的方法
     *
     * @param errorAttributes
     */
    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }

    /**
     * 根据code获取对应的HttpStatus
     *
     * @param errorAttributes
     */
    @Override
    protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
        int statusCode = (int) errorAttributes.get("code");
        return HttpStatus.valueOf(statusCode);
    }

    /**
     * 构建异常信息
     *
     * @param request
     * @param ex
     * @return
     */
    private String buildMessage(ServerRequest request, Throwable ex) {
        StringBuilder message = new StringBuilder("Failed to handle request [");
        message.append(request.methodName());
        message.append(" ");
        message.append(request.uri());
        message.append("]");
        if (ex != null) {
            message.append(": ");
            message.append(ex.getMessage());
        }
        return message.toString();
    }

    /**
     * 构建返回的JSON数据格式
     *
     * @param status       状态码
     * @param errorMessage 异常信息
     * @return
     */
    public static Map<String, Object> response(int status, String errorMessage) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", status);
        map.put("message", errorMessage);
        map.put("data", null);
        return map;
    }

}

5.8. Gateway过滤器
  • 使用GatewayToken验证
package com.gblfy.filter;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

/**
 * 网关过滤器
 *
 * @author gblfy
 * @date 2023-04-15
 */
@Component
public class TokenFilter implements GlobalFilter {
    @Value("${server.port}")
    private String serverPort;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取到ServerHttpRequest
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        List<String> token = request.getHeaders().get("token");
        if (token == null || token.size() == 0) {
            String msg = "token not is null ";
            DataBuffer buffer = response.bufferFactory().wrap(msg.getBytes());
            return response.writeWith(Mono.just(buffer));
        }
        // 网关转发到 会员服务时 会在请求头中传递的 网关端口号
        request = exchange.getRequest().mutate().header("gateway.port", serverPort).build();
        // 传递了token
        // 放行请求
        //        return chain.filter(exchange);
        return chain.filter(exchange.mutate().request(request).build());
    }
}

5.9. 动态网关demo
package com.gblfy;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * 动态网关demo
 *
 * @author gblfy
 * @date 2023-04-15
 */
@RestController
public class GatewayService implements ApplicationEventPublisherAware {
    private ApplicationEventPublisher publisher;
    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;

    @RequestMapping("addGateway")
    public String addGateway() {
        RouteDefinition definition = new RouteDefinition();
        Map<String, String> predicateParams = new HashMap<>(8);
        PredicateDefinition predicate = new PredicateDefinition();
        FilterDefinition filterDefinition = new FilterDefinition();
        Map<String, String> filterParams = new HashMap<>(8);
        URI uri = UriComponentsBuilder.fromUriString("lb://mayikt-member/").build().toUri();
        // 定义的路由唯一的id
        definition.setId("mayikt-member");
        predicate.setName("Path");
        //拦截请求匹配规则
        predicateParams.put("pattern", "/mayikt-member/**");
        predicate.setArgs(predicateParams);
        // 名称是固定的, 路径去前缀
        filterDefinition.setName("StripPrefix");
        filterParams.put("_genkey_0", "1");
        filterDefinition.setArgs(filterParams);

        definition.setPredicates(Arrays.asList(predicate));
        definition.setFilters(Arrays.asList(filterDefinition));
        definition.setUri(uri);
        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
        return "ok";
    }

    /**
     * 路由配置 最终都是存放在 Map集合中()
     * 底层基于 webflux实现
     *
     * @param routeId
     * @return
     */
    @RequestMapping("deleteGateway")
    public String deleteGateway(String routeId) {
//        routeDefinitionWriter.delete(Mono.just(routeId));
        this.routeDefinitionWriter.delete(Mono.just(routeId))
                .then(Mono.defer(() -> Mono.just(ResponseEntity.ok().build())))
                .onErrorResume(t -> t instanceof NotFoundException, t -> Mono.just(ResponseEntity.notFound().build())).subscribe();
        // 刷新路由配置
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
        return "ok";
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }
}

六、Gateway集群环境配置
6.1. 请求流向

在这里插入图片描述

6.2. Nginx配置

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;
	upstream backserver {
		server 192.168.0.105:81 weight=1;
		server 192.168.0.105:82 weight=1;
	}
    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location /{
		 proxy_pass http://backserver/;
		 proxy_set_header   Host             $host;
		 proxy_set_header   X-Real-IP        $remote_addr;						
		 proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
         index  index.html index.htm;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

6.3. 会员服务
/**
 * 会员服务提供接口
 *
 * @return
 */
@RequestMapping("/getMember")
public String getMember(HttpServletRequest request, @RequestHeader("gateway.port")String gatewayPort) {
    return "我是会员服务接口...端口:" + serverPort + ",请求IP来源:" + RemortIP.getClientIP(request) + ",转发端口来自:" + gatewayPort;
}
  • 测试
http://192.168.0.105/gblfy-member/getMember?token=111
七、Gateway如何获取用户真实IP
7.1. 模拟请求
curl -H "token:644064"  http://192.168.0.105/gblfy-member/getMember

从请求日志中可以看到在业务系统中获取的请求ip地址是gateway网关的服务器IP,实际请求的客户端IP是获取不到的,因为和业务系统进行真实交互的是网关节点,如何解决呢?

7.2. 实现思路

架构请求流向:浏览器(客户端)->nginx->网关->业务系统

  • 请求流向分析:
    真实的客户端与nginx进行交互,因此,需要想办法让nginx获取浏览器客户端的请求IP,然后携带真实客户端请求i平请求网关,网关携带nginx传递过来的IP参数进行请求业务系统就可以实现,获取真实客户端IP的目的。

  • 实现思路

  • 1.nginx获取浏览器客户端IP地址,并设置到请求头中,携带请求头参数请求网关服务节点

  • 2.网关服务节点获取nginx请求头中的IP参数,设置到请求头中,请求业务系统

  • 3.业务系统从请求投中获取实际浏览器客户端请求IP地址即可

注意事项:
1.nginx中设置请求头的参数建议自定义key,例如:X-Real-IP
2.网关需要从请求头中获取nginx传递过来的X-Real-IP参数

7.3. 构建nginx环境

Nginx在请求头中设置存放用户的真实的IP

location /{
		 proxy_pass http://backserver/;
		 proxy_set_header   Host             $host;
		 proxy_set_header   X-Real-IP        $remote_addr;						
		 proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
         index  index.html index.htm;
        }
7.4. 会员服务获取用户真实IP
@RequestMapping("/getMember")
public String getMember(HttpServletRequest request) {
    String xRealIp = request.getHeader("X-Real-IP");
    String gatewayPort = request.getHeader("gateway.port");
    return "我是会员服务接口...端口:" + serverPort + ",请求IP来源:" + xRealIp + ",转发端口来自:" + gatewayPort;
}
7.5. 测试
curl -H "token:644064"  http://192.168.0.105/gblfy-member/getMember

符合预期。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gblfy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值