基于http协议通信的APP安全策略的一点思考
声明一点,我没做过过任何商业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开发上保证数据安全的想法,希望起到抛砖引玉作用,欢迎留言讨论,质疑。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。