跳至主要內容

接口日志与加密(Gateway)

Mayee...大约 14 分钟

之前有一篇讲到在 Springboot 项目中做接口加解密,详见《接口日志与加密(SpringBoot)open in new window》一文,今天这篇将介绍在 Gateway 中做接口加解密。Gateway 属于 SpringCloud 家族,使用上 Gateway 时,一般都是微服务项目了,本文将会涉及一下几个要点:

  • 服务注册到 Nacos;
  • 网关路由分发;
  • 网关请求加解密;
  • 配置网关动态路由;
  • 路由分发负载均衡;

1. 安装启动 Nacos

  • 然后可参考 Nacos 文档open in new window 中的方式启动。注意,启动脚本中默认是集群模式,你可以在启动命令中显示指定模式,但为了使用方便建议修改默认为单机模式。这里以 Windows 脚本为例,打开刚刚解压好的文件夹,进入到 nacos/bin 目录下,找到 startup.cmd 文件;
  • 接着点右键编辑,打开搜索 set MODE,将set MODE="cluster"修改为set MODE="standalone",然后保存关闭;
  • 双击 startup.cmd ,此时会打开一个 Dos 窗口,等到窗口显示"Nacos started successfully in stand alone mode. use embedded storage,即表示启动成功;

此时已完成 Nacos 安装和启动,接下来进行网关工程搭建。

2. 工程创建及路由分发

  • 这里可参考官方文档,如果你搭建 Spring 项目见 Nacos Springopen in new window ,如果你搭建 Spring Boot 项目见 Nacos Spring Bootopen in new window ,如果你搭建 Spring Cloud 项目见 Nacos Spring Cloudopen in new window 。注意,Gateway 使用的是 WebFlux,是一个非阻塞异步的框架,和 SpringMvc 框架不同,因此不可以在网关工程的 pom.xml 文件中引入 spring-webmvc 的依赖。

  • Nacos 有默认保留的命名空间 public ,你可以使用这个命名空间。我们这里新建一个命名空间 dev_java

  • 连接 nacos 配置需要写在 bootstrap.yml 中,启动项目时 bootstrap.yml 优先被加载,之后 application.yml 被加载;
  • 注意,pom.xml 文件中需要引入 spring-cloud-starter-bootstrap 依赖才可以支持加载 bootstrap.yml 文件。这里我们给网关服务命名为:example-gateway;
  • 接着在 Nacos 上新增配置文件;
  • 注意 Data ID 命名中有 .yaml 后缀,配置格式选择 YAML ,因为内容不可以为空,这里我们随意写一个符合 yaml 语法的配置,稍后再进行修改,然后点击 发布
  • 在网关工程的 bootstrap.yml 文件中配置静态路由,表示匹配以 /demoA 开头的请求将被分发到 http://127.0.0.1:9501 这个服务中,以 /demoB 开头的请求将被分发到 http://127.0.0.1:9502 这个服务中;
  • 上述步骤完成后,网关工程搭建完成。接着再搭建两个普通的 SpringBoot 工程:example-service-aexample-service-b
  • 然后启动这三个服务;
  • 用 Postman 请求一下,可以看到正常响应;

3. 网关请求加解密

先来回顾一下在 SpringBoot 中实现加解密的思路,其本质是实现 RequestWrapperResponseWrapper 接口,创建两个实例,重写其中获取请求/响应参数的方法以支持加解密,然后在 Filter 中将原ServletRequestServletResponse 实例替换为支持加解密的实例。 在 Gateway 中的实现加解密的思路与上述类似,创建两个类,一个继承 ServerHttpRequestDecorator,一个继承 ServerHttpResponseDecorator ,重写其中获取请求/响应参数的方法以支持加解密,然后加在 GlobalFilter 过滤链中。

3.1. 创建支持加密的全局过滤器

这里判断请求头,携带了 crypto=true 才走加解密逻辑。注意 getOrder() 方法的返回值,值越小优先级越高,但必须小于 -1 ,修改请求/响应参数才有效。

@Component
public class CryptoFilter implements GlobalFilter, Ordered {
    private final static Logger log = Logger.getLogger(AuthFilter.class.getName());

