【微服务专题】深入理解与实践微服务架构(十四)之Gateway日志和超时功能实践

1,764 阅读14分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情

11. 网关日志记录

在网关定义日志全局过滤器,获取请求ID、时间、路径、客户端地址以及响应码等参数:

package com.deepinsea.common.filter;
​
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
​
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Optional;
import java.util.UUID;
​
/**
 * Created by deepinsea on 2022/6/28.
 * 访问日志全局过滤器
 */
@Slf4j
@Component
@Order(value = Integer.MIN_VALUE)
public class AccessLogGlobalFilter implements GlobalFilter {
​
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //filter的前置处理
        ServerHttpRequest request = exchange.getRequest();
        String requestId = UUID.randomUUID().toString(); //请求ID
        String date = new SimpleDateFormat("yyyy-MM-dd HH:mm yyyy-MM-dd HH:mm:ss.SSS").format(new Date()); //时间
        HttpMethod method = request.getMethod(); //请求方法
//        HttpHeaders headers = request.getHeaders(); //请求头
        MediaType contentType = request.getHeaders().getContentType();//请求类型(POST:表单或JSON, GET:空)
        String path = request.getPath().pathWithinApplication().value(); //请求路径
        MultiValueMap<String, String> requestParams = request.getQueryParams(); //请求参数
        //请求体(webflux是异步获取请求体,采用pub/sub订阅通知机制,注意处理400 BAD_REQUEST问题)
        //不打印请求体了,考虑到请求体size过大的问题
        Flux<DataBuffer> requestBody = request.getBody();
//        AtomicReference<String> bodyRef = new AtomicReference<>(); //直接返回bodyRef.get会出现200和400 BAD_REQUEST间隔出现问题
//        requestBody.subscribe(buffer -> {
//            CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
//            DataBufferUtils.release(buffer);
//            bodyRef.set(charBuffer.toString());
//        });
//        InetSocketAddress remoteAddress = request.getRemoteAddress(); //非真实IP
        String ip = getIP(request); //客户端真实IP
        //真实的url
        return chain
                //继续调用filter
                .filter(exchange)
                //filter的后置处理
                .then(Mono.fromRunnable(() -> {
                    ServerHttpResponse response = exchange.getResponse();
                    HttpStatus statusCode = response.getStatusCode(); //响应码
                    log.info("请求ID:{}, 请求时间:{}, 请求方法:{}, 数据类型:{}, 请求路径:{}, 客户端IP地址:{}, 请求参数:{}, 响应码:{}",
                            requestId, date, method, contentType, path, ip, requestParams, statusCode);
                }));
    }
​
    //下面为获取真实IP的全局静态变量
    private static final String IP_UNKNOWN = "unknown";
    private static final String IP_LOCAL = "127.0.0.1";
    private static final int IP_LEN = 15;
​
    /**
     * 获取客户端真实ip
     */
    public static String getIP(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        String ipAddress = headers.getFirst("x-forwarded-for");
        if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
            ipAddress = headers.getFirst("Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
            ipAddress = headers.getFirst("WL-Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
            ipAddress = Optional.ofNullable(request.getRemoteAddress())
                    .map(address -> address.getAddress().getHostAddress())
                    .orElse("");
            if (IP_LOCAL.equals(ipAddress)) {
                // 根据网卡取本机配置的IP
                try {
                    InetAddress inet = InetAddress.getLocalHost();
                    ipAddress = inet.getHostAddress();
                } catch (UnknownHostException e) {
                    // ignore
                }
            }
        }
        // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
        if (ipAddress != null && ipAddress.length() > IP_LEN) {
            int index = ipAddress.indexOf(",");
            if (index > 0) {
                ipAddress = ipAddress.substring(0, index);
            }
        }
        return ipAddress;
    }
}

注意网关的全局过滤器的作用范围是走了网关代理的请求,对于后端和网关真实服务地址是不生效的,因为没有走网关代理。如果需要所有请求都走网关,要么每个服务配置拦截器,要么内外网隔离网关和后端服务。

启动 service-openfeign 子模块,使用curl命令请求调用者的POST接口和网关的GET接口,测试日志打印:

C:\Users\deepinsea>curl -X POST -H "Content-Type:application/json" -d '{"name":"admin"}' http://localhost:9070/service-consumer-openfeign/consumer-openfeign/hello?ahh=111
hi, this is service-provider-api!
C:\Users\deepinsea>curl -d '123' http://localhost:9070/service-consumer-openfeign/consumer-openfeign/hello?ahh=111
hi, this is service-provider-nacos!
C:\Users\deepinsea>curl http://localhost:9070/service-gateway/gateway/api/hello?ahh=111
hello, 这里是service-gateway网关, 恭喜你请求了正确的路径!

控制台日志,如下所示:

2022-06-30 17:40:01 [reactor-http-nio-4] INFO  com.deepinsea.common.filter.AccessLogGlobalFilter - 请求ID:5e3ab22d-e00a-41a3-9590-2ab0e36935af, 请求时间:2022-06-30 17:40 2022-06-30 17:40:01.510, 请求方法:POST, 数据类型:application/json, 请求路径:/consumer-openfeign/hello, 客户端IP地址:192.168.174.1, 请求参数:{ahh=[111]}, 响应码:200 OK
2022-06-30 17:40:03 [reactor-http-nio-4] INFO  com.deepinsea.common.filter.AccessLogGlobalFilter - 请求ID:5a5594a9-d204-49c1-b080-b671694cc9ce, 请求时间:2022-06-30 17:40 2022-06-30 17:40:03.996, 请求方法:POST, 数据类型:application/x-www-form-urlencoded, 请求路径:/consumer-openfeign/hello, 客户端IP地址:192.168.174.1, 请求参数:{ahh=[111]}, 响应码:200 OK
2022-06-30 17:47:17 [reactor-http-nio-8] INFO  com.deepinsea.common.filter.AccessLogGlobalFilter - 请求ID:a3254b26-6678-46c4-bc9c-94ef6cf3dc0a, 请求时间:2022-06-30 17:47 2022-06-30 17:47:17.575, 请求方法:GET, 数据类型:null, 请求路径:/gateway/api/hello, 客户端IP地址:192.168.174.1, 请求参数:{ahh=[111]}, 响应码:200 OK

测试成功,成功记录到网关代理的请求和响应日志信息!

注意:post请求方式时Content-Type数据类型参数可为json或form表单,get请求方式时为null,注意判断方式。

完整的网关日志配置

上面的请求日志并没有记录请求体,因为gateway请求是基于webflux的原因,请求只能被消费一次。因此,日志要记录请求体比较麻烦。

下面我们来解决这个问题,并基于全局过滤器实现日志审计功能

首先先定义一个日志参数实体类AccessLog:

package com.deepinsea.common.log.trace;
​
import lombok.Data;
import org.springframework.http.HttpHeaders;
import org.springframework.util.MultiValueMap;
​
/**
 * Created by deepinsea on 2022/6/29.
 */
@Data
public class AccessLog {
    /**
     * 路径
     */
    private String path;
    /**
     * 协议(scheme)
     */
    private String scheme;
    /**
     * 请求方法
     */
    private String method;
    /**
     * 请求头
     */
    private HttpHeaders headers;
    /**
     * 目标地址
     */
    private String targetUri;
    /**
     * 远程地址
     */
    private String remoteAddress;
    /**
     * 查询参数
     */
    private MultiValueMap<String, String> queryParam;
    /**
     * 请求体
     */
    private String body;
}

然后创建全局过滤器LogFilter,对经过网关代理的请求进行记录:

package com.deepinsea.common.log.trace;
​
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
​
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.List;
​
/**
 * Created by deepinsea on 2022/6/29.
 */
