background

There is a financial project. The client values system security and requires that the interface request and response data be encrypted according to specific requirements to prevent sensitive business data from being captured.

Now that the design flow has been drawn up, the client has figured out how to decrypt the response data. The server has not implemented encryption of the response data.

In abstraction, the problem is essentially how to modify the response data.

Problem description

The project has used Spring Cloud Gateway technology, where response data can be intercepted.

The question now is how to modify the response data.

Key words: Spring Cloud gateway modify response body

The solution

Spring cloud gateway has provided sample ModifyResponseBodyGatewayFilterFactory modify the response body

The sample code reads as follows:

ModifyResponseBodyGatewayFilterFactory

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.cloud.gateway.filter.factory.rewrite;

import java.util.Map;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.cloud.gateway.support.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.support.DefaultClientResponse;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public class ModifyResponseBodyGatewayFilterFactory extends AbstractGatewayFilterFactory<ModifyResponseBodyGatewayFilterFactory.Config> {
    private final ServerCodecConfigurer codecConfigurer;

    public ModifyResponseBodyGatewayFilterFactory(ServerCodecConfigurer codecConfigurer) {
        super(ModifyResponseBodyGatewayFilterFactory.Config.class);
        this.codecConfigurer = codecConfigurer;
    }

    public GatewayFilter apply(ModifyResponseBodyGatewayFilterFactory.Config config) {
        return new ModifyResponseBodyGatewayFilterFactory.ModifyResponseGatewayFilter(config);
    }

    public static class Config {
        private Class inClass;
        private Class outClass;
        private Map<String, Object> inHints;
        private Map<String, Object> outHints;
        private String newContentType;
        private RewriteFunction rewriteFunction;

        public Config(a) {}public Class getInClass(a) {
            return this.inClass;
        }

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

        public Class getOutClass(a) {
            return this.outClass;
        }

        public ModifyResponseBodyGatewayFilterFactory.Config setOutClass(Class outClass) {
            this.outClass = outClass;
            return this;
        }

        public Map<String, Object> getInHints(a) {
            return this.inHints;
        }

        public ModifyResponseBodyGatewayFilterFactory.Config setInHints(Map<String, Object> inHints) {
            this.inHints = inHints;
            return this;
        }

        public Map<String, Object> getOutHints(a) {
            return this.outHints;
        }

        public ModifyResponseBodyGatewayFilterFactory.Config setOutHints(Map<String, Object> outHints) {
            this.outHints = outHints;
            return this;
        }

        public String getNewContentType(a) {
            return this.newContentType;
        }

        public ModifyResponseBodyGatewayFilterFactory.Config setNewContentType(String newContentType) {
            this.newContentType = newContentType;
            return this;
        }

        public RewriteFunction getRewriteFunction(a) {
            return this.rewriteFunction;
        }

        public <T, R> ModifyResponseBodyGatewayFilterFactory.Config setRewriteFunction(Class<T> inClass, Class<R> outClass, RewriteFunction<T, R> rewriteFunction) {
            this.setInClass(inClass);
            this.setOutClass(outClass);
            this.setRewriteFunction(rewriteFunction);
            return this;
        }

        public ModifyResponseBodyGatewayFilterFactory.Config setRewriteFunction(RewriteFunction rewriteFunction) {
            this.rewriteFunction = rewriteFunction;
            return this; }}public class ResponseAdapter implements ClientHttpResponse {
        private final Flux<DataBuffer> flux;
        private final HttpHeaders headers;

        public ResponseAdapter(Publisher<? extends DataBuffer> body, HttpHeaders headers) {
            this.headers = headers;
            if (body instanceof Flux) {
                this.flux = (Flux)body;
            } else {
                this.flux = ((Mono)body).flux(); }}public Flux<DataBuffer> getBody(a) {
            return this.flux;
        }

        public HttpHeaders getHeaders(a) {
            return this.headers;
        }

        public HttpStatus getStatusCode(a) {
            return null;
        }

        public int getRawStatusCode(a) {
            return 0;
        }

        public MultiValueMap<String, ResponseCookie> getCookies(a) {
            return null; }}public class ModifyResponseGatewayFilter implements GatewayFilter.Ordered {
        private final ModifyResponseBodyGatewayFilterFactory.Config config;

        public ModifyResponseGatewayFilter(ModifyResponseBodyGatewayFilterFactory.Config config) {
            this.config = config;
        }

        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(exchange.getResponse()) {
                public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                    Class inClass = ModifyResponseGatewayFilter.this.config.getInClass();
                    Class outClass = ModifyResponseGatewayFilter.this.config.getOutClass();
                    MediaType originalResponseContentType = (MediaType)exchange.getAttribute("original_response_content_type");
                    HttpHeaders httpHeaders = new HttpHeaders();
                    httpHeaders.setContentType(originalResponseContentType);
                    ModifyResponseBodyGatewayFilterFactory.ResponseAdapter responseAdapter = ModifyResponseBodyGatewayFilterFactory.this.new ResponseAdapter(body, httpHeaders);
                    DefaultClientResponse clientResponse = new DefaultClientResponse(responseAdapter, ExchangeStrategies.withDefaults());
                    Mono modifiedBody = clientResponse.bodyToMono(inClass).flatMap((originalBody) -> {
                        return ModifyResponseGatewayFilter.this.config.rewriteFunction.apply(exchange, originalBody);
                    });
                    BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass);
                    CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, exchange.getResponse().getHeaders());
                    return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
                        long contentLength1 = this.getDelegate().getHeaders().getContentLength();
                        Flux<DataBuffer> messageBody = outputMessage.getBody();
                        HttpHeaders headers = this.getDelegate().getHeaders();
                        if(! headers.containsKey("Transfer-Encoding")) {
                            messageBody = messageBody.doOnNext((data) -> {
                                headers.setContentLength((long)data.readableByteCount());
                            });
                        }

                        return this.getDelegate().writeWith(messageBody);
                    }));
                }

                public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
                    return this.writeWith(Flux.from(body).flatMapSequential((p) -> {
                        returnp; })); }};return chain.filter(exchange.mutate().response(responseDecorator).build());
        }

        public int getOrder(a) {
            return -2; }}}Copy the code

The implementation code

Based on the source code example, you can add similar logic to the gateway filter to modify the response data.

ResponseFilter


@Component
@Slf4j
public class ResponseFilter implements GlobalFilter.Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        URI uri = request.getURI();
        String url = uri.getPath();

        HttpStatus statusCode = exchange.getResponse().getStatusCode();
        if(Objects.equals(statusCode, HttpStatus.BAD_REQUEST) || Objects.equals(statusCode, HttpStatus.TOO_MANY_REQUESTS)){
            // If it is a special request, the response content is processed, not processed here
            return chain.filter(exchange);
        }

        // Modify the response body based on the specific service content
        return modifyResponseBody(exchange, chain);
    }

    /** * Modifies the response body *@param exchange
     * @param chain
     * @return* /
    private Mono<Void> modifyResponseBody(ServerWebExchange exchange, GatewayFilterChain chain)  {
        ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(exchange.getResponse()) {
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                MediaType originalResponseContentType = (MediaType)exchange.getAttribute("original_response_content_type");
                HttpHeaders httpHeaders = new HttpHeaders();
                AtomicBoolean isHttpCodeOK = new AtomicBoolean(true);
                httpHeaders.setContentType(originalResponseContentType);
                ResponseAdapter responseAdapter = new ResponseAdapter(body, httpHeaders);
                HttpStatus statusCode = this.getStatusCode();

		// The modified response body
                Mono modifiedBody = getModifiedBody(statusCode, isHttpCodeOK, responseAdapter, exchange);

                // Service switch: indicates whether to enable encryption. If yes, modify the response body to encrypt the response body data. Switches are retrieved from context. The only thing I care about here is a Boolean value.
		Boolean flag;

                BodyInserter bodyInserter;
                if(! flag) {// There is no need to modify the response data
                    bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
                }else {
		    // Response data needs to be modified
		    // The ByteArrayResource class is finally used to handle this
                    bodyInserter = BodyInserters.fromPublisher(modifiedBody, ByteArrayResource.class);
                }
                CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, httpHeaders);
                return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
                    Flux<DataBuffer> messageBody = outputMessage.getBody();
                    ServerHttpResponse httpResponse = this.getDelegate();
                    HttpHeaders headers = httpResponse.getHeaders();
                    if(! headers.containsKey("Transfer-Encoding")) {
                        messageBody = messageBody.doOnNext((data) -> {
                            headers.setContentLength((long)data.readableByteCount());
                        });
                    }
                    if(! isHttpCodeOK.get()){If the service process is not 200, an exception occurs. Set the httpCode status code to 500
                        httpResponse.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
                    }

                    return httpResponse.writeWith(messageBody);
                }));
            }

            public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
                return this.writeWith(Flux.from(body).flatMapSequential((p) -> {
                    returnp; })); }};return chain.filter(exchange.mutate().response(responseDecorator).build());
    }

    public class ResponseAdapter implements ClientHttpResponse {
        private final Flux<DataBuffer> flux;
        private final HttpHeaders headers;

        public ResponseAdapter(Publisher<? extends DataBuffer> body, HttpHeaders headers) {
            this.headers = headers;
            if (body instanceof Flux) {
                this.flux = (Flux)body;
            } else {
                this.flux = ((Mono)body).flux(); }}public Flux<DataBuffer> getBody(a) {
            return this.flux;
        }

        public HttpHeaders getHeaders(a) {
            return this.headers;
        }

        public HttpStatus getStatusCode(a) {
            return null;
        }

        public int getRawStatusCode(a) {
            return 0;
        }

        public MultiValueMap<String, ResponseCookie> getCookies(a) {
            return null; }}@Override
    public int getOrder(a) {
        return FilterOrderConstant.getOrder(this.getClass().getName());
    }

    private Mono getModifiedBody(HttpStatus httpStatus, AtomicBoolean isHttpCodeOK, ResponseAdapter responseAdapter, ServerWebExchange exchange){
        switch (httpStatus){
	    // A status code that requires special processing
            case BAD_REQUEST:
            case METHOD_NOT_ALLOWED:
                isHttpCodeOK.set(false);
                return getMono(HttpCode.BAD_REQUEST, exchange);
            case INTERNAL_SERVER_ERROR:
                isHttpCodeOK.set(false);
                return getMono(HttpCode.SERVER_ERROR, exchange);
            default:
		// Main processing flow
                returngetNormalBody(isHttpCodeOK, responseAdapter, exchange); }}private Mono getNormalBody(AtomicBoolean isHttpCodeOK, ResponseAdapter responseAdapter, ServerWebExchange exchange){
        DefaultClientResponse clientResponse = new DefaultClientResponse(responseAdapter, ExchangeStrategies.withDefaults());
        return clientResponse.bodyToMono(String.class).flatMap((originalBody) -> {
	    // Service switch: indicates whether to enable encryption. If yes, modify the response body to encrypt the response body data. Switches are retrieved from context. The only thing I care about here is a Boolean value.
	    Boolean flag;
	

            ObjectMapper objectMapper = new ObjectMapper();
            try {
                R r = objectMapper.readValue(originalBody, R.class);
                /** * Exception handling process */
                if(! r.getCode().equals(HttpCode.SUCCESS.getCode())){// If the service processing value is not 200, an exception occurs
                    isHttpCodeOK.set(false);
                    ErrorR errorR = new ErrorR()
                            .setCode(r.getCode())
                            .setMsg(r.getMsg());
                    String json = objectMapper.writeValueAsString(errorR);
                    log.info("json = {}", json);
                    if(! flag) {// If encryption is not required, the response body is not modified
                        return Mono.just(json);
                    }else {
                        // Encrypt the returned data encryptionUtil. encrypt(json, key)
                        [] [] [] [] [
                        byte[] encrypt = EncryptionUtil.encrypt("{}", key);
                        ByteArrayResource byteArrayResource = new ByteArrayResource(encrypt);
			// Modify the response body, encapsulated with byteArrayResource
                        returnMono.just(byteArrayResource); }}// Business process is 200, intercept data content
                Object data = r.getData();
                if(null == data){
                    // Return data if empty, return empty object
                    if(! flag) {// If encryption is not required, the response body is not modified
                        return Mono.just("{}");
                    }else {
                        // Encrypt the returned data encryptionUtil. encrypt(json, key)
                        [] [] [] [] [
                        byte[] encrypt = EncryptionUtil.encrypt("{}", key);
                        ByteArrayResource byteArrayResource = new ByteArrayResource(encrypt);
                        returnMono.just(byteArrayResource); }}/** * Main processing process */
                String json = objectMapper.writeValueAsString(data);
				
                if(! flag) {// If encryption is not required, the response body is not modified
                    return Mono.just(json);
                }else {
                    // Encrypt the returned data encryptionUtil. encrypt(json, key)
                    [] [] [] [] [
                    byte[] encrypt = EncryptionUtil.encrypt("{}", key);
                    ByteArrayResource byteArrayResource = new ByteArrayResource(encrypt);

		    // Modify the response body, encapsulated with byteArrayResource
                    returnMono.just(byteArrayResource); }}catch (Exception e){
                e.printStackTrace();
                log.error("convert originalBody error: " + e);
                returnMono.just(originalBody); }}); }private Mono getMono(HttpCode code, ServerWebExchange exchange){
        ObjectMapper objectMapper = new ObjectMapper();
        ErrorR errorR = new ErrorR()
                .setCode(code.getCode())
                .setMsg(code.getValue());

        try {
            String json = objectMapper.writeValueAsString(errorR);
            log.info("json = {}", json);
            // Switches are retrieved from context
            if(! flag) {// If encryption is not required, the response body is not modified
                return Mono.just(json);
            }else {
                // EncryptionUtil.encrypt(json, key); encrypt(string, byte) []
                byte[] encrypt = EncryptionUtil.encrypt(json, key);
                ByteArrayResource byteArrayResource = new ByteArrayResource(encrypt);
		// Modify the response body, encapsulated with byteArrayResource
                returnMono.just(byteArrayResource); }}catch (Exception e) {
            e.printStackTrace();
            log.error("get mono error: " + e);
            return Mono.just("\"code\": 500, \"msg\":\"error\""); }}}Copy the code