目錄
0x03 將基礎參數(app版本號,手機型號,sign等)放入http協議的header頭
0x00 四種API數據安全問題
- 接口請求地址 和 參數暴露
- 重要接口返回數據明文暴露
- APP登錄態請求的數據安全性問題
- 代碼層的數據安全問題
其中前三種問題,都可以使用加密來解決。
0x01 加密方式
- MD5
- AES 對稱加密算法,加密速度快,資源使用率高
- RSA 非對稱加密算法,加密效率低,數據量大的時候加密時間比較長
0x02 如何進行加密?
- 將基礎參數(app版本號,手機型號,sign等)放入http協議的header頭中
- 每次http請求都攜帶sign
- 保證sign的唯一性
- 請求參數、返回數據按安全性適當加密
- access_token
0x03 將基礎參數(app版本號,手機型號,sign等)放入http協議的header頭
一般情況下,會將業務相關的數據放在HTTP協議的Body中,而將其他基礎參數放在header頭中
基礎參數包括:
- sign 簽名
- app_type app類型(android、IOS、IOSPad等)
- app_version app版本號
- did 移動端設備的did (唯一的)可以理解爲設備號
- 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());
}
}