码迷,mamicode.com
首页 > 移动开发 > 详细

基于http协议通信的APP安全策略的一点思考

时间:2015-03-28 17:26:28      阅读:250      评论:0      收藏:0      [点我收藏+]

标签:http   app   aes   安全   

声明一点,我没做过过任何商业APP,以下想法仅仅是个人业余时间的一点思考,若你是专业人员,不吝赐教。


概述

微信开发过程中,会使用到微信服务器提供的API,这些API都是基于HTTP协议调用的,为什么我们自己的APP服务器不采用这种方式呢?

这种方式最直观的好处就是,API设计得足够好时,服务器只需要开发一次,无论前端是 WEB,APP ,APK...都通过http调用API请求数据并响应。

这种方式类似于传统C/S模型的开发,服务端/客户端定义相同序列的数据结构(称之为通信协议),差别在于现在用http协议,数据类型由之前的二进制流变成文本(XML/JSON)格式。

这篇文章主要介绍,在这种模式下的开发过程及安全性上的思考。


登录过程

基于HTTP的APP会遇到服务器无状态问题,比如客户端发起API调用:GetUserInfo,服务器如何判断这个用户是谁?

最先想到的就是,每一个API调用都带上用户的用户名和密码,这种方法确实太笨。

这里提供一个思路是:服务器保存很多带有锁的盒子,盒子里存储用户信息,用户登录成功之后,获得一把钥匙,以后的用户请求就只需要提供这把钥匙,简单过程如下:

1、用户提供用户名和密码发送登录请求

2、服务器检查用户名和密码是否正确,错误则直接失败。若通过检查,则生成一把访问令牌,把用户信息放到这把访问令牌对应的的盒子里,向用户返回访问令牌。

3、用户向服务器发出获取用户订单信息请求,并带上从服务器获取到的 访问令牌。

4、服务器用访问令牌打开盒子,获取到用户的基本信息,再从数据库查出用户订单信息,返回用户。

5、以后的用户和服务器交互重复 3、4步骤

这主要参考了微信公众号的做法:微信公众号:获取用户信息

技术分享

微信公众号 要获取一个关注者的信息,需要的条件是 AccessToken和这个关注者的ID(OpenID)。并没有提供公众号信息,而是用一个AccessToken(访问令牌)代替,也就是说微信服务器保存了一个 < 访问令牌,公众号信息 > 列表,通过AccessToken可以获取到一个微信公众号相关信息。

而这个AccessToken是怎么获取的呢?微信公众号:获取AccessToken

技术分享

微信公众号 要获取一个AccssToken需要提供 公众号ID(AppID)和一个密码(Secret),这不是相当于提供用户名和密码去获取AccessToken(访问令牌)吗?


通信过程的安全性

从上面的的介绍可知,每次用户都会发送HTTP请求,一个典型的交互过程如下:以JSON数据格式为例

Request:

{
    "Command": "GetOrderList",
    "AccessToken": "3bf63b28-bdd2-4bb1-80b0-8d5b42070222"
}

Response:

{
    "success": true,
    "OrderList": [
        {
            "NO": "20150327131072",
            "OrderTime": "2015-03-27 09:33:20",
            "TotalCost": 118,
            "Detail": [
                {
                    "Product": {
                        "GUID": "4bb603b2-0916-412d-ae51-b296c838673b",
                        "Name": "时蔬锅摊",
                        "MainPicture": "5(8).JPG"
                    },
                    "Count": 1,
                    "Price": 28
                },
                {
                    "GUID": "630e38dd-60ae-4ed0-98f0-affea23c5fee",
                    "Product": {
                        "GUID": "da9dce4a-101e-45dc-a88e-3b5e296ca092",
                        "Name": "香锅猪蹄",
                        "MainPicture": "2(1).JPG"
                    },
                    "Count": 1,
                    "Price": 58
                },
                {
                    "GUID": "6018c248-64e2-4185-98db-c1408aa0d482",
                    "Product": {
                        "GUID": "a405b104-f0eb-488c-ac41-a8a3de2f0bca",
                        "Name": "农家酥肉汤",
                        "MainPicture": "7(10).JPG"
                    },
                    "Count": 1,
                    "Price": 32
                }
            ]
        },
        {
            "NO": "20150325131079",
            "OrderTime": "2015-03-25 17:38:15",
            "TotalCost": 58,
            "Status": 2,
            "Detail": [
                {
                    "GUID": "77df4888-8c88-4c51-b669-7471a8ae975b",
                    "Product": {
                        "GUID": "da9dce4a-101e-45dc-a88e-3b5e296ca092",
                        "Name": "香锅猪蹄",
                        "MainPicture": "2(1).JPG"
                    },
                    "Count": 1,
                    "Price": 58
                }
            ]
        }
    ]
}
可以看到,用户获取自己的订单,提供了 访问令牌 ,返回用户订单,这里展示下显示效果~  技术分享