@Component
public class LogFilter implements GlobalFilter, Ordered {
​
    private Logger log = LoggerFactory.getLogger(LogFilter.class);
​
    private final ObjectMapper objectMapper = new ObjectMapper();
    private static final String START_TIME = "startTime";
    private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
​
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
​
        ServerHttpRequest request = exchange.getRequest();
        // 请求路径
        String path = request.getPath().pathWithinApplication().value();
        // 请求schema: http/https
        String scheme = request.getURI().getScheme();
        // 请求方法
        HttpMethod method = request.getMethod();
        // 路由服务地址
        URI targetUri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        // 请求头
        HttpHeaders headers = request.getHeaders();
        // 设置startTime
        exchange.getAttributes().put(START_TIME, System.currentTimeMillis());
        // 远程请求地址
        InetSocketAddress remoteAddress = request.getRemoteAddress();
        // 表单数据类型的请求体(注意:queryParam参数不能与ServerHttpRequest中的queryParams参数同名,否则会出现请求404的问题)
        MultiValueMap<String, String> queryParam = request.getQueryParams();
​
        AccessLog accessLog = new AccessLog();
        accessLog.setPath(path);
        accessLog.setScheme(scheme);
        accessLog.setMethod(method.name());
        accessLog.setTargetUri(targetUri.toString());
        accessLog.setRemoteAddress(remoteAddress.toString());
        accessLog.setHeaders(headers);
        accessLog.setQueryParam(queryParam);
​
        //GET请求处理
        if (method == HttpMethod.GET) {
            writeAccessRecord(accessLog);
        }
        //POST请求处理
        if (method == HttpMethod.POST) {
            Mono<Void> voidMono = null;
            //分别有三种类型数据:POST: application/x-www-form-urlencoded和application/json, GET: null
            if (headers.getContentType().equals(MediaType.APPLICATION_JSON)) {
                // JSON
                voidMono = readBody(exchange, chain, accessLog);
            }
            if (headers.getContentType().equals(MediaType.APPLICATION_FORM_URLENCODED)) {
                // x-www-form-urlencoded
                voidMono = readFormData(exchange, chain, accessLog);
            }
            if (voidMono != null) {
                return voidMono;
            }
        }
​
        return chain.filter(exchange);
    }
​
    //表单类型的请求体数据
    private Mono<Void> readFormData(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog accessLog) {
        Mono<Void> formDateBody = readBody(exchange, chain, accessLog);
        return formDateBody;
    }
​
    private Mono<Void> readBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog accessLog) {
​
        return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
​
            byte[] bytes = new byte[dataBuffer.readableByteCount()];
            dataBuffer.read(bytes);
            DataBufferUtils.release(dataBuffer);
            Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
                DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
                DataBufferUtils.retain(buffer);
                return Mono.just(buffer);
            });
​
            // 重写请求体,因为请求体数据只能被消费一次
            ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
                @Override
                public Flux<DataBuffer> getBody() {
                    return cachedFlux;
                }
            };
​
            ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
​
            return ServerRequest.create(mutatedExchange, messageReaders)
                    .bodyToMono(String.class)
                    .doOnNext(objectValue -> {
                        accessLog.setBody(objectValue);
                        writeAccessRecord(accessLog);
                    }).then(chain.filter(mutatedExchange));
        });
    }
​
    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
​
    /**
     * TODO 异步日志
     *
     * @param accessLog
     */
    private void writeAccessRecord(AccessLog accessLog) {
​
        log.info("\n start------------------------------------------------- \n " +
                        "请求路径:{}\n " +
                        "scheme:{}\n " +
                        "请求方法:{}\n " +
                        "目标服务:{}\n " +
                        "请求头:{}\n " +
                        "远程IP地址:{}\n " +
                        "查询参数:{}\n " +
                        "请求体:{}\n " +
                        "end------------------------------------------------- ",
                accessLog.getPath(), accessLog.getScheme(), accessLog.getMethod(), accessLog.getTargetUri(),
                accessLog.getHeaders(), accessLog.getRemoteAddress(), accessLog.getQueryParam(), accessLog.getBody());
    }
}

这里同样注意处理多种请求方式的数据类型返回请求体情况,post请求方式时Content-Type数据类型参数可为json或form表单,get请求方式时为null。

下面启动GET请求接口的两个互为负载均衡的 service-provider-nacos 服务,以及POST接口的服务消费者 service-consumer-openfeign,并启动网关服务,一共四个服务来进行请求测试:

2022-07-02 04:26:50 [boundedElastic-3] INFO  com.deepinsea.common.log.trace.LogFilter - 
 start------------------------------------------------- 
 请求路径:/gateway/api/hello
 scheme:http
 请求方法:GET
 目标服务:http://192.168.174.1:9070/gateway/api/hello?msg=test
 请求头:[Host:"localhost:9070", User-Agent:"curl/7.79.1", Accept:"*/*"]
 远程IP地址:/127.0.0.1:63577
 查询参数:{msg=[test]}
 请求体:null
 end------------------------------------------------- 
