APP後端開發雜談

Header頭

header 頭推薦加上字段:php

  • Authorization

用來存放access_token,經過該token對用戶進行認證。能夠理解其做用等同於cookie中的session_id,有該令牌就默認用戶已經認證登錄過。html

  • Signture 客戶端簽名

用來判斷該請求是不是客戶端APP發起的請求,如今最多見的兩種方法 一種 是經過ASE加密解密來實現。一種是經過比對信息摘要的方式。 最簡單的生成方式是給出一個隨機值和時間戳,還有和服務端約定好的鹽值按照必定的順序進行MD5加密。前端

signture = md5(nonce + timastamp + salt)
複製代碼

服務端收到該簽名的時候,按照約定的順序進行MD5加密比對簽名,若是服務端生成的簽名與客戶端簽名不一致,則可認爲不是客戶端發起的請求,此時不響應該請求。固然使用這種方法要同時提交timestamp 和 隨機字符串nonce。mysql

  • version 客戶端版本

用來存放客戶端版本號,主要是小版本,大版本的話通常服務端會開新接口。可是比較好的作法,是客戶端每一次發佈都要有一個版本號。就算是這次發佈,服務端沒作任何更改,只要客戶端新發布也應該給一個新的版本號。而後將這個版本號寫入header 頭中。這樣的好處是若是請求出現錯誤,咱們可以定位到是哪一個版本的APP出現了該錯誤,能夠更容易的定位復現錯誤。更詳細地還能夠把設備的操做系統和型號也在header頭提交。android

  • timestamp 請求時間戳

客戶端發送請求時的時間戳,可用於請求過時判斷。ios


客戶端簽名細節

  • 防止重放攻擊

雖然signture的存在,使咱們能夠判斷該請求是不是客戶端生成,從而只響應客戶端的請求。可是這樣安全性仍是不夠。git

一個典型的攻擊手法時,經過抓包客戶端生成的簽名不斷地進行接口請求。最多見的就是利用該簽名不停地請求短信驗證碼接口,直到服務器的驗證碼餘額耗光。web

防止重放攻擊的最有效方式就是保持signture的惟一性。客戶端必須生成惟一的signture,同時服務端也要保證對於一個signture只響應一次請求。redis

  • 保證客戶端簽名的惟一性

客戶端保證每次生成一個惟一地signture的最簡單方式是在生成簽名的方法中加入時間戳。sql

服務端保證對每個signture簽名,只使用一次的實現方法是:對每次請求作判斷,若是該請求攜帶的簽名沒有使用過,則響應該請求,並把該簽名記錄在服務端並標記爲已使用。若是查到該請求攜帶的簽名已經被標記爲已使用,則不響應該請求。

服務端對signture作記錄,通常經過3種方式:

  1. 寫進文件
  2. 寫進MySql數據庫
  3. 寫進Redis

寫進文件的弊端,在於沒法供分佈式系統使用。MySql的弊端在於請求數多時增長了數據庫的壓力。因此最好的方式仍是寫進Redis裏。

  • 增長簽名超時機制

咱們保證了客戶端簽名惟一性的方法是將每次請求的signtue記錄在服務端。可是隨着請求的增長,記錄的signture也愈來愈多。每次請求都要逐一比對之前全部請求記錄下的signture,這樣顯然是不合理。

因此咱們正確的方式,不該該是單純把signture寫進文件,mysql,或redis。而應該是作一個文件緩存,或是寫數據庫臨時表,或是redis緩存。

可是若是咱們只比對緩存期內的signture,攻擊者仍是能夠經過使用已到期被緩存清除的signture來進行重放攻擊。因而咱們引入了一個超時機制,若是該請求攜帶的timestamp 比當前的服務端時間相隔已經 大於singture緩存時間,則不響應這個請求。這樣就保證了,攻擊者沒法使用被緩存清除的signture進行重放攻擊。

固然超時時間也不能設置過小,由於客戶端請求到達服務端須要必定的時間。因此超時時間的設置應知足

(服務端當前時間 - 客戶端提交的timestamp) < 超時時間 < signture緩存到期時間
複製代碼

引入超時機制的另外一個好處是防止了一部分的中間人攻擊。由於劫持增長了請求的時間,由於超時機制的存在,可能使被劫持的請求失效。

  • 防止超時時間下溢攻擊

引入了超時機制之後,可能咱們通常都會這樣寫

if( (服務端當前時間 - 客戶端提交的timestamp) < 超時時間 ){
    超時了;
}
複製代碼

