《億級 Android 架構》 地址:https://xiaozhuanlan.com/topic/1934527806
本文首發在 Glow Tech Blog
雖然沒有完美的安全性,但我們所做的每一步都能加大被攻擊的難度。
本文將從用戶註冊流程出發,介紹下個人實踐中在提高數據安全性方面採用的一些策略方法,供讀者參考。下文將從 Android
和 服務端
兩部分來進行講解。
從註冊說起
用戶第一次打開app時便會進入註冊頁面。然後客戶端會要求用戶輸入用戶名、密碼並傳遞給服務端去創建一個新的user。此時通過明文傳遞用戶名密碼便是一個安全性隱患。或者說,如果有人監聽註冊API,那麼很快就可以竊取到很多用戶的賬戶信息,而且可以偷偷利用這些賬戶信息隨時獲取甚至更改用戶數據。
這對於任何一家企業而言都是非常可怕的。
全站Https
因此,爲了應對數據明文傳輸隱患這個問題,我們可以<u>採用Https方式通信</u>。在Android端推薦Square家的OkHttp3作爲網絡層,爲應用層提供Https服務。
下面先對Https的基本工作原理進行下介紹。
- 首先,客戶端去請求服務端的數字證書,這個證書包含了一個公鑰。該證書購買後存儲於我們自己服務器上。
- 當服務端收到客戶端請求後,會把這個數字證書回傳給客戶端,由於是公鑰,所以不害怕被竊取。
- 客戶端收到數字證書後,先去
驗證
證書的真實性。如果驗證通過,就會從裏面取出一個公鑰
。 - 客戶端本地生成一個
隨機數
,作爲未來的會話私鑰
,利用前面的公鑰進行加密
。 - 客戶端把
加密後會話私鑰
回傳給服務端,在這個過程中,即使加密後的會話私鑰
被竊取也不用擔心,因爲中間人並沒有解密私鑰
,所以讀不出裏面的會話私鑰
。 - 服務端接收到
加密會話私鑰
後,利用從CA購買證書時獲得的解密私鑰
進行解密讀出真實會話私鑰
。至此,客戶端與服務端同時擁有了一個只有它們二者知道的會話私鑰
,非對稱加密連接建立完成。 - 一旦客戶端和服務端連接建立起來後,未來的數據通信都利用這個
會話私鑰
進行對稱加密傳輸數據。
採用了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信息並進行校驗
。這裏可以創建一個python
的decorator
方法:
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