App架構設計經驗談:接口」安全機制」的設計

原創文章,轉載請註明:轉載自Keegan小鋼
並標明原文連接:http://keeganlee.me/post/practice/20160812
微信訂閱號:keeganlee_me算法

肯定功能需求

概述篇發佈出去後,收到不少人的大力支持,也收到了幾點關於功能需求的建議,主要在於幾點:json

  1. 只有微信登陸在App Store那邊審覈極可能通不過;
  2. 調用微信獲取用戶頭像和暱稱的接口須要企業微信號才行;
  3. 就算微信登陸也存在須要修改頭像和暱稱的需求。

關於第1點,細想一下就知道,只有第三方帳號登陸的確是通不過審覈的。由於提交審覈時必須提供測試帳號給App Store的審覈人員。審覈人員是不會使用本身的帳號進行測試的,不論是本身的微信、微博仍是手機號。以前我是掉過這個坑的,提交了一款以手機號+短信驗證碼登陸的App,但沒有提供測試帳號,結果被打回來了。因此,仍是須要創建本身的用戶體系,這一點沒法偷懶了。api

關於第2點,則是由於微信對這部分接口作了權限控制,只有經過了開發者資質認證纔有權開通此接口。但微信的開發者資質認證並不支持我的開發者。另外,還要交每一年300元的審覈費用。其實,未認證的開發者創建的App只有分享的權限,根本沒有登陸的權限。因此,微信登陸這條路根本通不了。所以,我決定不用微信登陸了,改用Github登陸。畢竟,面向的用戶羣是程序猿,而程序猿基本是人手一個Github帳戶。沒有Github帳戶的,稱不上合格的程序猿。也不考慮再加入微博、QQ、Facebook、Twitter等社交帳戶登陸。由於選擇太多容易混亂,我本身在某些平臺登陸時,就常常不記得上一次是用哪一個帳戶登陸的。數組

關於第3點,毫無疑問,修改頭像和暱稱的功能須要保留。安全

所以,最終的功能需求應該以下:服務器

  1. 手機號 + 短信驗證碼註冊
  2. 手機號 + 短信驗證碼登陸
  3. Github登陸
  4. 上傳圖片
  5. 修改頭像
  6. 修改暱稱
  7. 設置用戶技術棧標籤
  8. 獲取同棧之猿的內容列表
  9. 獲取關注之猿的內容列表
  10. 獲取同棧的用戶列表(未有關注之猿時獲取)
  11. 發佈問題
  12. 發佈分享
  13. 關注某條內容
  14. 取消關注內容
  15. 獲取內容的評論列表
  16. 添加評論
  17. 回覆評論
  18. 點贊評論
  19. 關注某用戶
  20. 取消關注某用戶
  21. 獲取某人詳細資料
  22. 獲取某人的發佈內容
  23. 獲取某人關注的人
  24. 獲取某人的粉絲列表
  25. 獲取個人消息
  26. 提交意見反饋
  27. 退出登陸

需求肯定,接着就能夠開始設計API了。微信

REST API

關於什麼是REST,我就不在這裏贅述了,直接推薦REST做者的經典論文:網絡

下面我只想用一些實例描述幾種架構風格在API定義方面的不一樣。session

假如如今要定義登陸、退出登陸、註冊、查詢用戶資料的接口,那麼,能夠這樣定義:數據結構

接口 方法 Endpoint
登陸 POST /user/login
退出登陸 POST /user/logout
註冊 POST /user/register
查詢用戶資料 GET /user/queryInfo

使用這種風格的貌似不少。也有些不是在URI中定義接口,而在參數中用method或action之類的參數名區分不一樣接口,示例以下:

接口 方法 參數
登陸 POST method=login
退出登陸 POST method=logout
註冊 POST method=register
查詢用戶資料 GET method=queryUserInfo

最後,再看下面這種接口的定義:

接口 方法 Endpoint
登陸 POST /sessions
退出登陸 DELETE /sessions/{session_id}
註冊 POST /users
查詢用戶資料 GET /users/{user_id}

這三種定義有什麼區別呢?其實,前面兩種能夠認爲都是 RPC(Remote Procedure Call) 風格的,而最後這種則可認爲是 REST 風格的。

RPC和REST區別在哪呢?

最直接的區別就是:RPC抽象的是過程,REST抽象的是資源。過程是以動詞爲核心,而資源是以名詞爲核心。也能夠簡單類比爲:RPC是面向過程的,REST是面向對象的。

