- 在一些针对数据比较敏感的项目中会对客户端与服务端之间交互的数据进行加密处理。在gateway网关服务中处理此项业务需要如何实现呢?
- gateway中自定义的filter,可以实现对请求前的处理以及返回后的处理。
- 请求顺序可以这样理解:请求-> 自定义的filter-A -> 自定义的filter-B -> 业务处理 -> 自定义的filter-B -> 自定义的filter-A -> 返回
- 利用这一特性,我们可以在处理业务代码之前添加对参数的统一解密处理,到达下游服务中的请求则为以及解密后的正常参数了。对于处理完成的结果,同样在返回之前对返回结果进行统一加密处理。
设计实现的目标
{
"userName":"xxx",
"password":"xxxxxxxx"
}
{
"msg":"xxx",
"code":"0",
"data":{}
}
{
"k":"kkkkkkkkkkkkk",
"v":"vvvvvvvvvvvvv"
}
{
"k":"kkkkkkkkkkkkk",
"v":"vvvvvvvvvvvvv"
}
第一步:把请求参数缓存到exchange中
public class ConstantFilter {
public final static String CACHED_REQUEST_BODY_OBJECT_KEY = "CACHED_REQUEST_BODY_OBJECT_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 ...");
Object cachedRequestBodyObject = exchange.getAttributeOrDefault(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, null);
if (cachedRequestBodyObject != null) {
return chain.filter(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;
}
}
第二步:请求参数解密处理
- gateway中我们并不能直接对请求参数进行修改,那么如何才能将加密的请求一解密后的方式路由到下游的服务中去呢?这里我们需要在filter中根据原来的请求对参数解密后创建新的请求。需要注意的是:解密后对请求头CONTENT_LENGTH需要重置,都在会出现读取的参数不完整的情况。
- 如果需要对参数进行解密处理,所以对于客户端的请求统一使用POST的请求方式。
- 加密解密的方法根据你的需要处理。(此处我使用的是RSA,AES结合的方式-之后会继续分享此种加密方式)
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)){
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());
int length = decrypBytes.length;
headers.remove(HttpHeaders.CONTENT_LENGTH);
headers.setContentLength(length);
newRequest = new ServerHttpRequestDecorator(newRequest) {
@Override
public HttpHeaders getHeaders() {
return headers;
}
};
exchange.getAttributes().put(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, decrypBytes);
return chain.filter(exchange.mutate().request(newRequest).build());
}
}else if("GET".equals(method)){
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;
}
}
第四步:在配置到指定的路由中去
- 如果所有的路由都需要,则自定义过滤器中可以直接实现全局过滤器,就无需配置到指定路由中去了,如:GlobalCacheRequestBodyFilter。
@Configuration
public class GatewayConfig {
@Bean
public GlobalCacheRequestBodyFilter globalCacheRequestBodyFilter(){
return new GlobalCacheRequestBodyFilter(-30);
}
@Bean
public RouteLocator appRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/game-app-server-api/api/**")
.filters(f -> f.filter(new AppReqDecryptFilter(-25))
.filter(new AppRespEncryptFilter(-22)) // AppRespEncryptFilter < -1 && AppRespEncryptFilter > GlobalLogFilter
.filter(new AppTokenFilter(-10)) // AppTokenFilter > AppReqDecryptFilter
)
.uri("lb://game-app-server-api")
.id("game-app-server-api")
).build();
}
}