App 与后台通信通常有采用 json 等文本协议或者采用二进制协议, 本文则主要总结了心悦俱乐部 App 的接入层从文本协议到二进制 jce 协议迭代过程中的技术方案, 包括协议规范安全性等方面的内容
背景
在移动客户端开发中, 基本都会需要与服务端进行数据交互对于一般的 App 来说, 通过 http 请求, 采用 json 格式的文本协议进行数据通信也就基本满足需求了在业务不断增加, 用户体量不断增长之后, 对用户体验的要求也越来越高对于需要进行频繁网络请求的 App 来说, 提高网络传输性能则是提高 App 响应速度, 优化用户体验的重要手段因此往往会引入二进制协议, 来减小数据包本文则主要总结了心悦俱乐部 App 的接入层从文本协议到二进制 jce 协议迭代过程中的技术方案, 包括协议规范安全性等方面的内容
文本协议方案
在 http 的数据请求中, 一般会采用 json 或者 xml 形式的文本协议特别是对于 App 或者 web 端的前后台交互更多的会采用 json 格式, 数据量相对 xml 较小, 协议字段可以增删改, 比较灵活
心悦 App 在前期, Native 模块与内嵌 H5 和 WEB 管理端使用的都是统一的 PHP 框架后台, 采用的是 http+json 的文本协议接入层
请求与响应
App 网络层发起 http 请求时, 一般会对 http header 做一些定制修改, 来传递一些通用数据, 通常会在 User-Agent 写入一些 App 和手机的信息, 例如操作系统版本机型 App 版本等等, 在 cookie 中写入登录态
例如: User-Agent:tgclub/4.1.0.63(OPPO R7;android 4.4.4;Scale/480;android;868979027609987)
在文本协议方案中, 不同的网络请求可以由不同的请求路径区分, 路径格式如下:
http://xxx.xxx.com/xyapp/api/{mod}/{act}?c_ver={version}
version: 协议版本号, 默认当前版本号码为 1.0
mod: 模块名称
act: 动作名称
如, 获取游戏信息, 接口名称为 game/get_game_info, 地址为: http://xxx.xx.com/xyapp/api/game/get_game_info
请求响应数据都包含在返回的 JSON 中, 按照与后台定义好的协议字段返回一般建议包括本次操作的返回码错误码和具体的业务数据心悦 App 的响应 JSON 中, 包含字段有: - status 返回码, 1 成功 0 失败 - data 返回业务数据, 当发生内部错误时, 会返回字段 errcode, 例如
{"status":0,"errcode":10002,"data":"签名校验错误"}:
可以看到, 虽然 json 数据的格式已经很简洁了, 但它依然把属性名称, 比如 status 和 data, 以及大括号, 引号这些用于表示数据格式的信息也进行传输了, 数据存在较多冗余
安全性
由于文本协议结构清晰意义明确, 所以方便解读, 但也存在较大的安全风险对于 App 后台服务来说, 也容易存在 API 泄漏, 第三方客户端伪造访问服务器对我们的服务或者流程造成安全危害因此需要一定的安全校验和加密措施
首先, 接入层要验证请求的合法性, 心悦 App 文本协议方案中采用的是校验签名的方式来验证发来的请求是否来自官方客户端和是否有效
HTTP 调用需以 GET 形式传递一下两个参数: 签名参数 (sn) 和时间戳(time_st)
签名参数的生成方式: 首先对 http 请求中的所有参数按照参数名的字典顺序排序, 参数名和参数值拼接成字符串, 再用每个设备独有的签名密钥 sn_key 对该字符串取 md5 值得到签名参数 (sn) 的值其中签名密钥 sn_key, 由后台根据用户设备 id 生成, 通过签名注册接口从服务器获取, 后台和客户端分别存储签名注册接口则用预先与 App 约定好的初始密钥进行签名校验, 保证签名安全性签名的参数包括所有的 get 参数和 post 参数后台收到客户端请求后用同样的签名方式计算 sn 参数, 与请求中的参数一致才对请求进行处理此外, 对于时间戳参数, 服务器会拒绝一定时间范围之外的请求, 防止请求重放攻击
例如请求 request 为:?param_b=1¶m_a=2&time_st=123,sn_key=aaa
那么签名参数 sn = md5('param_a2param_b1time_st123aaa')
其次, 若要保证请求数据中的敏感内容不被泄露, 需要对文本协议内容进行加密传输活采用 https 协议在心悦 App 的文本协议方案中, 采用的是对请求数据进行 CBC 模式的 AES 加密 AES 加密是一种对称加密算法, 同一个密钥可以同时用作信息的加密和解密加密密钥 pub_key 可以通过签名注册接口和 sn_key 一并返回, 后台和客户端分别存储 pub_key 用于加密解密签名注册接口可以使用客户端和服务器约定初始密钥加密解密, 并且这个密钥只在签名注册接口用一次
经过签名校验和数据加密之后, 基本上可以保证文本协议网络请求的安全性
二进制协议方案
二进制协议在网络传输中也有广泛使用, 具体的协议也有很多, 例如公司自研的 jce 协议, 谷歌的 Protocol buffer 等等
为优化 App 的网络请求速度和减小数据包大小, 并配合接入层后台往 C++ 框架改造, 心悦 App 的接入层网络数据传输协议切换成了二进制协议协议数据包的定义统一采用协议头 + 业务包体 (协议内容) 的方式, 协议头中用若干比特位定义协议版本号数据包长度等信息
请求与响应
在二进制协议方案中, 不同的网络请求使用同一域名, 后台根据请求结构体中的命令字参数进行路由转发由于客户端的数据集合需求比较多样, 后台则一般为服务化接口, 因此需要支持多命令字请求合并, 数据一并返回此外, 为进一步减小网络传输数据包, 协议可以考虑进行压缩, 同时为保证数据安全性, 数据加密也必不可少综上, 心悦 App 在进行二进制协议改造时, 制定了如下格式的二进制协议格式:
请求协议
响应协议
请求协议的协议头中包含业务标志协议版本号和数据包长度, 服务端只处理以业务标志开头的数据包截取协议头后, 用 PkgReq 结构体解析协议头中指定长度的数据包 PkgReq 包括明文的协议包头 PkgReqHead 和密文的二进制流 PkgReqHead 中会包括客户端生成的请求序列号, 密文的加密方式压缩方式等信息, 用这些信息解开加密压缩过的 PkgReqBodyPkgReqBody 包含通用请求数据 ReqHead 和业务请求数据 ReqBody 数组 ReqHead 中主要包括用户的设备信息 App 版本信息账号信息网络环境等等基础信息, ReqBody 则是具体请求命令字和业务请求结构体的封装若是多个命令字的合并请求则会有多个 ReqBody, 而 ReqHead 只需要有一份后台路由层根据 ReqBody 中的命令字 cmdid 将 ReqBody 中的 businessReqBody 字段转发到具体的后台服务进行处理并且 ReqHead 中设计了 guid 字段, 后台会存储用户的设备信息并且给用户分配唯一的 guid, 客户端拿到 guid 之后, 后续的请求就不需要上报不变的设备信息字段, 只需要上传 guid, 后端可以根据 guid 按需获取用户设备信息, 减小请求数据量
响应协议与请求协议的整体结构类似, 由于响应需要返回错误码或返回码给客户端, 并且存在合并请求, 因此设计了两层返回码在 RspHead 中有整体返回码 iRet, 作为路由层整体的处理结果; 每个 RspBody 中还有 ret, 作为该命令字对应的后台服务的返回结果每个 RspBody 中的 businessRspBody 在 iRet 和 ret 同时有效时才能用该命令字对应的响应结构体进行解包
此外, 命令字需要定义在枚举中, 命令字命名与协议中业务请求结构体和业务响应结构体命名保持对称, 如 GetSettingRequest 对应 GetSettingResponse, 命令字为命令字为 GetSetting=1001, 便于用命令字进行反射匹配出请求和响应具体的 Request 结构体和 Response 结构体示例:
安全性
二进制协议方案与文本协议方案类似, 都需要考虑数据安全性的问题二进制协议由于传输的数据包是二进制流, 抓包并不能直接看到结构体, 例如我们采用的 jce 协议, 必须知道完整的数据格式才能解析出原始数据在我们的方案中还采用了 https 对协议中的内容数据 PkgReqBody/PkgRspBody 进行先压缩后加密等操作保证安全性
在心悦 App 的二进制协议方案中, 采用的也是 AES 加密, 与文本协议不同的是采用 AES 的 GCM 模式 AES 作为一种分组对称加密算法, 需要对明文进行分组, 分组长度可为 128 或 256bits, 有 ECB,CBC,CTR 等多种模式, 这里不做具体介绍 GCM 模式可以提供对消息的加密和完整性校验, 具体原理这里不作详细介绍, 可以参考文章 什么是 AES-GCM 加密算法 AES-GCM 加密也需要密钥 key 初始向量 iv, 并且加密之后除了得到密文, 还会得到消息校验码在数据接收方可以通过这个校验码校验密文是否有篡改在具体实现中, 为增强安全性, iv 由请求序列号和 key 按照一定规则动态生成, 并将加密得到的校验码填写在协议包头 PkgReqHead/PkgRspHead 中, 在解密时需要作为验证条件
在安卓客户端, 对数据进行加密压缩的操作封装在了 NDK 代码中, 通过提供 so 库的方式给 Java 层调用, 并且校验 App 签名应用包名, 防止反编译, 可以进一步保证了安全性
小结
对于 App 和后台接入层的数据交互, 不管是长链接还是短链接, 其数据协议都可以存在文本协议和二进制协议两种类型文本协议直观描述性强, 容易理解便与调试, 并且协议修改方便, 但是数据冗余较多, 安全性稍差; 二进制协议格式精简冗余数据少, 窃听成本更高, 但是数据不直观调试略微复杂, 使用升级维护都需要约定好规则, 各有优劣, 因此可以根据不同的使用场景确定不同的方案
来源: https://cloud.tencent.com/developer/article/1070247