ThinkPHP5開發中API數據安全相關解決方案

目錄

0x00 四種API數據安全問題

0x01 加密方式

0x02 如何進行加密?

0x03 將基礎參數(app版本號,手機型號,sign等)放入http協議的header頭

0x04 AES加密解密算法的使用,sign算法生成

0x05 優化:設置sign失效時間

0x06 授權sign唯一性支持

0x07 APP和服務器端時間一致性解決方案


0x00 四種API數據安全問題

  1. 接口請求地址 和 參數暴露
  2. 重要接口返回數據明文暴露
  3. APP登錄態請求的數據安全性問題
  4. 代碼層的數據安全問題

其中前三種問題,都可以使用加密來解決。

0x01 加密方式

  1. MD5
  2. AES 對稱加密算法,加密速度快,資源使用率高
  3. RSA 非對稱加密算法,加密效率低,數據量大的時候加密時間比較長

0x02 如何進行加密?

  1. 將基礎參數(app版本號,手機型號,sign等)放入http協議的header頭中
  2. 每次http請求都攜帶sign
  3. 保證sign的唯一性
  4. 請求參數、返回數據按安全性適當加密
  5. access_token

0x03 將基礎參數(app版本號,手機型號,sign等)放入http協議的header頭

一般情況下,會將業務相關的數據放在HTTP協議的Body中,而將其他基礎參數放在header頭中

基礎參數包括:

  1. sign 簽名
  2. app_type  app類型(android、IOS、IOSPad等)
  3. app_version app版本號
  4. did 移動端設備的did (唯一的)可以理解爲設備號
  5. model app的機型

ThinkPHP5 相關函數:

request()->header() 獲取HTTP報文的header頭中的數據

0x04 AES加密解密算法的使用,sign算法生成

思路:

客戶端將header頭中的version did等字段加密成 sign 和version did 一起發給服務端,服務端將sign的解密,然後將解密後的值

和version did等比對。如果比對合同,那麼就通過效驗。

sign 加密需要客戶端工程師去做,解密則需要服務端工程師去做

相關函數:

http_build_query($arr) 將數組轉化爲&連接的字符串

parse_str($str,$arr) 將&連接的str轉化爲數組存在$arr中

代碼:

common下的公共加解密類庫

<?php
 
namespace app\common\lib;
/**
 * aes 加密 解密類庫
 *
 */
class Aes {
 
    private $hex_iv = '00000000000000000000000000000000'; # converted JAVA byte code in to HEX and placed it here
 
    private $key = null;
 
    function __construct() {
        $this->key = config('app.aeskey');
        $this->key = hash('sha256', $this->key, true);
    }
 
   
 
    public function encrypt($input)
    {
        $data = openssl_encrypt($input, 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA, $this->hexToStr($this->hex_iv));
        $data = base64_encode($data);
        return $data;
    }
 
    public function decrypt($input)
    {
        $decrypted = openssl_decrypt(base64_decode($input), 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA, $this->hexToStr($this->hex_iv));
        return $decrypted;
    }
 
    /*
      For PKCS7 padding
     */
 
    private function addpadding($string, $blocksize = 16) {
 
        $len = strlen($string);
 
        $pad = $blocksize - ($len % $blocksize);
 
        $string .= str_repeat(chr($pad), $pad);
 
        return $string;
 
    }
 
    private function strippadding($string) {
 
        $slast = ord(substr($string, -1));
 
        $slastc = chr($slast);
 
        $pcheck = substr($string, -$slast);
 
        if (preg_match("/$slastc{" . $slast . "}/", $string)) {
 
            $string = substr($string, 0, strlen($string) - $slast);
 
            return $string;
 
        } else {
 
            return false;
 
        }
 
    }
 
    function hexToStr($hex)
    {
 
        $string='';
 
        for ($i=0; $i < strlen($hex)-1; $i+=2)
 
        {
 
            $string .= chr(hexdec($hex[$i].$hex[$i+1]));
 
        }
 
        return $string;
    }
 
}

IAuth.php

<?php
/*
 * @Author: your name
 * @Date: 2020-06-26 22:30:46
 * @LastEditTime: 2020-07-02 16:40:23
 * @LastEditors: Please set LastEditors
 * @Description: In User Settings Edit
 * @FilePath: /myNewsApp/application/common/lib/IAuth.php
 */ 
namespace app\common\lib;
use app\common\lib\aes;

class IAuth{
    /**
     * 明文密碼加鹽加密
     * @param string $data 明文密碼
     * @return string
     */
    public static function setPassword($data){
        return md5($data.config('app.password_salt'));
    }

    /**
     * 檢查sign的合法性
     * @param string $sign
     * @param array $data  header頭中相關數據組成的數組
     * @return bool
     */
    public static function checkSignPass($sign='',$data){
        $str = (new Aes())->decrypt($sign);
        if(empty($str)){
            return false;
        }
        parse_str($str,$arr);
        if(!is_array($arr)
         || empty($arr['did'] 
         || $arr['did']!=$data['did'])
         || $arr['version']!=$data['version']){
            return false;
        }
        return true;

    }
    /**
     * 生成每次請求的sign
     * @param array $data
     * @return string
     */
    public static function setSign($data=[]){
        //1.按key排序
        ksort($data);
        //2.數組轉url傳參的格式 id=1&username=123這種格式
        $string = http_build_query($data);
        //3.通過aes來加密
        $string = (new Aes())->encrypt($string);
        return $string;
    }
}

Common控制器

<?php
/*
 * @Author: Shang Rui
 * @Date: 2020-07-02 12:48:34
 * @LastEditTime: 2020-07-02 16:43:00
 * @LastEditors: Please set LastEditors
 * @Description: In User Settings Edit
 * @FilePath: /myNewsApp/application/api/controller/Common.php
 */ 
