前言
本身作接口開發的時間也算不短了(三年),想寫這篇文章其實差很少已經有一年多的時間了。我將從下面的方向來對我所理解的接口設計作個總結:php
接口參數定義 -> 接口版本化的問題 -> 接口的安全性 -> 接口的代碼設計 -> 接口的可讀性 -> 接口文檔 -> 我遇到的坑html
接口參數定義
接口設計中往能夠抽象出一些新的公共參數,從事了近三年的接口開發工做中,我目前能想到了一些較爲常見的公共接口參數以下:android
公共參數 | 含意 | 定義該參數的意義 |
---|---|---|
timestamp | 毫秒級時間戳 | 1.客戶端的請求時間標示 2.後端能夠作請求過時驗證 3.該參數參與簽名算法增長簽名的惟一性 |
app_key | 簽名公鑰 | 簽名算法的公鑰,後端經過公鑰能夠獲得對應的私鑰 |
sign | 接口簽名 | 經過請求的參數和定義好的簽名算法生成接口簽名,做用防止中間人篡改請求參數 |
did | 設備ID | 設備的惟一標示,生成規則例如android的mac地址的md5和ios曾今udid(目前沒法獲取)的md5, 1:數據收集 2.便於問題追蹤 3.消息推送標示 |
接口版本化的問題
接口設計中有個算是歷史上的難題 -> 接口版本化。曾經也去調研了不少關於接口版本化的資料和設計,最後我獲得的結論大體以下:ios
接口的版本區分爲nginx
接口的安全性
接口的設計確定繞不開安全這兩個字,爲了達到儘量的安全,咱們須要儘量的增長被攻擊的難度,如下是我瞭解和使用到的一些常見的手段去增長接口的安全性(https這裏就不討論了):redis
過時驗證/簽名驗證/重訪攻擊/限流/轉義算法
僞代碼以下:後端
// 過時驗證 if (microtime(true)*1000 - $_REQUEST['timestamp'] > 5000) { throw new \Exception(401, 'Expired request'); }
// 簽名驗證(公鑰校驗) $params = ksort($_REQUEST); unset($params['sign']); $sign = md5(sha1(implode('-', $params) . $_REQUEST['app_key'])); if ($sign !== $_REQUEST['sign']) { throw new \Exception(401, 'Invalid sign'); }
/** * 重訪攻擊 * @params noise string 隨機字符串 */ $key = md5("{$_REQUEST['REQUEST_URI']}-{$_REQUEST['timestamp']}-{$_REQUEST['noise']}-{$_REQUEST['did']}"); if ($redisInstance->exists($key)) { throw new \Exception(401, 'Repeated request'); }
// 限流 $key = md5("{$_REQUEST['REQUEST_URI']}-{$_REQUEST['REMOTE_ADDR']}-{$_REQUEST['did']}"); if ($redisInstance->get($key) > 60) { throw new \Exception(401, 'Request limit'); } $redisInstance->incre($key);
// 轉義 $username = htmlspecialchars($_REQUEST['username']);
接口的代碼設計 -> 解耦業務 即插即用api
這個過程的關鍵字:抽象成類 前置中間件 注入
接着就是咱們代碼設計的層面了,如何抽象公共的部分與業務代碼解耦。安全
通常寫法, 定義個全局函數,而後每一個接口開始時調用該函數:
// 全局定義一個函數 function check () { // 校驗公共參數 # code ... // 校驗簽名 # code ... // 校驗頻率 # code ... // 等等... }
二般寫法, 定義個父類方法,而後每一個接口類繼承該接口,構造函數調用改方法,其實和上面的換湯不換藥:
// 父類方法 class father { public function __construct() { $this->check(); } public function check () { // 校驗公共參數 # code ... // 校驗簽名 # code ... // 校驗頻率 # code ... // 等等... } }
重點來了,我提倡的第三般寫法,對象鏈和前置中間件:
/** * 檢驗抽象類 */ abstract class Check { /** * 下一個check實體 * * @var object */ private $nextCheckInstance; /** * 校驗方法 * * @param Request $request 請求對象 */ abstract public function operate(Request $request); /** * 設置責任鏈上的下一個對象 * * @param Check $check */ public function setNext(Check $check) { $this->nextCheckInstance = $check; return $check; } /** * 啓動 * * @param Request $request 請求對象 */ public function start(Request $request) { $this->doCheck($request); // 調用下一個對象 if (! empty($this->nextCheckInstance)) { $this->nextCheckInstance->start($request); } } } // 校驗公共參數類 class ParamsCheck extends Check { public function operate() { // 校驗公共參數 # code ... } } // 校驗簽名類 class SignCheck extends Check { public function operate() { // 校驗簽名 # code ... } } // 等等... // 前置中間件類 class FrontMiddleware { public function run() { // 初始化一個:必傳參數校驗的check $checkParams = new ParamsCheck(); // 初始化一個:簽名check $checkSign = new SignCheck(); // 初始化一個:頻率check $checkFrequent = new FrequentCheck(); // 等等... // 構成對象鏈 $checkParams->setNext($checkSign) ->setNext($checkFrequent) ... // 啓動 $checkArguments->start(); } }
接口的可讀性
關於可讀性的不得不提到的就是RESTFUL,這裏我就不討論RESTFUL,你們能夠自行補充相關知識。關於接口設計可讀性的個人一些思考:
非RESTFUL: 資源/資源/操做(動詞), 例如 content/article/get -> 獲取內容資源下的一篇文章資源
RESTFUL: 資源/資源/資源, 例如 get content/article/1 -> 獲取內容資源下文章ID爲1的文章資源
非RESTFUL: get便於查nginx日誌,上傳資源post, 沒啥硬性要求
RESTFUL: 符合RESTFUL的思想
200 -> 正常
400 -> 缺乏公共必傳參數或者業務必傳參數
401 -> 接口校驗失敗 例如簽名
403 -> 沒有該接口的訪問權限
499 -> 上游服務響應時間超過接口設置的超時時間
500 -> 代碼錯誤
501 -> 不支持的接口method
502 -> 上游服務返回的數據格式不正確
503 -> 上游服務超時
504 -> 上游服務不可用
// 響應的格式 { "code": 200, "msg": "ok", "data": { } }
接口文檔
好的接口文檔就是生產力, swagger + api blueprint 自行google吧
我遇到的坑
這裏遇到的一個比較大的坑就是http協議歷史遺留的bug:
不區分url裏的空格 和加號 帶來的問題就是urldecode會把參數裏的+號轉爲空格,因此這種場景的就得使用rawurldecode防止+轉成空格。好比作接口的參數校驗的時候~