從上面的例子就能夠看出,前面兩種定義,每一個接口分別用了一個操做性的詞語去定義;而最後一種定義,登陸和退出登陸都屬於 /session 資源,註冊和查詢用戶資料都屬於 /user 資源,而後分別用POST、DELETE、GET等方法對同個資源定義不一樣操做。

我發現,還有些定義是RPC-REST混合的,例如,可能會這樣子定義:

接口 方法 Endpoint
登陸 POST /users/login
退出登陸 POST /users/logout
註冊 POST /users/register
查詢用戶資料 GET /users/{user_id}

若是再加個修改用戶資料的接口,多是這樣子的:

接口 方法 Endpoint
修改用戶資料 POST /users/{user_id}/update

給個人感受就是:好混亂!這種大部分都是在對REST有過很初淺的瞭解,但卻缺乏正確理解的狀況下作出的設計。或者是對於部分接口不知道該如何抽象爲資源,因此就直接用RPC方式去定義了。

其實,使用REST風格設計API,我以爲難點就在於如何抽象資源。使用RPC則相對容易不少。這時,也許有人就會提出疑問了。既然使用RPC比用REST更容易抽象出接口,那爲什麼還要用REST呢?要解答這個疑問,能夠從面向過程和麪向對象的角度去思考。咱們知道,面向過程的思考方式處理問題更直接簡單,那爲何咱們還要使用面向對象呢?至於這個問題的答案,我就再也不展開了。

API定義

本項目的API是打算使用REST方式定義的。那麼,首先,就是資源的Endpoint定義。根據前面的功能需求整理出如下資源,可能會有些遺漏:

Endpoint 資源
/files 文件
/files/{file_id} 某個文件
/sessions 會話
/sessions/{session_id} 某個會話
/users 用戶
/users/{user_id} 某用戶
/users/{user_id}/posts 某用戶發佈的內容
/users/{user_id}/following 某用戶關注的人
/users/{user_id}/followers 某用戶的粉絲
/posts 發佈的內容
/posts/{post_id} 某條內容
/posts/{post_id}/comments 某條內容的評論
/me 當前用戶
/me/posts 我發佈的內容
/me/stars 我星標的內容
/me/following 我關注的人
/me/followers 個人粉絲
/me/messages 個人消息

定義資源的Endpoint時,須要分清楚不一樣資源的層級關係。一個定義良好的URI,應該具備可讀性,即從URI自己便可知道它所表明的資源。另外,對於URI中的一些變量值,如{file_id}、{session_id}、{user_id}、{post_id}等,在傳值的時候必須確保不能爲空,能夠設置默認值。

接着,就須要對每一個資源定義操做的方法了。我傾向於使用如下四個方法:

方法 描述 示例 示例說明
POST 建立新資源 /posts 建立新內容
GET 查詢資源 /posts 查詢內容列表
PUT 修改資源 /posts/{post_id} 修改某條內容
DELETE 刪除資源 /posts/{post_id} 刪除某條內容

不過,並非全部資源都會開放這四個方法。例如,對/post是不開放PUT和DELETE方法的。對於以上資源,具體須要定義哪些方法,這裏就再也不列出來了。

而後,還要加入版本控制。畢竟,接口不是一成不變的,須要不斷改動升級版本應對各類變化。那麼,版本號要加在哪裏好呢?關於這個問題,網上有不少討論,有些人喜歡直接加在URI中,像這樣:

http://api.domain.com/v2.1/posts

有些人喜歡加在參數裏,像這樣:

http://api.domain.com/posts?version=2.1

也有些人喜歡加在Header裏,像這樣:

Accept: application/json;version=2.1

或者自定義Header

api-version: 2.1

不喜歡第一種方式的人,大部分理由是,URI表示資源,應該與版本無關。而第二種方式和第一種方式本質上是同樣的。大部分人建議使用第三種方式。不過,發現好多開放API都是採用第一種方式。在我看來,加在哪裏其實影響不大。在本項目中,我打算和大部分開放API同樣採用第一種方式便可。另外,若是版本號不提供,則默認爲採用最新版本的接口。

最後,再定義下響應的數據協議。初期打算使用JSON,後期可能會考慮使用Protocol Buffers。數據結構則以下:

{
    code:200,
    message: "success",
    data: { key1: value1, key2: value2, ... }
}
  • code: 錯誤碼
  • message: 描述信息,成功時爲"success",錯誤時則是錯誤信息
  • data: 成功時返回的數據,類型爲對象或數組

以前,我是喜歡將請求狀態碼和業務錯誤碼分開處理的。所以,這裏的code我以前喜歡將其定義爲業務錯誤碼。可是,若是按照REST風格來設計,仍是有統一的code更合適。所以,我此次嘗試下改變習慣。

API安全設計

安全設計方面,首先,我打算全面使用HTTPS。使用HTTPS,雖然犧牲了性能,但能夠解決大部分安全問題。另外,蘋果在以前的WWDC上就已宣佈,從2017年1月1日起,全部iOS應用將強制使用HTTPS。這其實也意味着,從2017年起,全部App都將會使用HTTPS,不僅是iOS。除非有個別比較奇葩,非要搞HTTP和HTTPS兩套。至於HTTPS的優化,則須要慢慢搞了。至於證書,本身弄個自簽名證書便可。後期須要支持Web版的話再找個靠譜的CA註冊證書。

其次,用戶鑑權方面則打算採用Token方式。用戶登陸以後分配一個accessToken和一個refreshToken,accessToken用於發起用戶請求,refreshToken用於更新accessToken。accessToken會設置有效期,能夠設爲24小時。而用戶退出登陸以後,accessToken和refreshToken都將做廢。從新登陸以後會分配新的accessToken和refreshToken。

而後,我還打算在App層級分配AppKeyAppSecret,Android和iOS分別分配一對。每次向服務端發送請求時,AppKey都必須帶上,服務端會對相應的AppKey進行校驗。而AppSecret則須要安全保存在客戶端,也不能在網絡上進行傳輸,防止泄露。AppSecret只用於加密一些安全性級別較高的數據,以及爲URL生成簽名。URL簽名算法步驟以下:

  1. 將全部參數按參數名進行升序排序;
  2. 將排序後的參數名和值拼接成字符串stringParams,格式:key1value1key2value2...;
  3. 在上一步的字符串前面拼接上請求URI的Endpoint,字符串後面拼接上AppSecret,即:stringURI + stringParams + AppSecret;
  4. 使用AppSecret爲密鑰,對上一步的結果字符串使用HMAC算法計算MAC值,這個MAC值就是簽名。

另外,若是爲了再增強安全性,參與簽名的參數列表中能夠再添加個timestamp字段,值爲發送請求時的時間戳,每次請求的時間戳都將不一樣,這樣不止增長了簽名的不可預測性,也能夠防止重放攻擊。服務端收到請求後先檢查時間戳離當前時間是否太久,若是太久則不予處理。不過,這還涉及到客戶端和服務端時間同步的問題。這個很難保持一致,就算使用長鏈接不斷獲取服務器時間,也會由於網絡緣由而存在延遲,並且在移動網絡延遲可能還會比較高。

還有另外一種方案,就是使用nonce字段,值爲一個較長的隨機數,而不是時間戳。每次請求的隨機數也都會不一樣,能夠達到一樣的效果。不過,採用這種方案的話,那服務器須要保存之前發送過的nonce。每次收到請求後先檢查nonce是否已存在,存在則不予處理。這樣,時間久了,nonce的量將會很是大。也有一種優化方案,那就是每次請求的nonce值由服務端生成併發送給客戶端。便是說,客戶端每次發送正式請求以前,須要先向服務端請求nonce值。這樣的話,服務端則能夠在有請求過來的時候才生成nonce,請求處理完以後則能夠刪除nonce。不過,弊端也很明顯,原本一次的請求變成了兩次。

不過,在個人這個項目中,初期我只要求增強簽名的不可預測性便可,而nonce方案具有更高的不可預測性。所以,我將採用的方案是:在客戶端本身生成nonce,但服務端不保存nonce,只要檢查請求中是否存在nonce便可。

URL簽名在每次發送請求時都須要附加在參數中,服務端接收到請求後會使用一樣的簽名算法計算簽名值,只有服務端計算出來的簽名值和接收到的簽名值一致時才認爲請求是安全的。

寫在最後

自此,API部分的設計就完成了。在此總結一下:

  1. 採用REST風格定義API,接口抽象成對資源的操做;
  2. 添加API版本控制,版本號嵌在URL中;
  3. 響應統一使用code、message、data的JSON數據格式;
  4. 全站採用HTTPS;
  5. 使用Token方式對用戶鑑權;
  6. 使用AppKey方式對應用鑑權;
  7. 使用URL簽名對請求鑑權;
  8. 參數中添加nonce值加強簽名的不可預測性。
相關文章
相關標籤/搜索