可是若是攻擊者 更改了客戶端時間,使客戶端提交的timestamp是一個比服務端時間還超前幾天或是幾年的時間戳,並生成了一個對應的signture。這個時候,當該請求響應後。攻擊者等待一段時間,等緩存中這個signture 失效了,攻擊者就能夠拿着這個signture和timestamp 進行重放攻擊了,由於這個timestamp 超前了服務端時間幾天或幾年,因此

服務端 - 客戶端提交的timestamp = 負數 < 超時時間
複製代碼

因此,經過負數小於超時時間,繞過了超時機制,使signture又能夠從新使用。固然這種狀況下的重放攻擊已經很弱了,由於signture使用過一次就會被緩存,因此經過下溢從新使用signture也要等到上一次signture的緩存失效了。則兩次攻擊之間,便必須隔一段緩存有效期。

因此,更合理的話,除了比對請求時間是否小於超時時間。還應該判斷:

服務端時間 - 客戶端提交的timestamp > 0
複製代碼
  • 保證客戶端服務端的時間一致性

引入超時機制的前提是客戶端和服務端的時間偏差在可接受的範圍。

試想一下,若是客戶端請求須要0.5s到達服務端,因此服務端的超時時間設置了1s。可是客戶端的時間比服務端慢了2s 。這個時候當客戶端的timestamp 提交到服務端時 原本應該是

服務端當前時間-客戶端提交的timestamp = 0.5 < 1 // 不超時
複製代碼

結果由於客戶端時間比服務端時間慢了2s,使timestamp 到達服務端時,變成了

服務端當前時間-客戶端提交的timestamp = 2.5s > 1 // 超時
複製代碼

因此客戶端服務端時間不一致的會形成客戶端全部請求都由於超時而沒法響應。

那麼如何保證服務端和客戶端的時間一致性呢? 一個最經常使用的解決方式就是:

服務端給出一個接口返回當前時間戳,
客戶端請求該接口獲取時間戳,加上該請求的響應時間與當前時間戳相減得出時間差。
而客戶端提交的timestamp 就是當前時間戳加上服務端與客戶端的時間差。
複製代碼

客戶端簽名生成方式

加密 仍是 信息摘要 ?

  • 信息摘要

信息摘要的好處在於服務端處理更簡單,只須要生成對應的簽名進行比對便可。

  • 加密

加密的方式生成簽名常見的能夠採用ASE加密,借鑑微信支付寶sdk簽名的生成方式,把header頭的重要的參數,都參與signture的生成。這樣的好處在於更加安全,若是傳輸中header頭的參數被劫持更改,會形成服務端驗籤失敗,則請求天然就不響應了。

Timestamp

timestamp 是要提交10位仍是13位的時間戳呢?

這個問題最多見於用PHP寫的APP後端。由於PHP中使用的時間戳是10位的,time()也是隻返回一個10位的時間戳。

可是,我仍是認爲應該使用一個13位的時間戳。至少你生成signture的時候應使用13位的時間戳,由於這樣實時性更強,防止客戶端太多的時候,不一樣的客戶端同時生成簽名時,出現signture相同的狀況。

因此,反正客戶端使用的是13位的時間戳,若是提交一個10位的時間戳,它也要進行截取。不如直接提交過來怎麼使用,服務端本身決定的。

<?php

// 13 位時間戳轉10位 進行比對
time() - ceil($timestamp/1000);

// 若是是生成13位時間戳進行比對
list($micro,$time) = expload(' ',microtime());
ceil(($time + $micro)*1000) - $timestamp;

// 更新 其實microtime()是能夠直接返回一個float數據,只須要傳一個常數true
ceil(microtime(true)*1000) - $timestamp
複製代碼

請求錯誤日誌

APP開發的其中一個難點就是錯誤定位難,復現難。因此寫日誌就很是重要了。 當前錯誤日誌所需記錄的信息應包含至少如下幾類信息:

  1. 發生錯誤的接口地址和時間
  2. 該請求的header頭中的access_token,其實更合理地應該經過access_token獲取用戶id並記錄,由於access_token是有可能更改的。
  3. 該請求的客戶端版本號,更詳細的話,還有操做系統和設備型號。這就是爲何前面提倡將這幾個參數寫在header頭裏每次請求都提交的緣由。客戶端版本號能夠用來判斷哪一個版本請求會出現錯誤,而後再決定如何更改。操做系統和設備型號主要用於給前端兼容性錯誤排查。
  4. 該請求產生錯誤信息。
  5. 該請求的http狀態碼。
  6. 業務層若是有錯誤狀態碼也須要記錄

