源码解析-Spring Cloud Gateway

375 阅读5分钟

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

源码解析-Spring Cloud Gateway,包括:能做什么、如何工作、过滤器、代码和配置使用说明

能做什么

性能:API高可用,负载均衡,容错机制。 安全:权限身份认证、脱敏,流量清洗,后端签名(保证全链路可信调用),黑名单(非法调用的限制)。 日志:日志记录(spainid,traceid)一旦涉及分布式,全链路跟踪必不可少。 缓存:数据缓存。 监控:记录请求响应数据,api耗时分析,性能监控。 限流:流量控制,错峰流控,可以定义多种限流规则。 灰度:线上灰度部署,可以减小风险。 路由:动态路由规则。

术语

  • Route(路由): 它由一个ID、一个目标URI、一组谓词和一组过滤器定义。 如果聚合谓词为true,则匹配路由。
  • Predicate(谓语): 可以匹配来自HTTP请求的任何内容,比如头部或参数。
  • Filter(过滤器): 可以在发送下游请求之前或之后修改请求和响应。

如何工作

image.png

  1. Gateway Handler Mapping接收请求,匹配路由;
  2. Gateway Web Handler通过过滤器链来处理请求;
  3. Filter允许修改HTTP请求和响应,作用在链上;
  4. Proxied Service代理具体业务。
  • org.springframework.cloud.gateway.config.GatewayAutoConfiguration 网关自动配置类。
  • org.springframework.web.server.ServerWebExchange HTTP请求-响应交互数据的约定,提供访问HTTP请求和响应属性的方法。
  • org.springframework.web.server.WebHandler 处理web请求。
  • org.springframework.cloud.gateway.filter.GlobalFilter 全局过滤器,不需要配置,作用在所有路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器。为请求业务的路由URI转换为真是业务服务地址的核心过滤器。 image.png
  • org.springframework.cloud.gateway.filter.GatewayFilter 网关路由过滤器,需要通过配置[spring.cloud.routes.filters]作用在具体路由上,可通过配置[spring.cloud.default-filters]作用在所有路由上。允许以某种方式修改传入的HTTP请求或传出的HTTP响应。
  • org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory 网关过滤器工厂。实现类有AddRequestHeaderGatewayFilterFactory、AddRequestParameterGatewayFilterFactory等 image.png
  • org.springframework.web.server.WebFilter Web相关过滤器接口。
  • org.springframework.cloud.gateway.filter.GatewayFilterChain 网关过滤链表。默认实现DefaultGatewayFilterChain。
  • org.springframework.cloud.gateway.handler.FilteringWebHandler
  • org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory 路由谓语工厂

过滤器

FilterPriorityDesc
RemoveCachedBodyFilterInteger.MIN_VALUE
AdaptCachedBodyGlobalFilterInteger.MIN_VALUE+1000
NettyWriteResponseFilter-1
ForwardPathFilter0
GatewayMetricsFilter0
RouteToRequestUrlFilter10000
LoadBalancerClientFilter10100
WebsocketRoutingFilterInteger.MAX_VALUE-1
NettyRoutingFilterInteger.MAX_VALUE
ForwardRoutingFilterInteger.MAX_VALUE
spring:
  cloud:
    gateway:
      routes:
        # 路由ID
      - id: after_route
        # 路由规则转发目标uri
        uri: lb://demo-service
        # 谓语信息
        predicates:
          # 以下配置等于(Cookie=mycookie,mycookievalue)
        - name: Cookie
          args:
            name: mycookie
            regexp: mycookievalue
        # 路由过滤器
        filters: 
          AddRequestHeader=X-Request-red, blue
        # 路由顺序,值越小优先级越高
        order: 0

默认的GatewayFilterChain信息: image.png

代码

  • AccessLogRequestFilter.java
@Component
@Slf4j
public class AccessLogRequestFilter implements GlobalFilter, Ordered {
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        Consumer<HttpHeaders> httpHeaders = httpHeader -> {
            httpHeader.set(Constants.LIGHT_GATEWAY_START, String.valueOf(System.currentTimeMillis()));
        };
        ServerHttpRequest serverHttpRequest = exchange.getRequest().mutate().headers(httpHeaders).build();
        return chain.filter(exchange.mutate().request(serverHttpRequest).build());
    }
}
  • AccessLogResponseFilter.java

接收到响应后记录日志

