开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情
接口出入参加解密的解决方案
接口出入参 加 解 密 的解决方案
背景
日常开发中为了数据安全,我们需要对接口请求的内容进行加密,防止直接通过接口请求查看而导致泄露的风险。本方案的目的是为了医保自律中加密接口出入参的数据,防止第三方直接窃取规则数据,本方案同样适用于其他产品中接口出入餐的加密解密。
解决思路
通过gateway中自定义的filter,可以实现对请求前的处理以及返回后的处理。
请求顺序可以这样理解:请求-> 自定义的filter-A -> 自定义的filter-B -> 业务处理 -> 自定义的filter-B -> 自定义的filter-A -> 返回 。
利用这一特性以及与前端的约定参数加密方式,我们可以在处理业务代码之前添加对参数的统一解密处理,到达下游服务中的请求则为以及解密后的正常参数了。对于处理完成的结果,同样在返回之前对返回结果进行统一加密处理。
与前端约定将是否需要加密解密放入头部某个参数中,也可以后台自己维护接口路径表,根据路径表中配置的信息来判断该接口是否需要加解密。
**
源代码解析**
后台这里一共弄了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中控制是否需要加解密。