    /*
     * default HttpMessageReader
     */
    private static final List<HttpMessageReader<?>> MESSAGE_READERS = HandlerStrategies.withDefaults().messageReaders();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        HttpHeaders headers = request.getHeaders();
        String crypto = headers.getFirst("crypto");
        // 如果没开启加密
        if (!"true".equals(crypto)) {
            return chain.filter(exchange).then(Mono.fromRunnable(() -> log.info("crypto not enable!")));
        }
        CryptoFilterContext context = new CryptoFilterContext();
        MediaType contentType = headers.getContentType();
        if (MediaType.APPLICATION_JSON.includes(contentType)) {
            return readBodyData(exchange, chain, context);
        }
        if (MediaType.APPLICATION_FORM_URLENCODED.includes(contentType)) {
            return readFormData(exchange, chain, context);
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        // 这里 order 必须高于 WRITE_RESPONSE_FILTER_ORDER ,修改请求/响应才能生效
        return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
    }
}

3.2. 读取并修改 MediaType 为 application/json 的请求体

注意必须创建新的 HttpHeaders,否则可能会出现请求被截断的情况。比如浏览器报错,ST 请求不支持,或 T 请求不支持,这其实是 POST、GET 的前两位被截掉了,导致出错; 注意 headers.remove(HttpHeaders.CONTENT_LENGTH); 方法,若解密请求体导致请求体内容长度变化,则需要移除请求头中的 CONTENT_LENGTH,否则解密后的请求体参数可能会被截断,导致下游服务无法识别参数。

private Mono<Void> readBodyData(ServerWebExchange exchange, GatewayFilterChain chain, CryptoFilterContext context) {
        /*
         * join the body
         */
        final ServerHttpRequest request = exchange.getRequest();
        Charset charset = Optional.ofNullable(request.getHeaders().getContentType())
                .map(MediaType::getCharset)
                .orElse(StandardCharsets.UTF_8);
        context.setCharset(charset);
        return DataBufferUtils.join(request.getBody())
                //这里是为了处理请求 body 为 null 时不往下走,当然一般有请求体不会为 null
                .defaultIfEmpty(exchange.getResponse().bufferFactory().wrap("{}".getBytes(context.getCharset())))
                .flatMap(dataBuffer -> {
                    byte[] bytes = new byte[dataBuffer.readableByteCount()];
                    dataBuffer.read(bytes);
                    DataBufferUtils.release(dataBuffer);
                    String originBody = new String(bytes, context.getCharset());
                    context.setOriginBody(originBody);
                    log.info("原始请求 body(密文): " + originBody);
                    
                    // do 解密
                    String decryptBody = new JSONObject().fluentPut("uname", "from crypto filter").toJSONString();
                    context.setDecryptBody(decryptBody);

                    // 这里是解密后的请求体
                    Flux<DataBuffer> fluxBody = Flux.defer(() -> {
                        DataBuffer buffer = new DefaultDataBufferFactory().wrap(decryptBody.getBytes(context.getCharset()));
                        DataBufferUtils.retain(buffer);
                        return Mono.just(buffer);
                    });
                    context.setFluxBody(fluxBody);

                    HttpHeaders headers = new HttpHeaders();
                    headers.putAll(exchange.getRequest().getHeaders());
                    headers.remove(HttpHeaders.CONTENT_LENGTH);
                    context.setRequestHeaders(headers);

                    RequestDecorator requestDecorator = new RequestDecorator(exchange.getRequest(), context);
                    ResponseDecorator responseDecorator = new ResponseDecorator(exchange.getResponse(), context);
                    /*
                     * mutate exchange with new ServerHttpRequest
                     */
                    ServerWebExchange mutatedExchange = exchange.mutate().request(requestDecorator).response(responseDecorator).build();
                    /*
                     * read body string with default messageReaders
                     */
                    return ServerRequest.create(mutatedExchange, MESSAGE_READERS)
                            .bodyToMono(String.class)
                            .then(chain.filter(mutatedExchange));
                });
    }

3.3. 读取并修改 MediaType 为 application/x-www-form-urlencoded 的请求体

注意必须创建新的 HttpHeaders,否则可能会出现请求被截断的情况。比如浏览器报错,ST 请求不支持,或 T 请求不支持,这其实是 POST、GET 的前两位被截掉了,导致出错; 注意 headers.remove(HttpHeaders.CONTENT_LENGTH); 方法,若解密请求体导致请求体内容长度变化,则需要移除请求头中的 CONTENT_LENGTH,否则解密后的请求体参数可能会被截断,导致下游服务无法识别参数。