@Component
@Slf4j
public class AccessLogResponseFilter implements GlobalFilter, Ordered {
    @Override
    public int getOrder() {
        return -2;
    }
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpResponse response = exchange.getResponse();
        DataBufferFactory bufferFactory = response.bufferFactory();
        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                    return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
                        DataBuffer join = bufferFactory.join(dataBuffers);
                        byte[] content = new byte[join.readableByteCount()];
                        join.read(content);
                        DataBufferUtils.release(join);
                        if (log.isInfoEnabled()) {
                            ServerHttpRequest serverHttpRequest = exchange.getRequest();
                            String startTime = serverHttpRequest.getHeaders().getFirst(Constants.LIGHT_GATEWAY_START);
                            String username = serverHttpRequest.getHeaders().getFirst(ServletUtils.HEADER_KEY_LIGHT_USERNAME);
                            long elapsedTime = -1;
                            if (!StringUtils.isEmpty(startTime)) {
                                elapsedTime = System.currentTimeMillis() - Long.valueOf(startTime).longValue();
                            }
                            log.info("{} {} \"{} {}\" {} {} {} ms {}", serverHttpRequest.getId(), IpUtils.getClientIp(serverHttpRequest), serverHttpRequest.getMethodValue(), serverHttpRequest.getURI().getPath(), exchange.getResponse().getStatusCode(), content.length, elapsedTime, username);
                        }
                        return bufferFactory.wrap(content);
                    }));
                }
                return super.writeWith(body); // if body is not a flux. never got there.
            }
        };
        return chain.filter(exchange.mutate().response(decoratedResponse).build());
    }
}
  • AuthenticationFilter.java
@Component
@Slf4j
public class AuthenticationFilter implements GlobalFilter, Ordered {

    @Autowired
    private BasicClient basicClient;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getHeaders().getFirst(Constants.AUTHORIZATION);

        if (StringUtils.isEmpty(token)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        try {
            String url = exchange.getRequest().getURI().getPath();
            String subPath = url.substring(url.indexOf("/", 1) + 1);
            String permission = subPath.replaceAll("/", ":");
            AuthenticateRsp authorizationRsp = basicClient.authenticate(permission, token);
            if (authorizationRsp == null || authorizationRsp.getCode() != 200 || authorizationRsp.getUser() == null) {
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return exchange.getResponse().setComplete();
            }
            Consumer<HttpHeaders> httpHeaders = httpHeader -> {
                httpHeader.set(ServletUtils.HEADER_KEY_LIGHT_USERNAME, authorizationRsp.getUser().getUserName());
                httpHeader.set(ServletUtils.HEADER_KEY_LIGHT_USERID, String.valueOf(authorizationRsp.getUser().getUserId()));
                httpHeader.set(ServletUtils.HEADER_KEY_LIGHT_DEPTID, String.valueOf(authorizationRsp.getUser().getDeptId()));
            };
            ServerHttpRequest serverHttpRequest = exchange.getRequest().mutate().headers(httpHeaders).build();
            return chain.filter(exchange.mutate().request(serverHttpRequest).build());
        } catch (Exception e) {
            log.error("Exception: ", e);
            return exchange.getResponse().setComplete();
        }
    }

    // modify Response Body
    private Mono<Void> authError(ServerHttpResponse resp, String mess) {
        resp.setStatusCode(HttpStatus.OK);
        resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        BaseRsp baseRsp = new BaseRsp();
        baseRsp.setStatus(RspStatus.UNAUTHORIZED.value());
        baseRsp.setMessage(mess);
        String returnStr = JSON.toJSONString(baseRsp);
        DataBuffer buffer = resp.bufferFactory().wrap(returnStr.getBytes(StandardCharsets.UTF_8));
        return resp.writeWith(Flux.just(buffer));
    }

    // modify Response Body
    private Mono<Void> operationExchange(ServerWebExchange exchange, GatewayFilterChain chain, AuthenticateRsp authorizationRsp) {
        // mediaType
        MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
        // read & modify body
        ServerRequest serverRequest = new DefaultServerRequest(exchange);
        Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
                .flatMap(body -> {
                    if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) {
                        // 对原先的body进行修改操作
                        JSONObject jsonObject = JSON.parseObject(body);
                        jsonObject.put("user", authorizationRsp.getUser());
//                        jsonObject.put("username", authorizationRsp.getUser().getUserName());
//                        jsonObject.put("deptId", authorizationRsp.getUser().getDeptId());
                        return Mono.just(jsonObject.toJSONString());
                    }
                    return Mono.empty();
                });
        BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(exchange.getRequest().getHeaders());
        headers.remove(HttpHeaders.CONTENT_LENGTH);
        CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
        return bodyInserter.insert(outputMessage, new BodyInserterContext())
                .then(Mono.defer(() -> {
                    ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
                            exchange.getRequest()) {
                        @Override
                        public HttpHeaders getHeaders() {
                            long contentLength = headers.getContentLength();
                            HttpHeaders httpHeaders = new HttpHeaders();
                            httpHeaders.putAll(super.getHeaders());
                            if (contentLength > 0) {
                                httpHeaders.setContentLength(contentLength);
                            } else {
                                httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                            }
                            return httpHeaders;
                        }

                        @Override
                        public Flux<DataBuffer> getBody() {
                            return outputMessage.getBody();
                        }
                    };
                    return chain.filter(exchange.mutate().request(decorator).build());
                }));
    }

    @Override
    public int getOrder() {
        return -100;
    }
}