請求返回格式

APP開發如今比較流行的仍是返回json格式而不是xml格式。

返回json格式的數據通常是這樣的:

{
    "status" :200,
    "message":"ok",
    "data"   :{},
}
status 返回請求狀態碼,通常複用http狀態碼。
message 返回請求消息,若是有錯誤這裏寫錯誤信息。
data 是返回的數據
複製代碼

可是我仍是比較喜歡如下這種返回方式

{
    "code":0
    "data":{}
}
// 爲何請求成功 要使用0做爲code狀態碼呢,0的第一感受不是false嗎?
嗯,錯誤狀況千千萬,而成功只有一種狀況。正數負數千千萬,而0也只有一個。
{
    "code":1001
    "message":"某個控制器請求出錯"
}
複製代碼

爲何呢?

由於不想用http status來傳達API請求狀態,http status 傳達的是通信層的狀態。API是爲了知足業務,返回的數據應包含業務層的狀態碼。業務層不和通信層耦合,不拿http status 取巧。

固然對於這點,喜歡使用http status 的同窗也有不一樣的見解,這就看我的的喜愛了。

我以爲使用code的好處在於:

  1. 咱們能夠自定義更多的狀態碼和錯誤信息。通常我會作一個接口錯誤地圖類,而後根據code的值獲取對應的message。
  2. 更好地對code進行分類定義,好比1000 開頭的表示 a 控制器各個接口的產生的各類錯誤 2000 開頭的表示 b控制器各個接口產生的各類錯誤。 -1 表示 錯誤地圖類中 未定義的錯誤。
  3. 業務層的狀態碼不和通信層狀態碼耦合,更詳細地展現業務層錯誤信息。
  4. 避免客戶端出現某個接口返回未考慮進去的非200 ok的http 狀態碼,而形成客戶端卡死的狀況。我喜歡在後端對http響應的狀態碼進行判斷,若是該請求的響應碼不是200 就把查看錯誤地圖類轉化爲對應的code狀態碼和錯誤message,寫入日誌,並把http 狀態碼改回200。這樣保證每次http請求基本都會返回200,可預知的錯誤都轉化爲返回的json數據中的code狀態碼。

Authorization

  • App後端開發不能使用session?

雖然app經過接口請求的方式與後端交互,沒有cookie,可是依然可使用session。session的實現不依賴於cookie,若是你把cookie中的session_id 可是打開session的令牌。那麼header頭中的Authorization 字段提交的access_token 一樣能夠當作令牌實現一樣的做用。

  • 是否容許帳戶同時在兩個以上的設備登錄

由於咱們經過Authorization來獲取認證,因此:

  1. 若是你容許同時登錄多臺設備,你只須要登錄後複用user表中的access_token。

  2. 若是那你不容許同時登錄多臺設備,則能夠選擇登錄時刷新access_token,這樣就使得其餘在線的設備請求頭中的Authorization字段提交的access_token與user表中的不匹配,天然就被擠下線了。

  • access_token的安全性問題

咱們經過access_token來獲取用戶,也就意味着access_token若是被劫持就等同於用戶的帳戶被盜。

你想一想一樣做爲獲取服務端session的令牌,使用cookie時,爲了安全咱們通常會作哪些呢?

  1. cookie在生成時就會被指定一個Expire值,這就是cookie的生存週期,在這個週期內cookie有效,超出週期cookie就會被清除
  2. 對cookie進行加密,嵌入時間戳保證每次加密後的密文不一樣
  3. 不容許跨域使用

因此,雖然signture的惟一性已經爲咱們證實了是APP發起的合法請求,可是嚴格來講咱們也不能單單對access_token 進行明文傳輸。 咱們能夠考慮在Authorization 字段不是簡單地傳輸access_token的值,能夠傳一個access_token和時間戳的加密字符串,在服務端再進行解密,並先判斷是否超時。若是要安全性高些,還能夠參考signture作惟一性處理。


版本升級

建議建一個版本升級表用來存放版本升級信息。而且要有是否強制更新字段。

咱們header頭提交version參數,寫日誌爲的都是不想失去對客戶端的控制,能更好的定位錯誤。可是app與傳統的web開發的一個區別,就是web開發頁面作了修改,全部的用戶都能看到修改,可是APP的話,只要用戶沒有更新,已修復的bug,對用戶而言 依舊存在。