技术分享

从上面通信过程可以看到,均是由明文在网络中传输中,,而对于用户订单这么敏感的数据,实在不妥。一旦在传输过程中,这个 访问令牌 被截获,后果不堪设想,也就是说我们要想办法去保证传输过程中的安全性。

一想到传输过程中的安全性,我们一下就想到了HTTPS,对传输的所有数据都进行加密。HTTPS无疑可以解决这个问题,它被设计的目的就是为了保证传输过程中的安全性。

当我们安全性不需要做到像 银行 那种级别,HTTPS是否是最佳的方案呢?我看未必,你可以向下看,接下来提供一种思路。

参考QQ邮箱的做法: QQ邮箱登陆页面

登录页面

技术分享

登录成功之后的页面 (这个是我QQ邮箱,欢迎来信)

技术分享


注意到了吗,登陆的页面采用 HTTPS ,成功之后采用普通  HTTP 协议!这就是方法!

我们的思路,用户登录的 API 需要HTTPS,获取到对称加密钥匙(Key),以后的数据通信双方均采用这个密匙进行对称加密(AES),通信过程只需之前的方式下,外包一个加解密壳,过程如下:

1、用户提供用户名和密码,通过发起HTTPS的登录请求

2、服务器验证登录信息正确性。若失败,则返回错误;若通过,则生成一个打开用户信息盒子的AccessToken(访问令牌) 和 后面数据对称加密的密匙(Key),把AccessToken,Key和用户信息存入盒子中,盒子的数据用 AccessToken进行索引

3、用户通过HTTP调用API,但JSON数据需要用 Key 进行对称加密

4、服务器解密数据,执行用户请求,并用 Key加密之后,把数据返回。

5、以后的通信重复 3、4

这里给出 Web端测试代码(cryptoJS实现),服务端(C# 实现)源码 。AES加解密这儿除了Key外,额外需要提供 IV

服务端实现

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Security.Cryptography;

namespace Cryption
{
    public class DoAES
    {
        private static int _KeySize = 128;
        private static CipherMode _CipherModel = CipherMode.CBC;
        private static PaddingMode _PaddingModel = PaddingMode.Zeros;

        public static string Encrypt(string strEncrypt, string strKey, string strIV)
        {
            if (string.IsNullOrEmpty(strEncrypt) || string.IsNullOrEmpty(strKey) || string.IsNullOrEmpty(strIV)) return string.Empty;
            try
            {
                byte[] keyArray = UTF8Encoding.UTF8.GetBytes(strKey);
                byte[] ivArray = UTF8Encoding.UTF8.GetBytes(strIV);
                byte[] toEncryptArray = UTF8Encoding.UTF8.GetBytes(strEncrypt);

                RijndaelManaged rDel = new RijndaelManaged();
                rDel.KeySize = _KeySize;
                rDel.Key = keyArray;
                rDel.IV = ivArray;
                rDel.Mode = _CipherModel;
                rDel.Padding = _PaddingModel;

                ICryptoTransform cTransform = rDel.CreateEncryptor();
                byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);

                return Convert.ToBase64String(resultArray, 0, resultArray.Length);
            }
            catch (Exception)
            {
                return string.Empty;
            }
        }

        public static string Decrypt(string strDecrypt, string strKey, string strIV)
        {
            if (string.IsNullOrEmpty(strDecrypt) || string.IsNullOrEmpty(strKey) || string.IsNullOrEmpty(strIV)) return string.Empty;
            try
            {
                byte[] keyArray = UTF8Encoding.UTF8.GetBytes(strKey);
                byte[] ivArray = UTF8Encoding.UTF8.GetBytes(strIV);
                byte[] toEncryptArray = Convert.FromBase64String(strDecrypt);

                RijndaelManaged rDel = new RijndaelManaged();
                rDel.KeySize = _KeySize;
                rDel.Key = keyArray;
                rDel.IV = ivArray;
                rDel.Mode = _CipherModel;
                rDel.Padding = _PaddingModel;

                ICryptoTransform cTransform = rDel.CreateDecryptor();
                byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);

                int nIndex = resultArray.Length - 1;
                for (; nIndex >= 0; nIndex--) if (resultArray[nIndex] != '\0') break;
                return UTF8Encoding.UTF8.GetString(resultArray, 0, nIndex + 1);
            }
            catch (Exception)
            {
                return string.Empty;
            }
        }
    }
}