private Mono<Void> readFormData(ServerWebExchange exchange, GatewayFilterChain chain, CryptoFilterContext context) {
        final ServerHttpRequest request = exchange.getRequest();
        Charset charset = Optional.ofNullable(request.getHeaders().getContentType())
                .map(MediaType::getCharset)
                .orElse(StandardCharsets.UTF_8);
        context.setCharset(charset);
        // MultiValueMap<String, String> multiValueMap
        return exchange.getFormData().doOnNext(multiValueMap -> {
            /*
             * repackage form data
             */
            String formData = packageFormData(multiValueMap, context.getCharset().name());
            log.info("原始请求 form-data(密文): " + formData);
            context.setOriginFormData(formData);
            
            // do 解密
            String decryptBody = "{\"uname\":\"from crypto filter\"}";
            context.setDecryptBody(decryptBody);

            // 这里是解密后的请求体
            Flux<DataBuffer> fluxBody = DataBufferUtils.read(
                    new ByteArrayResource(decryptBody.getBytes(context.getCharset())),
                    new NettyDataBufferFactory(ByteBufAllocator.DEFAULT),
                    decryptBody.getBytes(context.getCharset()).length);
            context.setFluxBody(fluxBody);

        }).then(Mono.defer(() -> {
            HttpHeaders headers = new HttpHeaders();
            headers.putAll(exchange.getRequest().getHeaders());
            headers.remove(HttpHeaders.CONTENT_LENGTH);
            context.setRequestHeaders(headers);

            RequestDecorator requestDecorator = new RequestDecorator(request, context);
            ResponseDecorator responseDecorator = new ResponseDecorator(exchange.getResponse(), context);
            ServerWebExchange mutateExchange = exchange.mutate().request(requestDecorator).response(responseDecorator).build();
            return chain.filter(mutateExchange);
        }));
    }

3.4. 支持解密请求体的 RequestDecorator

注意 httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked"); ,上文中若请求体解密了,会移除 contentLength ,这里给新的请求头设置 TRANSFER_ENCODING 为 chunked ,表示请求体的长度未知,不能按 contentLength 的长度来读取请求体,需要按数据块来读取,直到读取不到为止。

public class RequestDecorator extends ServerHttpRequestDecorator {

    private CryptoFilterContext context;

    public RequestDecorator(ServerHttpRequest delegate) {
        super(delegate);
    }

    public RequestDecorator(ServerHttpRequest delegate, CryptoFilterContext context) {
        super(delegate);
        this.context = context;
    }

    /**
     * change content-length
     *
     * @return
     */
    @Override
    public HttpHeaders getHeaders() {
        HttpHeaders headers = context.getRequestHeaders();
        long contentLength = headers.getContentLength();
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.putAll(headers);
        if (contentLength > 0) {
            httpHeaders.setContentLength(contentLength);
        } else {
            httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
        }
        return httpHeaders;
    }

    /**
     * read bytes to Flux<Databuffer>
     *
     * @return
     */
    @Override
    public Flux<DataBuffer> getBody() {
        Flux<DataBuffer> fluxBody = context.getFluxBody();
        if (Objects.nonNull(fluxBody)) {
            return fluxBody;
        }
        return super.getBody();
    }
}

3.5. 支持加密请求体的 RequestDecorator

注意若响应体太长,会出现响应体被截成多段,然后分多次响应,fluxBody.buffer().map() 方法可以解决分段响应的问题; 若下游服务开启了响应数据压缩,则这里必须对获取到的响应字节解压,然后才能转换为字符串加密。

public class ResponseDecorator extends ServerHttpResponseDecorator {
    private CryptoFilterContext context;

    public ResponseDecorator(ServerHttpResponse delegate) {
        super(delegate);
    }

    public ResponseDecorator(ServerHttpResponse delegate, CryptoFilterContext context) {
        super(delegate);
        this.context = context;
    }

    @Override
    public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
        Flux<? extends DataBuffer> fluxBody = Flux.from(body);
        ServerHttpResponse response = getDelegate();
        DataBufferFactory bufferFactory = response.bufferFactory();

