SpringCloud:學習Gateway網關攔截器的ServerWebExchange

1.Gateway的攔截器

咱們要在項目中實現一個攔截器,須要繼承兩個類:GlobalFilter, Orderedhtml

GlobalFilter:全局過濾攔截器,在gateway中已經有部分實現,具體參照:http://www.javashuo.com/article/p-xwtoynni-cy.htmljava

Ordered:攔截器的順序,很少說react

因而一個簡單的攔截器就有了web

@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {return chain.filter(exchange);
    }

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

Gateway的核心接口:GatewayFilter,GlobalFilter,GatewayFilterChain。具體介紹:http://www.javashuo.com/article/p-gzqxsibn-dk.htmlspring

Gateway的路由轉發規則介紹:http://www.javashuo.com/article/p-mzsyvrsr-gw.htmljson

2.簡介

咱們在使用Spring Cloud Gateway的時候,注意到過濾器(包括GatewayFilter、GlobalFilter和過濾器鏈GatewayFilterChain),都依賴到ServerWebExchange。segmentfault

這裏的設計和Servlet中的Filter是類似的,當前過濾器能夠決定是否執行下一個過濾器的邏輯,由GatewayFilterChain#filter()是否被調用來決定。而ServerWebExchange就至關於當前請求和響應的上下文。數組

ServerWebExchange實例不單存儲了Request和Response對象,還提供了一些擴展方法,若是想實現改造請求參數或者響應參數,就必須深刻了解ServerWebExchange。安全

3.ServerWebExchange

ServerWebExchange的註釋: ServerWebExchange是一個HTTP請求-響應交互的契約。提供對HTTP請求和響應的訪問,並公開額外的服務器端處理相關屬性和特性,如請求屬性。服務器

其實,ServerWebExchange命名爲服務網絡交換器,存放着重要的請求-響應屬性、請求實例和響應實例等等,有點像Context的角色。

3.1.全部接口

public interface ServerWebExchange {

    // 日誌前綴屬性的KEY,值爲org.springframework.web.server.ServerWebExchange.LOG_ID
    // 能夠理解爲 attributes.set("org.springframework.web.server.ServerWebExchange.LOG_ID","日誌前綴的具體值");
    // 做用是打印日誌的時候會拼接這個KEY對飲的前綴值,默認值爲""
    String LOG_ID_ATTRIBUTE = ServerWebExchange.class.getName() + ".LOG_ID";
    String getLogPrefix();

    // 獲取ServerHttpRequest對象
    ServerHttpRequest getRequest();

    // 獲取ServerHttpResponse對象
    ServerHttpResponse getResponse();
    
    // 返回當前exchange的請求屬性,返回結果是一個可變的Map
    Map<String, Object> getAttributes();
    
    // 根據KEY獲取請求屬性
    @Nullable
    default <T> T getAttribute(String name) {
        return (T) getAttributes().get(name);
    }
    
    // 根據KEY獲取請求屬性,作了非空判斷
    @SuppressWarnings("unchecked")
    default <T> T getRequiredAttribute(String name) {
        T value = getAttribute(name);
        Assert.notNull(value, () -> "Required attribute '" + name + "' is missing");
        return value;
    }

     // 根據KEY獲取請求屬性,須要提供默認值
    @SuppressWarnings("unchecked")
    default <T> T getAttributeOrDefault(String name, T defaultValue) {
        return (T) getAttributes().getOrDefault(name, defaultValue);
    } 

    // 返回當前請求的網絡會話
    Mono<WebSession> getSession();

    // 返回當前請求的認證用戶,若是存在的話
    <T extends Principal> Mono<T> getPrincipal();  
    
    // 返回請求的表單數據或者一個空的Map,只有Content-Type爲application/x-www-form-urlencoded的時候這個方法纔會返回一個非空的Map -- 這個通常是表單數據提交用到
    Mono<MultiValueMap<String, String>> getFormData();   
    
    // 返回multipart請求的part數據或者一個空的Map,只有Content-Type爲multipart/form-data的時候這個方法纔會返回一個非空的Map  -- 這個通常是文件上傳用到
    Mono<MultiValueMap<String, Part>> getMultipartData();
    