配置使用说明

spring:
  cloud:
    gateway:
      enabled: true
      # netty httpclient线程池配置
      httpclient:
        pool:
          # Type of pool for HttpClient to use, defaults to ELASTIC.
          type = ELASTIC
          # Only for type FIXED, the maximum number of connections before starting pending acquisition on existing ones. - 连接池最大数
          max-connections: 16
          # Only for type FIXED, the maximum time in millis to wait for aquiring. - 从池中获取连接的的最大等待时长
          acquire-timeout: 45000
          # Time in millis after which the channel will be closed. If NULL, there is no max idle time. - 连接的最大空闲时长,超时则关闭通道
          max-idle-time:
          # Duration after which the channel will be closed. If NULL, there is no max life time. - 连接的最大生命时长,超时则关闭通道
          max-life-time:
        # The connect timeout in millis, the default is 45s.
        connect-timeout: 45000
        # The response timeout.
        response-timeout: 60s
        # The max response header size.
        max-header-size: Integer.MAX_VALUE
        # The max initial line length.
        max-initial-line-length: Integer.MAX_VALUE
      actuator:
        verbose:
          # /actuator/gateway/routes监控信息中添加更多细节
          enabled: false
      discovery:
        locator:
          # 动态路由配置。 是否与服务发现组件进行结合,通过 serviceId(必须设置成大写) 转发到具体的服务实例。默认为false,设为true便开启通过服务中心的自动根据 serviceId 创建路由的功能。
          enabled: false
          # 请求路径上的服务名改为小写
          lower-case-service-id: true
      # 代理配置
      proxy:
      # SSL配置
      ssl:
      # Forwarded头部转发配置
      x-forwarded:
      # Redis限流配置
      redis-rate-limiter:
      # 默认限流
      default-filters:
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 100
            redis-rate-limiter.burstCapacity: 1000
            redis-rate-limiter.requestedTokens: 1
            key-resolver: "#{@userKeyResolver}"
            deny-empty-key: false
      # 路由配置
      routes:
        - id: light-demo1-service-route
          uri: lb://light-demo1-service/
          predicates:
            - Path=/demo1/**
          filters:
            - StripPrefix=1
        - id: light-demo2-service-route
          uri: lb://light-demo2-service/
          predicates:
            - Path=/demo2/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 60
                redis-rate-limiter.requestedTokens: 10
                key-resolver: "#{@userKeyResolver}"
          metadata:
            connect-timeout: 3000
            # The per-route response timeout.
            response-timeout: 5200

参考

cloud.spring.io/spring-clou…

www.cnblogs.com/wgslucky/p/…

blog.csdn.net/zxl64680192…

Spring Cloud Gateway 之 请求应答日志打印 blog.csdn.net/weixin_3408…

从零搭建Spring Cloud Gateway网关(二)—— 打印请求响应日志 www.lifengdi.com/archives/ar… github.com/lifengdi/sp…

Spring-Cloud-Gateway之请求响应日志打印过滤器 www.jianshu.com/p/9b781fb1a…

spring cloud gateway 统一响应结果 blog.csdn.net/zhouwangzon…

Spring Cloud Gateway 之 限流 zhuanlan.zhihu.com/p/62621868

spring-cloud-gateway限流 juejin.im/post/684490…

gateway nacos sentinel 三剑客强强组合 cloud.tencent.com/developer/a…

spring cloud gateway中Path Predicate规则类似/A/B/** /A/B/C/**时,无法定位到/A/B/C/**的问题 blog.csdn.net/Horizon_Zy/…

Spring Cloud Gateway中多规则限流(RequestRateLimiter覆盖问题) blog.csdn.net/lizz861109/…

完美解决spring cloud gateway 获取body内容并修改 blog.csdn.net/seantdj/art…

Spring Cloud Gateway 之获取请求体(Request Body)的几种方式 www.cnblogs.com/hyf-huangyo…