        // 解决响应分段传输问题
        Flux<DataBuffer> flux = fluxBody.buffer().map(dataBuffers -> {
            DataBuffer dataBuffer = bufferFactory.join(dataBuffers);
            byte[] content = new byte[dataBuffer.readableByteCount()];
            dataBuffer.read(content);
            // 释放掉内存
            DataBufferUtils.release(dataBuffer);

            //判断响应是否有压缩。下游服务可能会开启响应数据压缩,若开启了压缩,必须先解压才能得到原文
            boolean gzip = Objects.equals("gzip", response.getHeaders().getFirst("Content-Encoding"));
            if (gzip) {
                content = unCompress(content);
            }
            //原始响应
            String originResponse = new String(content, context.getCharset());
            // do 加密
            R data = JSON.parseObject(originResponse, R.class);
            data.setMessage("response from crypto filter");
            String cipherText = JSON.toJSONString(data);
            byte[] bytes = cipherText.getBytes(context.getCharset());
            if (gzip) {
                bytes = compress(bytes);
            }
            return bufferFactory.wrap(bytes);
        });
        return super.writeWith(flux);
    }

    private byte[] unCompress(byte[] bytes) {
        if (ObjectUtils.isEmpty(bytes)) {
            return new byte[]{};
        }
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
        try (GZIPInputStream gzip = new GZIPInputStream(in)) {
            byte[] buffer = new byte[1024];
            int n;
            while ((n = gzip.read(buffer)) >= 0) {
                out.write(buffer, 0, n);
            }
            return out.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new byte[]{};
    }

    private byte[] compress(byte[] bytes) {
        if (ObjectUtils.isEmpty(bytes)) {
            return new byte[]{};
        }
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try (GZIPOutputStream gzip = new GZIPOutputStream(out)) {
            gzip.write(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return out.toByteArray();
    }
}

3.6. 验证加密效果

上文中 data.setMessage("response from crypto filter"); 给响应的 message 字段赋值,与上图对比,可以看到响应体已经被修改了。注意请求头中需要有 crypto=true 才会走加解密。

4. 网关动态路由

上文,我们已经实现了网关路由分发,以及对请求体数据解密,对响应体数据加密。但缺点在于路由不够灵活。如果我们需要改变路由,或者增删路由规则,则要修改 bootstrap.yml 文件中的 routes 配置 ,为使配置生效,需要重新打包并启动服务。 若将 bootstrap.yml 文件中的 routes 配置移至 Nacos 中,则原配置文件中的路由可以删除掉,也可以保留,但没有意义。因为服务启动时,连接并读取到 Nacos 上的配置,会覆盖掉项目中的相同配置,此时项目中配置的路由就无意义了。 但这种方式依然不是动态路由方式,无论是从项目中还是 Nacos 上读取到的路由,都在服务启动时加载好了,后续增删改变路由配置,不会生效,除非重启服务。

  • 在 Nacos 上创建 example-gateway-router.json 文件,用来存放路由配置;
  • 将网关工程中的 resources/router/rouerList.json 内容拷贝至 example-gateway-router.json 文件 中;
  • 在 Nacos 上的 example-gateway.yaml 文件中配置如下,以启用动态路由,并指定路由配置文件;
  • 要想实现动态路由效果,需要实现 RouteDefinitionRepositoryApplicationEventPublisherAware 这两个接口;
@Slf4j
@Configuration
@ConditionalOnProperty(name = "dynamic.router.enable", havingValue = "true")
public class NacosRouteDefinitionRepository implements RouteDefinitionRepository, ApplicationEventPublisherAware {
    private ApplicationEventPublisher applicationEventPublisher;
    private static final Map<String, RouteDefinition> NACOS_ROUTE_DEFINITION = Collections.synchronizedMap(new LinkedHashMap<>());

    @Autowired
    private NacosRouterService nacosService;

    @Bean
    public Map<String, RouteDefinition> refreshNacosRouteDefinitionRepository() throws NacosException {
        nacosService.refreshRouter(this.assembleRouteDefinition);
        return NACOS_ROUTE_DEFINITION;
    }

    private final Consumer<RouteGroupEntity> assembleRouteDefinition = config -> {
        List<RouteEntity> routes = config.getRouteList();
        if (routes == null) {
            return;
        }
        routes.sort(Comparator.comparingInt(RouteEntity::getOrder));
        Set<String> tem = new HashSet<>(routes.size());
        List<FilterDefinitionExt> globalFilters = Optional.ofNullable(config.getGlobalFilters()).orElse(new ArrayList<>()).stream().map(m -> {
            FilterDefinitionExt filterDefinition = new FilterDefinitionExt();
            filterDefinition.setArgs(m.getArgs());
            filterDefinition.setName(m.getName());
            filterDefinition.setApply(m.getApply());
            filterDefinition.setNotApply(m.getNotApply());
            return filterDefinition;
        }).collect(Collectors.toList());
        List<PredicateDefinitionExt> globalPredicates = Optional.ofNullable(config.getGlobalPredicates()).orElse(new ArrayList<>()).stream().map(m -> {
            PredicateDefinitionExt predicateDefinition = new PredicateDefinitionExt(m.toString());
            predicateDefinition.setName(m.getName());
            return predicateDefinition;
        }).collect(Collectors.toList());

        int x = 1;
        for (FilterDefinition routFilterEntity : globalFilters) {
            log.info("Load GlobalFilter[{}/{}],name={}", x++, globalFilters.size(), routFilterEntity.getName());
        }
        x = 1;
        for (PredicateDefinitionExt routFilterEntity : globalPredicates) {
            log.info("Load GlobalPredicate[{}/{}],name={}", x++, globalFilters.size(), routFilterEntity.getName());
        }
        x = 1;
        for (RouteEntity eachRoute : routes) {
            RouteDefinition definition = new RouteDefinition();
            definition.setId(eachRoute.getId());
            definition.setOrder(eachRoute.getOrder());
            List<PredicateDefinition> predicateDefinitionList = Optional.ofNullable(eachRoute.getPredicates()).orElse(new ArrayList<>()).stream().map(m -> {
                PredicateDefinition predicateDefinition = new PredicateDefinition(m.toString());
                predicateDefinition.setName(m.getName());
                return predicateDefinition;
            }).collect(Collectors.toList());
            List<PredicateDefinition> predicateGlobal = globalPredicates.stream().filter(f -> {
                if (f.isNotApply(definition.getId())) {
                    return false;
                }
                return f.isApply(definition.getId());
            }).collect(Collectors.toList());
            predicateDefinitionList.addAll(predicateGlobal);
            predicateDefinitionList = predicateDefinitionList.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(PredicateDefinition::getName))), ArrayList::new));
            definition.setPredicates(predicateDefinitionList);
            List<FilterDefinition> filterDefinitionList = Optional.ofNullable(eachRoute.getFilters()).orElse(new ArrayList<>()).stream().map(m -> {
                FilterDefinition filterDefinition = new FilterDefinition();
                filterDefinition.setArgs(m.getArgs());
                filterDefinition.setName(m.getName());
                return filterDefinition;
            }).collect(Collectors.toList());
            List<FilterDefinition> filterGlobal = globalFilters.stream().filter(f -> {
                if (f.isNotApply(definition.getId())) {
                    return false;
                }
                return f.isApply(definition.getId());
            }).collect(Collectors.toList());
            filterDefinitionList.addAll(filterGlobal);
            filterDefinitionList = filterDefinitionList.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(FilterDefinition::getName))), ArrayList::new));
            definition.setFilters(filterDefinitionList);
            if (eachRoute.getMetadata() != null) {
                definition.setMetadata(eachRoute.getMetadata());
            }
            URI uri = UriComponentsBuilder.fromUriString(eachRoute.getUri()).build().toUri();
            definition.setUri(uri);
            NACOS_ROUTE_DEFINITION.put(definition.getId(), definition);
            tem.add(eachRoute.getId());
            log.info("Refresh[{}/{}] Order:{},RoutID:{},URL:{}, Value:{}", x++, tem.size(), eachRoute.getOrder(), eachRoute.getId(), eachRoute.getUri(), JSON.toJSONString(eachRoute.getPredicates()));
        }
        Set<String> exists = new HashSet<>(NACOS_ROUTE_DEFINITION.size());
        exists.addAll(NACOS_ROUTE_DEFINITION.keySet());
        x = 0;
        for (String k : exists) {
            if (tem.contains(k)) {
                continue;
            }
            NACOS_ROUTE_DEFINITION.remove(k);
            log.info("Remove[{}] RoutID:{}", x, k);
        }
        applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
    };


    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        return Flux.fromIterable(NACOS_ROUTE_DEFINITION.values());
    }

    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return route.flatMap(r -> {
            if (ObjectUtils.isEmpty(r.getId())) {
                return Mono.error(new IllegalArgumentException("id may not be empty"));
            }
            NACOS_ROUTE_DEFINITION.put(r.getId(), r);
            applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
            return Mono.empty();
        });
    }

    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return routeId.flatMap(id -> {
            if (NACOS_ROUTE_DEFINITION.containsKey(id)) {
                NACOS_ROUTE_DEFINITION.remove(id);
                applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
                return Mono.empty();
            }
            return Mono.defer(() -> Mono.error(new NotFoundException("RouteDefinition not found: " + routeId)));
        });
    }

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