JS端实现

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script src="/JS/core-min.js"></script>
    <script src="/JS/aes.js"></script>
    <script src="/JS/md5.js"></script>
    <script src="/JS/pad-zeropadding-min.js"></script>

    <script>
        var key = CryptoJS.enc.Latin1.parse('1234567812345678');
        var iiv = CryptoJS.enc.Latin1.parse('1234567812345678');
        var encrypted = CryptoJS.AES.encrypt('MessageCryptoJS你是哪一位?真是一个天大的误会', key, { iv: iiv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.ZeroPadding });
        var decrypted = CryptoJS.AES.decrypt('ibxq102lVOMJMfjrOR7fRpDM76ab3wJkCOGn/zSuz84=', key, { iv: iiv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.ZeroPadding });

        document.write(encrypted);
        document.write('<br/>');
        document.write(decrypted.toString(CryptoJS.enc.Utf8));
    </script>

</head>
<body>

</body>
</html>

逻辑上的疏漏

按照上面的描述,一次典型的请求过程如下:

RequestURL: http://aa.com.cn/orders

携带了 {"Key":"3bf63b28-bdd2-4bb1-80b0-8d5b42070222"}  通过AES加密之后的数据

问题是:服务器收到请求之后,它应该用哪一个盒子的Key(密匙)去解密呢?服务器没有任何判断依据。

这里给出一个思路,每次请求在URL中带上Token: http://aa.com.cn/orders?AccessToken= 3bf63b28-bdd2-4bb1-80b0-8d5b42070222,服务器通过AccessToken索引到盒子并打开,拿出 用户信息和加解密的Key


引入的新问题:URL的安全性

为了避免逻辑上的疏漏,在不知不觉中引入了新问题:当向服务器请求订单列表时,请求过程中我们仅仅提供了一个URL: http://aa.com.cn/orders?AccessToken= 3bf63b28-bdd2-4bb1-80b0-8d5b42070222

要是用户这个URL被泄露了,是否意味我们返回给用户的数据已经暴露(虽然是AES加密),因为在用户未退出系统之前,任何人都可以通过请求这个URL获取到数据!!!

目前用户手里具有的资料是:Key,AccessToken,截获URL的人已经知道 AccessToken,唯一不知道的就是Key。

这里我给出思路来自于签名,如何利用这个截获者不知道的Key来保护用户的URL,主要参考了 微信支付 的生成支付链接的方法,大体思路是:

1、用户对URL增加 时间戳 和 随机字符串 参数   http://aa.com.cn/orders?AccessToken= 3bf63b28-bdd2-4bb1-80b0-8d5b42070222 & Nonstr=kgekgekghe42g4ea2w & Timestamp=145246512

2、用户对URL的参数进行 MD5/SH1 进行签名,方式如下 :Sign = MD5(资源URL + AccessToken + Nostr + Timestamp + Key),然后把sign填在原生的URL后面生成正真的请求URL

参数说明:

资源URL = http://aa.com.cn/orders

AccessToken = 3bf63b28-bdd2-4bb1-80b0-8d5b42070222

Nonstr = kgekgekghe42g4ea2w

Timestamp = 145246512

Key = 12345678     (保存在用户的内存中,由服务器生成,HTTPS传回)

计算 Sign = MD5(http://aa.com.cn/orders3bf63b28-bdd2-4bb1-80b0-8d5b42070222kgekgekghe42g4ea2w14524651212345678) = gehhgeiklajkidjkie3uit3u9uoidiguyiffjkk


3、用户发起HTTP请求,URL = http://aa.com.cn/orders?AccessToken= 3bf63b28-bdd2-4bb1-80b0-8d5b42070222 & Nonstr=kgekgekghe42g4ea2w & Timestamp=145246512 & Sign = gehhgeiklajkidjkie3uit3u9uoidiguyiffjkk

4、服务器验证URL 的时间戳:如果和当前系统的时间戳差得太远,则判定请求不合法。

5、服务器验证 Sign:通过 AccessToken打开盒子,取出 Key ,同样用MD5签名,对比Sign值是否合法,不合法则判定请求不合法。

6、响应用户请求,返回AES加密之后的数据


以上,是我对于以HTTP作为通信方式的APP开发上保证数据安全的想法,希望起到抛砖引玉作用,欢迎留言讨论,质疑。


基于http协议通信的APP安全策略的一点思考

标签:http   app   aes   安全   

原文地址:http://blog.csdn.net/zhccl/article/details/44699835

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!