現在越來越多的公司以 API 的形式對外提供服務,這些 API 接口大多暴露在公網上,所以安全性就變的很重要了。最直接的風險如下:
- 非法使用 API 服務。(收費接口非法調用)
- 惡意攻擊和破壞。(數據篡改、DOS)
因此需要設計一些接口安全保護的方式來增強接口安全,在運輸層可添加 SSL 證書,上 HTTPS,在應用層主要是通過一些加密邏輯來實現。目前主流的兩種是在 HTTP Header 里加認證信息和 API 簽名。
大概的思路是在請求包帶上我們自己構造好的簽名,這個簽名必須滿足下面幾點:
a、唯一性,簽名是唯一的,可驗證目標用戶
b、可變性,每次攜帶的簽名必須是變化的
c、時效性,具有一定的時效,過期作廢
d、完整性,能夠對數據包進行驗證,防止篡改
請求身份
為開發者分配SecretKey(用於接口加密,確保不易被窮舉,生成算法不易被猜測)。
請求端構造簽名串
// 按首字母排序 function objKeySort(obj) { //排序的函數 var newkey = Object.keys(obj).sort(); //先用Object內置類的keys方法獲取要排序對象的屬性名,再利用Array原型上的sort方法對獲取的屬性名進行排序,newkey是一個數組 var newObj = {}; //創建一個新的對象,用於存放排好序的鍵值對 let str = '' for (var i = 0; i < newkey.length; i++) { //遍歷newkey數組 newObj[newkey[i]] = obj[newkey[i]]; //向新創建的對象中按照排好的順序依次增加鍵值對 str += (newkey[i] + obj[newkey[i]]) } return { newObj, preSign: str }; //返回排好序的新對象 } let data = { test: 1 } let dataAdd = { ...data, timestamp: Math.round(new Date() / 1000), // 時間戳時間轉換為秒 // nonce: Math.floor(Math.random() * 100000000), // 8位隨機數 nonce: guid(20), // 20位隨機數 } let secretKey = 'abc123'; let newData = objKeySort(dataAdd); let str = `${secretKey}${newData.preSign}` let signStr = md5Libs.md5(str); dataAdd.sign = signStr;
// nonce: "uoTgvAKQzJD0wFy3o18a"
// sign: "3b67af41142d1b8d5a2782a8479c8858"
// test: "1"
// timestamp: 1628323310
服務器端
<?php namespace app\api\middleware; use app\Request; use app\services\user\UserAuthServices; use test\enum\EnumStoreRedisKey; use test\exceptions\AuthException; use test\interfaces\MiddlewareInterface; use mySign\Sign; use think\exception\DbException; use think\facade\Cache; /** * Class SignVerifyApi * @package app\api\middleware * 驗證私鑰簽名的接口 (針對后端) */ class SignVerifyApi implements MiddlewareInterface { public function handle(Request $request, \Closure $next, bool $force = true) { $secretKey = 'abc123'; // 秘鑰 $where = $request->param(); // 驗證請求接口是否超時 開始 $time = time(); //獲取當前時間戳 $expire_date = 60; // 秒 / 請求和當前時間相差的時間. 如: 1分鍾之前的時間戳不能請求. 請求超時1分鍾的保錯. 請求時間戳和服務器時間相差60秒報錯 $redis_nonce_expire = 70; // 隨機字符串保存過期時間 $diff_time = $time - ($where['timestamp']); if ($diff_time > $expire_date) { return app('json')->fail('請求超時,請檢查請求時間和服務器時間是否相差過大'); } // 驗證請求接口是否超時 結束 // 防重放 // 保存隨機數到redis, 並驗證redis隨機數 開始 $redis_nonce = Cache::store('redis')->sIsMember(EnumStoreRedisKey::ADDRESS_CREATE_NONCE() . $secretKey, $where['nonce']); if ($redis_nonce) { return app('json')->fail('請勿使用請求過的參數'); } Cache::store('redis')->sAdd(EnumStoreRedisKey::ADDRESS_CREATE_NONCE() . $secretKey, $where['nonce']); // 查找隨機數列表元素數量 $count = Cache::store('redis')->sCARD(EnumStoreRedisKey::ADDRESS_CREATE_NONCE() . $secretKey); // redis 沒有隨機數的時候則設置過期時間, 否則每次都會重置 if ($count == 1) { Cache::store('redis')->expire(EnumStoreRedisKey::ADDRESS_CREATE_NONCE() . $secretKey, $redis_nonce_expire);//設置過期時間為秒 } // 保存隨機數到redis, 並驗證redis隨機數 結束 // 驗證簽名開始 $sign_result = Sign::generateSign($where, $secretKey); if ($sign_result['result']['sign'] !== $where['sign']) { return app('json')->fail('簽名驗證失敗'); } // 驗證簽名結束 return $next($request); } }