监听 Nacos 上 example-gateway-router.json 文件的变动,一旦监听到内容变化,马上刷新路由。 Tip:其实这种监听 Nacos 上配置文件的更改,是基于 HTTP 长轮询。客户端(java服务) 默认每隔 30 秒发送一个 HTTP 请求到 Nacos 服务端,超时时间为 30 秒。若这 30 秒内有配置内容更改,则请求会立刻响应给客户端,返回更改后的内容;若这 30 秒内无配置内容修改,则请求超时,没有任何内容返回,然后立刻发起下一次请求,周而复始。 这种做法的好处在于,它可以减轻 Nacos 服务端的压力,不长时间占用网络资源。如果换成推送的方式,需要使用 WebSocket 保持长连接,客户端与服务端需要始终保持连接,并且还需要做心跳检测、断线重连,这样会使连接变得不稳定。且配置文件一般很少改动,持续占用连接资源,这又会造成资源浪费。当大量服务使用同一个 Nacos 做为配置中心时,保持 Socket 连接还会增加 Nacos 服务端压力。因此,HTTP 长轮询是较好的方式。

@Slf4j
@Component
public class NacosRouterService {

    @Autowired
    private NacosRouterProps nacosRouterProps;
    @Autowired
    private NacosConfigManager nacosConfigManager;
    private ConfigService configService;

