【参数加密】前后端分离请求参数加密与响应结果加密处理

对于安全性要求的加强,避免出现篡改请求结果问题的出现,现对系统中所有的请求和结果响应进行加密处理。系统使用前后端分离设计架构,同时前端部分有Vue 项目也有 jQuery 项目。遇到坑最多的地方是Axios 的get方式与jQuery的get方式

Java 后台处理

  • 定义 request Filter CustomRequestFilter 处理请求参数,拦截所有请求进行解密
/**
 * 请求拦截器 -- 处理参数解密
 *
 * @author miniy
 */
@Setter
public class CustomRequestFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        ParameterRequestWrapper request = new ParameterRequestWrapper(req);
        filterChain.doFilter(request, servletResponse);
    }
}
  • 定义代理类处理参数
/**
 * 请求代理类
 *
 * @author miniy
 */
public class ParameterRequestWrapper extends HttpServletRequestWrapper {

    private byte[] body;
    private Map<String, String[]> params;

    public ParameterRequestWrapper(HttpServletRequest request) {
        super(request);
        String method = request.getMethod();
        if (method.toUpperCase().equals("GET")) {
            params = HttpHelper.getParamMap(request);
        } else {
            body = HttpHelper.getBodyString(request).getBytes(Charset.forName("UTF-8"));
        }

    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() {

        final ByteArrayInputStream bais = new ByteArrayInputStream(body);

        return new ServletInputStream() {

            @Override
            public int read() throws IOException {
                return bais.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }

        };
    }

    @Override
    public String getParameter(String name) {
        String[] values = params.get(name);
        if (values == null || values.length == 0) {
            return null;
        }
        return values[0];
    }

    @Override
    public Enumeration<String> getParameterNames() {
        Vector<String> v = new Vector<>();
        Set<Map.Entry<String, String[]>> entrySet = params.entrySet();
        for (Map.Entry<String, String[]> entry : entrySet) {
            v.add(entry.getKey());
        }
        Enumeration<String> en = v.elements();
        return v.elements();
    }

    @Override
    public String[] getParameterValues(String name) {
        return params.get(name);
    }
}
  • 注册Filter 到spring security,注册为最外层Filter,拦截所有请求 此实现方式存在问题,将自定义Filter 注册到spring security 中,spring security ignoring 后的url不会被过滤器拦截,改为spring注册

    image.png
  • 提供参数处理工具类

/**
 * 工具类
 */
@Slf4j
public class HttpHelper {

    /**
     * 处理 post 请求体参数
     *
     * @param request
     * @return
     */
    public static String getBodyString(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        String result = "";

        try (InputStream inputStream = request.getInputStream();
             BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")))) {
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
            result = sb.toString();
            if (!StrUtils.isEmpty(result)) {
                // 数据处理
                String[] split = result.split(";");
                String ras = split[0];
                // 解密
                Map<String, Object> map = YamlUtil.builder("application-config.yml");
                Map<String, Object> sys = (Map<String, Object>) map.get("sys");
                Map<String, String> requestKey = (Map<String, String>) sys.get("requestKey");
                String privateKey = requestKey.get("privateKey");
                String publicKey = requestKey.get("publicKey");
                RSA rsa = SecureUtil.rsa(privateKey, publicKey);
                byte[] decrypt = rsa.decrypt(ras, KeyType.PrivateKey);
                // todo 如果中文乱码,前台需要使用URL encode 加密 此處使用decode 解密
                String keyIV = IOUtils.toString(decrypt, "utf-8");
                String[] aesAttr = keyIV.split(";");
                String key = aesAttr[0];
                String iv = aesAttr[1];
                request.setAttribute("aesKey", key);
                request.setAttribute("aesIv", iv);

                if (split.length > 1) {
                    String bodyData = split[1];
                    AES aes = new AES(Mode.CBC, Padding.ZeroPadding, key.getBytes());
                    aes.setIv(new IvParameterSpec(iv.getBytes()));
                    result = aes.decryptStr(bodyData);
                }
            }

            result = xssBaseLeach(result);
            if (!isPassUrl(request.getRequestURI())) {
                // xss sql 过滤
                result = xssSqlLeach(result);
            }
        } catch (IOException e) {
            log.error(e.toString());
        }
        return result;
    }

