背景缘由这样, 之前项目里图方便和灵活, 封装了一个可以由前端组装查询条件的接口, 为了应对千变万化的查询需求变更..你懂的.
大概长这样:
/***
* json格式,var obj={}; obj.masterdataid = "=;382378" <br>
*
**/
public ApiResult<E> queryDataForCommon(String tableCode,String jsonArgs,String orderFields){
//... 大部分时间 tableCode和tableName一致了, 相当于表名/字段暴露了.
//还有像查询条件里面包含了 or 括号 等一类的条件,直接被云平台当xss攻击拦截了
}
渐渐这个接口也被封装成了通用组件接口, 并且衍生出了非常多的CV(复制黏贴)变种, 简单数了一下, 大大小小超过10个了, 真实业务需求各不相同, 也已经被应用散落在各个项目里.
在各个项目都逐渐都上了云之后,除了被识别为xss注入攻击, 还会有部分扫描工具能识别敏感数据明文传输. 设计考虑主要如下:
1 接口已被应用,不能直接修改.(数量多,也懒)
2 插件式添加, 项目自行选择.
3 可配置开关, 添加后也可以关.
加密方式选择的是对程加密, 或者转码脱敏.
还是简单来个图吧:
寻求SM4前端加密组件跟后端SM4加密匹配遇到了点困难, 转入为待办项, 按照实际需求目前也不需要这么高强度的脱敏密文处理, 于是加解密换成了hex+encode, 开始使用了base64+hex, 参数长度膨胀过度了, 放弃掉.
关键核心代码示意:
前端:
参数加密->发起请求
<script>
function stringToHex(str){
var val = new Array();
for(var i=0;i<str.length;i++){
val.push(str.charCodeAt(i).toString(16));
}
return val.join("")*
}
var obj = {};
obj.clsCode = "tableCode";
var hexData2 = stringToHex(encodeURI(JSON.stringify(obj)));
$.post({
url:'',
param:{dcbEncryptParam:hexData2}
...省略
});
</script>
后端代码:
dcbsecurity.yml配置:
#需要加密参数访问的API
request:
encrypt:
#支持 SM4 base64 hex,需要与前端约定, 目前是hex(encodeURI(jsparam))
algorithm:
# 根据配置.可以配置通配符, 如 /infoclass/admin/*, 也可以配置全量地址, 多个使用 逗号","分割
apiList: /infoclass/admin/*,/infoclassdata/web/listDataByClsCode
拦截器注册:
@Bean
public FilterRegistrationBean<RequestDecodeFilter> requestDecodeFilter() {
FilterRegistrationBean<RequestDecodeFilter> registrationBean = new FilterRegistrationBean<RequestDecodeFilter>();
RequestDecodeFilter requestDecodeFilter = new RequestDecodeFilter();
registrationBean.setFilter(requestDecodeFilter);
//根据配置添加需要参数解密的api
if(apiList!=null&&apiList.length>0) {
registrationBean.setUrlPatterns(Arrays.asList(apiList));
}else{
List<String> urls = new ArrayList<>();
//为空时默认添加一个
urls.add("/encryptRequestParamToAdd");
registrationBean.setUrlPatterns(urls);
}
registrationBean.setOrder(99);
return registrationBean;
}
请求解密拦截器:
package com.dcboot.base.config.security.filter;
import com.alibaba.fastjson.JSON;
import com.dcboot.base.config.security.support.DecryptHttpServletRequestWrapper;
import com.dcboot.base.config.web.WebFilterConfig;
import com.dcboot.base.util.ApiResult;
import com.dcboot.module.util.CommonUtil;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 参数加密请求处理过滤器
* @Author fs_wuhaixin
* @Date 2022/6/2017:01
*/
public class RequestDecodeFilter implements Filter {
private String targetApis = "";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
targetApis = filterConfig.getInitParameter(WebFilterConfig.TARGET_APIS);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
String dcbEncryptParam = request.getParameter(WebFilterConfig.ENCODE_PARAM_NAME);
if(CommonUtil.isNotBlank(dcbEncryptParam)){
filterChain.doFilter(new DecryptHttpServletRequestWrapper((HttpServletRequest)request),response);
}else{
//如果有开启目标拦截API,则判断请求路径是否在拦截API列表中.
if(targetApis!=null&&!"".equals(targetApis)){
String requestApi = ((HttpServletRequest)request).getRequestURI().replace(((HttpServletRequest) request).getContextPath(),"");
if(targetApis.contains(requestApi)){
renderError((HttpServletResponse) response);
return;
}else{
filterChain.doFilter(request,response);
}
}else{
filterChain.doFilter(request,response);
}
}
}
public void renderError(HttpServletResponse resp) throws IOException {
ApiResult result = ApiResult.error(500,"您请求的api已要求使用加密参数访问,请联系接口提供方!");
resp.reset();
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset = UTF-8");
resp.getWriter().write(JSON.toJSONString(result));
resp.flushBuffer();
return;
}
}
解密参数Request包装类,重点重写 getParameterMap方法.
/**
* 用于封装解码后重新封装httpRequest
* @Author fs_wuhaixin
* @Date 2022/7/10:11
*/
public class DecryptHttpServletRequestWrapper extends HttpServletRequestWrapper {
private Map<String,Object> decryptParamMap;
public DecryptHttpServletRequestWrapper(HttpServletRequest request) throws UnsupportedEncodingException {
super(request);
String dcbEncryptParam = request.getParameter(WebFilterConfig.ENCODE_PARAM_NAME);
String decryptedParam = decryptParam(dcbEncryptParam);
decryptParamMap = JSONObject.parseObject(decryptedParam,LinkedHashMap.class,Feature.OrderedField);
}
@Override
public String getParameter(String name) {
return (String) this.decryptParamMap.get(name);
}
@Override
public String[] getParameterValues(String name) {
return getParameterMap().get(name);
}
@Override
public Map<String, String[]> getParameterMap(){
Map<String, String[]> newParamMap = new LinkedHashMap<>();
for(Map.Entry<String,Object> entry:this.decryptParamMap.entrySet()){
newParamMap.put(entry.getKey(),new String[]{entry.getValue().toString()});
}
return newParamMap;
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(decryptParamMap.keySet());
}
// todo. 前端SM4加密问题未解决,目前先支持Hex+自定义加解密.
private String decryptParam(String encryptData) throws UnsupportedEncodingException {
return URLDecoder.decode(HexUtil.decodeHexStr(encryptData, StandardCharsets.UTF_8),StandardCharsets.UTF_8.name());
}
单元测试类(已通过):
/**
* 明文传参测试
* 响应结果{"code":"500","data":"","message":"您请求的api已要求使用加密参数访问,请联系接口提供方!","success":false}
*/
@Test
public void testInfoDataPostUnCrypt(){
String url = "http://localhost:8080/dcb/infoclass/web/listDataByClsCode?clsCode=cpw_test_01";
String str = HttpUtil.get(url);
log.info(str);
}
/**
* 密文传参测试
* 需要在 dcbsecurity.yml中添加apiList配置,支持表达式,如 /infoclass/*
* 响应结果与旧接口无异.
*/
@Test
public void testInfoDataPost(){
String url = "http://localhost:8080/dcb/infoclass/web/listDataByClsCode?dcbEncryptParam=842252347428253232636c73436f64652532323a2532326370775f746573745f3031253232253744";
String str = HttpUtil.get(url);
log.info(str);
}
至此, 前后端分离的参数加解密插件已完成. 可独立封装为spring-boot maven模块使用.
后续:
当天下午又优化了一下, 做了个逻辑调整和简单的加密.
逻辑调整为: 若没配置yml中的ApiList, 默认拦截所有, 并拦截判断参数请求是否包含ENCRYPT_PARAM_NAME参数, 若存在, 则默认按照加密请求处理.
若请求参数中不包含加密请求参数, 同时又存在于encrypt_api_list中, 则返回警告.
另外一个改动是做了个自定义的简单加密处理, 往hex里添加随机数, 类似加盐的处理.解密方法就不贴了, 有兴趣的同学自行解密一下:
function dcbStringEncrpyt(str){
var rand1 = getRandNum(10);
var val = new Array();
for(var i=0;i<str.length;i++){
val.push(str.charCodeAt(i).toString(16));
}
var hexStr = val.join("");
hexStr = hexStr.substring(0,rand1)+""+rand1+hexStr.substring(rand1);
hexStr = ""+rand1+hexStr;
return hexStr
}