2022-07-02 04:27:02 [reactor-http-nio-16] INFO  com.deepinsea.common.log.trace.LogFilter - 
 start------------------------------------------------- 
 请求路径:/consumer-openfeign/hello
 scheme:http
 请求方法:POST
 目标服务:http://192.168.174.1:9020/consumer-openfeign/hello?token
 请求头:[Host:"localhost:9070", User-Agent:"curl/7.79.1", Accept:"*/*", Content-Type:"application/x-www-form-urlencoded", content-length:"5"]
 远程IP地址:/127.0.0.1:63579
 查询参数:{token=[null]}
 请求体:'123'
 end------------------------------------------------- 
2022-07-02 04:27:14 [reactor-http-nio-1] INFO  com.deepinsea.common.log.trace.LogFilter - 
 start------------------------------------------------- 
 请求路径:/consumer-openfeign/hello
 scheme:http
 请求方法:POST
 目标服务:http://192.168.174.1:9020/consumer-openfeign/hello?msg=test
 请求头:[Host:"localhost:9070", User-Agent:"curl/7.79.1", Accept:"*/*", Content-Type:"application/json", content-length:"13"]
 远程IP地址:/127.0.0.1:63583
 查询参数:{msg=[test]}
 请求体:'{name:test}'
 end------------------------------------------------- 

注意:这里由于构建动态路由比较耗时间(注册中心服务构建优先级较低的耗时最高),并且由于日志的Order较大(位于过滤器最下游)的原因,在启动项目后请求正常的接口前端会报404的错误(后台日志输出正常)

这时需要持久化路由到存储中,并且设置合理的请求的连接超时时间和响应时间

另外SpringBoot默认日志的实现方式为logback(l像logback-spring.xml 放到 resources 目录即可被SpringBoot识别),我们可以除了在控制台输出日志,还可以配置日志输出到文件中:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
​
    <!-- 日志输出路径(./logs为父项目的根目录,/logs为当前磁盘的根目录), 建议相对路径方式 -->
    <property name="LOGS" value="./service-gateway/src/main/resources/logs" />
​
    <appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 控制台自定义字体颜色 -->
        <!-- 字体颜色配置方案一 -->
<!--        <layout class="ch.qos.logback.classic.PatternLayout">-->
<!--            <Pattern>-->
<!--&lt;!&ndash;                %black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{1.}): %msg%n%throwable&ndash;&gt;-->
<!--                %date{yyyy-MM-dd HH:mm:ss} | %highlight(%-5level) | %boldYellow(%thread) | %boldGreen(%logger) | %msg%n"-->
<!--            </Pattern>-->
<!--        </layout>-->
        <!-- 字体颜色配置方案二(推荐) -->
        <encoder>
            <!--格式化输出:%d:表示日期,%thread:表示线程名,%-5level:级别从左显示5个字符宽度,%msg:日志消息,%n:是换行符-->
            <pattern>%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger) - %cyan(%msg%n)</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
​
    <appender name="RollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOGS}/logback-gateway.log</file>
        <encoder
                class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <Pattern>%d %p %C{1.} [%t] %m%n</Pattern>
        </encoder>
​
        <rollingPolicy
                class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- rollover daily and when the file reaches 10 MegaBytes -->
            <fileNamePattern>${LOGS}/archived/logback-gateway-%d{yyyy-MM-dd}.%i.log
            </fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy
                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>50MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
    </appender>
​
    <!--配置异步日志-->
    <appender name="Async_Appender" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="RollingFile"/>
    </appender>
​
    <!-- LOG everything at INFO level -->
    <root level="info">
        <!-- 日志打印开关 -->
        <appender-ref ref="Async_Appender" />
        <!-- 控制台打印开关 -->
        <appender-ref ref="Console" />
    </root>
​
    <!-- LOG "cn.idea360*" at TRACE level additivity:是否向上级logger传递打印信息。默认是true-->
    <logger name="cn.idea360.gateway" level="info" additivity="false">
        <appender-ref ref="RollingFile" />
        <appender-ref ref="Console" />
    </logger>
​
    <!-- 以下部分可适当注释 -->
    <!--自定义 log -->
<!--    <logger name="org.springframework.web" level="ERROR"/>-->
<!--    <logger name="org.springboot.sample" level="ERROR"/>-->
<!--    <logger name="com.deepinsea" level="DEBUG"/>-->
​
    <!-- 开发、测试环境 -->
<!--    <springProfile name="dev,test">-->
<!--        <root level="info">-->
<!--            <appender-ref ref="RollingFile" />-->
<!--            <appender-ref ref="Console" />-->
<!--        </root>-->
<!--    </springProfile>-->
​
    <!-- 所有环境都要记录错误日志 -->
