Spring Cloud Gateway:使用ReadBodyPredicateFactory读取request的body,可能无法匹配404
需求
获取request中的body内容
常规用法
SCG自带ReadBodyPredicateFactory断言,可以将body中的内容读取到exchange对象中,使用exchange.getAttribute("cachedRequestBodyObject")获取
但是当body为空时,无法匹配该路由,导致返回404错误。
- SCG 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}" #自定义断言处理器
java code:
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes().route("r_fapi", r -> r.path("/fapi/**")
.and().asyncPredicate(new GwReadBodyPredicateFactory()
.applyAsync(new GwReadBodyPredicateFactory.Config()
.setPredicate(String.class,new TestRequestBody())))
.uri("lb://LIZZ-GATEWAY")).build();
}