前言
自己做介面開發的時間也算不短了(三年),想寫這篇文章其實差不多已經有一年多的時間了。我將從下面的方向來對我所理解的介面設計做個總結:
介面引數定義 -> 介面版本化的問題 -> 介面的安全性 -> 介面的程式碼設計 -> 介面的可讀性 -> 介面文件 -> 我遇到的坑
介面引數定義
介面設計中往可以抽象出一些新的公共引數,從事了近三年的介面開發工作中,我目前能想到了一些較為常見的公共介面引數如下:
公共引數 | 含意 | 定義該引數的意義 |
---|---|---|
timestamp | 毫秒級時間戳 | 1.客戶端的請求時間標示 2.後端可以做請求過期驗證 3.該引數參與簽名演算法增加簽名的唯一性 |
app_key | 簽名公鑰 | 簽名演算法的公鑰,後端通過公鑰可以得到對應的私鑰 |
sign | 介面簽名 | 通過請求的引數和定義好的簽名演算法生成介面簽名,作用防止中間人篡改請求引數 |
did | 裝置ID | 裝置的唯一標示,生成規則例如android的mac地址的md5和ios曾今udid(目前無法獲取)的md5, 1:資料收集 2.便於問題追蹤 3.訊息推送標示 |
介面版本化的問題
介面設計中有個算是歷史上的難題 -> 介面版本化。曾經也去調研了很多關於介面版本化的資料和設計,最後我得到的結論大致如下:
- 介面的版本區分為
- 大版本
- 原則:大版本的數量最多控制到5個以內(我個人跟傾向於3個),超過版本限制的版本提示升級到新版本
- 方案
- uri攜帶版本號,例如:v1/user/get
- 請求引數,例如:user/get?v=1.0
- 小版本
- 原則:自己把控吧?
- 方案
- uri攜帶版本號,例如:v1/user/get_01
- 請求引數,小數點右邊就是小版本,例如:user/get?v=1.1
- 大版本
介面的安全性
介面的設計肯定繞不開安全這兩個字,為了達到儘可能的安全,我們需要儘可能的增加被攻擊的難度,以下是我瞭解和使用到的一些常見的手段去增加介面的安全性(https這裡就不討論了):
過期驗證/簽名驗證/重放攻擊/限流/轉義
虛擬碼如下:
// 過期驗證
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 隨機字串或隨機正整數,與 Timestamp 聯合起來, 用於防止重放攻擊 例如騰訊雲是6位隨機正整數
*/
$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']);
複製程式碼
介面的程式碼設計 -> 解耦業務 即插即用
這個過程的關鍵字:抽象成類 前置中介軟體 注入
接著就是我們程式碼設計的層面了,如何抽象公共的部分與業務程式碼解耦。
一般寫法, 定義個全域性函式,然後每個介面開始時呼叫該函式:
// 全域性定義一個函式
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)
...
// 啟動
$checkParams->start();
}
}
複製程式碼
介面的可讀性
關於可讀性的不得不提到的就是RESTFUL,這裡我就不討論RESTFUL,大家可以自行補充相關知識。關於介面設計可讀性的我的一些思考:
- url
- 非RESTFUL: 資源/資源/操作(動詞), 例如 content/article/get -> 獲取內容資源下的一篇文章資源
- RESTFUL: 資源/資源/資源, 例如 get content/article/1 -> 獲取內容資源下文章ID為1的文章資源
- method
- 非RESTFUL: get便於查nginx日誌,上傳資源post, 沒啥硬性要求
- RESTFUL: 符合RESTFUL的思想
- request params: 個人更青睞於下劃線命名,適當的單詞縮寫
- response params: 響應的code要符合http status
- 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防止+轉成空格。比如做介面的引數校驗的時候~