fred-ye / summary

my blog
43 stars 9 forks source link

[Security]采用消息验证码提高移动端和服务器数据交互的安全性 #42

Open fred-ye opened 9 years ago

fred-ye commented 9 years ago

采用消息验证码提高移动端和服务器数据交互的安全性

以前一直都有一个疑问,如果客户端在和服务器端进行通信时,如果在中途客户端发送给服务器端的数据被不法分子拦截到了,然后不法分子修改了我的请求数据,再发送给服务器。那岂不是很危险,有没有什么方式来避免这种情况。举个例子,假设我登录网上银行进行转账操作,我想向'Milo'转1000元,于是我的操作向Server端送的数据是{from:'Fred', to:'Milo', amount: 1000},假设有不法分子'Abhi'截取到了这个数据报,然后把它改成{from:'Fred', to:'Abhi', amount: 90000}。于是情况就糟高了,银行在完全不知情的情况下,把我的钱转出了90000给'Abhi'。接着各种纠纷便来了。如何解决这个问题呢?

我记得之前在做一个APP,我们会调用到腾讯微博的API, 在腾讯微博中有一个上传多张图片(记得不清楚)的API,是属于一个高级接口,在调用时,腾讯要求开发者将请求的参数字段名的升序进行排列,然后采用SHA1算法生成消息摘要,或者说进行签名,将签名数据附在URL后传过去。当时,自己还不是很理解为什么要这样,后来在实际项目中也碰到了采用这种方式,提高数据交互的安全性。在此记录一下。

还是回到最初的这个问题,我们可以采用如下思路,当A想要将数据传递给B时,在发送数据的同时,把对应的消息摘要也发送过去。当B接收到数据时,根据传过来的数据重新计算消息摘要,然后比对A传过来的消息摘要,如果二者一致,则认为当前请求的数据是安全的,然后进行相应的处理。消息摘要的生成可以采用多种算法,如HmacHash256, MD5,SHA1等。

具体做法如下:

  1. 在开发时,移动端和服务器端的开发人员可以约定一个规则生成数据的消息摘要,最简单就是按照请求参数的数据进行升序排列,生成消息摘要。如数据{from:'Fred', to:'Milo', amount: 1000},首先按照参数的升序,依次是amount, from, to 对应的字段,假设我们为了使可读性更好,决定在每两个字段间用竖线(|)隔开。依照这个规则,构造原始数据为1000|Fred|Milo
  2. 选择一个消息摘要算法生成消息摘要。我们采用HmacHash256,它的安全性较高,其密钥为ABCDEFGHIJK, 数据1000|Fred|Milo生成的消息摘要为6ffa87395f4679512163b502be3921db5b085fae05f46a7fe22f111b7dadd586
  3. 当我们向服务器端发送数据时,假设其API为: http://chinabank.com/tranfer. 我们采用POST方式提交数据,请求体是一个JSON, 原本为{from:'Fred', to:'Milo', amount: 1000},由于我们采用了消息摘要,于是此时的请求体为{from:'Fred', to:'Milo', amount: 1000, signature:'6ffa87395f4679512163b502be3921db5b085fae05f46a7fe22f111b7dadd586'}。当服务器端拿到这个数据后。会重新利用HmacHash256算法,采用ABCDEFGHIJK生成摘要,然后将生成的摘要请求体中的signature进行比对,如果相同则处理本次请求,如果不同,则认为是非法请求,丢弃。
  4. 需要注意的是,HmacHash256算法是已公开的。因此,安全性取决于密钥,这也就是柯克霍夫原则(数据的安全基于密钥而不是算法的保密)。而且Client端和Server端采用的密钥是一致的,这个是一定不要泄露的。

以下是一段采用HmacHash256实现消息验证码的的代码片断:

String macKey = "ABCDEFGHIJK";
String macData = "the data string";

Mac mac = Mac.getInstance("HmacSHA256");
byte[] secretByte = macKey.getBytes("UTF-8");
byte[] dataBytes = macData.getBytes("UTF-8");
SecretKey secret = new SecretKeySpec(secretByte, "HmacSHA256");

mac.init(secret);
byte[] doFinal = mac.doFinal(dataBytes);
String result = "";
for (int i = 0; i < doFinal.length; i++) {
    result += Integer.toHexString(
            (0x000000ff & doFinal[i]) | 0xffffff00).substring(6);
}
System.out.println(result);

将byte数组转成String, 也可采用如下代码,其原理可以参看Java中byte与16进制字符串的互换原理

public static String bytesToHexString(byte[] src) {
    StringBuilder stringBuilder = new StringBuilder("");
    if (src == null || src.length <= 0) {
        return null;
    }
    for (int i = 0; i < src.length; i++) {
        int v = src[i] & 0xFF;
        String hv = Integer.toHexString(v);
        if (hv.length() < 2) {
            stringBuilder.append(0);
        }
        stringBuilder.append(hv);
    }
    return stringBuilder.toString();
}

我们是这么做的

在我们的项目中采用了类似上面的思路做了一个安全的enhancement。不过和上面的思路有一些区别。我们不仅会把这个加密后的数据signature传给Server端,同时还会把当前时间timestamp传递过去。为了和最初设计的请求体结构保持一致,我们将signaturetimestamp放在http请求的Header中传递。当Server端接收到数据时:

  1. 首先从Header中提取timestamp,如果发现timestamp的值和和当前时间相差较大如(20分钟),便返回一个错误的消息如:{error_code:101010, error_msg:"request too old"}。同时,timestamp也是signature明文的组成部份,这样可以避免重放攻击。
  2. 如果发现timestamp的值是在可以接受范围之类的,则开始提取请求体中的参数,按一定的顺序排列,然后再进行提取其消息散列码。
  3. 比对生成的消息散列码和http请求Header中的signature值,如果相同,则认为本次请求是合法,然后进行相应的业务处理,如果不相同,则认为本次请求是非法的,丢弃本次请求,或者执行其它的操作。