    @PostConstruct
    public void configService() {
        // 初始化 configService
        this.configService = nacosConfigManager.getConfigService();
    }

    /**
     * @Description: 获取配置
     * @return: com.alibaba.fastjson.JSONObject
     * @Author: Bobby.Ma
     * @Date: 2021/11/14 23:57
     */
    public RouteGroupEntity getRouter() throws NacosException {
        String config = configService.getConfig(nacosRouterProps.getDataId(), nacosRouterProps.getGroup(), 500);
        return JSONObject.parseObject(config, RouteGroupEntity.class);
    }

    /**
     * @param config
     * @Description: 发布配置
     * @return: void
     * @Author: Bobby.Ma
     * @Date: 2021/11/15 0:01
     */
    public void pushRouter(String config) throws NacosException {
        JSONObject object = JSON.parseObject(config);
        configService.publishConfig(nacosRouterProps.getDataId(), nacosRouterProps.getGroup(),
                object.toString(SerializerFeature.PrettyFormat), ConfigType.JSON.getType());
    }

    /**
     * @Description: 监听 nacos server 上的改动
     * @return: void
     * @Author: Bobby.Ma
     * @Date: 2021/11/15 0:03
     */
    public void refreshRouter(Consumer<RouteGroupEntity> assembleRouteDefinition) throws NacosException {
        // 工程启动时,从nacos 中读取配置
        RouteGroupEntity router = this.getRouter();
        assembleRouteDefinition.accept(router);

        Listener listener = new AbstractListener() {
            @Override
            public void receiveConfigInfo(String config) {
                log.info("listener router, data-id: {}, group: {}", nacosRouterProps.getDataId(), nacosRouterProps.getGroup());
                RouteGroupEntity routeGroup = JSON.parseObject(config, RouteGroupEntity.class);
                assembleRouteDefinition.accept(routeGroup);
            }
        };
        //设置监听 nacos 变动
        configService.addListener(nacosRouterProps.getDataId(), nacosRouterProps.getGroup(), listener);
    }
}
  • 重新启动网关服务,在 Postman 中请求 http://127.0.0.1:9501/demoA./get ,可以看到请求正常响应,这里就不放图了,和第三节中最后响应一模一样;

