1.Gateway的攔截器
我們要在項目中實現一個攔截器,需要繼承兩個類:GlobalFilter, Ordered
GlobalFilter:全局過濾攔截器,在gateway中已經有部分實現,具體參照:https://www.cnblogs.com/liukaifeng/p/10055862.html
Ordered:攔截器的順序,不多說
於是一個簡單的攔截器就有了
@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。具體介紹:https://www.cnblogs.com/bjlhx/p/9786478.html
Gateway的路由轉發規則介紹:https://segmentfault.com/a/1190000019101829
2.簡介
我們在使用Spring Cloud Gateway的時候,注意到過濾器(包括GatewayFilter、GlobalFilter和過濾器鏈GatewayFilterChain),都依賴到ServerWebExchange。
這里的設計和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