使用Spring Cloud Gateway网关设计开放平台

使用Spring Cloud Gateway网关设计开放平台

调用接口那一方一般称之为ISV,独立软体开发商(independent software vendor),开放平台这一方称之为ISP,网络服务提供者(Internet Service Provider)。通常情况下需要为ISV分配一个appKey和appSecret,可以简单的理解为用户名密码,有了这个才能正常调用开放平台接口。

为了保证请求参数的合法性,客户端需要生成一个签名串,然后开放平台需要校验这个签名串。ISV可以通过appKey和AppSecret来生成签名串,这样就能保证客户端请求是合法的,服务端需要校验签名串是否合法,appKey是否合法,这里开放平台会提供一套签名算法,常见的有:支付宝开放平台签名算法

如何设计一个开放平台

开放平台的一个重要部分就是鉴权,鉴权功能和具体的业务无关,可以单独拿出来做,如果是单体应用的话可以把这部分操作写在一个Controller中。如果是微服务体系的话把鉴权部分放在网关是一个不错的选择,因为网关是一个统一入口,在入口处做好鉴权,后续的微服务不需要再做鉴权处理了,只需实现自己的业务逻辑即可。

在Spring Cloud微服务体系当中,充当网关的角色常见有两个,一个是Zuul,另一个是Spring Cloud Gateway。Zuul本质是一个Servlet,IO模型是BIO,阻塞式,而Spring Cloud Gateway基于Netty开发的,IO模型是AIO,也就是异步IO,在处理高并发请求场景下,Spring Cloud Gateway具有明显优势。两者各有优缺点,Zuul优点是架构简单,扩展起来比较方便,缺点是在处理高并发请求下稍显力不从心,Spring Cloud Gateway优点是高性能,可以处理高并发请求,缺点是架构复杂,需要了解异步编程、Netty、React等框架基本原理,调试起来比较困难。

本篇拿Spring Cloud Gateway来演示如何设计一个简单的开放平台。

首先简单介绍下Spring Cloud Gateway的基本功能,作为网关,首要的功能是路由功能,简单理解就是将一个A请求变成B请求,类似于Nginx的反向代理。另一个功能请求过滤,Spring Cloud Gateway允许开发者实现自定义过滤器,用来处理当前请求。

Spring Cloud Gateway的路由配置有两种,一种是写在配置文件里面,一种使用代码实现(Java DSL)。

  • 使用配置文件
server:  
  port: 8090  

spring:  
  cloud:  
    gateway:  
      routes:  
        - id: host_route  
          uri: https://www.baidu.com/  
          pedicates:  
            - Path=/

上面这个配置,在浏览器访问http://localhost:8090,页面会出现百度首页。

另一种使用Java代码形式:

@Bean  
public RouteLocator customRouteLocator(RouteLocatorBuilder routeBuilder) {  
    return routeBuilder.routes()  
            .route("host_route", r ->  
                    r.path("/")  
                     .uri("https://www.baidu.com/")  
            )  
            .build();  
}

其效果跟配置文件是一样的,如果路由配置固定不变可写在配置文件中,如果涉及到动态改变路由配置,就必须写在Java代码中了,因为代码更灵活。

假设我们我们的开放平台接口地址为:http://open.xx.com,该接口提供一个参数method,表示接口名,通过接口名来决定具体请求哪个微服务。比如访问http://open.xx.com/?method=goods.get,转发到商品微服务http://192.168.1.1:8080/getGoods,如下图所示:

网关请求

由此可见,在网关需要配置一套路由:

spring:
  cloud:
    gateway:
      routes:
        - id: getGoods
          uri: http://192.168.1.1:8080
          predicates:
            - Path=/getGoods
        - id: getOrder
          uri: http://192.168.1.2:8080
          predicates:
            - Path=/getOrder

接下来需要在网关中做几件事情:

  1. 鉴权,鉴权失败返回错误码
  2. 鉴权通过,进行路由转发

这些事情都可以在全局过滤器中执行,过滤器代码如下:

package com.example.gateway.filter;

import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

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

/**
 网关入口过滤器
 * @author tanghc
 */
@Component
public class IndexFilter implements WebFilter, Ordered {

    private static Map<String, String> methodPathMap = new HashMap<>(16);
    // 存放接口名对应的path
    static {
        methodPathMap.put("goods.get", "/getGoods");
        methodPathMap.put("order.get", "/getOrder");
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        // 获取请求参数
        Map<String, String> query = exchange.getRequest().getQueryParams().toSingleValueMap();
        // 鉴权
        this.check(query);

        String method = query.get("method");
        String path = methodPathMap.get(method);
        if (path != null) {
            // 复制一个新的request
            ServerHttpRequest newRequest = exchange.getRequest()
                    .mutate()
                    // == 关键在这里,重新定义转发的path
                    .path(path)
                    .build();
            // 复制一个新的exchange,request用新的
            ServerWebExchange newExchange = exchange
                    .mutate()
                    .request(newRequest)
                    .build();
            // 向后转发新的exchange
            return chain.filter(newExchange);
        }
        return chain.filter(exchange);
    }

    /**
     * 鉴权
     * @param query
     */
    private void check(Map<String, String> query) {

    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

}

这里通过一个map来存放method和path的对应关系,然后通过重写request中的path来实现转发功能。在浏览器请求http://localhost:8090/?method=order.get,转发到http://192.168.1.1:8080/getGoods

至此开放平台的一个基本功能就实现了,不过依然存在两个问题:

  1. 路由配置是写死的,无法动态加载
  2. 接口名对应的Path是写死的,无法动态变更

针对第一个问题,解决办法是使用Java代码(Java DSL)来配置路由,具体的思路是让微服务来维护路由关系,然后网关在启动完毕后去各个微服务端拉取路由配置,保存到本地。

第二个问题,可以使用动态配置,如Spring Cloud Config或阿波罗配置来动态改变。

以上涉及到的所有功能在SOP中已经全部实现。