接口出入参加解密的解决方案

175 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情

接口出入参加解密的解决方案

接口出入参 的解决方案

背景

日常开发中为了数据安全,我们需要对接口请求的内容进行加密,防止直接通过接口请求查看而导致泄露的风险。本方案的目的是为了医保自律中加密接口出入参的数据,防止第三方直接窃取规则数据,本方案同样适用于其他产品中接口出入餐的加密解密。

解决思路

通过gateway中自定义的filter,可以实现对请求前的处理以及返回后的处理。

请求顺序可以这样理解:请求-> 自定义的filter-A -> 自定义的filter-B -> 业务处理 -> 自定义的filter-B -> 自定义的filter-A -> 返回 。

利用这一特性以及与前端的约定参数加密方式,我们可以在处理业务代码之前添加对参数的统一解密处理,到达下游服务中的请求则为以及解密后的正常参数了。对于处理完成的结果,同样在返回之前对返回结果进行统一加密处理。

与前端约定将是否需要加密解密放入头部某个参数中,也可以后台自己维护接口路径表,根据路径表中配置的信息来判断该接口是否需要加解密。

**

源代码解析**

image.png 后台这里一共弄了3个过滤器:GlobalCacheRequestBodyFilter,ReqDecryptFilter,RespEncryptFilter,下面逐一介绍。

设计order参数,让三个过滤器按照顺序请求(order值小的排在前)方便理解这里设置为:

GlobalCacheRequestBodyFilter:-3

ReqDecryptFilter:-2

RespEncryptFilter:-1

负数可以使得这三个过滤在其余代码过滤器之前执行,如不放心可以设置的更小,顺序就是GlobalCacheRequestBodyFilter -> ReqDecryptFilter-> RespEncryptFilter

三个过滤器实现GlobalFilter, Ordered接口,重写getOrder和filter方法。

GlobalCacheRequestBodyFilter可处理一些数据判断,缓存一部分数据至exchange 的一个自定义属性中

/**
 * @Description post请求时,将 request body 中的内容 copy 一份,记录到 exchange 的一个自定义属性中
 */
@Slf4j
@Component
public class GlobalCacheRequestBodyFilter implements GlobalFilter, Ordered {

    @Override
    public int getOrder() {
        return -3;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        List<String> needDesHeaders = exchange.getRequest().getHeaders().get(FilterConstant.NEED_DES_HEADER);
        String needDes = needDesHeaders != null ? needDesHeaders.get(0) : FilterConstant.REQ_RES_NO_ENCRYPT;
        exchange.getAttributes().put(FilterConstant.NEED_DES_HEADER, needDes);
        if (!StringUtils.equals(needDes, FilterConstant.REQ_RES_ENCRYPT)) {
            return chain.filter(exchange);
        }

        ServerHttpRequest oldRequest = exchange.getRequest();
        String method = oldRequest.getMethodValue();
        if (!"POST".equals(method)) {
            return chain.filter(exchange);
        }
        // 将 request body 中的内容 copy 一份,记录到 exchange 的一个自定义属性中
        Object cachedRequestBodyObject = exchange.getAttributeOrDefault(FilterConstant.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(FilterConstant.CACHED_REQUEST_BODY_OBJECT_KEY, bytes))
                .then(chain.filter(exchange));
    }

}

代码如上,与前端约定将是否需要加密解密放入头部参数,这里也可以后台自己维护接口路径表,根据路径表中配置的信息来判断该接口是否需要加解密。

 

ReqDecryptFilter中处理参数的解密:

gateway中我们并不能直接对请求参数进行修改,那么如何才能将加密的请求解密后的再传递到下游的服务中去呢?这里我们需要在filter中根据原来的请求对参数解密后再创建新的请求。需要注意的是:post一般入参放在body中,解密重新写入参数后,由于body长度被改变,请求头的CONTENT_LENGTH也应当重置。

/**
 * @Description 请求参数解密
 */
@Slf4j
@Component
public class ReqDecryptFilter implements GlobalFilter, Ordered {