5. 路由分发负载均衡

上面我们在配置路由中的 uri 时给的是一个 http:// 协议地址,它对应了我们下游的一个服务,但生产环境下的服务是集群式的,这样给固定地址就显得不太合适了,因此需要做负载均衡,路由到下游集群服务中随机的一个,或者可以设置权重。

5.1. 负载均衡

在网关 pom.xml 文件中增加 spring-cloud-starter-loadbalancer 依赖,使其支持 lb:// 的负载均衡协议。 此时在 Postman 中请求 http://127.0.0.1:9501/demoB./get ,注意在请求头中加上 load-balance=true ,这不是必须的,只是因为我在路由配置的 predicates 中增加对此请求头的判断,可以看到请求正常响应;

{
      "id": "router-lb",
      "predicates": [
        {
          "name": "Path",
          "values": [
            "/demoB/**"
          ]
        },{
          "name": "Header",
          "values": ["load-balance, true"]
        }
      ],
      "filters": [
        {
          "name": "PreserveHostHeader"
        }
      ],
      "uri": "lb://example-service-b",
      "order": 5
    }

5.2. 路由配置

完整的路由配置如下,这里做个简要说明。 predicates 中 "name": "Header"配置, 表示请求头中要携带 load-balance 且值为 true 才会匹配到该路由; filters 中 PrefixPath 配置,表示给请求统一加前缀,如:前端发起请求的 uri 为 /prefixA/get ,则分发到下游服务时就变成了 /prefix/prefixA/get/prefix 为自定义配置; filters 中 StripPrefix 配置,表示截取掉请求的前缀,如:前端发起请求的 uri 为 /strip/stripB/get , 则分发到下游服务时就变成了 /stripB/get ,截取部分以 / 划分,截取长度由 parts 参数指定。 至于为什么 args 下面的参数名是 prefix 和 parts,可以看 PrefixPathGatewayFilterFactoryStripPrefixGatewayFilterFactory 这两个类中,有一个静态内部类 Config,其中的私有属性即为配置的参数名。 添加/移除请求头,添加/移除请求参数,可以看 AddRequestHeaderGatewayFilterFactoryRemoveRequestHeaderGatewayFilterFactoryAddRequestParameterGatewayFilterFactoryRemoveRequestParameterGatewayFilterFactory

{
  "routeList": [
    {
      "id": "router-http",
      "predicates": [
        {
          "name": "Path",
          "values": [
            "/demoA/**"
          ]
        }
      ],
      "filters": [
        {
          "name": "PreserveHostHeader"
        }
      ],
      "uri": "http://127.0.0.1:9501",
      "order": 5
    },
    {
      "id": "router-lb",
      "predicates": [
        {
          "name": "Path",
          "values": [
            "/demoB/**"
          ]
        },{
          "name": "Header",
          "values": ["load-balance, true"]
        }
      ],
      "filters": [
        {
          "name": "PreserveHostHeader"
        }
      ],
      "uri": "lb://example-service-b",
      "order": 5
    },
    {
      "id": "router-prefix",
      "predicates": [
        {
          "name": "Path",
          "values": [
            "/prefixA/get"
          ]
        }
      ],
      "filters": [
        {
          "name": "PrefixPath",
          "args": {
            "prefix": "/prefix"
          }
        }
      ],
      "uri": "lb://example-service-a",
      "order": 5
    },
    {
      "id": "router-strip",
      "predicates": [
        {
          "name": "Path",
          "values": [
            "/strip/stripB/get"
          ]
        }
      ],
      "filters": [
        {
          "name": "StripPrefix",
          "args": {
            "parts": "1"
          }
        }
      ],
      "uri": "lb://example-service-b",
      "order": 5
    }
  ]
}

上述的 predicates、 filters 写法也有 yaml 文件中的写法,但一般不会在 yaml 文件中配置路由,这里就不介绍那样的写法了。


Tip:本文完整示例代码已上传至 Giteeopen in new window