安全

AppPush同AppInterface交互,通过HTTP访问进行。在交互中,交互消息要进行身份验证和数据加密。

身份验证

客户在管理端配置app“消息推送接口及密钥”等信息时,页面会生成一个“授权码”token, 这个值用于交互双方进行身份鉴别。客户程序需要保存。交互双方在请求、响应过程中,json 体必须携带3个参数:

参数 类型 描述
signature 字符串 App身份签名,signature由app的“授权码”token、时间戳timestamp、随机字符串random生成
timestamp 整数(64bit) 时间戳(1970年1月1日 00:00:00 GMT以来的毫秒数)
random 字符串 128位的UUID 通用唯一识别码

身份验证流程如下:

1. 将token、timestamp、random三个参数进行字典序排序;
2. 将三个参数字符串拼接成一个字符串进行sha1加密;
3. 开发者获得加密后的字符串可与signature对比,进行身份验证;

Java实现签名字符signature生成: /**

 * 生成签名字符串
 * @param token
 * @param timestamp
 * @param random
 * @return
 * @throws
 */
public static String genSignature ( String token, 
        long timestamp, String random ) throws NoSuchAlgorithmException  
{
    String[] arr = { token, timestamp + "", random };
    //根据元素的自然顺序对指定对象数组按升序进行排序
    Arrays.sort(arr);

    String str = arr[0] + arr[1] + arr[2];
    byte[] bt = str.getBytes();

    MessageDigest md = MessageDigest.getInstance("SHA1");
    md.update(bt);

    return bytes2Hex(md.digest());        
}

/**
 * 字节数组转换成16进制字符串
 * @param bts
 * @return
 */
private static String bytes2Hex( byte[] bts ) 
{
    StringBuilder des = new StringBuilder();
    String tmp = null;
    for (int i = 0; i < bts.length; i++) 
    {
        tmp = (Integer.toHexString(bts[i] & 0xFF));
        if (tmp.length() == 1) 
            des.append( "0" );

        des.append( tmp );
    }

    return des.toString();
}

PHP实现签名字符signature生成: /**

 * 生成签名字符串
 * @param type $token
 * @param type $timestamp
 * @param type $random
 * @return type         
 */
public static function encodeSha1 ($token, $timestamp, $random) {
    $arr = array($token, $timestamp.'', $random);
    //按升序对给定数组的值排序
    sort($arr);       

    $str = $arr[0].$arr[1].$arr[2];

    return sha1($str);
}

数据加密

交互双方采用对称加密,秘钥EncodingKey由客户在管理端配置app“消息推送接口及密钥”信息时,由客户自己填写或者随机生成。这个值客户程序需要保存。由如下实例加密、解密函数,对每次交互的json体进行加、解密。加密采用AES算法(RIJNDAEL,128)。

Java实现AES算法(128位)加密、解密 /**

 * AES/ECB/PKCS5Padding 加密,只支持128位(16个字节)秘钥
 *      为了方便查看,对密文进行了base64编码
 * @param oriStr           源字符串
 * @param encryptKey       秘钥
 * @return                 密文
 * @throws Exception
 */
public static String aesEncrypt(String oriStr, String encryptKey) throws Exception
{
    if ( encryptKey == null || encryptKey.length() != 16)
        throw new Exception( "secret key must 16 byte!" );

    SecretKeySpec skey = new SecretKeySpec(encryptKey.getBytes(), "AES");
    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, skey);
    byte[] crypted = cipher.doFinal(oriStr.getBytes());

    return new BASE64Encoder().encode(crypted);
}

/**
* AES/ECB/PKCS5Padding 解密,只支持128位(16个字节)秘钥
*         为了方便查看,对密文进行了base64编码
 * @param ciphertext      密文
 * @param encryptKey      秘钥
 * @return           源字符串
 * @throws Exception
 */
public static String aesDecrypt(String ciphertext, String encryptKey) throws Exception
{
    if ( encryptKey == null || encryptKey.length() != 16)
        throw new Exception( "secret key must 16 byte!" );

    SecretKeySpec skey = new SecretKeySpec(encryptKey.getBytes(), "AES");
    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, skey);
    byte[] output = cipher.doFinal( new BASE64Decoder().decodeBuffer(ciphertext));

    return new String(output);
}

Php实现AES算法(128位)加密、解密 /**

 * AES/ECB/PKCS5Padding 加密,只支持128位(16个字节)秘钥
 *     为了方便查看,对密文进行了base64编码
 * @param type $oriStr          源字符串
 * @param type $encryptKey      秘钥
 * @return type                 密文
 */
public static function aedEncrypt($oriStr, $encryptKey) 
{
    if ( strlen($encryptKey) != 16 ) {
        throw new Exception("secret key must be 16 byte!");
    }

    $size = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_ECB);
    $pad = $size - (strlen($oriStr) % $size);
    $oriStr = $oriStr . str_repeat(chr($pad), $pad);

    $td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_ECB, '');
    $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
    mcrypt_generic_init($td, $encryptKey, $iv);

    $data = mcrypt_generic($td, $oriStr);
    mcrypt_generic_deinit($td);
    mcrypt_module_close($td);

    return base64_encode($data);
}

/**
 * AES/ECB/PKCS5Padding 解密,只支持128位(16个字节)秘钥
 *     为了方便查看,对密文进行了base64编码
 * @param type $ciphertext     密文
 * @param type $encryptKey     秘钥
 * @return type                源字符串
 */
public static function aesDecrypt($ciphertext, $encryptKey) 
{
    if ( strlen($encryptKey) != 16 ) {
        throw new Exception("secret key must be 16 byte : ".$encryptKey);
    }

    $decrypted = mcrypt_decrypt( MCRYPT_RIJNDAEL_128, $encryptKey, base64_decode($ciphertext), MCRYPT_MODE_ECB );
    $dec_s = strlen($decrypted);
    $padding = ord($decrypted[$dec_s - 1]);
    return substr($decrypted, 0, -$padding);
}

AppInterface处理流程

a).AppInterface收到http请求,用秘钥EncodingKey调用函数aesDecrypt对接收的“字节流”解密。
b).解析获取到身份验证所需的signature、timestamp、random,用“授权码”token调用函数genSignature产生一个signature字符进行比较,相同则验证通过。
c).处理消息。
d).生成一个random,根据服务器的timestamp,“授权码”token调用函数genSignature生成一个signature串。
e).根据接口说明生成json,结合秘钥EncodingKey调用函数aedEncrypt生成加密的字节流,在http请求的响应中输出密文。