namespace app\api\controller;
use think\Controller;
use app\common\lib\Aes;
use app\common\lib\IAuth;
use app\common\lib\exception\ApiException;

/**
 * API模塊公共控制器
 */
class Common extends Controller{
    public $headers = [];
   /**
    * 初始化方法
    */
   public function _initialize(){
        // $this->testAes();
       $this->checkRequestAuth();

   }
   /**
    * 檢查每次app請求的數據是否合法
    */
   public function checkRequestAuth(){

       //首先需要獲取header頭
       $headers = request()->header();
       //todo
       //sign加密
       //基礎參數校驗
       if(empty($headers['sign'])){
            throw new ApiException('sign不存在',400);
       }
       if(!in_array($headers['app_type'],config('app.apptypes'))){
           throw new ApiException('app_type不合法',400);
       }
       //校驗sign的合法性
       if(!IAuth::checkSignPass($headers['sign'],$headers)){
           throw new ApiException('授權碼sign失敗!',401);
       }
       $this->headers = $headers;

   } 
   
   public function testAes(){
       $data = [
           'did'=>'123',
           'version'=>1,
       ];
       $decode = IAuth::setSign($data);
       echo $decode;
       echo (new Aes())->decrypt($decode);
       exit;
   }
}

0x05 優化:設置sign失效時間

思路:如果sign解密 和 加密之間的時間差 超過了 該時間 ,我們就認爲 sign無效。例如:黑客對前端向後端發送的數據進行了抓包,但是如果黑客抓包改包發送請求 這一系列操作 超過了我們配置的失效時間,我們後端就認爲sign無效了。

代碼:

加密:

       $data = [
           'did'=>'123',
           'version'=>1,
           'time'=>Time::get13TimeStamp(),
       ];
       $decode = IAuth::setSign($data);

解密:

    /**
     * 檢查sign的合法性
     * @param string $sign
     * @param array $data  header頭中相關數據組成的數組
     * @return bool
     */
    public static function checkSignPass($sign='',$data){
        $str = (new Aes())->decrypt($sign);
        if(empty($str)){
            return false;
        }

        parse_str($str,$arr);
        if(time()-ceil($arr['time']/1000) > config('app.app_sign_time')){
            return false;
        }
        if(!is_array($arr)
         || empty($arr['did'] 
         || $arr['did']!=$data['did'])
         || $arr['version']!=$data['version']){
            return false;
        }
        return true;

    }

生成13位時間戳:

一般情況下時間戳都是10位,我們可以使用microtime:返回字符串 "microsec sec" ,其中 sec 爲自 Unix 紀元(0:00:00 January 1, 1970 GMT)起的秒數,microsec 爲微秒部分。

在微妙數部分取三位 和 十位秒數部分拼接,得到13位的時間戳

爲什麼要生成13位的時間戳,因爲更多位數的時間戳 被加密後唯一性更強一點

代碼:

<?php
/*
 * @Author: your name
 * @Date: 2020-07-07 10:15:34
 * @LastEditTime: 2020-07-07 10:28:27
 * @LastEditors: Please set LastEditors
 * @Description: In User Settings Edit
 * @FilePath: /myNewsApp/application/common/lib/Time.php
 */ 
namespace app\common\lib;

class Time{
    /**
     * @description: 獲取13位的時間戳
     * @param {type} 
     * @return: int
     */    
    public static function  get13TimeStamp(){
        list($t1,$t2)=explode(' ',microtime());
        return $t2.ceil($t1*1000);
    }
}

0x06 授權sign唯一性支持

思路:每個sign只能使用一次。

當一個sign被效驗成功後,我們就在 文件/MySQL/Redis中 寫入一個標記,標記該sign已經被效驗。

當下次一個sign被髮過來,我們讀取標記,判斷sign是否已經被使用過了。

如果你的代碼都保存在同一臺服務器上 ,可以寫入文件中,建議使用TP5的Cache機制寫入文件

如果是分佈式架構,建議寫入到MySQL或者Redis中

補充:

Cache::get(鍵名,鍵值,過期時間) 寫入緩存文件(位置:cache文件夾下)

Cache::set(鍵名) 讀取緩存文件中對應的內容。

cache() 封裝的助手函數

一般情況下我們的緩存文件不能一直保存,這樣會佔用服務器的資源。所以必須有一個過期時間。但是這樣不就只能保證:每個sign在過期時間內只能使用一次嗎?只要將緩存過期時間 > sign的失效時間,就可以實現每個sign只能使用一次的效果了。

代碼:

       //校驗sign的合法性
       if(!IAuth::checkSignPass($headers['sign'],$headers)){
           throw new ApiException('授權碼sign失敗!',401);
       }
       Cache::set($headers['sign'],1,config('app.app_sign_cache_time'));
        //檢查sign的唯一性
        if(Cache::get($sign)){
            return false;
        }

0x07 APP和服務器端時間一致性解決方案

問題背景:app端 和服務器端 的時間 並不一定完全一致,可以存在一個時間差,這爲之前的sign失效時間的驗證造成了問題。

解決思路:

服務器端開一個接口,讓app端可以獲取到服務器端的時間

<?php
/*
 * @Author: your name
 * @Date: 2020-07-07 11:39:29
 * @LastEditTime: 2020-07-07 11:45:24
 * @LastEditors: Please set LastEditors
 * @Description: In User Settings Edit
 * @FilePath: /myNewsApp/application/api/controller/Time.php
 */ 
namespace app\api\controller;
use think\Controller;

class Time extends Controller{
    public function index(){
        return show(config('api.success_code'),'獲取時間成功!',time());
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章