1. 程式人生 > >1.APP後端開發系列:登陸系統設計中的注意問題

1.APP後端開發系列:登陸系統設計中的注意問題

想寫這個系列很久了,因為之前做這個東西花費了大量的精力,有必要分享出來與大家共享。以前也寫了一些關於 APP後端開發的系列文章 由於當初功力不夠,很多問題描述不清楚或者解決方案過於複雜、不嚴謹等。

這一次查了很多資料,問了很多相關人士。準備再結合自己實際工作中的問題再次進行一些補充。就先從登陸的設計開始吧!

越想越糊塗

之前再做這一部分的時候,總想著複雜的技術,說出去多屌炸天呀。一般來說登陸的流程是:

image

當時對於安全性過度痴迷,確走偏了道路。首先提交的時候怕資訊被人劫持,因此客戶端在上傳時,進行AES加密,服務端解密出結果。服務端返回的資訊也會AES加密,然後客戶端解密。

然後這裡又帶來另外一個問題:加密資訊放在了客戶端,那麼一但客戶端被反編譯,hacker拿到祕鑰,那麼對於服務端來說加密就沒有任何意義了。又為了不在客戶端儲存這麼敏感的資訊,就像祕鑰由服務端下發。這樣子服務端可隨時對祕鑰進行變更。

到這裡又帶來了一個新的問題,感腳一切又回到了起點:下發祕鑰要走http,那麼依然可能被人劫持。這時候該是加密還是怎麼弄呢?如果加密,客戶端又放了一個祕鑰過去。那這個祕鑰依然可能被人反編譯。不能再從服務端獲取這個祕鑰吧?為了確保上個祕鑰的安全,生產下一個祕鑰……

當時就是陷入了這樣的死迴圈,不可自拔。現在想想真是too young too simple!

簡單、有效

首先在這裡還是先說一下:如果你的產品剛剛起步,不要過於糾結效能、安全

先說效能:你的產品才推出的時候,冷啟動的使用者數一般來說不會超過1000人(這已經是很不得了的冷啟動人數了)。然後你的併發也不會超過100。這種級別的訪問,相信機器硬體就可以幫助你解決。如果你的條件遠遠超出以上規模,那麼你的實力絕對足以應付即將發生的事情。
談談安全:安全這個事情,從一開始就要考慮,但是不能過於糾結(我之後可以講講我在做簡訊驗證碼這一部分的遇到的一個經歷)。過早介入,會導致系統開發速度降低,過早做了一些不需要的事情(hacker來搞你也是需要成本的,在你沒價值的時候,沒人願意來搞你)。所以早期應該重視開發成本,抓緊時間,早日上線。

另外,安全與效能有時候也是魚與熊掌。

演化之路

這一部分會有一些程式碼與圖來進行說明。在安全方面逐步演進。前面說前期開發只要快就好,但是這裡也要注意一個問題,就是後續升級能夠彌補前面的錯,要給未來升級留下餘地。因為否則你的系統始終留下了一個隱患。

實現功能就好

這是最開始的階段,重點考慮功能實現。使用者提交username + pwd 服務端驗證通過後,返回一個令牌token。

這裡需要注意的幾個部分是要為未來的升級做好準備。我經常遇到的幾個初期設計是:

  1. 驗證通過後,把使用者uid+username+salt等md5後,作為token返回到客戶端。
  2. 對token加入時間戳,過期後客戶端重新提交username + pwd驗證後再發一個token到客戶端
  3. 服務端生成一個token後下發到客戶端,客戶端按照約定的規則加密後請求服務端。

先說第一種帶來的問題:生成的token永久不變,那麼別人獲取到一個token就可以無限制的進行請求。直到你關閉了這個介面為止。為後續安全設計增加了成本。

第二種問題就有點老火了,雖然看似token只在一定時間範圍內有效了,但是其實更不安全了。首先客戶端需要儲存使用者的使用者名稱與密碼,如果使用者手機平時不注重安全,很容易被人竊取。

第三種設計方案,這是我原先幹過的一件事,是這三種方案中最垃圾的設計。得出的教訓就是:絕不能把任何加密的事情交給客戶端。這樣子靈活性大打折扣。舉例:還是升級介面了,現在本來token生成只是服務端的事情,服務端隨時可動態改變規則,現在由於客戶端也參與進來了,這事兒就麻煩了,你一改,客戶端也要跟著改。沒有任何靈活性可言。切記:客戶端就接收,然後轉發回服務端就好了。別再客戶端進行加密!!!

經過這些坑的歷練,參考oauth2.0,我現在採用以下方案:

使用者提交username + pwd後,服務端返回以下資訊:

{
    "access_token":"2YotnFZFEjr1zCsicMWpAA",
    "expires_in":3600,
    "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA"
}

access_token 是用來進行訪問的介面的,expires_in 是他的過期時間,到達過期時間後,需要用 refresh_token 來請求服務端重新整理 access_token

這裡幾個重點是:refresh_token 僅能使用一次,使用一次後,將被廢棄。另外這個 access_token 只在 expires_in 有效期內有效。

注意: 這裡的 expires_in 僅返回秒數就好了。別返回時間戳。因為各個平臺計算s的時間戳,不一致,這樣子做更方便處理。

訪問頻率控制

上面我們簡單實現了功能,現在app的流量上來了,有些功能也很複雜,如果某個介面訪問量太大,會導致伺服器崩潰,需要分別對每個介面每次訪問設定頻率(也可以統一設定每個介面訪問的頻率)。

一般我的做法是加入一箇中間件。每一個介面的訪問頻率做好一個對應的配置檔案。比如:
* a介面 5s內可訪問1次
* b介面 10s內可訪問1次(可能非常耗時,如果同時過多請求會導致伺服器崩潰)

那麼就把 access_token 與這些關聯起來。這裡需要用到redis。當用戶A進來訪問了 a介面 那麼設定這個token 5s內不能再次訪問。

    if ($redis->get($key)) {
        // 無法訪問,還未到時間

        return ;
    }

    // 設定頻率控制key
    $redis->setex($key, $expires, $value);

    // 訪問介面


這裡需要考慮幾個問題:

設定的訪問時間要合理。舉例:客戶端一般啟動的時候會請求多個介面,那麼當這些請求到達後,服務端可能拒絕其中一部分訪問(因為在頻率控制內)

一般來說不需要對所有的介面都進行頻率控制,僅僅針對重要的內容以及效能上有要求的介面進行頻率控制。

賬號安全考慮

現在又進一步了,需要考慮使用者賬號安全的問題。比如:QQ,有時候會提醒我們你的賬號在香港登陸了。如果不是自己所為,趕快修改密碼之類的。

實現這個功能,你需要記錄每次登陸、啟動時每個token對應的ip地址。如果ip地址與上次的ip不在同一個範圍(這個規則由自己定,因為有的運營商ip經常變化,比如:長城)。就提醒使用者是不是他自己所為,如果不是,就趕快修改密碼。

現在很多app在開發之初,都是可以多個裝置同時登陸。這樣帶來的安全問題也很多。如果要做成單個裝置登陸,需要每個token對應一個deviceToken。

這一部分就不繼續深入討論下去了。

防DNS劫持

安全工作做得再好,如果有人能夠獲得大量合法使用者的token,來請求你的藉口,你也無法識別,因為從行為來看,這一切都是合法使用者再進行。

以前為了防止別人獲取到合法的資訊,我才弄出了很狗血的客戶端加密方法。導致後期升級的時候,諸多問題。這個東西其實很簡單,使用https來進行請求(可以個人關鍵介面使用)

token

才開始做app服務端的時候,總想著token的設計。怎麼才能生成一個好的token呢?現在想想真不知道當初怎麼想的。

token的生成

首先搞明白這個token的作用就是一個令牌,用來標記一個使用者的身份。那麼首先他要唯一。其次他從客戶端上傳後,服務端能夠驗證這個token是由服務端生成的。

所以token生成只要滿足以上目的,你隨意就好了。當然別把敏感資訊暴露出去了。

常用的一種生成方式:

  1. 該使用者的uid,如:8888
  2. 該使用者的口令,如: 123123
  3. 使用者對應的salt,如:abcd
  4. 過期時間戳,如:1468293948

把上面幾部分拼接起來:888:123123:abcd:1468293948

token = md5(‘888:123123:abcd:1468293948’);

token的驗證

對於token也有兩種方法進行驗證。一是:服務端生成後,將token儲存起來(redis或者mysql中)。客戶端穿上來之後,檢查是否有該token,如果有取出對應的資訊,比如uid,驗證是否匹配。

另一種方法是:根據上傳的uid,生成對應的token,然後進行比較token結果是否一致(要保障該演算法如果給定的值一定,結果必須唯一。常用md5)。

對於個人而言更傾向於第二種方案。第一種方案效率更高(可使用redis儲存這個token),但是如果redis一但雪崩,就會造成所有使用者登入失效,一定時間內不可登陸。初期越簡單、越可靠更好。

總結

這一部分沒有太多程式碼,主要是思路。還有涉及到H5的登陸問題也沒有說到。下篇文章會把APP中登陸後,如果搞定H5登陸的問題進行闡述。