SpringCloud Alibaba 2021微服务实战十五 gateway网关过滤器的应用,请求响应加解密操作,请求加入自定义参数
对于程序员做业务开发来说。也许开发的代码很少,但功能实现所要的限制却不少尤其对于与外部公司的接口很多,但各种要求不同包括数据格式,加解密,增加请求header等等,这些如果在后端实现的话,代码改动会很多,同时也增加了后端代码的不统一性,维护起来很麻烦,现就一些外部公司接口要求在网关处进行配置的应用:一,对请求参数的修改类型,比如加解密等等,在一些针对数据比较敏感的项目中会对客户端与服务端之间交互的
对于程序员做业务开发来说。也许开发的代码很少,但功能实现所要的限制却不少尤其对于与外部公司的接口很多,但各种要求不同包括数据格式,加解密,增加请求header等等,这些如果在后端实现的话,代码改动会很多,同时也增加了后端代码的不统一性,维护起来很麻烦,现就一些外部公司接口要求在网关处进行配置的应用:
一,对请求参数的修改类型,比如加解密等等,
在一些针对数据比较敏感的项目中会对客户端与服务端之间交互的数据进行加密处理。在gateway网关服务中处理此项业务需要如何实现呢?
gateway中自定义的filter,可以实现对请求前的处理以及返回后的处理。
请求顺序可以这样理解:请求-> 自定义的filter-A -> 自定义的filter-B -> 业务处理 -> 自定义的filter-B -> 自定义的filter-A -> 返回
利用这一特性,我们可以在处理业务代码之前添加对参数的统一解密处理,到达下游服务中的请求则为以及解密后的正常参数了。对于处理完成的结果,同样在返回之前对返回结果进行统一加密处理。
思路伪代码:
// 原来的请求 如:
{
"userName":"xxx",
"password":"xxxxxxxx"
}
// 原来的返回 如:
{
"msg":"xxx",
"code":"0",
"data":{}
}
// 加密后的请求 如:
{
"k":"kkkkkkkkkkkkk", // 被RSA加密的AES的key
"v":"vvvvvvvvvvvvv" // 原来的请求参数全部进行AES加密 AES({"userName":"xxx","password":"xxxxxxxx"})
}
// 加密后的返回 如:
{
"k":"kkkkkkkkkkkkk", // 被RSA加密的AES的key
"v":"vvvvvvvvvvvvv" // 原来的请求参数全部进行AES加密 AES({"msg":"xxx","code":"0","data":{}})
}
// 这样,在网络传输中遍没有明文的形式出现。避免了被抓包倒是敏感数据泄露。
// 为了开发方便:在请求头中添加 bg-debug 参数,若bg-debug=1则该请求无需走加密流程。接下来就是实现此功能的详细操作。
过滤器代码,配置参考以前的文章:
//常量
public class ConstantFilter {
// 设置到 exchange.getAttributes()中的key
public final static String CACHED_REQUEST_BODY_OBJECT_KEY = "CACHED_REQUEST_BODY_OBJECT_KEY";
// 设置到 exchange.getAttributes()中的key
public final static String BG_DEBUG_KEY = "BG_DEBUG";
// 请求参数,返回参数 加密
public final static String REQ_RES_ENCRYPT = "0";
// 请求参出,返回结果 无需加密
public final static String REQ_RES_NALMORE = "1";
}
GlobalCacheRequestBodyFilter:把请求中的body复制到exchange,便于在之后使用。也可以理解为同一个线程中,多个filter之间共享的域。(有点像ThreadLocal的感觉)
public class GlobalCacheRequestBodyFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(GlobalCacheRequestBodyFilter.class);
private int order;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("GlobalCacheRequestBodyFilter ...");
// 将 request body 中的内容 copy 一份,记录到 exchange 的一个自定义属性中
Object cachedRequestBodyObject = exchange.getAttributeOrDefault(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, null);
// 如果已经缓存过,略过
if (cachedRequestBodyObject != null) {
return chain.filter(exchange);
}
// 如果没有缓存过,获取字节数组存入 exchange 的自定义属性中
return DataBufferUtils.join(exchange.getRequest().getBody())
.map(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
return bytes;
}).defaultIfEmpty(new byte[0])
.doOnNext(bytes -> exchange.getAttributes().put(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, bytes))
.then(chain.filter(exchange));
}
@Override
public int getOrder() {
return this.order;
}
public GlobalCacheRequestBodyFilter(int order){
this.order = order;
}
}
也可以用这个:
@Component
public class CacheBodyGatewayFilter implements Ordered, GlobalFilter {
public static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (exchange.getRequest().getHeaders().getContentType() == null) {
return chain.filter(exchange);
} else {
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
DataBufferUtils.retain(dataBuffer);
Flux<DataBuffer> cachedFlux = Flux
.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
exchange.getAttributes().put(CACHE_REQUEST_BODY_OBJECT_KEY, cachedFlux);
return chain.filter(exchange.mutate().request(mutatedRequest).build());
});
}
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
第二步:请求参数解密处理
gateway中我们并不能直接对请求参数进行修改,那么如何才能将加密的请求一解密后的方式路由到下游的服务中去呢?这里我们需要在filter中根据原来的请求对参数解密后创建新的请求。需要注意的是:解密后对请求头CONTENT_LENGTH需要重置,都在会出现读取的参数不完整的情况。
如果需要对参数进行解密处理,所以对于客户端的请求统一使用POST的请求方式。
加密解密的方法根据你的需要处理。(此处我使用的是RSA,AES结合的方式-之后会继续分享此种加密方式)
@Component
@Slf4j
public class DecodeGatewayFilter implements GlobalFilter, Ordered {
public static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
//获取request body
Flux<DataBuffer> cachedBody = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY);
if(cachedBody!=null){
String raw = toRaw(cachedBody);
JSONObject jsonObject = JSON.parseObject(raw);
log.info("request body is:{}", jsonObject);
//Todo 获取到RequestBody 做你想做的操作
}
ServerHttpRequest.Builder builder = request.mutate();
return chain.filter(exchange.mutate().request(builder.build()).build());
}
@Override
public int getOrder() {
return -99;
}
/**
* 用于获取请求参数
*
* @param body
* @return
*/
private static String toRaw(Flux<DataBuffer> body) {
AtomicReference<String> rawRef = new AtomicReference<>();
body.subscribe(buffer -> {
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
DataBufferUtils.release(buffer);
rawRef.set(Strings.fromUTF8ByteArray(bytes));
});
return rawRef.get();
}
}
解密操作
public class AppReqDecryptFilter implements GatewayFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(AppReqDecryptFilter.class);
private int order;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 设置是否加密标识
List<String> bgDebugHeaders = exchange.getRequest().getHeaders().get("bg-debug");
String bgDebug = bgDebugHeaders != null ? bgDebugHeaders.get(0) : ConstantFilter.REQ_RES_ENCRYPT;
exchange.getAttributes().put(ConstantFilter.BG_DEBUG_KEY, bgDebug);
// 获取请求的方法
ServerHttpRequest oldRequest = exchange.getRequest();
String method = oldRequest.getMethodValue();
URI uri = oldRequest.getURI();
if ("POST".equals(method)){
// 尝试从 exchange 的自定义属性中取出缓存到的 body
Object cachedRequestBodyObject = exchange.getAttributeOrDefault(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, null);
if (cachedRequestBodyObject != null) {
byte[] decrypBytes;
try {
byte[] body = (byte[]) cachedRequestBodyObject;
String rootData = new String(body); // 客户端传过来的数据
decrypBytes = body;
if(ConstantFilter.REQ_RES_ENCRYPT.equals(bgDebug)) {
JSONObject jsonObject = JSONObject.parseObject(rootData);
String encryptKey = (String) jsonObject.get("k");
String encryptData = (String) jsonObject.get("v");
String key = RSAUtils.privateDecrypt(encryptKey);
String decryptData = AESUtil.AESDecrypt(encryptData, key, "CBC");
decrypBytes = decryptData.getBytes();
}
}catch (Exception e){
logger.error("客户端数据解析异常:{}", e.toString());
throw new AppException(ErrorCode.SYS_PARAMS_ERROR.code(), "客户端数据解析异常");
}
// 根据解密后的参数重新构建请求
DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory();
Flux<DataBuffer> bodyFlux = Flux.just(dataBufferFactory.wrap(decrypBytes));
ServerHttpRequest newRequest = oldRequest.mutate().uri(uri).build();
newRequest = new ServerHttpRequestDecorator(newRequest) {
@Override
public Flux<DataBuffer> getBody() {
return bodyFlux;
}
};
// 构建新的请求头
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
// 由于修改了传递参数,需要重新设置CONTENT_LENGTH,长度是字节长度,不是字符串长度
int length = decrypBytes.length;
headers.remove(HttpHeaders.CONTENT_LENGTH);
headers.setContentLength(length);
// headers.set(HttpHeaders.CONTENT_TYPE, "application/json");
newRequest = new ServerHttpRequestDecorator(newRequest) {
@Override
public HttpHeaders getHeaders() {
return headers;
}
};
// 把解密后的数据重置到exchange自定义属性中,在之后的日志GlobalLogFilter从此处获取请求参数打印日志
exchange.getAttributes().put(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, decrypBytes);
return chain.filter(exchange.mutate().request(newRequest).build());
}
}else if("GET".equals(method)){ // todo 暂不处理
Map requestQueryParams = oldRequest.getQueryParams();
return chain.filter(exchange);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return this.order;
}
public AppReqDecryptFilter(int order){
this.order = order;
}
}
第三部:返回结果加密操作
public class AppRespEncryptFilter implements GatewayFilter, Ordered {
private int order;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String bgDebug = exchange.getAttributeOrDefault(ConstantFilter.BG_DEBUG_KEY, ConstantFilter.REQ_RES_ENCRYPT);
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.buffer().map(dataBuffer -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffer);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
//释放掉内存
DataBufferUtils.release(join);
// 正常返回的数据
String rootData = new String(content, Charset.forName("UTF-8"));
byte[] respData = rootData.getBytes();
if(ConstantFilter.REQ_RES_ENCRYPT.equals(bgDebug)){
// 对数据进行加密
String randomKey = AESUtil.getRandomKey();
String encryptData = AESUtil.AESEncrypt(rootData, randomKey, "CBC");
String encryptRandomKey = RSAUtils.publicEncrypt(randomKey);
JSONObject json = new JSONObject();
json.put("k", encryptRandomKey);
json.put("v", encryptData);
respData = json.toJSONString().getBytes();
}
// 加密后的数据返回给客户端
byte[] uppedContent = new String(respData, Charset.forName("UTF-8")).getBytes();
return bufferFactory.wrap(uppedContent);
}));
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
@Override
public int getOrder() {
return this.order;
}
public AppRespEncryptFilter(int order){
this.order = order;
}
}
二
项目场景
最近做的项目中,在网关需要对一些请求,加上自己一些自定义参数,参数值是不断变化的,在要代码中进行一些操作才能得到参数的值,参数不能写死
问题描述:
我们看springcloud gateway的文档,可以看到他有很多内置的filter供我们使用
其中有AddRequestParameter这一个filter是和我们的需求最接近的,但文档的配置方法是在yml配置文件中进行参数配置,如下
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
filters:
- AddRequestParameter=red, blue
这样的缺点就是参数名和值都写死的,不方便我们动态赋值
分析和解决:
既然他这个filter能给请求加参数,在源码中,就一定有名和值的赋值操作
我们打开AddRequestParameter的源码
public class AddRequestParameterGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
public AddRequestParameterGatewayFilterFactory() {
}
public GatewayFilter apply(NameValueConfig config) {
return new GatewayFilter() {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI uri = exchange.getRequest().getURI();
StringBuilder query = new StringBuilder();
String originalQuery = uri.getRawQuery();
if (StringUtils.hasText(originalQuery)) {
query.append(originalQuery);
if (originalQuery.charAt(originalQuery.length() - 1) != '&') {
query.append('&');
}
}
String value = ServerWebExchangeUtils.expand(exchange, config.getValue());
query.append(config.getName());
query.append('=');
query.append(value);
try {
URI newUri = UriComponentsBuilder.fromUri(uri).replaceQuery(query.toString()).build(true).toUri();
ServerHttpRequest request = exchange.getRequest().mutate().uri(newUri).build();
return chain.filter(exchange.mutate().request(request).build());
} catch (RuntimeException var9) {
throw new IllegalStateException("Invalid URI query: \"" + query.toString() + "\"");
}
}
public String toString() {
return GatewayToStringStyler.filterToStringCreator(AddRequestParameterGatewayFilterFactory.this).append(config.getName(), config.getValue()).toString();
}
};
}
}
照着源码改下
接着我自己封装了一个给request添加参数的工具类
public class RequestAddParaUtils {
public ServerHttpRequest addPara(ServerWebExchange exchange , String paraName , String paraValue){
URI uri = exchange.getRequest().getURI();
StringBuilder query = new StringBuilder();
String originalQuery = uri.getRawQuery();
if (StringUtils.hasText(originalQuery)) {
query.append(originalQuery);
if (originalQuery.charAt(originalQuery.length() - 1) != '&') {
query.append('&');
}
}
query.append(paraName);
query.append('=');
query.append(paraValue);
URI newUri = UriComponentsBuilder.fromUri(uri).replaceQuery(query.toString()).build(true).toUri();
ServerHttpRequest request = exchange.getRequest().mutate().uri(newUri).build();
return request;
}
}
传入filter的exchange对象以及要添加的参数名和值即可
其中这2段代码是根据新的uri对象构建了一个新的request对象
URI newUri = UriComponentsBuilder.fromUri(uri).replaceQuery(query.toString()).build(true).toUri();
ServerHttpRequest request = exchange.getRequest().mutate().uri(newUri).build();
再看看filter中的代码吧
ServerHttpRequest newRequest = requestAddParaUtils.addPara(exchange, "userid", userid);
return chain.filter(exchange.mutate().request(newRequest).build());
根据新的request对象构建了新的exchange对象,传入传入过滤器链中,这样就给request加上了我们的想要的参数了
更多推荐
所有评论(0)