gateway GlobalFilter 签名校验,获取Post请求体
原文链接:https://blog.csdn.net/lance_lan/article/details/103885177
前言
网上有很多方式获取Post请求内容,尝试了好多种方式,都不是最佳的使用方式。
方式一
网上大多的解决方会有很多坑,网上说最大只能1024B(点击快速传送),个人没有采用
if ("POST".equals(method)) {
//从请求里获取Post请求体
String bodyStr = resolveBodyFromRequest(serverHttpRequest);
URI uri = serverHttpRequest.getURI();
ServerHttpRequest request = serverHttpRequest.mutate().uri(uri).build();
DataBuffer bodyDataBuffer = stringBuffer(bodyStr);
Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
request = new ServerHttpRequestDecorator(request) {
@Override
public Flux<DataBuffer> getBody() {
return bodyFlux;
}
};
//封装request,传给下一级
return chain.filter(exchange.mutate().request(request).build());
} else if ("GET".equals(method)) {
Map requestQueryParams = serverHttpRequest.getQueryParams();
//TODO 得到Get请求的请求参数后,做你想做的事
return chain.filter(exchange);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
方式二
方法二,也是我一直想要采用的方式,但最终也放弃了。
方案1
选择是使用代码的形式配置路由,在路由里面配置ReadBodyPredicate预言类。
RouteLocatorBuilder.Builder serviceProvider = builder.
routes().route("info-service",
r -> r.readBody(String.class, requestBody -> {
log.info("requestBody is {}", requestBody);
return true;
}).and().path("/info/test").
filters(f -> {
f.filter(requestFilter);
return f;
})
.uri("info-service"));
RouteLocator routeLocator = serviceProvider.build();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
然后通过全局Filter中的 exchange的方式获取属性的形式获取
String body = exchange.getAttribute("cachedRequestBodyObject");
- 1
获取post请求的字符串形式以后呢,转换也比较麻烦,如果不是form表单形式的post请求还比较好转换,如果是form表单的形式,再加上"multipart/form-data"的形式,直接获取的就是http请求过来的数据,没有封装。
方案2
个人比较倾向的方式,就是采用yml的配置形式。通过继承ReadBodyPredicateFactory的形式,可以达到route编码的形式
参考代码地址:https://github.com/spring-cloud/spring-cloud-gateway/issues/1307
继承ReadBodyPredicateFactory
@Component
public class GatewayReadBodyPredicate extends ReadBodyPredicateFactory {
public static final String REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";
@Override
public AsyncPredicate<ServerWebExchange> applyAsync(Config config) {
config.setPredicate(t -> true);
return super.applyAsync(config);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
routes:
- id: info-service
uri: lb://info-service
predicates:
- Path=/info/test/**
- name: GatewayReadBodyPredicate
args:
inClass: java.lang.String
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
springcloud-gateway,github上给出的配置中,可以将配置信息中的参数的inClass设置为org.springframework.util.MultiValueMap
,但是在实际使用过程中,如果是文件和form表单一起提交的话,抛出异常。
github截图
异常信息如下:
org.springframework.web.server.UnsupportedMediaTypeStatusException: 415 UNSUPPORTED_MEDIA_TYPE
"Content type 'multipart/form-data;boundary=-------------------------
-873485462073103209590464' not supported for bodyType=org.springframework.util.MultiValueMap<?, ?>"
- 1
- 2
- 3
个人没有找到合适的类型来接收请求内容类型是**“multipart/form-data”**的java类型参数,最终放弃使用这种形式。
方式三(有效方案)
通过Filter的形式,转换新的request请求,然后获取body内容。
(参考地址找不见了,o(╥﹏╥)o)
代码注释很清楚,就不在多说.
主要涉及几个内部类
SynchronossPartHttpMessageReader
SynchronossFormFieldPart(请求参数类型)
SynchronossFilePart(文件类型)
- 1
- 2
- 3
- 4
- 5
关键性代码
DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
DataBufferUtils.retain(dataBuffer);
final Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
final ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
final ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
return cacheBody(mutatedExchange, chain, params);
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
获取转换后的ServerWebExchange以后,分析解析其内容。
private Mono<Void> cacheBody(ServerWebExchange exchange, GatewayFilterChain chain, Map<String, String> params) {
final HttpHeaders headers = exchange.getRequest().getHeaders();
if (headers.getContentLength() == 0) {
return chain.filter(exchange);
}
final ResolvableType resolvableType;
if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(headers.getContentType())) {
resolvableType = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class);
// 通过这里,大家也能了解一些,为什么在上面预言类里直接使用org.springframework.util.MultiValueMap不行,因为还要传入Part类型。
} else {
resolvableType = ResolvableType.forClass(String.class);
}
return MESSAGE_READERS.stream().filter(reader -> reader.canRead(resolvableType,
exchange.getRequest().getHeaders().getContentType())).findFirst()
.orElseThrow(() -> new IllegalStateException("no suitable HttpMessageReader.")).readMono(resolvableType,
exchange.getRequest(), Collections.emptyMap()).flatMap(resolvedBody -> {
if (resolvedBody instanceof MultiValueMap) {
@SuppressWarnings("rawtypes")
MultiValueMap<String, Object> map = (MultiValueMap) resolvedBody;
map.keySet().forEach(key -> {
// SynchronossPartHttpMessageReader
Object obj = map.get(key);
List<Object> list = (List<Object>) obj;
for (Object object : list) {
if (object.getClass().toString().equals("class org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader$SynchronossFilePart")) {
// 过滤如果是SynchronossFilePart这个文件类型,就是传入的文件参数,做签名校验的时候,我这里没有验签文件体
continue;
}
// 通过反射的形式获取这个类型SynchronossPartHttpMessageReader下面的私有类SynchronossFormFieldPart的参数值
Field[] fields = object.getClass().getDeclaredFields();
try {
for (Field field : fields) {
field.setAccessible(true);
// 保存到传入map中
params.put(key, field.get(object) + "");
}
} catch (IllegalAccessException e) {
e.printStackTrace();
LogUtils.info(e.getLocalizedMessage());
}
}
});
} else {
// post请求中,在请求地址中的参数,如果做鉴权,也要考虑到
}
// 验签或者其他操作
return chain.filter(exchange);
});
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
完整的验签代码:
// 保存HttpMessageReader
private static final List<HttpMessageReader<?>> MESSAGE_READERS = HandlerStrategies.withDefaults().messageReaders();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
LogUtils.info("访问地址:" + request.getURI().toString());
// 请求参数上的url地址
Map<String, String> params = new HashMap<>();
request.getQueryParams().forEach((key, items) -> {
params.put(key, items.get(0));
});
if ("GET".equals(request.getMethodValue())) {
return this.checkSign(params, chain, exchange);
} else if ("POST".equals(request.getMethodValue())) {
return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
DataBufferUtils.retain(dataBuffer);
final Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
final ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
final ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
return cacheBody(mutatedExchange, chain, params);
});
}
return chain.filter(exchange);
}
/***
* 验证签名
* @author Lance lance_lan_2016@163.com
* @date 2020-01-07 09:57
* @param params
* @param chain
* @param exchange
* @return reactor.core.publisher.Mono<java.lang.Void>
*
* */
private Mono<Void> checkSign(Map<String, String> params, GatewayFilterChain chain,
ServerWebExchange exchange) {
LogUtils.info("校验参数集合:" + params);
if (!MD5Sign.checkSign(appSecret, params)) {
// 返回json格式
JsonResponse jsonResponse = new JsonResponse();
jsonResponse.errorAuth();
exchange.getResponse().setStatusCode(HttpStatus.OK);
exchange.getResponse().getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(JsonUtils.toString(jsonResponse).getBytes())));
}
return chain.filter(exchange);
}
@SuppressWarnings("unchecked")
private Mono<Void> cacheBody(ServerWebExchange exchange, GatewayFilterChain chain, Map<String, String> params) {
final HttpHeaders headers = exchange.getRequest().getHeaders();
if (headers.getContentLength() == 0) {
return chain.filter(exchange);
}
final ResolvableType resolvableType;
if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(headers.getContentType())) {
resolvableType = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class);
} else {
resolvableType = ResolvableType.forClass(String.class);
}
return MESSAGE_READERS.stream().filter(reader -> reader.canRead(resolvableType,
exchange.getRequest().getHeaders().getContentType())).findFirst()
.orElseThrow(() -> new IllegalStateException("no suitable HttpMessageReader.")).readMono(resolvableType,
exchange.getRequest(), Collections.emptyMap()).flatMap(resolvedBody -> {
if (resolvedBody instanceof MultiValueMap) {
@SuppressWarnings("rawtypes")
MultiValueMap<String, Object> map = (MultiValueMap) resolvedBody;
map.keySet().forEach(key -> {
// SynchronossPartHttpMessageReader
Object obj = map.get(key);
List<Object> list = (List<Object>) obj;
for (Object object : list) {
if (object.getClass().toString().equals("class org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader$SynchronossFilePart")) {
continue;
}
Field[] fields = object.getClass().getDeclaredFields();
try {
for (Field field : fields) {
field.setAccessible(true);
params.put(key, field.get(object) + "");
}
} catch (IllegalAccessException e) {
e.printStackTrace();
LogUtils.info(e.getLocalizedMessage());
}
}
});
} else {
if (null != resolvedBody) {
String path = null;
try {
path = URLDecoder.decode(((Object) resolvedBody).toString(), "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
LogUtils.error(e.getLocalizedMessage());
}
if (null != path) {
String items[] = path.split("&");
for (String item: items) {
String subItems[] = item.split("=");
if (null != subItems && subItems.length == 2) {
params.put(subItems[0], subItems[1]);
}
}
}
}
}
return this.checkSign(params, chain, exchange);
});
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)