服务网关

Author Avatar
丁起男 08月 22,2021
  • 在其它设备中阅读本文章

服务网关

微服务架构的诸多问题

  • 客户端多次请求不同的微服务,增加客户端代码或配置的复杂性
  • 认证复杂,每个服务都需要独立认证
  • 存在跨域请求,在一定场景下处理相对复杂

这些问题可以借助api网关来解决

所谓的api网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等

gateway

spring cloud gateway是spring公司基于spring5,springboot2和project reactor等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一api路由管理方式。它的目标是替代Netflix zuul,其不仅提供统一的路由方式,并且基于filter链的方法提供了网关基本的功能,例如:安全、监控和限流

优点

  • 性能强劲:是第一代网关zuul的1.6倍
  • 功能强大:内置了很多实用的功能,例如转发、监控、限流等
  • 设计优雅:容易扩展

缺点

  • 其实现依赖netty与webflux,不是传统的servlet模型,学习成本高
  • 不能将其部署在tomcat、jetty等servlet容器里,只能打成jar包运行
  • 需要springboot2.0及以上的版本,才支持

使用

  1. 导入依赖

    		<!--注意:此模块不能引入 starter-web-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
    
  2. 在配置文件中添加路由规则

    spring:
      cloud:
        gateway:
          routes: #路由数组,(路由:就是指当请求满足什么样的条件时,转发到指定微服务)
            - id:  #当前路由的标识,要求唯一,默认值是uuid
              uri:  #请求最终被转发的地址
              order:  #路由的优先级,数字越小代表优先级越高
              predicates: #断言,条件判断,返回是boolean 转发请求要满足的条件
                - Path=/xxx/** #当请求路径满足path指定的规则时,此路由信息才会正常转发
              filters: #过滤器,在请求请求传递过程中,对请求做一些处理
                - StripPrefix=1 #在请求转发之前去掉一层路径
    

整合nacos

  1. 加入nacos依赖

    		<dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
    
  2. 启动类上添加注解:@EnableDiscoveryClient

  3. 添加nacos配置文件

    spring:
        nacos:
          discovery:
            server-addr: IP地址:端口号
    
  4. 修改getaway配置

    spring:
      cloud:
        gateway:
          discovery:
            locator:
              enabled: true #让gateway可以发现nacos中的微服务
          routes: 
            - id: 
              uri: lb://nacos中的服务名称 #lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
              order: 
              predicates: 
              filters: 
    

    也可以不编写routes,gateway具有默认路由

    规则:http://网关ip:网关端口号/nacos中的服务名称/具体的url路径

执行流程

基本概念

路由(route)是gateway中最基本的组件之一,表示一个具体的路由信息载体。主要定义了下面几个信息

  • id:路由标识符,区别于其它route
  • uri:路由指向的目的地uri,即客户端请求最终被转发到的微服务
  • order:用于多个route之间的排序,数值越小排序越靠前,匹配优先级越高
  • predicate:断言的作用是进行条件判断,只有断言都返回true,才会真正执行路由
  • filter:过滤器用于修改请求和响应信息

执行流程

  1. 客户端向gateway发送请求
  2. 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文
  3. 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给RoutePredicateHandlerMapping
  4. RoutePredicateHandlerMapping负责匹配路由信息,并根据路由断言判断路由是否可用
  5. 如果断言返回true,由FilteringWebHandler创建过滤器链并调用
  6. 请求会依次经过PreFilter->微服务->PostFilter的方法,最终返回响应

断言

predicate(断言)用于进行条件判断,只有断言都返回true,才会真正的执行路由

内置路由断言工厂

gateway包括许多内置的断言工厂,所有这些断言都与http请求的不同属性匹配

  • 基于datetime类型的断言工厂

    • AfterRoutePredicateFactory:接收一个日期参数,单独请求日期是否晚于指定日期
    • BeforeRoutePredicateFactory:接收一个日期参数,单独请求日期是否早于指定日期
    • BetweenRoutePredicateFactory:接收两个日期参数,判断请求日期是否在指定时间段内

    -After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]

  • 基于远程地址的断言工厂:

    • RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中

    -RemoteAddr=192.168.1.1/24

  • 基于cookie的断言工厂:

    • CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求 cookie是否具有给定名称且值与正则表达式匹配

    -Cookie=chocolate, ch.

  • 基于header的断言工厂

    • HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否

      具有给定名称且值与正则表达式匹配

    -Header=X-Request-Id, \d+

  • 基于host的断言工厂

    • HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则

    -Host=**.testhost.org

  • 基于Method请求方法的断言工厂

    • MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配

    -Method=GET

  • 基于Path请求路径的断言工厂

    • PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则

    -Path=/foo/

  • 基于Query请求参数的断言工厂

    • QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具

      有给定名称且值与正则表达式匹配

    -Query=baz, ba.

  • 基于路由权重的断言工厂

    • WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发

    -Weight=group3, 1

自定义路由断言

案例:指定年龄在指定范围内的人,可用访问

/**
 * 自定义路由断言工厂类
 * 要求:1.名字必须是 配置+RoutePredicateFactory
 *       2.必须继承AbstractRoutePredicateFactory<配置类>
 */
