api 簽名流程 API接口簽名驗證


現在越來越多的公司以 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);
    }
}

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM