談談移動應用的安全性實踐

《億級 Android 架構》 地址:https://xiaozhuanlan.com/topic/1934527806

本文首發在 Glow Tech Blog
雖然沒有完美的安全性,但我們所做的每一步都能加大被攻擊的難度。

281665-29572b7461c3d279.jpeg

本文將從用戶註冊流程出發,介紹下個人實踐中在提高數據安全性方面採用的一些策略方法,供讀者參考。下文將從 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

281665-43eb9bcba074a07e
發佈了45 篇原創文章 · 獲贊 12 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章