@Component
public class AgeRoutePredicateFactory
        extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {

    /**
     * 构造函数
     */
    public AgeRoutePredicateFactory() {
        super(AgeRoutePredicateFactory.Config.class);
    }

    /**
     * 读取配置文件中参数值,给他赋值到配置类中的属性上
     * @return
     */
    @Override
    public List<String> shortcutFieldOrder() {
        //这个位置的顺序必须跟配置文件中的值顺序对应
        return Arrays.asList("minAge","maxAge");
    }

    /**
     * 断言逻辑
     * @param config
     * @return
     */
    @Override
    public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) {
        return new Predicate<ServerWebExchange>() {
            @Override
            public boolean test(ServerWebExchange serverWebExchange) {
                //接收前台传入age参数
                String ageStr = serverWebExchange.getRequest().getQueryParams().getFirst("age");
                //判断是否为空
                if (ageStr ==null || "".equals(ageStr)){
                    return false;
                }
                //逻辑判断
                int age = Integer.parseInt(ageStr);
                if (age > config.getMaxAge() || age < config.getMinAge()){
                    return false;
                }
                return true;
            }
        };
    }

    /**
     * 配置类,用于接收配置文件中的对应参数
     */
    @Data
    public static class Config{
        private int minAge;
        private int maxAge;
    }
}

使用

predicates: 
	- Age=18,60 #自定义断言,年龄18-60可用访问

过滤器

过滤器就是在请求的传递过程中,对请求和响应做一些操作

在gateway中,filter的生命周期有两个

  • pre:请求由网关发往微服务时调用
  • post:请求由微服务响应回网关时调用

过滤器分为:

  • GatewayFilter(局部过滤器):应用到单个路由或者一个分组的路由上
  • GlobalFilter(全局过滤器):应用到所有的路由上

自定义局部过滤器

案例:控制各种日志的开关

编写配置类

@Component
public class LogGatewayFilterFactory
        extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {

    /**
     * 构造
     */
    public LogGatewayFilterFactory(){
        super(LogGatewayFilterFactory.Config.class);
    }

    /**
     * 读取配置文件的参数到配置类中
     * @return
     */
    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("consoleLog","cacheLog");
    }

    /**
     * 过滤器逻辑
     * @param config
     * @return
     */
    @Override
    public GatewayFilter apply(Config config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                if (config.isConsoleLog()){
                    System.out.println("consoleLog已开启");
                }
                if (config.isCacheLog()){
                    System.out.println("cacheLog已开启");
                }
                return chain.filter(exchange);
            }
        };
    }

    /**
     * 配置类
     */
    @Data
    public static class Config{
        private boolean consoleLog;
        private boolean cacheLog;
    }
}

使用

filters: 
	- Log=true,false

自定义全局过滤器

内置的过滤器已经可用完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现

案例:权限校验

编写过滤器:

@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    /**
     * 编写过滤器逻辑
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取token
        String token = exchange.getRequest().getQueryParams().getFirst("token");
        //认证失败
        if (!"admin".equals(token)){
            log.info("认证失败");
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        //放行
        return chain.filter(exchange);
    }

    /**
     * 用来标识当前过滤器的优先级
     * 返回值越小,优先级越高
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

全局过滤器不需要再配置文件中手动设置,即可生效

限流

从1.6.0版本开始,sentinel提供了gateway的适配模块,可用提供两种资源维度的限流

  • route维度:即在spring配置文件中配置的路由条目,资源名对应的routeId
  • 自定义api维度:用户可用利用sentinel提供的api来自定义一些分组

route维度

  1. 导入依赖
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
        </dependency>

  1. 编写配置类

基于Sentinel 的Gateway限流是通过其提供的Filter来完成的,使用时只需注入对应的

SentinelGatewayFilter实例以及 SentinelGatewayBlockExceptionHandler 实例即可

@Configuration
public class GatewayConfig {

    private final List<ViewResolver> viewResolvers;

    private final ServerCodecConfigurer serverCodecConfigurer;

    public GatewayConfig(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    // 初始化一个限流的过滤器
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    // 配置初始化的限流参数
    @PostConstruct
    public void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        rules.add(
                new GatewayFlowRule("product_route") //资源名称,对应路由id
                .setCount(1) // 限流阈值
                .setIntervalSec(1)); // 统计时间窗口,单位是秒,默认是 1 秒
        GatewayRuleManager.loadRules(rules);
    }

    // 配置限流的异常处理器
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    // 自定义限流异常页面
    @PostConstruct
    public void initBlockHandlers() {
        BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
            public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                Map map = new HashMap<>();
                map.put("code", 0);
                map.put("message", "接口被限流了");
                return ServerResponse.status(HttpStatus.OK).
                        contentType(MediaType.APPLICATION_JSON_UTF8).
                        body(BodyInserters.fromObject(map));
            }
        };
        GatewayCallbackManager.setBlockHandler(blockRequestHandler);
    }
}

自定义api维度

配置类

@Configuration
public class GatewayConfig {

    private final List<ViewResolver> viewResolvers;

    private final ServerCodecConfigurer serverCodecConfigurer;

    public GatewayConfig(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    // 初始化一个限流的过滤器
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    // 配置初始化的限流参数
    @PostConstruct
    public void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        rules.add(new GatewayFlowRule("product_api1") //分组名
                  .setCount(1) // 限流阈值
                  .setIntervalSec(1)); // 统计时间窗口,单位是秒,默认是 1 秒
        rules.add(new GatewayFlowRule("product_api2").setCount(1).setIntervalSec(1));
        GatewayRuleManager.loadRules(rules);
    }

    // 配置限流的异常处理器
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    // 自定义限流异常页面
    @PostConstruct
    public void initBlockHandlers() {
        BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
            public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                Map map = new HashMap<>();
                map.put("code", 0);
                map.put("message", "接口被限流了");
                return ServerResponse.status(HttpStatus.OK).
                        contentType(MediaType.APPLICATION_JSON_UTF8).
                        body(BodyInserters.fromObject(map));
            }
        };
        GatewayCallbackManager.setBlockHandler(blockRequestHandler);
    }

    //自定义API分组
    @PostConstruct
    private void initCustomizedApis() {
        Set<ApiDefinition> definitions = new HashSet<>();
        //定义分组1
        ApiDefinition api1 = new ApiDefinition("product_api1")//分组名
                //设置规则,可用设置多个规则
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    //设置一个规则 以/product-serv/product/api1 开头的请求
                    add(new ApiPathPredicateItem().setPattern("/product-serv/product/api1/**").
                            setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
                }});
        //定义分组2
        ApiDefinition api2 = new ApiDefinition("product_api2")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    // 以/product-serv/product/api2/demo1 完成的url路径匹配
                    add(new ApiPathPredicateItem().setPattern("/product-serv/product/api2/demo1"));
                }});
        definitions.add(api1);
        definitions.add(api2);
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }
}