Skip to content

接口日志与加密(Gateway)

约 4356 字大约 15 分钟

SpringCloudSpringGateway接口日志接口加密

2021-11-18

之前有一篇讲到在 Springboot 项目中做接口加解密,详见《接口日志与加密(SpringBoot)》一文,今天这篇将介绍在 Gateway 中做接口加解密。Gateway 属于 SpringCloud 家族,使用上 Gateway 时,一般都是微服务项目了,本文将会涉及一下几个要点: 服务注册到 Nacos; 网关路由分发; 网关请求加解密; 配置网关动态路由; 路由分发负载均衡;

1. 安装启动 Nacos

  • 然后可参考 Nacos 文档 中的方式启动。注意,启动脚本中默认是集群模式,你可以在启动命令中显示指定模式,但为了使用方便建议修改默认为单机模式。这里以 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,即表示启动成功;

  • 接着打开浏览器输入:http://127.0.0.1:8848/nacos 即可看到登录页,用户名和密码均为 nacos,点击 提交 后进入主页;

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

2. 工程创建及路由分发

  • 这里可参考官方文档,如果你搭建 Spring 项目见 Nacos Spring ,如果你搭建 Spring Boot 项目见 Nacos Spring Boot ,如果你搭建 Spring Cloud 项目见 Nacos Spring Cloud 。注意,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:本文完整示例代码已上传至 GitHub