Spring Cloud Gateway 修改HTTP响应信息

2022-07-03 11:25:43

实践Spring Cloud的过程中,使用Gateway作为路由组件,并且基于Gateway实现权限的验证、拦截、过滤,对于下游微服务的响应结果,我们总会有需要修改以统一数据格式,或者修改过滤用户没有权限看到的数据信息,这时候就需要有一个能够修改响应体的Filter。

Spring Cloud Gateway 版本为2.1.0
在当前版本,ModifyRequestBodyGatewayFilterFactory是官方提供的修改响应体的参考类,This filter is BETA and may be subject to change in a future release.,类的注释中说明这个类在以后版本中会改进,实际使用可以参考实现功能,但是性能影响较大,不过没有别的选择还是得选择这个。

官方文档:https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/single/spring-cloud.html#_modify_response_body_gatewayfilter_factory

实现

最终代码

先贴最终代码

publicclassResponseDecryptionGlobalFilterimplementsGlobalFilter, Ordered{privatestatic Logger log= LoggerFactory.getLogger(ResponseDecryptionGlobalFilter.class);@OverridepublicintgetOrder(){// 控制在NettyWriteResponseFilter后执行return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER-1;}@Overridepublic Mono<Void>filter(ServerWebExchange exchange, GatewayFilterChain chain){returnprocessResponse(exchange, chain);}private Mono<Void>processResponse(ServerWebExchange exchange, GatewayFilterChain chain){// 路由中如果不需要过滤则不进行过滤if(!BooleanUtils.isTrue()){return chain.filter(exchange);}
        ServerHttpResponseDecorator responseDecorator=newServerHttpResponseDecorator(exchange.getResponse()){@Overridepublic Mono<Void>writeWith(Publisher<?extendsDataBuffer> body){
                String originalResponseContentType= exchange.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
                HttpHeaders httpHeaders=newHttpHeaders();
                httpHeaders.add(HttpHeaders.CONTENT_TYPE, originalResponseContentType);
                ResponseAdapter responseAdapter=newResponseAdapter(body, httpHeaders);
                DefaultClientResponse clientResponse=newDefaultClientResponse(responseAdapter, ExchangeStrategies.withDefaults());
                Mono<String> rawBody= clientResponse.bodyToMono(String.class).map(s-> s);
                BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter= BodyInserters.fromPublisher(rawBody, String.class);
                CachedBodyOutputMessage outputMessage=newCachedBodyOutputMessage(exchange, exchange.getResponse().getHeaders());return bodyInserter.insert(outputMessage,newBodyInserterContext()).then(Mono.defer(()->{
                            Flux<DataBuffer> messageBody= outputMessage.getBody();
                            Flux<DataBuffer> flux= messageBody.map(buffer->{
                                CharBuffer charBuffer= StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
                                DataBufferUtils.release(buffer);// 将响应信息转化为字符串
                                String responseStr= charBuffer.toString();if(StringUtils.isNotBlank(responseStr)){try{
                                        JSONObject result= JSONObject.parseObject(responseStr);
                                        System.out.println(dataFilter(result));if(result.containsKey("data")){
                                            responseStr=dataFilter(result);}else{
                                            log.error("响应结果序列化异常:{}", responseStr);}}catch(JSONException e){
                                        log.error("响应结果序列化异常:{}", responseStr);}}returngetDelegate().bufferFactory().wrap(responseStr.getBytes(StandardCharsets.UTF_8));});
                            HttpHeaders headers=getDelegate().getHeaders();// 修改响应包的大小,不修改会因为包大小不同被浏览器丢掉
                            flux= flux.doOnNext(data-> headers.setContentLength(data.readableByteCount()));returngetDelegate().writeWith(flux);}));}};return chain.filter(exchange.mutate().response(responseDecorator).build());}/**
     * 权限数据过滤
     *
     * @param result
     * @return
     */private StringdataFilter(JSONObject result){
        Object data= result.get("data");return result.toJSONString();}privateclassResponseAdapterimplementsClientHttpResponse{privatefinal Flux<DataBuffer> flux;privatefinal HttpHeaders headers;@SuppressWarnings("unchecked")privateResponseAdapter(Publisher<?extendsDataBuffer> body, HttpHeaders headers){this.headers= headers;if(bodyinstanceofFlux){
                flux=(Flux) body;}else{
                flux=((Mono) body).flux();}}@Overridepublic Flux<DataBuffer>getBody(){return flux;}@Overridepublic HttpHeadersgetHeaders(){return headers;}@Overridepublic HttpStatusgetStatusCode(){return null;}@OverridepublicintgetRawStatusCode(){return0;}@Overridepublic MultiValueMap<String, ResponseCookie>getCookies(){return null;}}}

踩过的坑

  • 响应体报文过大: 起初直接读取buffer的响应信息,包小的情况没有问题,但是包大了会抛出json无法转换异常,因为没能读取完整的响应内容,参考ModifyRequestBodyGatewayFilter,等待buffer全部读完再转为数组,然后执行处理。本质原因是底层的Reactor-Netty的数据块读取大小限制导致获取到的DataBuffer实例里面的数据是不完整的。
  • 修改响应信息后,响应的ContentLength会发生变化,忘记修改response中的Content-Length长度,导致前端请求无法获取修改后的响应结果。
flux= flux.doOnNext(data-> headers.setContentLength(data.readableByteCount()));
  • order值必须小于-1,因为覆盖返回响应体,自定义的GlobalFilter必须比NettyWriteResponseFilter处理完后执行。order越小越早进行处理,越晚处理响应结果。

理解ServerWebExchange

先看ServerWebExchange的注释:

Contract for an HTTP request-response interaction. Provides access to the HTTP request and response and also exposes additional server-side processing related properties and features such as request attributes.

翻译一下大概是:

ServerWebExchange是一个**HTTP请求-响应交互的契约。**提供对HTTP请求和响应的访问,并公开额外的服务器端处理相关属性和特性,如请求属性。

ServerWebExchange有点像Context的角色,我把它理解为http请求信息在Filter透传的容器,之所以称之为容器,因为它可以存储我们像放进去的数据。


注意:
ServerHttpRequest是一个只读类,因此需要通过下面例子的方法来进行修改,对于读多写少的场景,这种设计模式是值得借鉴的

ServerHttpRequest newRequest = request.mutate().headers("key","value").path("/myPath").build();
ServerWebExchange newExchange = exchange.mutate().response(responseDecorator).build();

参考:https://juejin.im/post/5cdfb8fe6fb9a07f04201838#heading-10

  • 作者:帷幄庸者
  • 原文链接:https://blog.csdn.net/weiwoyonzhe/article/details/90814680
    更新时间:2022-07-03 11:25:43