    // 返回Spring的上下文
    @Nullable
    ApplicationContext getApplicationContext();   

    // 這幾個方法和lastModified屬性相關
    boolean isNotModified();
    boolean checkNotModified(Instant lastModified);
    boolean checkNotModified(String etag);
    boolean checkNotModified(@Nullable String etag, Instant lastModified);
    
    // URL轉換
    String transformUrl(String url);    
   
    // URL轉換映射
    void addUrlTransformer(Function<String, String> transformer); 

    // 注意這個方法,方法名是:改變,這個是修改ServerWebExchange屬性的方法,返回的是一個Builder實例,Builder是ServerWebExchange的內部類
    default Builder mutate() {
         return new DefaultServerWebExchangeBuilder(this);
    }

    interface Builder {      
         
        // 覆蓋ServerHttpRequest
        Builder request(Consumer<ServerHttpRequest.Builder> requestBuilderConsumer);
        Builder request(ServerHttpRequest request);
        
        // 覆蓋ServerHttpResponse
        Builder response(ServerHttpResponse response);
        
        // 覆蓋當前請求的認證用戶
        Builder principal(Mono<Principal> principalMono);
    
        // 構建新的ServerWebExchange實例
        ServerWebExchange build();
    }
}

注意到ServerWebExchange#mutate()方法,ServerWebExchange實例能夠理解爲不可變實例。

若是咱們想要修改它,須要經過mutate()方法生成一個新的實例,後面會修改請求以及響應時會用到,暫時不作介紹

4.ServerHttpRequest

ServerHttpRequest實例是用於承載請求相關的屬性和請求體,Spring Cloud Gateway中底層使用Netty處理網絡請求。

經過追溯源碼,能夠從ReactorHttpHandlerAdapter中得知ServerWebExchange實例中持有的ServerHttpRequest實例的具體實現是ReactorServerHttpRequest。

之因此列出這些實例之間的關係,是由於這樣比較容易理清一些隱含的問題,例如:ReactorServerHttpRequest的父類AbstractServerHttpRequest中初始化內部屬性headers的時候把請求的HTTP頭部封裝爲只讀的實例:

// HttpHeaders類中的readOnlyHttpHeaders方法,其中ReadOnlyHttpHeaders屏蔽了全部修改請求頭的方法,直接拋出UnsupportedOperationException
public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) {
    Assert.notNull(headers, "HttpHeaders must not be null");
    if (headers instanceof ReadOnlyHttpHeaders) {
        return headers;
    }
    else {
        return new ReadOnlyHttpHeaders(headers);
    }
}

4.1.全部接口

ublic interface HttpMessage {
    
    // 獲取請求頭,目前的實現中返回的是ReadOnlyHttpHeaders實例,只讀
    HttpHeaders getHeaders();
}    

public interface ReactiveHttpInputMessage extends HttpMessage {
    
    // 返回請求體的Flux封裝
    Flux<DataBuffer> getBody();
}

public interface HttpRequest extends HttpMessage {

    // 返回HTTP請求方法,解析爲HttpMethod實例
    @Nullable
    default HttpMethod getMethod() {
        return HttpMethod.resolve(getMethodValue());
    }
    
    // 返回HTTP請求方法,字符串
    String getMethodValue();    
    
    // 請求的URI
    URI getURI();
}    