版本升級表設計

字段名 類型 備註
id int 主鍵id
app_type varchar 客戶端版本類型 ios or android
version int 開發版本號
version_code varchar 客戶端版本號(1.0.2)
upagrade_desc varchar 更新提示語
apk_url varchar 更新包連接
is_force tinyint 是否強制更新
created_at int 建立時間
status tinyint 是否已發佈

有了版本升級表之後,咱們就能更方便直觀地管理查看咱們發佈的版本。

並且咱們能夠在打開APP時請求接口,查詢版本設計表得到最新的版本與header頭提交的version字段做對比,判斷是否須要更新,彈出更新窗口。

對於須要強制更新的版本,彈窗應設置爲不容許用戶點擊取消,必定要更新才能使用該APP。這樣咱們就能夠把一些重大更新或者修復一些重要bug的版本設爲強制更新,不更新就不讓繼續使用。

用戶分析

爲了更好地進行用戶分析,咱們還能夠建一個APP登錄記錄表。 打開APP時就經過header把用戶信息記錄起來,用來作用戶分析。用戶日活量,月活量。

客戶端一打開就將數據發給該接口就行,無論請求是否成功,客戶端都不須要關心。

app_active_log 表

字段名 類型 備註
id int 主鍵id
app_type varchar 客戶端版本類型 ios or android
version int 開發版本號
version_code varchar 客戶端版本號(1.0.2)
model varchar 設備型號 小米 蘋果
uid int 用戶id
created_at int 建立時間

這個表的另外一個功能還能夠統計某個版本的用戶量或是Android仍是IOS用戶多,方便咱們更新版本時選擇先開發IOS版或是安卓版,或者出現bug決定哪一個版本先修復。

客戶端異常監控,分析

常見的APP端異常:

  1. crash 使用APP過程當中忽然出現閃退
  2. 卡頓 出現畫面卡頓
  3. Exception 程序被catch起來的Exception
  4. ANR 出現提示無響應彈框(Android)

咱們在服務端寫日誌,在header頭提交設備信息這些都是爲了更好地定位客戶端的錯誤。可是咱們的日誌只能記錄接口調用異常。對於客戶端的異常卻無能爲力。

爲此,咱們應該和客戶端配合。把客戶端產生異常按期上報到服務端。方便客戶端工程師定位復現並修復客戶端異常

咱們能夠建一個 app_crap 表來統計收集 crash 卡頓 Exception ANR的次數和影響用戶量 用戶數

字段名 類型 備註
id int 主鍵id
app_type varchar 客戶端版本類型 ios or android
version int 開發版本號
version_code varchar 客戶端版本號(1.0.2)
model varchar 設備型號 小米 蘋果
type tinyint 端異常類型 卡頓 閃退
description varchar 描述
created_at int 建立時間

固然,客戶端記錄這些數據比較麻煩.一個更好的解決方案是在客戶端中集成第三方服務提供的SDK,將這些數據提交到第三方平臺,客戶端工程師能夠登陸第三方平臺查看客戶端異常統計。

經常使用的第三方平臺 :

  1. 聽雲
  2. OneAPM

消息推送

  • 原生方式
  1. 客戶端輪詢 不推薦
  2. 服務端主動推客戶端 實現難度大
  • 第三方推送服務
  1. 極光推送 推薦使用restful api接口 比其餘SDK用起來更方便
  2. 百度雲推送
  3. 信鴿

APP後端開發工具推薦

接口調試神器,發起一個http請求

抓包神器,能夠抓APP發送過來的請求,查看是否有請求提交的參數都是什麼

php一個http 請求包,經過composer 安裝快速使用,可用來寫接口的測試代碼,模擬發起http請求,比起postman的優勢在於,經過代碼實現,自定義更方便。

手機模擬器,能夠在電腦上模擬多個Android系統的手機

內網映射工具。app開發一個麻煩的地方在於沒法本地調試,由於客戶端須要請求有域名或公網ip的服務端代碼,雖然公司有測試服務器,可是有些時候測試服上有不少人同時使用,我git提交了修改後的服務端代碼不能立刻reset hard生效。或者測試服不在我開發的分支。ngrok的好處是內網映射,給你的電腦綁定一個域名。而客戶端測試時填寫這個域名能訪問到你電腦的服務端代碼,實時調試更方便。


以上就是我作APP後端開發的一些總結,因爲是第一次開發APP後端,水平有限,還請你們多多指教

相關文章
相關標籤/搜索