API网关

API网关一般伴随着微服务架构出现,不同的微服务一般会有不同的网络地址,特别是每个微服务还会部署多份服务实例,外部客户端如果调用时一是如果需要三个微服务组合才能完成一次调用,客户端需要知道这三个微服务的地址,端口号等.有可能会出现如下问题:

  • 客户端多次请求不同的微服务,增加了客户端的复杂性
  • 存在跨域请求,在某些场景下处理相对复杂
  • 认证复杂及重复,每个微服务都需要独立认证
  • 重构困难,随着项目的迭代和产品的发展,可能需要重新划分微服务,如将多个微服务合并为一个或将一个微服务分拆为多个,此时客户端调用微服务时成本又会增加
  • 有一些微服务可能放置于防火墙后或其他访问限制,直接访问会不可达

    所以出现了API网关,它是介于客户端和服务端之间的桥梁,所有外部请求都会先给过网关层再路由到相应的微服务进行处理,根据这一特性我们一般会在网关层用来处理安全,性能,监控等通用的功能等,而不会用来处理业务逻辑.

    API网关

网关的优势及选型

  • 易于监控,认证
  • 减少客户端与各个微服务之间的交互,直接和网关交互

网关技术选型

  • 自研
  • Nginx衍生的Kong
  • Netflex Zuul:zuul1,zuul2(闭源)
  • Spring Cloud Gateway

下面我们主要以spring cloud gateway为例来讲网关.
Spring cloud gateway包含spring 5,spring boot 2,project reactor,它提供一种简单有效的路由转发请求,并提供横切关注点,如安全性,监控/指标和弹性等,它的特性如下:

  • 基于Java 8 编码
  • 支持Spring framework 5
  • 支持spring boot 2
  • 支持动态路由
  • 支持基于HTTP请求的路由匹配(path,method,header,host etc)
  • 过滤器作用于匹配的路由,修改下游HTTP请求和HTTP响应(增加/修改头部,增加/修改请求参数,改写请求路径)
  • 支持spring cloud discovery client,与服务发现和注册配合使用
  • 支持websocket,使用非阻塞API(主要与zuul1比较,zuul1是基于Servlet实现,基于阻塞I/O,不支持任何长连接,每次I/O操作都是从工作线程中选择一个执行,request-per-thread)
  • 基于Filter链提供了身份验证,监控,负载均衡,限流,降级与应用检测等功能

spring cloud gateway重要概念

  • 路由:路由信息由ID,目标URL,一组断言,一组filter.若断言为真,则请求URL和配置匹配.
  • Filter:分两类gateway filter和globalfilter,过滤器将会对请求和响应进行修改处理.
  • 断言:自定义匹配来自http request中的任务信息如请求头和参数

实践:
application.yaml

server:
port: ${port:8180}
ipAddr: ${ipAddr:127.0.0.1}
spring:
zipkin:
base-url: http://127.0.0.1:9411
discovery-client-enabled: false #
sleuth:
sampler:
probability: 1.0 #采样百分比 range[0.1~1.0]
application:
name: service-gateway
cloud:
gateway:
discovery:
locator:
enabled: true
lowerCaseServiceId: true
#          lower-case-service-id: true
routes:
- id: service_product
uri: lb://service-product
order: 1 #数字越小,越先执行,即优先级越高
predicates:
- Path=/api/product/**
filters:
- StripPrefix=1
- id: service_order
uri: lb://service-order
order: 1 #数字越小,越先执行,即优先级越高
predicates:
- Path=/api/order/**
filters:
- StripPrefix=1
globalcors:
cors-configurations:
'[/**]':
allow-credentials: true #允许携带认证信息
allowed-origins: #允许跨域的源(域名和IP),设置*为全部
- '*'
allowed-headers: '*' # 允许跨域请求里的header字段,*为全部
allowed-methods: #允许跨域的方法
- GET
- POST
- OPTIONS
max-age: 3600 # 跨域允许的有效期
nacos:
discovery:
server-addr: ${server.ipAddr}:8848
namespace: 31885c53-49eb-4031-9450-89e78cfb48fa
logging:
level:
root: info
#  pattern:
#    console:  '[%-5level] %clr(%d{HH:mm:ss}){cyan} %logger - %clr(%msg){green} [%clr(%thread{20}){cyan}]%n'

上述配置文件包括应用名称,端口号以及注册中心的配置和配置中心的配置,跨域请求配置.日志打印级别及控制台打印格式,还可以指定日志文件存储等,最核心的是路由的配置.注意uri是使用lb:// 这里使用了负载均衡.

我们还会在代码中实现一个token过滤器,代码片段如下:

    @Value("${auth.skip.urls:skip urls not config yet}")
private String[] skipAuthUrls; //忽略验证的urls
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath().trim();
Assert.notEmpty(skipAuthUrls, "skip auth urls is not null");
//如果当前路径不需要验证就放行
if (Stream.of(skipAuthUrls).anyMatch(url -> url.equalsIgnoreCase(path.trim()))) {
log.debug("{} 不需要鉴权,its order {}", path.trim(), getOrder());
return chain.filter(exchange);
}
//TODO 获取客户端 传来的token信息进行安全鉴定
//TODO 比如使用token去redis里查看是否过期,是否在黑名单 中等
//TODO 验证通过后走过滤器链即可
//TODO 验证未通过 直接返回客户端 相应的提示信息
return chain.filter(exchange);
}
/**
* Get the order value of this object.
* <p>Higher values are interpreted as lower priority. As a consequence,
* the object with the lowest value has the highest priority (somewhat
* analogous to Servlet {@code load-on-startup} values).
* <p>Same order values will result in arbitrary sort positions for the
* affected objects.
*
* @return the order value
* @see #HIGHEST_PRECEDENCE
* @see #LOWEST_PRECEDENCE
*/
@Override
public int getOrder() {
return -100;
}

以上代码片段处理了需要跳过鉴权的url,比如 注册,登录等
还通过Order接口指定了执行的顺序,这里的Order,数字越小,优先级越高.
以及鉴权过程,这里为了脱敏只写了TODO 提示.
完整代码 请参照github.com/gisonwin