public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage {
    
    // 鏈接的惟一標識或者用於日誌處理標識
    String getId();   
    
    // 獲取請求路徑,封裝爲RequestPath對象
    RequestPath getPath();
    
    // 返回查詢參數,是隻讀的MultiValueMap實例
    MultiValueMap<String, String> getQueryParams();

    // 返回Cookie集合,是隻讀的MultiValueMap實例
    MultiValueMap<String, HttpCookie> getCookies();  
    
    // 遠程服務器地址信息
    @Nullable
    default InetSocketAddress getRemoteAddress() {
       return null;
    }

    // SSL會話實現的相關信息
    @Nullable
    default SslInfo getSslInfo() {
       return null;
    }  
    
    // 修改請求的方法,返回一個建造器實例Builder,Builder是內部類
    default ServerHttpRequest.Builder mutate() {
        return new DefaultServerHttpRequestBuilder(this);
    } 

    interface Builder {

        // 覆蓋請求方法
        Builder method(HttpMethod httpMethod);
         
        // 覆蓋請求的URI、請求路徑或者上下文,這三者相互有制約關係,具體能夠參考API註釋
        Builder uri(URI uri);
        Builder path(String path);
        Builder contextPath(String contextPath);

        // 覆蓋請求頭
        Builder header(String key, String value);
        Builder headers(Consumer<HttpHeaders> headersConsumer);
        
        // 覆蓋SslInfo
        Builder sslInfo(SslInfo sslInfo);
        
        // 構建一個新的ServerHttpRequest實例
        ServerHttpRequest build();
    }         
}

5.ServerHttpResponse

ServerHttpResponse實例是用於承載響應相關的屬性和響應體,Spring Cloud Gateway中底層使用Netty處理網絡請求。

經過追溯源碼,能夠從ReactorHttpHandlerAdapter中得知ServerWebExchange實例中持有的ServerHttpResponse實例的具體實現是ReactorServerHttpResponse。

之因此列出這些實例之間的關係,是由於這樣比較容易理清一些隱含的問題,例如:ReactorServerHttpResponse構造函數初始化實例的時候,存放響應Header的是HttpHeaders實例,也就是響應Header是能夠直接修改的。

public ReactorServerHttpResponse(HttpServerResponse response, DataBufferFactory bufferFactory) {
    super(bufferFactory, new HttpHeaders(new NettyHeadersAdapter(response.responseHeaders())));
    Assert.notNull(response, "HttpServerResponse must not be null");
    this.response = response;
}

5.1.全部接口

public interface HttpMessage {
    
    // 獲取響應Header,目前的實現中返回的是HttpHeaders實例,能夠直接修改
    HttpHeaders getHeaders();
}  

public interface ReactiveHttpOutputMessage extends HttpMessage {
    
    // 獲取DataBufferFactory實例,用於包裝或者生成數據緩衝區DataBuffer實例(建立響應體)
    DataBufferFactory bufferFactory();

    // 註冊一個動做,在HttpOutputMessage提交以前此動做會進行回調
    void beforeCommit(Supplier<? extends Mono<Void>> action);

    // 判斷HttpOutputMessage是否已經提交
    boolean isCommitted();
    
    // 寫入消息體到HTTP協議層
    Mono<Void> writeWith(Publisher<? extends DataBuffer> body);

    // 寫入消息體到HTTP協議層而且刷新緩衝區
    Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body);
    
    // 指明消息處理已經結束,通常在消息處理結束自動調用此方法,屢次調用不會產生反作用
    Mono<Void> setComplete();
}

public interface ServerHttpResponse extends ReactiveHttpOutputMessage {
    
    // 設置響應狀態碼
    boolean setStatusCode(@Nullable HttpStatus status);
    
    // 獲取響應狀態碼
    @Nullable
    HttpStatus getStatusCode();
    
    // 獲取響應Cookie,封裝爲MultiValueMap實例,能夠修改
    MultiValueMap<String, ResponseCookie> getCookies();  
    
    // 添加響應Cookie
    void addCookie(ResponseCookie cookie);  
}

6.ServerWebExchangeUtils和上下文屬性

ServerWebExchangeUtils裏面存放了不少靜態公有的字符串KEY值(這些字符串KEY的實際值是org.springframework.cloud.gateway.support.ServerWebExchangeUtils. + 下面任意的靜態公有KEY)

