• 在一些针对数据比较敏感的项目中会对客户端与服务端之间交互的数据进行加密处理。在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则该请求无需走加密流程。接下来就是实现此功能的详细操作。
第一步:把请求参数缓存到exchange中
  • 一些自定义的常量
/**
 * @calssName ConstanFilter
 * @Description gateway中的参量
 * @Author jiangshaoneng
 * @DATE 2020/9/27 16:47
 */
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的感觉)
/**
 * @calssName AppCacheRequestBodyFilter
 * @Description 将 request body 中的内容 copy 一份,记录到 exchange 的一个自定义属性中
 * @Author jiangshaoneng
 * @DATE 2020/9/27 14:42
 */
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;
    }
}

第二步:请求参数解密处理
  • gateway中我们并不能直接对请求参数进行修改,那么如何才能将加密的请求一解密后的方式路由到下游的服务中去呢?这里我们需要在filter中根据原来的请求对参数解密后创建新的请求。需要注意的是:解密后对请求头CONTENT_LENGTH需要重置,都在会出现读取的参数不完整的情况。
  • 如果需要对参数进行解密处理,所以对于客户端的请求统一使用POST的请求方式。
  • 加密解密的方法根据你的需要处理。(此处我使用的是RSA,AES结合的方式-之后会继续分享此种加密方式)
/**
 * @calssName AppSignFilter
 * @Description APP端,请求参数解密
 * @Author jiangshaoneng
 * @DATE 2020/9/27 14:15
 */
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;
    }
}
第三部:返回结果加密操作
/**
 * @calssName AppEncryptFilter
 * @Description APP端,接口返回的数据进行加密处理
 * @Author jiangshaoneng
 * @DATE 2020/9/27 10:14
 */
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。
/**
 * @calssName GatewayConfig
 * @Description 网关配置,全局过滤器在全部的路由中都生效,各个路由中可配置指定的过滤器,仅对单个路由生效。
 * @Author jiangshaoneng
 * @DATE 2020/9/25 14:26
 */
@Configuration
public class GatewayConfig {

    /**
     * 全局过滤器:缓存请求body
     */
    @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))   // GlobalCacheRequestBodyFilter > order > GlobalLogFilter
                        .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();
    }
}
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