    /**
     * 处理 get 方式请求参数
     *
     * @param request
     * @return
     */
    public static Map<String, String[]> getParamMap(HttpServletRequest request) {
        HashMap<String, String[]> hashMap = new HashMap<>();

        String result;
        try {
            result = request.getQueryString();
            String regex = "params=";
            if (result.indexOf(regex) > -1) {
                // 解决+ 号变成空格的处理
                result = result.replace(regex, "").replace("+", "%2B");
                result = URLUtil.decode(result);
            }
            String[] split = result.split(";");
            String ras = split[0];
            // 解密
            Map<String, Object> map = YamlUtil.builder("application-config.yml");
            Map<String, Object> sys = (Map<String, Object>) map.get("sys");
            Map<String, String> requestKey = (Map<String, String>) sys.get("requestKey");
            String privateKey = requestKey.get("privateKey");
            String publicKey = requestKey.get("publicKey");
            RSA rsa = SecureUtil.rsa(privateKey, publicKey);
            byte[] decrypt = rsa.decrypt(ras, KeyType.PrivateKey);
            // todo 如果中文乱码,前台需要使用URL encode 加密 此處使用decode 解密
            String keyIV = IOUtils.toString(decrypt, "utf-8");
            String[] aesAttr = keyIV.split(";");
            String key = aesAttr[0];
            String iv = aesAttr[1];
            request.setAttribute("aesKey", key);
            request.setAttribute("aesIv", iv);
            if (split.length > 1) {
                String bodyData = split[1];
                AES aes = new AES(Mode.CBC, Padding.ZeroPadding, key.getBytes());
                aes.setIv(new IvParameterSpec(iv.getBytes()));
                result = aes.decryptStr(bodyData);
                //xss sql 注入处理
                result = xssBaseLeach(result);
                if (!isPassUrl(request.getRequestURI())) {
                    // xss sql 过滤
                    result = xssSqlLeach(result);
                }

                Map<String, Object> toMap = JSONUtils.jsonObjToMap(JSONUtils.ToJsonObj(result));
                for (String mapKey : toMap.keySet()) {
                    hashMap.put(mapKey, new String[]{toMap.get(mapKey).toString()});
                }
            }

        } catch (IOException e) {
            log.error(e.toString());
        }
        return hashMap;
    }

    /**
     * 过滤 参数中存在的 js 代码
     *
     * @param body
     * @return
     */
    private static String xssBaseLeach(String body) {
        return HtmlUtils.removeHtmlTag(body, "script");
    }

    /**
     * xss 字符替换
     *
     * @param body
     * @return
     */
    private static String xssSqlLeach(String body) {

        if (body == null || body.isEmpty()) {
            return body;
        }
        StringBuilder sb = new StringBuilder(body.length());
        for (int i = 0; i < body.length(); i++) {
            char c = body.charAt(i);
            switch (c) {
                case '>':
                    sb.append("》");// 转义大于号
                    break;
                case '<':
                    sb.append("《");// 转义小于号
                    break;
                case '\'':
                    sb.append("‘");// 转义单引号
                    break;
                case '\"':
                    sb.append('"');// 转义双引号
                    break;
                case '&':
                    sb.append("&");// 转义&
                    break;
                default:
                    String s1 = c + "";
                    String s = s1.replaceAll(".*([';]+|(--)+).*", "");
                    sb.append(s);
                    break;
            }

        }
        return sb.toString();
    }

    private static boolean isPassUrl(String url) {
        Map<String, Object> map = YamlUtil.builder("application-config.yml");
        Map<String, Object> sys = (Map<String, Object>) map.get("sys");
        List<String> whitelist = (List<String>) sys.get("xssList");
        return whitelist.contains(url);
    }

}
  • 定义 response Filter 拦截所有响应结果
/**
 * 响应拦截器 -- 处理参数加密
 *
 * @author miniy
 */
public class CustomResponseFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        HttpServletResponse res = (HttpServletResponse) response;
        ResultResponseWrapper responseWrapper = new ResultResponseWrapper(res);
        chain.doFilter(request, responseWrapper);
        byte[] content = responseWrapper.getContent();
        if (content.length > 0) {
            // 响应结果解密
            String json = IOUtils.toString(content, "utf-8");
            ResponseUtil.out((HttpServletRequest) request, (HttpServletResponse) response, JSONUtils.ToJsonObj(json));
        }
    }
}
  • 定义代理类
/**
 * 响应代理类
 *
 * @author miniy
 */
public class ResultResponseWrapper extends HttpServletResponseWrapper {

    private ByteArrayOutputStream buffer;
    private ServletOutputStream outputStream;

