一、网关(Gateway)

1.1 什么是网关(Gateway)?

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

1.2 为什么需要网关?

微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端(pc androud ios 平板)要如何去调用这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调用。总的来说,网关具有以下功能:

  • 协议转换,路由转发
  • 流量聚合,对流量进行监控,日志输出
  • 作为整个系统的钱罐,对流量进行控制,有限流的作用
  • 作为系统的前端界面,外部流量只能通过网关才能访问系统
  • 可以在网关层做权限判断
  • 可以在网关层做缓存

1.3 Gateway快速入门

增强版

要求功能:客户端访问api网关,网关将请求转到商品微服务

步骤一:创建一个api-gateway工程(微服务),并在pom文件中,引入gateway依赖

java 中springcloud 单独一个controller拦截器 spring cloud gateway 拦截器_gateway

步骤二:创建网关的主启动类

java 中springcloud 单独一个controller拦截器 spring cloud gateway 拦截器_gateway_02

步骤三:修改配置文件

#普通版

server:
  port: 8000

spring:
  cloud:
    gateway:
      routes:
        - id: shop-product #路由的唯一标识,只要不重复都可以,如果不写默认的是通过UUID产生的,一般写成被路由服务的名称
          uri: http://locahhost/8081 #被路由的地址
          order: 0 #优先级,数字越小,优先级越高
          predicates:  # 断言,执行路由的判断条件,只有断言都为真,才会执行路由
            - Path=/product/**
        - id: shop-order
          uri: http://locahhost/8091
          order: 0
          predicates:
            - Path=/order/**
#          过滤器,可以再请求前或者请求后做一些动作
#          filters:
#            -StripPrefix=1
    #将网关添加到注册中心
    nacos:
      server-addr: localhost:8848
  application:
    name: my-gateway
#增强版

server:
  port: 8000

spring:
  cloud:
    gateway:
      routes:
        - id: shop-product #路由的唯一标识,只要不重复都可以,如果不写默认的是通过UUID产生的,一般写成被路由服务的名称
          uri: lb://shop-product #被路由的地址, lb代表lb协议
          order: 0 #优先级,数字越小,优先级越高
          predicates:  # 断言,执行路由的判断条件,只有断言都为真,才会执行路由
            - Path=/product/**
#            - Age=18,60
        - id: shop-order
          uri: lb://shop-order
          order: 0
          predicates:
            - Path=/order/**
#          过滤器,可以再请求前或者请求后做一些动作
#          filters:
#            -StripPrefix=1
    #将网关添加到注册中心
    nacos:
      server-addr: localhost:8848
  application:
    name: my-gateway

增强版与普通版的区别:普通版路由路径是写死的,当微服务的端口修改时,还需要修改网关的配置文件,比较麻烦,增强版将网关作为一个微服务,注册到注册中心,然后从注册中心中下载其他微服务的服务名

步骤四:通过网关访问微服务

java 中springcloud 单独一个controller拦截器 spring cloud gateway 拦截器_gateway_03

还有一个简写版实用性不大,就不再赘述。

1.4 Gateway的实现原理

1.4.1 路由

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

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

1.4.2 执行流程

执行流程大体如下:

1. Gateway Client(网关客户端)向Gateway Server发送请求

2. 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文

3. 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给 RoutePredicateHandlerMapping

4. RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用

5. 如果过断言成功,由FilteringWebHandler创建过滤器链并调用

6. 请求会一次经过PreFilter--微服务--PostFilter的方法,最终返回响应

java 中springcloud 单独一个controller拦截器 spring cloud gateway 拦截器_gateway_04

1.4.3 断言

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

断言就是说: 在 什么条件下 才能进行路由转发。

内置路由断言工厂 

java 中springcloud 单独一个controller拦截器 spring cloud gateway 拦截器_优先级_05

实操几个断言:

server:
  port: 8000

spring:
  cloud:
    gateway:
      routes:
        - id: shop-product #路由的唯一标识,只要不重复都可以,如果不写默认的是通过UUID产生的,一般写成被路由服务的名称
          uri: lb://shop-product #被路由的地址, lb代表lb协议
          order: 0 #优先级,数字越小,优先级越高
          predicates:  # 断言,执行路由的判断条件,只有断言都为真,才会执行路由
            - Path=/product/**  #访问路径
            - Before=2020-11-28T00:00:00.000+08:00 # 表示在2020前访问
            - Method=POST             # 请求方式必须为POST

    nacos:
      server-addr: localhost:8848
  application:
    name: my-gateway

1.4.3.2 自定义断言

假设我们的应用仅仅让age在(min,max)之间的人来访问。

步骤一:在配置文件中,配置一个Age的断言

server:
  port: 8000

spring:
  cloud:
    gateway:
      routes:
        - id: shop-product #路由的唯一标识,只要不重复都可以,如果不写默认的是通过UUID产生的,一般写成被路由服务的名称
          uri: lb://shop-product #被路由的地址, lb代表lb协议
          order: 0 #优先级,数字越小,优先级越高
          predicates:  # 断言,执行路由的判断条件,只有断言都为真,才会执行路由
            - Path=/product/**
            - Age=18,60

步骤二:自定义一个工厂, 实现断言方法

package com.dhy.gateway.config;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

/**
 * @program: springcloud-father
 * @description: zidingyiduanyan
 * @author: 杜航宇
 * @create: 2021-07-08 11:21
 **/
