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