Spring Cloud Gateway:使用ReadBodyPredicateFactory讀取request的body,可能無法匹配404

需求

獲取request中的body內容

解決方案

SCG自帶ReadBodyPredicateFactory斷言,可以將body中的內容讀取到exchange對象中,使用exchange.getAttribute("cachedRequestBodyObject")獲取,但是當body爲空時,無法匹配該路由,導致返回404錯誤。

常規用法

  • GCS route配置如下
        - id: r_fapi #測試
          uri: lb://LIZZ-GATEWAY
          predicates:
            - Path=/fapi/**
            - name: ReadBodyPredicateFactory #讀取body斷言
              args:
                inClass: "#{T(String)}" #body數據類型
                predicate: "#{@testRequestBody}" #自定義斷言處理器
  •  自定義斷言處理器
/**
 * @description: ReadBodyPredicateFactory 判斷器
 * @author: lizz
 * @date: 2020/6/6 17:03
 */
@Component
public class TestRequestBody implements Predicate {

    /**
     * 根據內容判斷是否匹配該路由
     * @param o body的內容
     * @return ture-匹配成功,false-匹配失敗
     */
    @Override
    public boolean test(Object o) {
        //可以對body內容進行判斷處理
        //這裏不做處理直接返回成功
        return true;
    }
}
  •  在filter中的exchange讀取body
/**
 * @description: 輸出請求記錄
 * @author: lizz
 * @date: 2020/2/28 1:09 下午
 */
@Component
public class LogFilter implements GlobalFilter, Ordered {
...

@Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String requestBody = exchange.getAttribute("cachedRequestBodyObject");
        logger.info("fgwRequest req={},body={}", JSON.toJSONString(exchange.getRequest()), requestBody);

        //添加header
        //ServerHttpRequest newReq = request.mutate()
        //        .header(HEADER_REQUEST_ID, rid)
        //        .build();
        //生成新的exchange
        //ServerWebExchange newExchange = exchange.mutate().request(newReq).build();
        //return chain.filter(newExchange);
        return chain.filter(exchange);
    }
...
}

自定義用法

目的:body爲空也可以匹配到該路由。

處理辦法:自建一個ReadBodyPredicateFactory在處理過程中添加.thenReturn(true)

  • 根據ReadBodyPredicateFactory自建GwReadBodyPredicateFactory
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.handler.AsyncPredicate;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.stereotype.Component;
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.Mono;

import java.util.List;
import java.util.Map;
import java.util.function.Predicate;

/**
 * @description: 自定義ReadBodyPredicateFactory,copy之ReadBodyPredicateFactory
 *      實現request body爲空時也可以匹配如有成功。
 * @author: lizz
 * @date: 2020/6/8 14:22
 */
@Component
public class GwReadBodyPredicateFactory extends AbstractRoutePredicateFactory<GwReadBodyPredicateFactory.Config> {

    protected static final Log log = LogFactory.getLog(GwReadBodyPredicateFactory.class);

    private static final String TEST_ATTRIBUTE = "read_body_predicate_test_attribute";

    private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";

    private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies
            .withDefaults().messageReaders();

    public GwReadBodyPredicateFactory() {
        super(GwReadBodyPredicateFactory.Config.class);
    }

    public GwReadBodyPredicateFactory(Class<GwReadBodyPredicateFactory.Config> configClass) {
        super(configClass);
    }

    @Override
    @SuppressWarnings("unchecked")
    public AsyncPredicate<ServerWebExchange> applyAsync(GwReadBodyPredicateFactory.Config config) {
        return new AsyncPredicate<ServerWebExchange>() {
            @Override
            public Publisher<Boolean> apply(ServerWebExchange exchange) {
                Class inClass = config.getInClass();

                Object cachedBody = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY);
                Mono<?> modifiedBody;
                // We can only read the body from the request once, once that happens if
                // we try to read the body again an exception will be thrown. The below
                // if/else caches the body object as a request attribute in the
                // ServerWebExchange so if this filter is run more than once (due to more
                // than one route using it) we do not try to read the request body
                // multiple times
                if (cachedBody != null) {
                    try {
                        boolean test = config.predicate.test(cachedBody);
                        exchange.getAttributes().put(TEST_ATTRIBUTE, test);
                        return Mono.just(test);
                    } catch (ClassCastException e) {
                        if (log.isDebugEnabled()) {
                            log.debug("Predicate test failed because class in predicate "
                                    + "does not match the cached body object", e);
                        }
                    }
                    return Mono.just(false);
                } else {
                    return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange,
                            (serverHttpRequest) -> ServerRequest
                                    .create(exchange.mutate().request(serverHttpRequest)
                                            .build(), messageReaders)
                                    .bodyToMono(inClass)
                                    .doOnNext(objectValue -> exchange.getAttributes().put(
                                            CACHE_REQUEST_BODY_OBJECT_KEY,
                                            //去除多餘符號
                                            objectValue.toString().replaceAll("[\n\t\r'']", "")))
                                    .map(objectValue -> config.getPredicate()
                                            .test(objectValue))
                                    //總是返回true
                                    .thenReturn(true));
                }
            }

            @Override
            public String toString() {
                return String.format("ReadBody: %s", config.getInClass());
            }
        };
    }

    @Override
    @SuppressWarnings("unchecked")
    public Predicate<ServerWebExchange> apply(GwReadBodyPredicateFactory.Config config) {
        throw new UnsupportedOperationException(
                "GwReadBodyPredicateFactory is only async.");
    }

    public static class Config {

        private Class inClass;

        private Predicate predicate;

        private Map<String, Object> hints;

        public Class getInClass() {
            return inClass;
        }

        public GwReadBodyPredicateFactory.Config setInClass(Class inClass) {
            this.inClass = inClass;
            return this;
        }

        public Predicate getPredicate() {
            return predicate;
        }

        public GwReadBodyPredicateFactory.Config setPredicate(Predicate predicate) {
            this.predicate = predicate;
            return this;
        }

        public <T> GwReadBodyPredicateFactory.Config setPredicate(Class<T> inClass, Predicate<T> predicate) {
            setInClass(inClass);
            this.predicate = predicate;
            return this;
        }

        public Map<String, Object> getHints() {
            return hints;
        }

        public GwReadBodyPredicateFactory.Config setHints(Map<String, Object> hints) {
            this.hints = hints;
            return this;
        }

    }
}
  • route配置,predicates換成自建的GwReadBodyPrediscateFactory即可
        - id: r_fapi #測試
          uri: lb://LIZZ-GATEWAY
          predicates:
            - Path=/fapi/**
            - name: GwReadBodyPredicateFactory #讀取body斷言
              args:
                inClass: "#{T(String)}" #body數據類型
                predicate: "#{@testRequestBody}" #自定義斷言處理器

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章