    public ResultResponseWrapper(HttpServletResponse response) {
        super(response);

        buffer = new ByteArrayOutputStream();
        outputStream = new WrapperOutputStream(buffer);
    }

    @Override
    public ServletOutputStream getOutputStream() {
        return outputStream;
    }

    @Override
    public void flushBuffer() throws IOException {
        if (null != outputStream) {
            outputStream.flush();
        }
    }

    public byte[] getContent()
            throws IOException {
        flushBuffer();
        return buffer.toByteArray();
    }

    class WrapperOutputStream extends ServletOutputStream {
        private ByteArrayOutputStream bos;

        public WrapperOutputStream(ByteArrayOutputStream bos) {
            this.bos = bos;
        }

        @Override
        public void write(int b)
                throws IOException {
            bos.write(b);
        }

        @Override
        public boolean isReady() {

            // TODO Auto-generated method stub
            return false;

        }

        @Override
        public void setWriteListener(WriteListener arg0) {
            // TODO Auto-generated method stub

        }
    }

}
  • ~~注册过滤器 1. spring security 最后一个过滤器 ~~此实现方式存在问题,将自定义Filter 注册到spring security 中,spring security ignoring 后的url不会被过滤器拦截,改为spring注册

    image.png
  • 提供工具类
/**
 * 响应加密工具类
 */
@Slf4j
public class ResponseUtil {

    public static void out(HttpServletRequest request, HttpServletResponse response, Object json) {
        try {
            // 响应结果加密
            // 方式1 aes 加密返回
            String key = (String) request.getAttribute("aesKey");
            String iv = (String) request.getAttribute("aesIv");
            AES aes = new AES(Mode.CBC, Padding.ZeroPadding, key.getBytes());
            aes.setIv(new IvParameterSpec(iv.getBytes()));
            String data = JSON.toJSONString(json);
            String result = aes.encryptBase64(data);

            // 方式2 rsa 加密返回
         /*   Map<String, Object> map = YamlUtil.builder("application-config.yml");
            Map<String, Object> sys = (Map<String, Object>) map.get("sys");
            Map<String, String> requestKey = (Map<String, String>) sys.get("responseKey");
            String privateKey = requestKey.get("privateKey");
            String publicKey = requestKey.get("publicKey");
            RSA rsa = SecureUtil.rsa(privateKey, publicKey);
            String data = JSON.toJSONString(json);
            // 防止中文乱码先进行 data url 编码
            String encode = URLUtil.encode(data);
            String result = rsa.encryptBase64(encode, KeyType.PublicKey);*/

            response.setContentType("text/html;charset=utf-8");
            response.setHeader("Content-type", "application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_OK);
            response.getWriter().print(result);
        } catch (java.io.IOException e) {
            log.error(e.toString());
        }
    }
}

编辑更改过滤器的注册
因注册到spring security 组件上无法拦截ignoing 的请求,更改为spring boot 方式注册,注意点为order 排序的设置,响应最简单设置为最大就好。关键点是请求filter的位置非常重要。这里要放在 spring security 内置过滤器前,spring CorsFilter 之后,此处多次测试猜的数为-100,暂未找到更科学方法。

@Bean
    public FilterRegistrationBean requestFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new CustomRequestFilter());
        registration.addUrlPatterns("/*");
//        registration.setOrder(Integer.MIN_VALUE);
        registration.setOrder(-100);
        registration.setName("requestFilter");
        return registration;
    }

    @Bean
    public FilterRegistrationBean responseFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new CustomResponseFilter());
        registration.addUrlPatterns("/*");
        registration.setOrder(Integer.MAX_VALUE);
        registration.setName("responseFilter");
        return registration;
    }

JsonUtils 工具类

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.serializer.SerializerFeature;

import java.util.List;
import java.util.Map;

/**
 * json 处理工具类
 */
public class JSONUtils {

    public static JSONObject parseObj(Object object) {
        return JSONUtil.parseObj(object);
    }

    /**
     * 字符串转JSONArray
     *
     * @param json
     * @return
     */
    public static JSONArray toJsonList(String json) {
        return JSON.parseArray(json);
    }


    /**
     * 对象转化为字符串
     *
     * @param obj
     * @return
     */
    public static String JsonToString(Object obj) {
        return JSON.toJSONString(obj);
    }

    /**
     * 带类型信息的转json串
     *
     * @param obj
     * @return
     */
    public static String JsonToStringAndType(Object obj) {
        return JSON.toJSONString(obj, SerializerFeature.WriteClassName);
    }

