Security Connection Between App Client and Server

App 项目在是否依赖后端服务方面,可以划分为两类:
一类是独立的客户端(Native App),无服务端;
另外一类是客户端和服务端(Web Server)结合,客户端的部分功能,需要 Web 后端提供服务。

接下来将主要讨论:
客户端和服务端结合的这类应用, 基于 HTTP 协议进行数据传输,在数据安全方面的处理手段。

首先,进行安全的数据通信,目的是为了对客户端请求进行:安全的身份认证, 防止数据泄密,防止数据被篡改。 为了达到这个目的,下面将分类讨论具体的措施。

一、安全认证

由于 HTTP 请求是无状态的,而且客户端 App 没有类似浏览器 cookie 的机制。
因此,App 客户端需要在每一次通信时,携带标记信息。 服务端将依据该标记信息,进行用户身份认证。

1. 明文传输标记信息
例如,每次通信传输用户名和密码,或者传输手机号,邮箱等用户唯一标识数据。

  # 读取 henry 博主的所有博客
  http://api_host/blogs?name='henry'&password='123'
缺点是显而易见的:在网络上传输,非常不安全。容易被篡改和攻击。

2. 密文传输标记信息
首先,服务端设计两个规则:

a. user_key
  用户身份的唯一标记,等同于用户名。

  用途:user_key 用在获取用户个人信息等 API 接口。
       前提是该类信息不涉及用户个人隐私,如头像,昵称等。

  优点:使用 user_key 密文,避免暴露系统内用户唯一标记。

  取值:采用 uuid,Ruby 中可以这样取得
  SecureRandom.uuid
b. token:
  用户登录成功后的唯一标记,等同于用户名+密码。
  将为每一个 token 设置一个过期时间,时长可以根据需求来定,如 30 天。

  用途:token 用在每一个需要验证用户身份的请求中。

  优点:避免每次验证用户身份,都输入用户名和密码;
       若服务端用户身份信息修改,则对 token 做过期处理,
       此时,任何 App 客户端的 token 都将过期,客户端将引导用户重新登录。

  取值:采用20位的随机 base64 字符串,Ruby 中可以这样获得
  SecureRandom.base64(length).tr('+/=lIO0', 'htdskrq')

  # 我们将容易混淆的“lIO0”这类字符进行了替换。
  # 同时,对于 “+/=” 该字符,在 HTTP 请求的 URL 中会引起错乱,也进行了替换。

引入 user_key 和 token 之后,API 请求示例如下:

  # 基于 user_key 和 token 进行身份认证,以及有效性验证
  http://api_host/blogs?user_key='abacddc2-59a8-4eb0-a3be-ec20722db547'&token='1wWP1m1W739uRPsXgPo8'

二、防止泄密

HTTP 请求在网络传输中,容易被截取。 因此通信中的敏感数据,容易被泄露。

1. 基于 HTTPS 协议,进行加密传输
HTTPS 协议规则:报文中的任何东西都被加密,包括所有报头和荷载。
因此,攻击者截取请求后,所能知道的只有在两者之间有一连接这一事实。
所以,截取到了密文,也将无法识别。 特点:

处理策略简单,但是客户端和服务端程序调整较大;
安全性高,但是加解密性能开销大。

2. 基于证书
客户端和服务端基于证书,对需要传输的数据,进行加解密。特点:

传输的数据是密文;
只对请求的数据部分做加解密处理,资源开销较小;
获取数据,有证书,即可解密。

基于 “RSA公钥加密算法” 的 Ruby 代码示例如下:

require "openssl"
require 'base64'
require 'cgi'

class RsaSignatureUtil

  PRIVATE_KEY = "your_private_key"
  PUBLIC_KEY = "your_public_key"

  class << self
    def sign(content)
      rsa_private_key = OpenSSL::PKey::RSA.new(PRIVATE_KEY)
      CGI.escape(Base64.encode64(rsa_private_key.sign(OpenSSL::Digest::SHA1.new, content)))
    end

    def verify(content, sign)
      rsa_public_key = OpenSSL::PKey::RSA.new(PUBLIC_KEY)
      rsa_public_key.verify(OpenSSL::Digest::SHA1.new, Base64.decode64(URI.unescape(sign)), content)
    end
  end

end

# 签名
parameters = "name='henry'&password='123'"

sign = RsaSignatureUtil.sign(parameters)
parameters << "&sign_type='RSA'"
parameters << "&sign='#{sign}'"

url = "http://api_host/blogs?#{parameters}"

# 验签
content = "notify_data=#{params[:notify_data]}"
sign = params[:sign]
RsaSignatureUtil.sign(content, sign)

三、防止篡改

由于前面列举的 HTTP 请求在网络传输中,容易被截取。 因此请求容易被篡改和伪造。

1. 参数签名
签名和验签规则如下:

对网络请求中的数据部分,做参数签名;
请求服务端是,除了发送原有的数据之外,还携带着参数签名的值;

服务器端取到数据,基于同样的算法,对获取的数据做签名;
签名结果与客户端发送的签名做比较;

如果相同,则数据可信;
否则,说明数据被修改了。

四、总结

由于纯粹的 HTTP 请求,在网络传输中极不安全。
因此,需要对涉及用户隐私,或者商业机密的数据做加密处理。
但是,加解密会提高程序的复杂度,同时增大系统的计算开销,以及增加前端和后端系统,连接调试的困难。
个人倾向于:有选择性的对 API 接口进行加密处理。
对于安全性要求较高的服务,使用 HTTPS 进行数据传输;
对于安全性要求一般的服务,可以使用简单的数据加密,参数签名等;
对于开放性的内容服务,可以使用明文。

2013-06-02

rocket-wing