這些字符串KEY值通常是用於ServerWebExchange的屬性(Attribute,見上文的ServerWebExchange#getAttributes()方法)的KEY

這些屬性值都是有特殊的含義,在使用過濾器的時候若是時機適當能夠直接取出來使用,下面逐個分析。

1.PRESERVE_HOST_HEADER_ATTRIBUTE:

  是否保存Host屬性,值是布爾值類型,寫入位置是PreserveHostHeaderGatewayFilterFactory,使用的位置是NettyRoutingFilter,做用是若是設置爲true,HTTP請求頭中的Host屬性會寫到底層Reactor-Netty的請求Header屬性中。

2.CLIENT_RESPONSE_ATTR:

  保存底層Reactor-Netty的響應對象,類型是reactor.netty.http.client.HttpClientResponse

3.CLIENT_RESPONSE_CONN_ATTR:

  保存底層Reactor-Netty的鏈接對象,類型是reactor.netty.Connection。

4.URI_TEMPLATE_VARIABLES_ATTRIBUTE:

  PathRoutePredicateFactory解析路徑參數完成以後,把解析完成後的佔位符KEY-路徑Path映射存放在ServerWebExchange的屬性中,KEY就是URI_TEMPLATE_VARIABLES_ATTRIBUTE。

5.CLIENT_RESPONSE_HEADER_NAMES:

  保存底層Reactor-Netty的響應Header的名稱集合。

6.GATEWAY_ROUTE_ATTR:

  用於存放RoutePredicateHandlerMapping中匹配出來的具體的路由(org.springframework.cloud.gateway.route.Route)實例,經過這個路由實例能夠得知當前請求會路由到下游哪一個服務。

7.GATEWAY_REQUEST_URL_ATTR:

  java.net.URI類型的實例,這個實例表明直接請求或者負載均衡處理以後須要請求到下游服務的真實URI。

8.GATEWAY_ORIGINAL_REQUEST_URL_ATTR:

  java.net.URI類型的實例,須要重寫請求URI的時候,保存原始的請求URI。

9.GATEWAY_HANDLER_MAPPER_ATTR:

  保存當前使用的HandlerMapping具體實例的類型簡稱(通常是字符串」RoutePredicateHandlerMapping」)。

10.GATEWAY_SCHEME_PREFIX_ATTR:

  肯定目標路由URI中若是存在schemeSpecificPart屬性,則保存該URI的scheme在此屬性中,路由URI會被從新構造,見RouteToRequestUrlFilter。

11.GATEWAY_PREDICATE_ROUTE_ATTR:

  用於存放RoutePredicateHandlerMapping中匹配出來的具體的路由(org.springframework.cloud.gateway.route.Route)實例的ID。

12.WEIGHT_ATTR:

  實驗性功能(此版本還不建議在正式版本使用)存放分組權重相關屬性,見WeightCalculatorWebFilter。

13.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR:

  存放響應Header中的ContentType的值。

14.HYSTRIX_EXECUTION_EXCEPTION_ATTR:

  Throwable的實例,存放的是Hystrix執行異常時候的異常實例,見HystrixGatewayFilterFactory。

15.GATEWAY_ALREADY_ROUTED_ATTR:

  布爾值,用於判斷是否已經進行了路由,見NettyRoutingFilter。

16.GATEWAY_ALREADY_PREFIXED_ATTR:

  布爾值,用於判斷請求路徑是否被添加了前置部分,見PrefixPathGatewayFilterFactory。

ServerWebExchangeUtils提供的上下文屬性用於Spring Cloud Gateway的ServerWebExchange組件處理請求和響應的時候,內部一些重要實例或者標識屬性的安全傳輸和使用

使用它們可能存在必定的風險,由於沒有人能夠肯定在版本升級以後,原有的屬性KEY或者VALUE是否會發生改變,若是評估過風險或者規避了風險以後,能夠安心使用。

例如咱們在作請求和響應日誌(相似Nginx的Access Log)的時候,能夠依賴到GATEWAY_ROUTE_ATTR,由於咱們要打印路由的目標信息。

7.修改數據

7.1.修改請求路徑

能夠將原來的請求路徑進行修改,也能夠修改其餘屬性,具體能夠看: interface Builder下的方法

@Slf4j
@Component
public class ModifyRequestFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();//修改請求路徑
        String newServletPath = "/test"
        ServerHttpRequest newRequest = request.mutate().path(newServletPath).build();return chain.filter(exchange.mutate().request(decorator).build());
    }

7.2.修改請求數據