    /**
     * Json 字符串转为 JSONObject
     * JSONObject 对象类似Map
     * 直接通过get()方法 获取对象
     *
     * @return
     */
    public static com.alibaba.fastjson.JSONObject ToJsonObj(String json) {
        return JSON.parseObject(json);
    }

    public static cn.hutool.json.JSONObject ToJsonObj(Object obj) {
        return JSONUtil.parseObj(obj);
    }

    /**
     * map 转 Bean
     *
     * @param map
     * @param beanClass
     * @param <T>
     * @return
     */
    public static <T> Object toBean(Map map, Class<T> beanClass) {
        return JSONUtil.toBean(JSONUtil.parseFromMap(map), beanClass, true);
    }

    public static <T> Object toBean(com.alibaba.fastjson.JSONObject jsonObject, Class<T> beanClass) {
        return JSONUtil.toBean(JSONUtil.parseObj(jsonObject.toJSONString()), beanClass, true);
    }


    /**
     * JsonArray转java的List
     *
     * @param jsonArray
     * @param c
     * @return
     */
    public static List JSONArrayToList(JSONArray jsonArray, Class c) {
        String string = com.alibaba.fastjson.JSONObject.toJSONString(jsonArray, SerializerFeature.WriteClassName);
        List list = com.alibaba.fastjson.JSONObject.parseArray(string, c);
        return list;
    }

    public static Map jsonObjToMap(com.alibaba.fastjson.JSONObject jsonObject) {
        return JSONUtil.toBean(JSONUtil.parseObj(jsonObject.toJSONString()), Map.class);
    }

}

Java结束

前端处理

  • jQuery引入CryptoJS与jsencrypt支持
view.ajaxFilter = function () {

      var ajax = $.ajax;//  修改ajax方法的默认实现
      $.ajax = function (options) {
          // aes 数据加密
          var u32 = uuid(32);
          var u16 = uuid(16);
          var key = CryptoJS.enc.Latin1.parse(u32);
          var iv = CryptoJS.enc.Latin1.parse(u16);

          if (options.type == 'get' || options.type == 'GET') {
              if (options.url.indexOf('?') > -1) {
                  var split = options.url.split('?');
                  var params = split[1];
                  var paramObj = params.split("&");
                  for (var i = 0; i < paramObj.length; i++) {
                      options.data[paramObj[i].split("=")[0]] = unescape(paramObj[i].split("=")[1]);
                  }
                  options.url = split[0];
              }
          }
          // data加密
          if (typeof options.data == 'object') {
              options.data = JSON.stringify(options.data);
          }

          var encrypted = CryptoJS.AES.encrypt(options.data, key, {
              iv: iv,
              mode: CryptoJS.mode.CBC,
              padding: CryptoJS.pad.ZeroPadding
          });
          // ras 数据加密
          var publicKey = ' 11';
          jsencrypt = new JSEncrypt();
          jsencrypt.setPublicKey(publicKey);
          // todo 如果中文乱码,前台需要使用URL encode 加密 此處使用decode 解密
          var ras = jsencrypt.encrypt(u32 + ";" + u16);

          options.data = ras + ";" + encrypted.toString();


          var dataFilter = options.dataFilter;    //  对用户配置的success方法进行代理  
          function ns(datas, type) {
              // 数据解密
              // 方式1 aes 解密
              var decrypt = CryptoJS.AES.decrypt(datas, key, {
                  iv: iv,
                  mode: CryptoJS.mode.CBC,
                  padding: CryptoJS.pad.ZeroPadding
              });
              var data = decrypt.toString(CryptoJS.enc.Utf8);

              // 方式2 rsa 解密
              /* var privateKey = '私密';
               var decrypt = new JSEncrypt();
               decrypt.setPrivateKey(privateKey);
               var uncrypted = decrypt.decryptLong2(datas);
               var data = decodeURIComponent(uncrypted);*/
              return data;
          }

          options.dataFilter = ns;
          return ajax(options);
      }
  }
  • Vue安装crypto-js与jsencrypt
npm install jsencrypt
npm install crypto-js
import Vue from 'vue'
import vueAxios from 'vue-axios'
import axios from 'axios'
import { getToken } from '@/utils/auth'
import { uuid } from '@/utils/utils'
import cryptoJs from 'crypto-js'
import JSEncrypt from 'jsencrypt'
import LE from '@/assets/config'
import store from '../store'
import router from '../router'
import { Toast } from 'vant'

