小程序接口加密

作者: 吴蜀黍 | 来源:发表于2018-08-08 19:05 被阅读65次

    场景

    小程序请求的所有接口参数必须加密,后台返回数据也需要加密,并且增加Token验证


    一、小程序端功能编写

    1.下载一份Js版的aesUtil.js源码。【注:文章末尾会贴出所有的相关类文件】
    2.下载一份Js版的md5.js源码。
    3.在pulic.js中进行加解密操作代码如下,其中秘钥和秘钥偏移量要与后台的一致。

    var CryptoJS = require('aesUtil.js'); //引用AES源码js
    var md5 = require('md5.js')
    
    var key = CryptoJS.enc.Utf8.parse("76CAA1C88F7F8D1D"); //十六位十六进制数作为秘钥
    var iv = CryptoJS.enc.Utf8.parse('91129048100F0494'); //十六位十六进制数作为秘钥偏移量
    //解密方法
    function Decrypt(word) {
      var encryptedHexStr = CryptoJS.enc.Hex.parse(word);
      var srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr);
      var decrypt = CryptoJS.AES.decrypt(srcs, key, {
        iv: iv,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
      });
      var decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
      return decryptedStr.toString();
    }
    //加密方法
    function Encrypt(word) {
      var srcs = CryptoJS.enc.Utf8.parse(word);
      var encrypted = CryptoJS.AES.encrypt(srcs, key, {
        iv: iv,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
      });
      return encrypted.ciphertext.toString().toUpperCase();
    }
    
    //暴露接口
    module.exports.Decrypt = Decrypt;
    module.exports.Encrypt = Encrypt;
    

    4.在网络请求帮助类中进行参数的加密和返回数据的解密操作。

    var aes = require('../utils/public.js')
    var md5 = require("../utils/md5.js")
    
    ...
     
    /**
     * 网络请求
     */
    function request(method, loading, url, params, success, fail) {
      var url = BASE_URL + url;
      //请求参数转为JSON字符串
      var jsonStr = JSON.stringify(params);
      console.log(url + '  params=> ' + jsonStr)
      //根据特定规则生成Token
      var token = productionToken(params);
      //加密请求参数
      var aesData = aes.Encrypt(jsonStr)
      console.log('请求=>明文参数:' + jsonStr)
      console.log('请求=>加密参数:' + aesData)
      ...
      wx.request({
              url: url,
              method: method,
              header: {
                'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
                'Token': token
              },
              data: {
                aesData: aesData 
              },
              // data: params,
              success: function(res) {
                 //判断请求结果是否成功
                if (res.statusCode == 200 && res.data != '' && res.data != null) {
                  //解密返回数据
                  console.log('返回=>加密数据:' + res.data);
                  var result = aes.Decrypt(res.data);
                  console.log('返回=>明文数据:'+result);
                  success(JSON.parse(result))
                } else {
                  fail()
                }
              },
              fail: function(res) {
                fail()
              },
            })
    }
    

    其中生成Token的规则,【生成Token的规则可根据具体的业务逻辑自己定义,我这里使用的规则是根据请求参数的字母排序取其value并加上当前时间戳再进行MD5加密】

    /**
     * 生成Token
     */
    function productionToken(params) {
      var obj = util.objKeySort(params);
      var value = '';
      for (var item in obj) {
        value += obj[item];
      }
      //加上当前时间戳   
      value += util.getTokenDate(new Date())
      //去除所有空格
      value = value.replace(/\s+/g, "")
      //进行UTF-8编码
      value = encodeURI(value);
      //进行MD5码加密
      value = md5.hex_md5(value)
      return value;
    }
    //util的排序函数
    function objKeySort(obj) {
     //先用Object内置类的keys方法获取要排序对象的属性名,再利用Array原型上的sort方法对获取的属性名进行排序,newkey是一个数组
      var newkey = Object.keys(obj).sort();
      //创建一个新的对象,用于存放排好序的键值对  
      var newObj = {}; 
     //遍历newkey数组
      for (var i = 0; i < newkey.length; i++) {
      //向新创建的对象中按照排好的顺序依次增加键值对
        newObj[newkey[i]] = obj[newkey[i]]; 
      }
      //返回排好序的新对象
      return newObj; 
    }
    
    二、服务端功能编写

    由于初学SpringMVC,使用的方式不一定是最优最好的,如有不妥善之处,请各位看官多多指教

    思路:

    • 通过过滤器拦截请求参数,通过自定义参数包装器对参数进行解密。
    • 在拦截器获取请求的Token并生成服务器端Token进行验证。
    • 对返回参数通过JSON转换器进行加密处理。


      思路图

    1.重写HttpServletRequestWrapper,在自定义的HttpServletRequestWrapper 中对参数进行处理

    /**
     * Describe:请求参数包装器 主要作用的过滤参数并解密
     * Created by 吴蜀黍 on 2018-08-07 09:37
     **/
    @Slf4j
    public class ParameterRequestWrapper extends HttpServletRequestWrapper {
    
        private Map<String, String[]> params = new HashMap<>();
    
        @SuppressWarnings("unchecked")
        public ParameterRequestWrapper(HttpServletRequest request) {
            // 将request交给父类,以便于调用对应方法的时候,将其输出,其实父亲类的实现方式和第一种new的方式类似
            super(request);
            //将参数表,赋予给当前的Map以便于持有request中的参数
            this.params.putAll(request.getParameterMap());
            this.modifyParameterValues();
        }
    
        //重载一个构造方法
        public ParameterRequestWrapper(HttpServletRequest request, Map<String, Object> extendParams) {
            this(request);
            addAllParameters(extendParams);//这里将扩展参数写入参数表
        }
    
        private void modifyParameterValues() {//将parameter的值去除空格后重写回去
    
            //获取加密数据
            String aesParameter = getParameter(Constants.NetWork.AES_DATA);
            log.debug("[modifyParameterValues]==========>加密数据:{}", aesParameter);
            //解密
            String decryptParameter = null;
            try {
                decryptParameter = AesUtils.decrypt(aesParameter, Constants.AES.AES_KEY);
                log.debug("[modifyParameterValues]==========> 解密数据:{}", decryptParameter);
                Map<String, Object> map = JSON.parseObject(decryptParameter);
                Set<String> set = map.keySet();
                for (String key : set) {
                    params.put(key, new String[]{String.valueOf(map.get(key))});
                }
                aesFlag(true);
            } catch (CommonBusinessException e) {
                aesFlag(false);
                log.error("[modifyParameterValues]", e);
                log.debug("[modifyParameterValues]==========>", e);
            }
        }
    
        /**
         * 解密成功标志
         */
        private void aesFlag(boolean flag) {
            params.put(Constants.NetWork.AES_SUCCESS, new String[]{String.valueOf(flag)});
        }
    
        @Override
        public Map<String, String[]> getParameterMap() {
    //        return super.getParameterMap();
            return params;
        }
    
        @Override
        public Enumeration<String> getParameterNames() {
            return new Vector<>(params.keySet()).elements();
        }
    
        @Override
        public String getParameter(String name) {//重写getParameter,代表参数从当前类中的map获取
            String[] values = params.get(name);
            if (values == null || values.length == 0) {
                return null;
            }
            return values[0];
        }
    
        public String[] getParameterValues(String name) {//同上
            return params.get(name);
        }
    
    
        public void addAllParameters(Map<String, Object> otherParams) {//增加多个参数
            for (Map.Entry<String, Object> entry : otherParams.entrySet()) {
                addParameter(entry.getKey(), entry.getValue());
            }
        }
    
    
        public void addParameter(String name, Object value) {//增加参数
            if (value != null) {
                if (value instanceof String[]) {
                    params.put(name, (String[]) value);
                } else if (value instanceof String) {
                    params.put(name, new String[]{(String) value});
                } else {
                    params.put(name, new String[]{String.valueOf(value)});
                }
            }
        }
    }
    

    新建过滤器,在拦截器中调用自定义的参数包装器

    /**
     * Describe:请求参数过滤器
     * Created by 吴蜀黍 on 2018-08-07 10:02
     **/
    @Slf4j
    public class ParameterFilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            //使用自定义的参数包装器对参数进行处理
            ParameterRequestWrapper requestWrapper = new ParameterRequestWrapper((HttpServletRequest) servletRequest);
            filterChain.doFilter(requestWrapper, servletResponse);
        }
    
        @Override
        public void destroy() {
        }
    }
    

    web.xml中对过滤器进行配置

       <!--过滤器-->
        <filter>
            <filter-name>parameterFilter</filter-name>
            <filter-class>com.xxx.xxx.config.filter.ParameterFilter</filter-class>
        </filter>
        <filter-mapping>
            <filter-name>parameterFilter</filter-name>
            <!-- 过滤所有以.json结尾的资源-->
            <url-pattern>*.json</url-pattern>
        </filter-mapping>
    

    AES加解密操作

      /**
     * Describe:AES 加密
     * Created by 吴蜀黍 on 2018-08-03 17:47
     **/
    public class AesUtils {
        private static final String CHARSET_NAME = "UTF-8";
        private static final String AES_NAME = "AES";
        private static final String ALGORITHM = "AES/CBC/PKCS7Padding";
        private static final String IV = Constants.AES.AES_IV;
    
        static {
            Security.addProvider(new BouncyCastleProvider());
        }
    
        /**
         * 加密
         */
        public static String encrypt(@NotNull String content, @NotNull String key) throws CommonBusinessException {
            try {
                Cipher cipher = Cipher.getInstance(ALGORITHM);
                SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(CHARSET_NAME), AES_NAME);
                AlgorithmParameterSpec paramSpec = new IvParameterSpec(IV.getBytes());
                cipher.init(Cipher.ENCRYPT_MODE, keySpec, paramSpec);
                return ParseSystemUtil.parseByte2HexStr(cipher.doFinal(content.getBytes(CHARSET_NAME)));
            } catch (Exception ex) {
                throw new CommonBusinessException("加密失败");
            }
        }
    
        /**
         * 解密
         */
        public static String decrypt(@NotNull String content, @NotNull String key) throws CommonBusinessException {
            try {
                Cipher cipher = Cipher.getInstance(ALGORITHM);
                SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(CHARSET_NAME), AES_NAME);
                AlgorithmParameterSpec paramSpec = new IvParameterSpec(IV.getBytes());
                cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec);
                return new String(cipher.doFinal(Objects.requireNonNull(ParseSystemUtil.parseHexStr2Byte(content))), CHARSET_NAME);
            } catch (Exception ex) {
                throw new CommonBusinessException("解密失败");
            }
        }
    
    }
    

    2.新建拦截器,验证Token以及解密的判断

      ...
      
        @Override
        public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {
            //如果不是映射到方法直接通过
            if (!(handler instanceof HandlerMethod)) {
                return true;
            }
            //判断参数包装器中对请求参数的解密是否成功
            boolean aesSuccess = Boolean.parseBoolean(httpServletRequest.getParameter(Constants.NetWork.AES_SUCCESS));
            if (!aesSuccess) {
                this.sendMsg(Constants.NetWork.CODE_DECRYPTION_FAILURE, Constants.NetWork.MEG_AES_FAIL, httpServletResponse);
                return false;
            }
            //获取客户端上传Token
            String token = httpServletRequest.getHeader(Constants.NetWork.TOKEN_HEAD_KEY);
            if (StringUtils.isNullOrEmpty(token)) {
                sendMsg(Constants.NetWork.CODE_TOKEN_INVALID, Constants.NetWork.MSG_TOKEN_EMPTY, httpServletResponse);
                return false;
            }
            //验证Token的有效性
            if (!TokenUtils.verificationToken(token, httpServletRequest.getParameterMap())) {
                sendMsg(Constants.NetWork.CODE_TOKEN_INVALID, Constants.NetWork.MSG_TOKEN_INVALID, httpServletResponse);
                return false;
            }
            return true;
        }
    
        /**
         * 验证失败 发送消息
         */
        private void sendMsg(String msgCode, String msg, HttpServletResponse httpServletResponse) throws IOException {
            httpServletResponse.setContentType("application/json; charset=utf-8");
            PrintWriter writer = httpServletResponse.getWriter();
            String jsonString = JSON.toJSONString(StandardResult.create(msgCode, msg));
            try {
                //对验证失败的返回信息进行加密
                jsonString = AesUtils.encrypt(jsonString, Constants.AES.AES_KEY);
            } catch (CommonBusinessException e) {
                e.printStackTrace();
                jsonString = null;
                log.error("[sendMsg]", e);
            }
            writer.print(jsonString);
            writer.close();
            httpServletResponse.flushBuffer();
        }
      ...
    
    

    在spring中对拦截器注册

    ...
        <mvc:interceptors>
            <!-- 使用bean定义一个Interceptor,直接定义在mvc:interceptors根下面的Interceptor将拦截所有的请求 -->
            <mvc:interceptor>
                <!-- 拦截所有请求 -->
                <mvc:mapping path="/**"/>
                <!-- 需排除拦截的地址 -->
                <!--<mvc:exclude-mapping path="/"/>-->
                <bean class="com.xxx.xxx.config.interceptor.AsyncHandlerInterceptor"/>
            </mvc:interceptor>
        </mvc:interceptors>
    ...
    

    Token的验证

    
    /**
     * Describe:Token帮助类
     * Created by 吴蜀黍 on 2018-08-04 14:48
     **/
    @Slf4j
    public class TokenUtils {
        /**
         * 验证Token
         *
         * @param token 客户端上传Token
         * @param mapTypes 请求参数集合 
         * @return boolean
         */
        public static boolean verificationToken(String token, Map mapTypes) {
            try {
                return StringUtils.saleEquals(token, getToken(mapTypes));
            } catch (UnsupportedEncodingException e) {
                log.error("[verificationToken]", e);
                return false;
            }
        }
    
    
        /**
         * 通过客户端请求参数产生Token  
         */
        private static String getToken(Map mapTypes) throws UnsupportedEncodingException {
            List<String> mapKes = new ArrayList<>();
            for (Object obj : mapTypes.keySet()) {
                String value = String.valueOf(obj);
                //去除参数中的加密相关key
                if (StringUtils.saleEquals(value, Constants.NetWork.AES_SUCCESS) ||
                        StringUtils.saleEquals(value, Constants.NetWork.AES_DATA)) {
                    break;
                }
                mapKes.add(value);
            }
            //排序key
            Collections.sort(mapKes);
            StringBuilder sb = new StringBuilder();
            for (String key : mapKes) {
                String value = ((String[]) mapTypes.get(key))[0];
                sb.append(value);
            }
            //加上时间戳,去除所有空格 进行MD5加密
            String string = sb.append(DateUtils.getDateStr(DateUtils.FORMAT_YYYYMMDDHH)).toString().replace(" ", "");
            return MD5.getMD5(URLEncoder.encode(string, "UTF-8"));
        }
    }
    

    3.对返回数据进行加密处理,新建JSON转换器继承自阿里的FastJsonHttpMessageConverter

    /**
     * Describe:Json转换器 将返回数据加密
     * Created by 吴蜀黍 on 2018-08-07 13:57
     **/
    @Slf4j
    public class JsonMessageConverter extends FastJsonHttpMessageConverter {
    
        @Override
        protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException,
                HttpMessageNotWritableException {
            OutputStream out = outputMessage.getBody();
            try {
                String jsonString = JSON.toJSONString(object);
                log.debug("[writeInternal]======>返回明文数据:{}" + jsonString);
                //对返回数据进行AES加密
                jsonString = AesUtils.encrypt(jsonString, Constants.AES.AES_KEY);
                log.debug("[writeInternal]======>返回加密数据:{}" + jsonString);
                out.write(jsonString.getBytes());
            } catch (CommonBusinessException e) {
                e.printStackTrace();
                log.error("[writeInternal]======>", e);
            }
            out.close();
        }
    }
    

    spring中对JSON转换器进行配置

    ...
    <mvc:message-converters>
            <!--<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">-->
            <bean class="com.xxx.xxx.config.converter.JsonMessageConverter">
                <property name="supportedMediaTypes">
                    <list>
                        <value>text/html;charset=UTF-8</value>
                        <value>application/json</value>
                        <value>application/xml;charset=UTF-8</value>
                    </list>
                </property>
                <property name="features">
                    <list>
                        <!-- 默认的意思就是不配置这个属性,配置了就不是默认了 -->
                        <!-- 是否输出值为null的字段 ,默认是false-->
                        <value>WriteMapNullValue</value>
                        <value>WriteNullNumberAsZero</value>
                        <value>WriteNullListAsEmpty</value>
                        <value>WriteNullStringAsEmpty</value>
                        <value>WriteNullBooleanAsFalse</value>
                        <value>WriteDateUseDateFormat</value>
                    </list>
                </property>
            </bean>
        </mvc:message-converters>
    ...
    
    三、测试

    1.控制器

    
    /**
     * Describe:加解密测试
     * Created by 吴蜀黍 on 2018-08-08 11:13
     **/
    @Slf4j
    @Controller
    @RequestMapping(value = "/test")
    public class TestController {
    
        @RequestMapping(value = "/test.json")
        @ResponseBody
        private StandardResult test(Test test) {
            log.debug("[TestController]======> 接口参数:{}", test.toString());
            return StandardResult.createSuccessObj("测试成功");
        }
    }
    

    2.测试结果


    客户端
    服务端

    在后台自动加解密模块中,原本是打算都在JSON转换器中处理,通过readInternal()解密,再通过writeInternal()加密,奈何调试的过程中总会出现一些未知错误,如有相关大神,请帮忙指点迷津!通过过滤器来处理参数有些大材小用的意思,如果哪位有更好的方案和处理方式欢迎留言,感激不尽!!!

    四、客户端JS下载

    客户端JS下载,密码:qxql

    相关文章

      网友评论

      • 菜鸟学编程:可以防止机器刷票吗
        吴蜀黍:我写的这个目前不行 我Token时间取值到小时的 防止刷票可以再加机器唯一码 IP地址 等等
      • 31bb883ec5a8:谢谢博主,2018一起加油!懒惰是人的天敌!:smile:

      本文标题:小程序接口加密

      本文链接:https://www.haomeiwen.com/subject/gjstbftx.html