ServerHttpRequest的getBody方法獲取的是Flux<DataBuffer>,遍歷這個DataBuffer就能夠取出數據

@Slf4j
@Component
public class ModifyRequestBodyGlobalFilter implements GlobalFilter {

    private final DataBufferFactory dataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        // 新建一個ServerHttpRequest裝飾器,覆蓋須要裝飾的方法
        ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(request) {

            @Override
            public Flux<DataBuffer> getBody() {
                Flux<DataBuffer> body = super.getBody();
                InputStreamHolder holder = new InputStreamHolder();
                body.subscribe(buffer -> holder.inputStream = buffer.asInputStream());
                if (null != holder.inputStream) {
                    try {
                        // 解析JSON的節點
                        JsonNode jsonNode = objectMapper.readTree(holder.inputStream);
                        Assert.isTrue(jsonNode instanceof ObjectNode, "JSON格式異常");
                        ObjectNode objectNode = (ObjectNode) jsonNode;
// JSON節點最外層寫入新的屬性 objectNode.put("userId", accessToken); DataBuffer dataBuffer = dataBufferFactory.allocateBuffer(); String json = objectNode.toString(); log.info("最終的JSON數據爲:{}", json); dataBuffer.write(json.getBytes(StandardCharsets.UTF_8)); return Flux.just(dataBuffer); } catch (Exception e) { throw new IllegalStateException(e); } } else { return super.getBody(); } } }; // 使用修改後的ServerHttpRequestDecorator從新生成一個新的ServerWebExchange return chain.filter(exchange.mutate().request(decorator).build()); } private class InputStreamHolder { InputStream inputStream; } }

7.3.修改路由

路由信息等存儲在ServerWebExchange的屬性中的,修改後從新存進去接口覆蓋

@Slf4j
@Component
public class ModifyRequestFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        String serviceName = exchange.getAttribute(GatewayConstant.SERVICE_NAME);

        //修改路由
        Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        Route newRoute = Route.async()
                .asyncPredicate(route.getPredicate())
                .filters(route.getFilters())
                .id(route.getId())
                .order(route.getOrder())
                .uri(GatewayConstant.URI.LOAD_BALANCE+serviceName).build();

        exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR,newRoute);return chain.filter(exchange);
    }
}

7.4.修改響應體

@Slf4j
@Component
public class ModifyResponseFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String requestId = exchange.getAttribute(GatewayConstant.REQUEST_TRACE_ID);
        ServerHttpResponse originalResponse = exchange.getResponse();
        DataBufferFactory bufferFactory = originalResponse.bufferFactory();
        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(fluxBody.map(dataBuffer -> {
                        byte[] content = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(content);
                        //釋放掉內存
                        DataBufferUtils.release(dataBuffer); 
              //原始響應
String originalbody
= new String(content, Charset.forName("UTF-8")); //修改後的響應體 finalBody = JSON.toJSONString("{test:'test'}"); return bufferFactory.wrap(finalBody.getBytes()); })); } return super.writeWith(body); } }; return chain.filter(exchange.mutate().response(decoratedResponse).build()); }
}

 8.ServerWebExchange對比Servlet

這裏總結部分我在寫代碼中遇到的一些不一樣與相應代替辦法

8.1.request.setAttribute

在HttpServletRequest中:request.setAttribute(「key」, "value");

而在ServerHttpRequest中並沒有Attribute相關操做,能夠存數據的HttpHeader也是read-only的

替代:ServerWebExchange --> exchange.getAttributes().put(「key」, "value");

 8.2.request.getHeader()

在HttpServletRequest中:request.getHeader("test");

替代:ServerHttpRequest -->request.getHeaders().getFirst(「test」)

getFirst的緣由是獲取的到Headers裏面的List數組,遍歷Header以下:

HttpHeaders headers = request.getHeaders();
for (Map.Entry<String,List<String>> header:headers.entrySet()) {
  String key = header.getKey();
  List<String> values = header.getValue()
}

 

內容參考:http://throwable.coding.me/2019/05/18/spring-cloud-gateway-modify-request-response

相關文章
相關標籤/搜索