我們使用SpringCloud Gateway做微服務網關的時候,經常需要在過濾器Filter中讀取到Post請求中的Body內容進行日誌記錄、簽名驗證、權限驗證等操作。我們知道,Request的Body是隻能讀取一次的,如果直接通過在Filter中讀取,而不封裝回去回導致後面的服務無法讀取數據。
SpringCloud Gateway 內部提供了一個斷言工廠類ReadBodyPredicateFactory,這個類實現了讀取Request的Body內容並放入緩存,我們可以通過從緩存中獲取body內容來實現我們的目的。
1、分析ReadBodyPredicateFactory
public AsyncPredicate<ServerWebExchange> applyAsync(ReadBodyPredicateFactory.Config config) {
return (exchange) -> {
Class inClass = config.getInClass();
Object cachedBody = exchange.getAttribute("cachedRequestBodyObject");
if (cachedBody != null) {
try {
boolean test = config.predicate.test(cachedBody);
exchange.getAttributes().put("read_body_predicate_test_attribute", test);
return Mono.just(test);
} catch (ClassCastException var7) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Predicate test failed because class in predicate does not match the cached body object", var7);
}
return Mono.just(false);
}
} else {
return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap((dataBuffer) -> {
DataBufferUtils.retain(dataBuffer);
final Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
return Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount()));
});
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
return ServerRequest.create(exchange.mutate().request(mutatedRequest).build(), messageReaders).bodyToMono(inClass).doOnNext((objectValue) -> {
exchange.getAttributes().put("cachedRequestBodyObject", objectValue);
exchange.getAttributes().put("cachedRequestBody", cachedFlux);
}).map((objectValue) -> {
return config.predicate.test(objectValue);
});
});
}
};
}
通過查看ReadBodyPredicateFactory內部實現,我們可以看到,該工廠類將request body內容讀取後存放在 exchange的cachedRequestBodyObject中。那麼我們可以通過代碼:exchange.getAttribute(“cachedRequestBodyObject”); //將body內容取出來。
知道如何取body內容後,我們只需將該工廠類註冊到yml配置文件中的predicates,然後從Filter中獲取即可。
2、配置ReadBodyPredicateFactory
查看ReadBodyPredicateFactory關於配置的代碼:
public <T> ReadBodyPredicateFactory.Config setPredicate(Class<T> inClass, Predicate<T> predicate) {
this.setInClass(inClass);
this.predicate = predicate;
return this;
}
配置該工廠類需要兩個參數:
inClass:接收body內容的對象Class,我們用字符串接收,配置String即可。
Predicate:Predicate的接口實現類,我們自定義一個Predicate的實現類即可。
自定義Predicate實現,並註冊Bean。
/**
* 用於readBody斷言,可配置到yml
* @return
*/
@Bean
public Predicate bodyPredicate(){
return new Predicate() {
@Override
public boolean test(Object o) {
return true;
}
};
}
兩個參數都有了,直接在yml中配置:
predicates:
- Path=/card/api/**
- name: ReadBodyPredicateFactory #使用ReadBodyPredicateFactory斷言,將body讀入緩存
args:
inClass: '#{T(String)}'
predicate: '#{@bodyPredicate}' #注入實現predicate接口類
3、編寫自定義GatewayFilterFactory
編寫自己的過濾器工廠類,讀取緩存的body內容,並支持在配置文件中配置。
public class ReadBodyGatewayFilterFactory
extends AbstractGatewayFilterFactory<ReadBodyGatewayFilterFactory.Config> {
private Logger logger = LoggerFactory.getLogger(ReadBodyGatewayFilterFactory.class);
private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";
public ReadBodyGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return ((exchange, chain) -> {
//利用ReadBodyPredicateFactory斷言,會將body讀入exchange的cachedRequestBodyObject中
Object requestBody = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY);
logger.info("request body is:{}", requestBody);
return chain.filter(exchange);
});
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("withParams");//將參數放入
}
public static class Config {
private boolean withParams;//接收配置的參數值,可以隨便寫
public boolean isWithParams() {
return withParams;
}
public void setWithParams(boolean withParams) {
this.withParams = withParams;
}
}
}
將ReadBodyGatewayFilterFactory工程類在容器中注入。
/**
* 注入ReadBody過濾器
* @return
*/
@Bean
public ReadBodyGatewayFilterFactory readBodyGatewayFilterFactory() {
return new ReadBodyGatewayFilterFactory();
}
到此,我們的Filter類也可以在yml配置文件中直接配置使用了。
4、完整的yml配置
- id: body_route #讀取post中的body路由
order: 5
uri: lb://API-CARD
filters:
- ReadBody=true #使用自定義的過濾器工廠類,讀取request body內容
predicates:
- Path=/card/api/**
- name: ReadBodyPredicateFactory #使用ReadBodyPredicateFactory斷言,將body讀入緩存
args:
inClass: '#{T(String)}'
predicate: '#{@bodyPredicate}' #注入實現predicate接口類
OK,以上是通過ReadBodyPredicateFactory這個類讀取到request body內容。
另外springcloud gateway內部還提供了ModifyRequestBodyGatewayFilterFactory類用於修改body內容,既然能修改,自然也能獲取body,大家可自行去研究。