谈谈移动应用的安全性实践

作者: wingjay | 来源:发表于2016-08-30 22:46 被阅读1217次

    本文首发在 Glow Tech Blog
    虽然没有完美的安全性,但我们所做的每一步都能加大被攻击的难度。

    本文将从用户注册流程出发,介绍下个人实践中在提高数据安全性方面采用的一些策略方法,供读者参考。下文将从 Android服务端 两部分来进行讲解。

    从注册说起

    用户第一次打开app时便会进入注册页面。然后客户端会要求用户输入用户名、密码并传递给服务端去创建一个新的user。此时通过明文传递用户名密码便是一个安全性隐患。或者说,如果有人监听注册API,那么很快就可以窃取到很多用户的账户信息,而且可以偷偷利用这些账户信息随时获取甚至更改用户数据。

    这对于任何一家企业而言都是非常可怕的。

    全站Https

    因此,为了应对数据明文传输隐患这个问题,我们可以<u>采用Https方式通信</u>。在Android端推荐Square家的OkHttp3作为网络层,为应用层提供Https服务。

    下面先对Https的基本工作原理进行下介绍。

    1. 首先,客户端去请求服务端的数字证书,这个证书包含了一个公钥。该证书购买后存储于我们自己服务器上。
    2. 当服务端收到客户端请求后,会把这个数字证书回传给客户端,由于是公钥,所以不害怕被窃取。
    3. 客户端收到数字证书后,先去验证证书的真实性。如果验证通过,就会从里面取出一个公钥
    4. 客户端本地生成一个随机数,作为未来的会话私钥,利用前面的公钥进行加密
    5. 客户端把加密后会话私钥回传给服务端,在这个过程中,即使加密后的会话私钥被窃取也不用担心,因为中间人并没有解密私钥,所以读不出里面的会话私钥
    6. 服务端接收到加密会话私钥后,利用从CA购买证书时获得的解密私钥进行解密读出真实会话私钥。至此,客户端与服务端同时拥有了一个只有它们二者知道的会话私钥,非对称加密连接建立完成。
    7. 一旦客户端和服务端连接建立起来后,未来的数据通信都利用这个会话私钥进行对称加密传输数据。

    采用了https后,我们所有网络传输的数据都由明文变成了密文,即使中间有人能够监听到数据包,也不能轻易获取user的帐户密码信息。

    听起来,安全性问题基本解决了。

    然而实际上,在步骤3用户需要去验证数字证书时,如果<u>这个验证过程被欺骗了呢</u>?

    试想这样一种场景,如果在最开始,攻击者就拦截掉客户端与服务端的通信。当客户端在请求证书时,攻击者回传一个他自己的假证书,而且攻击者已经通过其他手段欺骗用户在手机上信任了这个假证书,那么当客户端接收到证书并去验证时,是<u>可以通过的</u>

    这也就意味着,一旦客户端遭受这样的攻击,未来客户端都会与一个虚假的中间人通信,而且中间人也可以拿着客户端传来的信息去与我们的服务端通信,而这个过程客户端和我们服务端完全不知道中间人的存在,这是很大的安全隐患。

    SSL Pinning

    为了防止客户端被虚假证书欺骗,我们采取的方式是<u>把我们自己的公钥直接绑定给每个客户端</u>,当客户端收到证书后,<u>与绑定的公钥进行验证</u>,从而防止虚假证书的入侵。

    在Android端,我们利用OkHttp3提供的CertificatePinner实现证书绑定

    OkHttpClient client = new OkHttpClient.Builder()
      .certificatePinner(
            new CertificatePinner.Builder().add("your_host", "your_public_key").build())
            .build();
    

    至此,我们可以利用更为安全的https协议来传输用户名和密码来继续上面的注册流程。

    Token机制

    回到注册流程,当服务端拿到用户名密码后,会去创建一个新的user,同时我们会<u>基于用户相关信息</u>生成一个Token并回传给客户端。客户端在接收到Token后需要在本地进行存储。另外,由于每个http请求都是无状态的,因此未来客户端如果想把user_id等信息传递给服务端时,就必须通过Token来传递,才能识别出某个请求的来源。

    那么,我们应该如何在Android和服务端的代码里具体实现Token的传递解析有效性验证机制呢?

    1. 首先在Android端,为了把Token信息存入到所有请求的header里供服务端使用,我们采用了okhttp3提供的interceptor接口来。

    OkHttpClient client = new OkHttpClient.Builder()
           .addInterceptor(new Interceptor() {
             @Override
             public Response intercept(Chain chain) throws IOException {
               Request request = chain.request();
               Request.Builder newRequestBuilder = request.newBuilder();
               String token = getAuthToken();
               if (!TextUtils.isEmpty(token)) {
                 newRequestBuilder.addHeader("Authorization", token);
               }
    
               Request newRequest = newRequestBuilder.build();
               return chain.proceed(newRequest);
             }
           })
           .build();
    

    2. 然后在服务端,我们需要解析客户端传递过来的Token信息并进行校验。这里可以创建一个pythondecorator方法:

    def mobile_request(func):
       @functools.wraps(func)
       def wrapped(*args, **kwargs):
           kwargs = kwargs if kwargs or {}
        if request.headers.get('Authorization'):
            encrypted_token = request.headers.get('Authorization')
            isValid, user_id = check_token(encrypted_token) //解析并验证token有效性
            if not isValid:
                abort(498) //token无效,返回498状态码
            user = get_user_by_id(user_id)
            if not user:
                abort(1001) //找不到user,自定义601状态码
                kwargs['user_id'] = user_id //成功解析出user_id
        return func(**kwargs)
       return wrapped   
    
    @app.route("/www/index")
    @mobile_request // 使用decorator包装方法
    def get_user(**kwargs):
        user_id = kwargs['user_id'] // 取出decorator中封装好的user_id
        return db.get_user(user_id) // 利用user_id进行逻辑处理
    

    3. 最后,请求结果返回到客户端,如果通过监测状态码发现返回结果是与Token相关的error/异常,则表示Token失效,此时我们让用户强制重新登录,生成新Token。这一步仍然可以在上面的interceptor里进行。

    OkHttpClient client = new OkHttpClient.Builder()
           .addInterceptor(new Interceptor() {
             @Override
             public Response intercept(Chain chain) throws IOException {
               ... //put token into newRequest
               Response response = chain.proceed(newRequest); // 获取服务端返回结果
               switch(response.code()) {
                 case ResponseCode.USER_NOT_FOUND: // 自定义状态码: 1001 找不到user
                    eventBus.post(new UserNotFoundEvent()); // 强制logout
                    break;
                 case ResponseCode.TOKEN_EXPIRED: // 498 token失效
                    eventBus.post(new TokenExpiredEvent()); // 强制logout
                    break;
                 default:
                    break;
               }
               return response;
             }
           })
           .build();
    

    至此,我们完成了Android端和服务端的Token传递、解析和失效处理。

    因此,在完善了Token的管理机制后,我们未来的http请求中只要带上这个Token,就可以畅通无阻地去服务端做与自身user相关的各种操作了。

    那么,既然Token像家里门禁卡一样,<u>只要拥有就能进入我们服务端并获取这个特定user的所有数据</u>。那也就意味着,<u>一旦攻击者窃取了某个user的Token</u>,那在Token失效前,攻击者随时可以利用这个Token获取这个user的一切信息。

    遇到Token被盗,该怎么办呢?

    调整Token过期时间

    针对Token被盗这种威胁,我们可以缩短Token的过期时间的方法。这样即使一个Token泄漏了,在一段时间后,这个Token也会自动失效。当然这也做会需要用户频繁登录获取新Token;而且失效前的这段时间内,攻击者仍然是可以直接连上服务端随意获取数据的。

    Request签名

    这种方法也是OAuth推荐的一种方法,其原理是<u>在客户端和服务端统一好某种加密方法和一个密钥</u>,这个密钥同时存储在客户端和服务端。每次客户端准备发起一个请求时,利用这种加密算法和密钥,针对<u>该请求的API和参数</u>进行计算得到一个数,称之为这个Request的签名,然后我们把这个签名放入到Request中。当服务端接收到Request后,就可以利用相同的加密算法和密钥来验证其中签名的真实性。

    OkHttpClient client = new OkHttpClient.Builder()
            .addInterceptor(new Interceptor() {
              @Override
              public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                String sign = RequestSignUtil.sign(request);
                HttpUrl url = request.url().newBuilder()
                    .addQueryParameter("request_sign", sign)
                    .build();
                Request newRequest = request.newBuilder().url(url).build();
                return chain.proceed(newRequest);
              }
            })
            .build();
    

    通过对每一个Request签名,可以确保服务端接收到的所有Request都来自我们自己的客户端。即使有人得到了Token想伪造Request,他也不知道如何计算Request签名,从而减小了Token被盗的危害。

    当然,每种安全方法都有漏洞,Request签名的方法意味着我们必须在客户端保存好加密算法和密钥,可以通过代码混淆、密钥存储到.so文件等方法来提高破解难度,这里就不再细述了。

    小结

    上文中,从注册流程开始,介绍了我们在数据安全性方面采取的一些策略和相关实现代码,希望能对读者有帮助。

    最后,笔者认为虽然没有完美的安全性,但我们所做的每一步都能加大被攻击的难度。

    如果有问题欢迎联系我。

    谢谢!

    wingjay

    https://github.com/wingjay

    相关文章

      网友评论

      • f5a911427197:做过支付类应用,https是最基础的,对json里面的数据还得分情况加密,非敏感数据用3DES对称加密,敏感数据(密码,cvv,身份证等)还得用rsa加一层,当然也有sign这些。主要是为了应对Heartbleed这种极端情况出现。还有https的证书验证,需要处理证书过期问题(自己开发应用问题不大,但是支付类有行业规定的过期时间),就涉及证书动态下发的问题,这又是很长的话题了。。。一般情况用不了这么复杂的流程,https就能hold住,还得看业务需求。
      • 捡淑:马克
      • im_panlei:高产
        wingjay:@SmartTalk :octocat:

      本文标题:谈谈移动应用的安全性实践

      本文链接:https://www.haomeiwen.com/subject/kilqettx.html