前言
例行每半年一次的工作轮换,接手了同事的网关服务
年底了工作不是很忙,看了下前人的代码,虽然都能读懂,但感觉应该可以再优雅一点
于是把网关的相关知识又翻阅了一下
官方资料
PS:这里如果按新方案调整的话,在结构上会看起来更清晰、可读性上会得到一定的提高
但学习研究是一回事,我肯定不会去直接修改前人的代码,我们还是要对运行稳定的项目持一点敬畏心,搞得不好,手一抖就是一个BUG
原方案 - 请求体修改
- 自定义 Filter 实现 GlobalFilter, Ordered 接口
- 重写 filter 方法,具体操作如下
// 这里的 exchange 是 过滤器入参 ServerWebExchange
ServerRequest sr = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());
// 在 flatMap 方法里做请求体的修改
Mono modifiedBody = sr.bodyToMono(String.class).flatMap
原方案 - 响应体修改
- 自定义 Filter 实现 GlobalFilter, Ordered 接口
- 自定义 ResponseDecorator 继承 ServerHttpResponseDecorator 类,顶层是 ServerHttpResponse 接口
- 重写 ServerHttpResponseDecorator 的 writeWith 接口,以此实现 responseBody 的修改,加密加签
@Override@SuppressWarnings(value = "unchecked")public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {if (body instanceof Flux) {Flux<DataBuffer> fluxBody = (Flux<DataBuffer>) body;return super.writeWith(fluxBody.buffer().map(dataBuffers -> {DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();DataBuffer join = dataBufferFactory.join(dataBuffers);byte[] content = new byte[join.readableByteCount()];join.read(content);// 释放掉内存DataBufferUtils.release(join);String originalResponseBody = new String(content, StandardCharsets.UTF_8);// 修改响应体String updatedResponseBody = modifyBody(originalResponseBody);getDelegate().getHeaders().setContentLength(updatedResponseBody.getBytes().length);return bufferFactory().wrap(updatedResponseBody.getBytes());}));}return super.writeWith(body);}
- 在 filter 方法中,添加 response 装饰器
ResponseDecorator decorator = new ResponseDecorator(exchange.getResponse());
Mono<Void> filter = chain.filter(exchange.mutate().response(decorator).build());
return filter;
新方案 - 修改客户端请求体
创建请求过滤器 RequestModifyFilter
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;import javax.annotation.Resource;
import java.util.Objects;/*** 修改客户端请求体* @author weiheng* @date 2024-02-01**/
@Component
@Slf4j
public class RequestModifyFilter implements GlobalFilter, Ordered {@Resourceprivate ModifyRequestBodyGatewayFilterFactory modifyRequestBodyFilter;@Resourceprivate RequestRewriteFunction requestRewriteFunction;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest request = exchange.getRequest();MediaType mediaType = request.getHeaders().getContentType();String method = request.getMethodValue();if (Objects.equals(HttpMethod.POST.name(), method) && MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) {// 如果是POST请求,并且请求体是 application/json 格式return modifyRequestBodyFilter.apply(new ModifyRequestBodyGatewayFilterFactory.Config().setRewriteFunction(byte[].class, byte[].class, requestRewriteFunction)).filter(exchange, chain);} else {return filter(exchange, chain);}}@Overridepublic int getOrder() {return FilterPriorityConstant.REQUEST_BODY_DECRYPT_AND_SIGNATURE_VERIFICATION;}
}
RequestRewriteFunction
import cn.hutool.core.util.RandomUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;/*** 修改请求体* @author weiheng* @date 2024-01-31**/
@Slf4j
@Component
public class RequestRewriteFunction implements RewriteFunction<byte[], byte[]> {/*** Applies this function to the given arguments.** @param serverWebExchange the first function argument* @param bytes the second function argument* @return the function result*/@Overridepublic Publisher<byte[]> apply(ServerWebExchange serverWebExchange, byte[] bytes) {ServerHttpRequest request = serverWebExchange.getRequest();HttpHeaders headers = request.getHeaders();String requestId = headers.getFirst(HttpConstant.HEADER_REQUEST_ID);JSONObject requestBody = JSON.parseObject(bytes);log.info(">>>>> requestId[{}], 原始请求体[{}]", requestId, requestBody);try {// TODO 写自己的请求拦截业务,比如请求体的 【解密、验签】// 测试,看业务服务的 controller类请求体中是否添加了该属性 - 自测OKrequestBody.put("traceId", RandomUtil.randomInt());log.info(">>>>> requestId[{}], 修改后请求体[{}]", requestId, requestBody);return Mono.just(requestBody.toString().getBytes());} catch (Exception e) {log.error(">>>>> requestId[{}], 修改请求体出错", requestId, e);throw e;}}}
新方案 - 修改服务端响应体
创建过滤器 ResponseModifyFilter
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;import javax.annotation.Resource;
import java.util.Objects;/*** 修改服务端响应体* @author weiheng* @date 2024-02-01**/
@Component
@Slf4j
public class ResponseModifyFilter implements GlobalFilter, Ordered {@Resourceprivate ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilter;@Resourceprivate ResponseRewriteFunction responseRewriteFunction;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest request = exchange.getRequest();MediaType mediaType = request.getHeaders().getContentType();String method = request.getMethodValue();if (Objects.equals(HttpMethod.POST.name(), method) && MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) {// 如果是POST请求,并且请求体是 application/json 格式return modifyResponseBodyFilter.apply(new ModifyResponseBodyGatewayFilterFactory.Config().setRewriteFunction(byte[].class, byte[].class, responseRewriteFunction)).filter(exchange, chain);} else {return filter(exchange, chain);}}@Overridepublic int getOrder() {return FilterPriorityConstant.RESPONSE_BODY_ENCRYPT_AND_ADD_SIGNATURE;}
}
ResponseRewriteFunction
import cn.hutool.core.util.RandomUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;/*** 修改响应体* @author weiheng* @date 2024-01-31**/
@Slf4j
@Component
public class ResponseRewriteFunction implements RewriteFunction<byte[], byte[]> {/*** Applies this function to the given arguments.** @param serverWebExchange the first function argument* @param bytes the second function argument* @return the function result*/@Overridepublic Publisher<byte[]> apply(ServerWebExchange serverWebExchange, byte[] bytes) {ServerHttpRequest request = serverWebExchange.getRequest();HttpHeaders headers = request.getHeaders();String requestId = headers.getFirst(HttpConstant.HEADER_REQUEST_ID);JSONObject responseBody = JSON.parseObject(bytes);log.info(">>>>> requestId[{}], 原始响应体[{}]", requestId, responseBody);try {// TODO 在这里写自己的业务逻辑,比如响应体的 加密、加签// 测试,看客户端拿到的响应体中,是否有该属性 - 亲测OKresponseBody.put("traceResponse", RandomUtil.randomInt(1000, 9999));log.info(">>>>> requestId[{}], 修改后的响应体[{}]", requestId, responseBody);return Mono.just(responseBody.toString().getBytes());} catch (Exception e) {log.error(">>>>> requestId[{}], 修改响应体出错", requestId, e);throw e;}}}
把所有过滤的优先级统一管理
自定义 FilterPriorityConstant 常量类
PS:项目中有好几个过滤器,原先都是在各自的类中定义的优先级,这里统一放到常量里进行管理
这里并未展示真实项目中的所有过滤器,仅作示例
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;/*** 过滤器优先级定义* @author weiheng* @date 2024-01-31**/
public class FilterPriorityConstant {/*** Useful constant for the highest precedence value.* @see java.lang.Integer#MIN_VALUE*/public static int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;/*** Useful constant for the lowest precedence value.* @see java.lang.Integer#MAX_VALUE*/public static int LOWEST_PRECEDENCE = Integer.MAX_VALUE;/** 商户权限校验 - 根据header的商户ID做接口权限校验 */public static int MERCHANT_AUTH_VALIDATE = 2;/** 请求体解密验签 */public static int REQUEST_BODY_DECRYPT_AND_SIGNATURE_VERIFICATION = 3;/** 响应体加密加签 value: -2 */public static int RESPONSE_BODY_ENCRYPT_AND_ADD_SIGNATURE = NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
}
以上请求体和响应体的修改,亲测OK