@Component
// 泛型为自定义的配置类,用来存储配置中真的值
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
    @Data
    @NoArgsConstructor
    //这里的Static不要漏,需要静态方法
    public static class Config {
        private Integer minAge;
        private Integer maxAge;
    }

    //读取配置文件中的值,并配置为配置类中的属性
    public AgeRoutePredicateFactory(){
        super(AgeRoutePredicateFactory.Config.class);
    }

    public List<String> shortcutFieldOrder(){
        return Arrays.asList("minAge","maxAge");
    }

    public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config){
        return (exchange) -> {
            String age = exchange.getRequest().getQueryParams().getFirst("age");
            if (StringUtils.isNotEmpty(age)) {
                int a = Integer.parseInt(age);
                return a >= config.minAge && a <= config.maxAge;
            }
            return true;
        };
}
}

步骤三:启动测试

测试发现,当age在(18,60)可以访问,其它范围不能访问

java 中springcloud 单独一个controller拦截器 spring cloud gateway 拦截器_gateway_06

java 中springcloud 单独一个controller拦截器 spring cloud gateway 拦截器_微服务_07

二、过滤器

上边一期学习了服务网关的断言,断言决定了请求由哪一个路由处理。在路由处理之前,需要经过“pre”类型的过滤器处理,处理返回响应后,可以由“post”类型的过滤器处理。

2.1  过滤器的作用

由过滤器工作流程点可以知道过滤器有着非常重要的作用,在“pre”类型的过滤器可以实现参数校验、权限校验、流量监控、日志输出、协议转换等功能,在“post”类型的过滤器中可以做响应内容、响应头的修改、日志输出、流量监控等功能。要弄清楚为什么需要网关这一层,就不得不提到过滤器的作用了。
当我们有很多个服务时,以图10-3中的user-service、goods-service、sales-service等服务为例,客户端在请求各个服务的API时,每个服务都需要做相同的事情,比如鉴权、限流、日志输出等。

2.2 过滤器的分类

2.2.1 局部过滤器

局部过滤器是针对单个路由的过滤器。

2.2.1.1 内置局部过滤器

在SpringCloud Gateway中内置了很多不同类型的网关路由过滤器。


server:
  port: 8000

spring:
  cloud:
    gateway:
      routes:
        - id: shop-product #路由的唯一标识,只要不重复都可以,如果不写默认的是通过UUID产生的,一般写成被路由服务的名称
          uri: lb://shop-product #被路由的地址, lb代表lb协议
          order: 0 #优先级,数字越小,优先级越高
          predicates:  # 断言,执行路由的判断条件,只有断言都为真,才会执行路由
            - Path=/product/**
            - Age=18,60


        - id: shop-order
          uri: lb://shop-order
          order: 0
          predicates:
            - Path=/order/**
#          过滤器,可以再请求前或者请求后做一些动作
         filters:
             - setStatus=2000 #将返回的状态值修改为2000
             - StripPrefix=1  #StripPrefix参数表示在将请求发送到下游之前从请求中剥离的路径个数。
    #将网关添加到注册中心
    nacos:
      server-addr: localhost:8848
  application:
    name: my-gateway

2.2.2  全局过滤器

全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。

2.2.2.1 内置全局过滤器

java 中springcloud 单独一个controller拦截器 spring cloud gateway 拦截器_gateway_08

2.2.2.2 自定义全局过滤器(权限统一校验)

在下面的案例中,将讲述如何编写自己的GlobalFilter,该GlobalFilter会校验请求中是否包含请求参数“token”,如果不包含请求参数“token”,则不转发路由;否则,执行正常的逻辑。代码如

package com.dhy.gateway.filter;

import lombok.val;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @program: springcloud-father
 * @description: dengluquanxian
 * @author: XXX
 * @create: 2021-07-08 17:12
 **/
@Component
public class LoginFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        // 获取token头
        String token = request.getHeaders().getFirst("token");//获取token
        if (!StringUtils.isEmpty(token)){
            if("admin".equals(token)){
                return chain.filter(exchange); //如果token存在,且输入的登录名与tokenz中的一直,放行
            }
        }
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);//权限不足,需要登录
        return null;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}