<!--    <root level="ERROR">-->
<!--        <appender-ref ref="ASYNC_APPENDER"/>-->
<!--    </root>-->
    <!-- 分割线 --></configuration>

这样console和日志目录下就都有日志了。

网关日志部分到此结束。关于 Webflux 的学习刚入门,觉得可以像 Rxjava 那样在 onNext 中拿到异步数据,然而在 post 获取body中没生效。经测试可知 getBody 获得的数据输出为null,而自己通过 Flux.create 创建的数据可以在订阅者中获取到。

参考spring-cloud-gateway过滤器实践

12. 请求超时配置

    # 网关
    gateway:
      # 启用开关(默认开启)
      enabled: true
      discovery:
        locator:
          # 开启从注册中心动态创建路由的功能,利用微服务名进行动态路由(在真实服务请求路径上加上/服务名)
          # 例如:http://localhost:9070/service-provider-nacos/provider-nacos/hello
          enabled: true
          # 服务名转为小写(默认yml配置就是小写,这里只是保证一下)
          lower-case-service-id: true
      # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务] --本质就是反向代理
      routes:
        - id: service-provider-nacos             # 当前路由的标识, 要求唯一
          uri: lb://service-provider-nacos       # lb指的是从 nacos 中按照名称获取微服务,并遵循负载均衡策略路由请求(动态路由)
          order: 10 # 路由的优先级,数字越小代表路由的优先级越高
          predicates: # 断言(就是路由转发要满足的条件)
            - Path=/provider-nacos/**             # 当请求路径满足Path指定的规则时,才进行路由转发
#          filters:
#            - AddRequestHeader=gateway, blue
          ## 配置过滤器(局部)
          filters:
#            - AddResponseHeader=X-Response-Foo, Bar
            - AddResponseHeader=licence, value
            ## AuthorizeGatewayFilterFactory自定义过滤器配置,值为true需要验证授权,false不需要
#            - Authorize=true
        # 我们⾃定义的路由 ID,保持唯⼀
        - id: service-gateway
          # ⽬标服务地址(部署多实例,不能加子路径)
#          uri: http://localhost:9070 # 指定具体的微服务地址(原真实服务地址还是可以访问,如果要限制走网关可以加token验证机制)
#          uri: https://www.baidu.com # 指定具体的微服务地址
          uri: lb://service-gateway
          # gateway⽹关从服务注册中⼼获取实例信息然后负载后路由
          # 断⾔:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默认⽅法来将 Predicate 组合成其他复杂的逻辑(⽐如:与,或,⾮)。
          predicates:
            # 当请求的路径为http://localhost:9070/looptest/gateway/api/hello时,转发到http://localhost:9070/gateway/api/hello
            - Path=/looptest/gateway/api/hello # 本身就是基于path的反向代理
#            - Path=/**
          filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
            - StripPrefix=1 # 转发之前去掉1层路径(去除原始请求路径中的前1级路径,即/looptest去除)
#            - SetStatus=220 # 修改原始响应的状态码
            # 名称必须为过滤器工厂类名的前缀(Log),而且参数只能有两个,由于NameValueConfig里只定义了两个属性
#            - Log=testName,testValue #自定义局部过滤器
      # 超时配置
      httpclient:
        connect-timeout: 10000
        response-timeout: 5s

13. 请求跨域配置

    # 网关
    gateway:
      # 启用开关(默认开启)
      enabled: true
      discovery:
        locator:
          # 开启从注册中心动态创建路由的功能,利用微服务名进行动态路由(在真实服务请求路径上加上/服务名)
          # 例如:http://localhost:9070/service-provider-nacos/provider-nacos/hello
          enabled: true
          # 服务名转为小写(默认yml配置就是小写,这里只是保证一下)
          lower-case-service-id: true
      # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务] --本质就是反向代理
      routes:
        - id: service-provider-nacos             # 当前路由的标识, 要求唯一
          uri: lb://service-provider-nacos       # lb指的是从 nacos 中按照名称获取微服务,并遵循负载均衡策略路由请求(动态路由)
          order: 10 # 路由的优先级,数字越小代表路由的优先级越高
          predicates: # 断言(就是路由转发要满足的条件)
            - Path=/provider-nacos/**             # 当请求路径满足Path指定的规则时,才进行路由转发
#          filters:
#            - AddRequestHeader=gateway, blue
          ## 配置过滤器(局部)
          filters:
#            - AddResponseHeader=X-Response-Foo, Bar
            - AddResponseHeader=licence, value
            ## AuthorizeGatewayFilterFactory自定义过滤器配置,值为true需要验证授权,false不需要
#            - Authorize=true
        # 我们⾃定义的路由 ID,保持唯⼀
        - id: service-gateway
          # ⽬标服务地址(部署多实例,不能加子路径)
#          uri: http://localhost:9070 # 指定具体的微服务地址(原真实服务地址还是可以访问,如果要限制走网关可以加token验证机制)
#          uri: https://www.baidu.com # 指定具体的微服务地址
          uri: lb://service-gateway
          # gateway⽹关从服务注册中⼼获取实例信息然后负载后路由
          # 断⾔:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默认⽅法来将 Predicate 组合成其他复杂的逻辑(⽐如:与,或,⾮)。
          predicates:
            # 当请求的路径为http://localhost:9070/looptest/gateway/api/hello时,转发到http://localhost:9070/gateway/api/hello
            - Path=/looptest/gateway/api/hello # 本身就是基于path的反向代理
#            - Path=/**
          filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
            - StripPrefix=1 # 转发之前去掉1层路径(去除原始请求路径中的前1级路径,即/looptest去除)
#            - SetStatus=220 # 修改原始响应的状态码
            # 名称必须为过滤器工厂类名的前缀(Log),而且参数只能有两个,由于NameValueConfig里只定义了两个属性
#            - Log=testName,testValue #自定义局部过滤器
      # 超时配置
      httpclient:
        connect-timeout: 10000
        response-timeout: 5s
      # 全局的跨域处理
      globalcors:
        add-to-simple-url-handler-mapping: true # 是否将当前cors配置加入到SimpleUrlHandlerMapping中,解决options请求被拦截问题
        corsConfigurations:
          '[/**]':
            allowedOrigins: # 允许哪些网站的跨域请求
              - "http://www.deepinsea.top"
              - "http://pic.deepinsea.top"
            allowedMethods: # 允许的跨域ajax的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" # 允许在请求中携带的头信息
            allowCredentials: true # 是否允许携带cookie
            maxAge: 360000 # 这次跨域检测的有效期

另外,还有SSL安全配置,我们下面直接参考官网配置(自己就先不配置了)。

14. SS证书配置

网关可以通过遵循通常的 Spring 服务器配置来监听 HTTPS 上的请求。以下示例显示了如何执行此操作:

示例 62.application.yml

server:
  ssl:
    enabled: true
    key-alias: scg
    key-store-password: scg1234
    key-store: classpath:scg-keystore.p12
    key-store-type: PKCS12

您可以将网关路由路由到 HTTP 和 HTTPS 后端。如果您要路由到 HTTPS 后端,则可以使用以下配置将网关配置为信任所有下游证书:

示例 63.application.yml

spring:
  cloud:
    gateway:
      httpclient:
        ssl:
          useInsecureTrustManager: true

使用不安全的信任管理器不适合生产。对于生产部署,您可以使用一组已知证书配置网关,它可以使用以下配置信任这些证书:

示例 64.application.yml

spring:
  cloud:
    gateway:
      httpclient:
        ssl:
          trustedX509Certificates:
          - cert1.pem
          - cert2.pem

如果 Spring Cloud Gateway 没有配置受信任的证书,则使用默认的信任存储(您可以通过设置javax.net.ssl.trustStore系统属性来覆盖它)。

TLS 握手

网关维护一个客户端池,用于路由到后端。通过 HTTPS 进行通信时,客户端会启动 TLS 握手。许多超时与此握手相关。您可以配置这些超时,可以按如下方式配置(显示默认值):

示例 65.application.yml

spring:
  cloud:
    gateway:
      httpclient:
        ssl:
          handshake-timeout-millis: 10000
          close-notify-flush-timeout-millis: 3000
          close-notify-read-timeout-millis: 0

Spring Cloud Gateway网关的基本配置就到这里告一段落了,关于限速、降级以及熔断在下面熔断器集成后再回过头来学习。

下面是集成Sentinel熔断器,搭配网关来实现服务熔断功能:

欢迎点赞,谢谢大佬了ヾ(◍°∇°◍)ノ゙