在网络传输中如何校验数据没有被恶意篡改?

基于签名算法实现对数据的校验

Posted by MasterJen on December 6, 2018

Hey Sign

Experience is the best teacher.–实践出真知.

在今天的blog 开始之前,先加一个小插曲,最近在负责一个项目的支付模块,有个需求就是服务器主动和客户端进行连接会话,那么用什么技术呢?

经过小组内的讨论,最后决定用WebSocket.

那么什么是WebSocket?

WebSocket 是一种网络通信协议.RFC6455 定义了它的通信标准.

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议.

为什么需要WebSocket呢?

了解计算机网络协议的人,应该都知道:HTTP 协议是一种无状态的、无连接的、单向的应用层协议.它采用了请求/响应模型.通信请求只能由客户端发起,服务端对请求做出应答处理.

这种通信模型有一个弊端:HTTP 协议无法实现服务器主动向客户端发起消息.

这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦.

大多数 Web 应用程序将通过频繁的异步JavaScript和XML(AJAX)请求实现长轮询.轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开). 

WebSocket无疑是此需求的一个解决方案.

WebSocket 连接允许客户端和服务器之间进行全双工通信,以便任一方都可以通过建立的连接将数据推送到另一端.WebSocket 只需要建立一次连接,

就可以一直保持连接状态.这相比于轮询方式的不停建立连接显然效率要大大提高.      

其工作流程如下:

WebSocket工作流程

如何工作的呢?

Web浏览器和服务器都必须实现 WebSockets 协议来建立和维护连接.由于 WebSockets 连接长期存在,与典型的HTTP连接不同,对服务器有重要的影响.

基于多线程或多进程的服务器无法适用于 WebSockets,因为它旨在打开连接,尽可能快地处理请求,然后关闭连接.任何实际的 WebSockets 服务器端实现都需要一个异步服务器.

说白了就是它可以保证服务端主动与客户端发送请求,保持连接状态,这也是它的最大的特点.

有了这些,我们就可以去方便的使用WebSocket实现功能了,至于如何实现,在此就不详细介绍了.如果大家想要详细了解的话,可以阅读下此博主,上面介绍的还是很全的.

但是又有一个问题出现了,就是在支付的时候或者数据传输的时候,如何知道数据没有被恶意篡改呢?

回到我们今天的主题:签名算法

首先说一下大致的思路,就是当传输过来时,我们可以让客户端进行加密运算,其算法和后台算法一致,便于后台进行运算校验,

前端对所有的数据进行加密后,带着签名,发送给服务器,此时服务器可以对所有的参数进行处理,得到后台根据数据处理得到的签名,然后与前端的签名进行比对,

如果不一致,就说明中间的数据被恶意篡改了,那么就可以对此次请求进行返回,如果一致,说明数据是安全的,那么就可以处理请求了.

当然前提是 前端与后台的秘钥是一致的 ,都会根据秘钥进行计算,然后得到签名.

爱码如下:

  测试类:
    
     // 模拟前台 对数据进行加密
            Map<String,String[]> map = new HashMap<>();
            map.put("username",new String[]{"lisi"});
            
            // 当然在开发中 密码不能是明文传输,也需要进行加密 可以通过DES算法进行加密
            map.put("password",new String[]{"123412"});
    
    //        对map 进行加密
            String sign1 = SignUtil.getSignStr(map);
         
            // 此sign 是根据参数进行加密得到的
            // 秘钥 YH348shdjsdf8  秘钥是前端和后台 一致的,用来进行验证
            String signStr = SignUtil.getSign(sign1, "YH348shdjsdf8");
         
            // 将sign 放到参数中   这个Sign 通过秘钥产生的
            map.put("sign",new String[]{"681cb6a8a014cb51f25743e68aba9179"});
            // 后台处理数据
           // 首先得到 Map  对map进行处理 除了sign 不处理
            String houTai = SignUtil.getSignStr(map);
    
            // 通过数据库 得到秘钥 YH348shdjsdf8  然后进行计算得到 sign
            String signHouTai = SignUtil.getSign(houTai, "YH348shdjsdf8");
    
            // 得到map中的sign 然后与后台中得到 的 map进行比较 如果一致 说明没有被修改,如果不一致,说明数据被修改了
            if(signHouTai.equals(map.get("sign")[0])){
                System.out.println("安全");
            }else {
                System.out.println("数据被篡改了");
            }
            System.out.println(map.get("sign")[0]+" 前端计算Sign");
            System.out.println(signHouTai+ "后台计算Sign");

           数据被篡改了
           681cb6a8a014cb51f25743e68aba9179 前端计算Sign
           58b428b3ad1c9ff38d03995a56066fe5 后台计算Sign   
            
            如果对 username 加上一栏"wangwu"
             map.put("username",new String[]{"lisi","wangwu"});
                
            安全
            681cb6a8a014cb51f25743e68aba9179 前端计算Sign
            681cb6a8a014cb51f25743e68aba9179后台计算Sign
            
              对password 进行处理 减去一个值
              map.put("password",new String[]{"1234"});
              数据被篡改了
              681cb6a8a014cb51f25743e68aba9179 前端计算Sign
              b75e43ec693a6c2921db9f04d7d7f6e7后台计算Sign

SignUtil工具如下:

   /**
     *  参数 就是Request  中的 参数  注意  签名 不能进行加密  运算会造成 死循环的
     * @param params
     * @return
     */
    public static String getSignStr(Map<String, String[]> params) {
     if (params == null || params.isEmpty()) {
            return "";
      }
       //把所有参数,按照Key升序排列
       
       List<String> keys = new ArrayList<>(params.keySet());
       Collections.sort(keys);//升序排列
       StringBuilder builder = new StringBuilder();
       try {
            for (String key : keys) {
//                 如果  是签名  的key  的话 continue;
                if ("sign".equals(key)) {
                    continue;
                }
//              得到  其 value  数组
               String[] values = params.get(key);
               if (values != null && values.length > 0) {
                    for (String value : values) {
                        builder.append("&");
                        builder.append(key).append("=");
                        builder.append(URLEncoder.encode(value, "utf-8"));
                    }
               }
           }
            return builder.toString();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     *   得到  签名  根据秘钥 和 参数
     * @param str
     * @param secret
     * @return
     */
    public static String getSign(String str, String secret) {
        // 通过  MD5进行 加密计算
        return DigestUtils.getMD5(str + secret);
    }

如此便可以完成签名算法了,对数据进行校验了,当然前提是不能让别人知道你的秘钥,如果攻击者知道了你的秘钥,那么后果不堪设想了.