    @Override
    public int getOrder() {
        return -2;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 设置是否加密标识
        String needDes = exchange.getAttributeOrDefault(FilterConstant.NEED_DES_HEADER, FilterConstant.REQ_RES_NO_ENCRYPT);
        ServerHttpRequest oldRequest = exchange.getRequest();
        String method = oldRequest.getMethodValue();
        URI uri = oldRequest.getURI();
        if (StringUtils.equals(needDes, FilterConstant.REQ_RES_ENCRYPT)) {
            // 获取请求的方法
            if ("POST".equals(method)) {
                Object cachedRequestBodyObject = exchange.getAttributeOrDefault(FilterConstant.CACHED_REQUEST_BODY_OBJECT_KEY, null);
                byte[] decrypBytes;
                try {
                    byte[] body = (byte[]) cachedRequestBodyObject;
                    String rootData = new String(body);
                    decrypBytes = body;
                    if (FilterConstant.REQ_RES_ENCRYPT.equals(needDes) && StringUtils.isNotEmpty(rootData)) {
                        JSONObject jsonObject = JSONObject.parseObject(rootData);
                        String encryptData = (String) jsonObject.get("value");
                        String decryptData = URLDecoder.decode(RSAUtil.privateDecipher(encryptData, FilterConstant.privateKey), "UTF-8");
                        decrypBytes = StringUtils.isEmpty(decryptData) ? "{}".getBytes() : decryptData.getBytes();
                    }

                } catch (Exception e) {
                    log.error("post前端数据解析异常:{}", e.toString());
                    throw new CdrBizException("post前数据解析异常");
                }

                // 根据解密后的参数重新构建请求
                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;
                    }
                };

                return chain.filter(exchange.mutate().request(newRequest).build());
            } else if ("GET".equals(method)) {
                MultiValueMap<String, String> requestQueryParams = oldRequest.getQueryParams();
                List<String> valueList = requestQueryParams.getOrDefault("value", null);
                if (!CollectionUtils.isEmpty(valueList)) {
                    try {
                        String value = valueList.get(0);
                        String decData = RSAUtil.privateDecipher(value, FilterConstant.privateKey);
                        List<String> query = new ArrayList<>();
                        MultiValueMap<String, String> newRequestQueryParams = new LinkedMultiValueMap<>();
                        if (JSONValidator.from(decData).validate()) {
                            MultiValueMap<String, String> multiValueMap = JSONObject.parseObject(decData, MultiValueMap.class);
                            for (String queryKey : multiValueMap.keySet()) {
                                List<String> queryValues = multiValueMap.get(queryKey);
                                for (String queryValue : queryValues) {
                                    queryValue = URLEncoder.encode(queryValue, "UTF-8");
                                    query.add(queryKey + "=" + queryValue);
                                    newRequestQueryParams.add(queryKey, queryValue);
                                }
                            }
                        } else {
                            for (String queryKeyValues : decData.split("&")) {
                                String[] queryKeyValue = queryKeyValues.split("=");
                                String queryKey = queryKeyValue[0];
                                String queryValue = queryKeyValue.length > 1 ? URLEncoder.encode(queryKeyValue[1], "UTF-8") : "";
                                query.add(queryKey + "=" + queryValue);
                                newRequestQueryParams.add(queryKey, queryValue);
                            }
                        }
                        URI newUri = UriComponentsBuilder.fromUri(uri)
                                .replaceQuery(String.join("&", query))
                                .build(true)
                                .toUri();

                        ServerHttpRequest newRequest = oldRequest.mutate().uri(newUri).build();
                        return chain.filter(exchange.mutate().request(newRequest).build());
                    } catch (Exception e) {
                        log.error("get前端数据解析异常:{}", e.toString());
                        throw new CdrBizException("get前端数据解析异常");
                    }
                }
            }
        }
        return chain.filter(exchange);
    }
}

根据post与get请求的入参放置不同,到不同的地方去获取入参并放入新请求中。由于前端使用的是jsencrypt,对中文的加密不友好,所以参数均被转义后再加密。所以需要解密后要先解除转义。

post请求在body中,约定放入key为“value”中,取出并解密后重新根据老请求构造新请求,修改body与请求头数据。get请求同理,取出解密即可,get由于放在query中,不需要重新构造body和请求头,重新构造URI即可。根据私钥解密

通过return chain.filter(exchange.mutate().request(newRequest).build());传递新请求。

 

RespEncryptFilter中处理出参的加密:

重写exchange的response,新建一个ServerHttpResponseDecorator重写writeWith方法即可,根据私钥加密。

 /**
 * @Description 接口返回的数据进行加密处理
 */
@Slf4j
@Component
public class RespEncryptFilter implements GlobalFilter, Ordered {

    @Override
    public int getOrder() {
        return -1;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String needDes = exchange.getAttributeOrDefault(FilterConstant.NEED_DES_HEADER, FilterConstant.REQ_RES_NO_ENCRYPT);

        if (FilterConstant.REQ_RES_ENCRYPT.equals(needDes)) {
            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 (FilterConstant.REQ_RES_ENCRYPT.equals(needDes)) {
                                // 对数据进行加密
                                try {
                                    CdrReturnData cdrReturnData = JSONArray.parseObject(rootData, CdrReturnData.class);
                                    if (cdrReturnData.getRetData() != null) {
                                        String retData = JSONObject.toJSONString(cdrReturnData.getRetData());
                                        String encryptData = RSAUtil.privateEncipher(retData, FilterConstant.privateKey);
                                        cdrReturnData.setRetData(encryptData);
                                        respData = JSONObject.toJSONString(cdrReturnData).getBytes();
                                    }

                                } catch (Exception e) {
                                    log.error("服务端数据加密异常:{}", e.toString());
                                    throw new CdrBizException("服务端数据加密异常");
                                }
                            }

                            // 加密后的数据返回给客户端
                            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());
        }
        return chain.filter(exchange);
    }
}

 

前端引入JSEncrypt与encryptlong修改http请求方法,添加是否需要加解密参数needDes

axiosInstance.interceptors.request.use(function(config) {
  if(CIS_NEV == 'dev' || CIS_NEV == "build_dev"){
    config.headers.needDes = ''
  }
  let { needDes = '' } = config.headers;
  globalObject[config.url] = needDes;
  var jsencrypt = new JSEncrypt();
  jsencrypt.setPublicKey(publicKey);
  // Do something before request is sent
  config.headers.common['x-csrf-token'] = _utils.getCookie('csrfToken');
  config.headers.common['token'] = _utils.getSessionStorage('token');
  config.headers.common['moduleId'] = _utils.getSessionStorage('moduleId');
  if(config.params && config.params.responseType){
    config['responseType'] = config.params.responseType
  }
  if (config.method === 'get') {
    // 如果是get请求,且params是数组类型如arr=[1,2],则转换成arr=1&arr=2,不转换会显示为arr[]=1&arr[]=2
    config.paramsSerializer = function(params) {
      return qs.stringify(params, { arrayFormat: 'repeat' });
    };
    if(config.params && needDes == 1){
      let str = Object.keys(config.params).map(item => item+'='+config.params[item]).join('&')
      config.params = {
        value:jsencrypt.encryptLong(str)
      }
    }
  }
  if(config.method === 'post' && config.data && needDes == 1){
    let str = jsencrypt.encryptLong(encodeURIComponent(JSON.stringify(config.data)))
    let obj = {
      value:str
    }
    config.data = obj
  }
  return config;
});

// response inject
axiosInstance.interceptors.response.use(
  (res) => {
    if(globalObject[res.config.url] == 1){
      res.data.retData = JSON.parse(RSADECRY.decryptLongByPublicKey(res.data.retData,publicKey))
    }
    _msg.hideLoading();
    if (res.data.retCode === 0) {
      return res.data.retData;
    }else if(!res.data.error){
      return res.data
    } else if (res.data.retCode === 403) {
      _msg.toast('token无效,请重登录', 'error');
      _baseAppHelper.loginOut();
    } else {
      _msg.toast(res.data.retInfo, 'error');
      return Promise.reject(res.data.retInfo);
    }
  },
  (error) => {
    _msg.hideLoading();
    _msg.toast('系统异常,请稍后重试', 'error');
    return Promise.reject(error);
  }
);

加密工具:

采取RSA加密,唯一注意点根据生成秘钥初始长度不同,长数据无法一次性加密,需要分段加密。详情见附件。

使用方法

网关服务中添加GlobalCacheRequestBodyFilter,ReqDecryptFilter,RespEncryptFilter三个过滤器,前端或修改GlobalCacheRequestBodyFilter中控制是否需要加解密。