Vue.use(vueAxios, axios)
Vue.use(Toast).use(cryptoJs)

// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api 的 base_url
timeout: 5000
})

const u32 = uuid(32)
const u16 = uuid(16)
const key = cryptoJs.enc.Latin1.parse(u32)
const iv = cryptoJs.enc.Latin1.parse(u16)

// request拦截器
service.interceptors.request.use(
(config) => {
  const con = config
  if (store.getters.token) {
    con.headers[LE.RequestTokenKey] = getToken()
  }
  // data数据加密
  if (typeof con.data === 'object') {
    con.data = JSON.stringify(con.data)
  }
  const encrypted = cryptoJs.AES.encrypt(con.data, key, {
    iv: iv,
    mode: cryptoJs.mode.CBC,
    padding: cryptoJs.pad.ZeroPadding
  })
  // ras 数据加密
  var publicKey = '123123'
  const jsencrypt = new JSEncrypt()
  jsencrypt.setPublicKey(publicKey)
  const ras = jsencrypt.encrypt(u32 + ';' + u16)

  if (con.method === 'post' || con.method === 'POST') {
    con.headers['Content-Type'] = 'application/json; charset=utf-8'
    con.data = ras + ';' + encrypted.toString()
  } else if (con.method === 'get' || con.method === 'GET') {
    con.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
    const params = ras + ';' + encrypted.toString()
    con.params = { params }
  }
  return con
},
(error) => {
  Promise.reject(error)
},
)

// response 拦截器
service.interceptors.response.use(
response => {
  let res = response.data
  // 数据解密
  // 方式1 aes 解密
  const decrypt = cryptoJs.AES.decrypt(res, key, {
    iv: iv,
    mode: cryptoJs.mode.CBC,
    padding: cryptoJs.pad.ZeroPadding
  })
  res = cryptoJs.enc.Utf8.stringify(decrypt).toString()
  res = JSON.parse(res)

  if (res.code === LE.response.NO_AUTH) {
    store.dispatch('FedLogOut')
      .then(() => {
        router.push({ path: '/login' })
      })
  } else if (res.code === LE.response.OK) {
    return res
  } else {
    const msg = '状态码:' + res.code + '异常提醒【' + res.msg + '】'
    Toast.fail({
      message: msg
    })
  }
}
,
error => {
  store.dispatch('FedLogOut')
    .then(() => {
      router.push({ path: '/login' })
    })
  Promise.reject(error)
},
)

export default service

加密方式

上诉把程序以及思路提供,可以根据自己需要的加密解密方式进行处理,以下两种方式我进行了尝试最终选择了第二种。

  • 方式一:RAS 加密
    • 什么是RAS,RSA加密算法是一种非对称加密算法,由私钥与公钥组成一对密钥,通过公钥加密私钥界面方式处理。公钥可以暴漏在客户端,私钥必须保密存储在服务端。非对称加密算法安全性更高,但解密效率特别慢
    • 在请求时,使用请求的公钥对请求参数进行加密,达到服务端过滤器时,使用私钥进行解密。完成请求参数的加密。
    • 在服务端响应时,服务端采用响应的公钥加密,客户端私钥进行解密。完成结果的加密。上述中,使用两对密钥处理。优点安全性强。缺点1 响应私钥存储在客户端 2 当出现大文本或者长文本进行加解密时效率非常慢,同时大部分工具类不支持长文本的直接解密,需要使用分段加密-分段解密处理,尽量不要这样使用
  • 方式二:RAS + AES 加密
    • 什么是AES,AES算法称为密码学中的高级加密标准,这个标准用来替代原先的DES。是一种对称加密的算法。加解密效率较快,但安全性相对较弱。
    • 请求时,客户端生成AES的密钥Key以及偏移量IV,对文本内容进行对称加密,然后使用RAS对客户端生成AES的密钥Key以及偏移量IV进行非对称加密,进行服务端传递。
    • 响应时,后台使用传递的客户端生成AES的密钥Key以及偏移量IV进行结果加密,客户端是用同一个密钥Key以及偏移量IV进行解密。RAS + AES 方式加密是指采用RAS算法对AES的加密的密钥key(32位)以及偏移量iv(16位)进行非对称加密,使用AES算法对需要传递的文本内容进行对称加密。这种方式即可以保证安全性又能提高文本解析的效率。推荐使用。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT从业者的职业生涯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值