接口的安全性主要圍繞Token、Timestamp和Sign三個機制展開設計,保證接口的數據不會被篡改和重復調用,下面具體來看:
(1)Token授權機制:(Token是客戶端訪問服務端的憑證)--用戶使用用戶名密碼登錄后服務器給客戶端返回一個Token(通常是UUID),並將Token-UserId以鍵值對的形式存放在緩存服務器中。服務端接收到請求后進行Token驗證,如果Token不存在,說明請求無效。
(2)時間戳超時機制:(簽名機制保證了數據不會被篡改)用戶每次請求都帶上當前時間的時間戳timestamp,服務端接收到timestamp后跟當前時間進行比對,如果時間差大於一定時間(比如5分鍾),則認為該請求失效。時間戳超時機制是防御DOS攻擊的有效手段。
(3)簽名機制:將 Token 和 時間戳 加上其他請求參數再用MD5或SHA-1算法(可根據情況加點鹽)加密,加密后的數據就是本次請求的簽名sign,服務端接收到請求后以同樣的算法得到簽名,並跟當前的簽名進行比對,如果不一樣,說明參數被更改過,直接返回錯誤標識。
/** * @desc 接受參數處理 */ private function dealParam(){ //接受header參數--系統參數 $systemParam=getAllHeadersParam(); //接受body數據--業務參數(json格式) $data=file_get_contents('php://input'); //讀取配置文件中的私鑰信息 $api_apiKey=C('api_apiKey'); $privatekey=$api_apiKey[$systemParam['token']]; $arr['token'] =$systemParam['token']; //服務端分配的標識(不同客戶端需使用不同的標識) $arr['timestamp']=$systemParam['timestamp']; //時間戳,UTC時間,以北京時間東八區(+8)為准 $arr['version'] =$systemParam['version']; //版本號 $arr['sign'] =$systemParam['sign']; //簽名 $arr['source'] =$systemParam['source']; //來源(0-安卓/1-IOS/2-H5/3-PC/4-php/5-java)
$arr['data'] =json_decode($data,true); //業務參數json格式
$arr['method'] =$data['method']; //訪問接口,格式:模型名.方法名
return $arr;
}
/* * @desc 獲取所有以HTTP開頭的header參數 * @return array */ private function getAllHeadersParam(){ $headers = array(); foreach($_SERVER as $key=>$value){ if(substr($key, 0, 5)==='HTTP_'){ $key = substr($key, 5); $key = str_replace('_', ' ', $key); $key = str_replace(' ', '-', $key); $key = strtolower($key); $headers[$key] = $value; } } return $headers; }
/* * @desc 簽名校驗 * @param $token string 服務端分配的標識(不同客戶端需使用不同的標識) * @param $timestamp string 時間戳,UTC時間,以北京時間東八區(+8)為准 * @param $version string 版本號 * @param $sign string 簽名 * @param $source int 來源(0-安卓/1-IOS/2-H5/3-PC/4-php/5-java) * @param $privatekey string 私鑰 * @param $data 業務參數json格式
* @return bool */ private function checkAuth($token,$timestamp,$version,$sign,$source,$privatekey,$data){ //參數判斷 if(empty($token)){ E('token不能為空!'); } if(empty($timestamp)){ E('時間戳不能為空!'); } if(empty($version)){ E('版本號不能為空!'); } if(empty($data)){ E('業務參數不能為空!'); } if(empty($source) && $source<>'0'){ E('來源不能為空!'); } if(empty($sign)){ E('簽名不能為空!'); } if(empty($privatekey)){ E('私鑰不能為空!'); } //時間校驗 $expire_second=C('expire_second',null,10); $timestamp_t=$timestamp+$expire_second; if($timestamp_t<time()){ E('請求已經過期!'); } $public= D('public'); $datas=$this->original; //系統參數 $paramArr=array( 'token'=>$token, 'timestamp'=>$timestamp, 'version'=>$version, 'source'=>$source, 'data'=>$data, ); //按規則拼接為字符串 $str = $this->createSign($paramArr,$this->privatekey); if($str != $this->sign){ E('驗簽錯誤!'); } return true; }
sign生成規則及步驟:
① 第一步:將所有需要發送至服務端的請求參數(空參數值的參數、文件、字節流、sign除外)按照參數名ASCII碼從小到大排序(字典序)
注意:
l 參數名ASCII碼從小到大排序(字典序);
l 如果參數的值為空不參與簽名;
l 文件、字節流不參與簽名;
l sign不參與簽名;
l 參數名、參數值區分大小寫;
② 第二步:將排序后的參數按照URL鍵值對的格式(即key1=value1&key2=value2…)拼接成字符串strA;
③ 第三步:在strA后面拼接上apiKey得到striSignTemp字符串,將strSignTemp字符串轉換為小寫字符串后進行MD5運算,MD5運算后得到值作為sign的值傳入服務端;
示例(所有參數、參數值均為示例,開發人員參考格式即可):
token:cd171009328172Ad3sc
apiKey:cd13H2ddd22212ds1da
① 第一步(獲取到的請求參數並按照參數名ASCII碼從小到大排序):
token=cd173309328172Ad322
data={"userName":"18817201899",goods:["addrId":323,{"skuNo":"p12232-023","count":3},{"skuNo":"p12232-013","count":1}]}
timestamp=1507537036
version=v3.6.0
② 第二步(按規則拼接為字符串strA):
token=cd171009328172Ad3sc&data={"userName":"18817201899",goods:["addrId":323,{"skuNo":"p12232-023","count":3},{"skuNo":"p12232-013","count":1}]}timestamp=1507537036&version=v3.6.0
③ 第三步(生成sign):
1)待簽名字符串strSignTemp:
token=cd171009328172Ad3sc&data={"userName":"18817201899",goods:["addrId":323,{"skuNo":"p12232-023","count":3},{"skuNo":"p12232-013","count":1}]}timestamp=1507537036&version=v3.6.0cd13H2ddd22212ds1da
2)轉換為小寫字符串
strtolower()
3)MD5加密后的密文
6D556D52822658FD47F7FE362544CEE1
/* * @desc 簽名函數 * @param $paramArr 系統參數 * @param $apiKey 私鑰 * @return string 返回簽名 */ private function createSign ($paramArr,$apiKey) { ksort($paramArr); $sign=''; foreach ($paramArr as $key => $val) { if ($key != '' && $val != '') { $sign .= $key."=".$val."&"; } } $sign=rtrim($sign,"&"); $sign.=$apiKey; $sign=strtolower($sign); $sign = md5($sign); return $sign; }
(4)拒絕重復調用:客戶端第一次訪問時,將簽名sign存放到緩存服務器中,超時時間設定為跟時間戳的超時時間一致,二者時間一致可以保證無論在timestamp限定時間內還是外 URL都只能訪問一次。如果有人使用同一個URL再次訪問,如果發現緩存服務器中已經存在了本次簽名,則拒絕服務。如果在緩存中的簽名失效的情況下,有人使用同一個URL再次訪問,則會被時間戳超時機制攔截。這就是為什么要求時間戳的超時時間要設定為跟時間戳的超時時間一致。拒絕重復調用機制確保URL被別人截獲了也無法使用(如抓取數據)。
/** * @desc 限制請求接口次數 * @return bool */ private function ask_count(){ $client_ip = $this->sys_get_client_ip(); $ask_url = $this->sys_GetCurUrl(); //限制次數 $limit_num = C('api_ask_limit',null,5); //有效時間內,單位:秒 $limit_time = C('api_ask_time'); $now_time = time(); $valid_time = $now_time - $limit_time; $ipwhere['creatime'] = array('EGT',date('Y-m-d H:i:s',$valid_time)); $ipwhere['ip_name'] = $client_ip; $ipwhere['ask_url'] = $ask_url; $check_result = M('log_ip_ask')->where($ipwhere)->count(); if($check_result !=='0'){ if($check_result >= $limit_num){ E('已經超出了限制次數!'); } } //執行插入 $add_data = array( 'ip_name'=>$client_ip, 'ask_url'=>$ask_url, 'creatime'=>date('Y-m-d H:i:s',time()) ); $result = M('log_ip_ask')->data($add_data)->add(); if($result===false){ E('寫入記錄失敗!'); } return true; }
/** * 獲取客戶端IP地址 * @param integer $type 返回類型 0 返回IP地址 1 返回IPV4地址數字 * @param boolean $adv 是否進行高級模式獲取(有可能被偽裝) * @return mixed */ private function sys_get_client_ip($type = 0,$adv=false) { $type = $type ? 1 : 0; static $ip = NULL; if ($ip !== NULL) return $ip[$type]; if($adv){ if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); $pos = array_search('unknown',$arr); if(false !== $pos) unset($arr[$pos]); $ip = trim($arr[0]); }elseif (isset($_SERVER['HTTP_CLIENT_IP'])) { $ip = $_SERVER['HTTP_CLIENT_IP']; }elseif (isset($_SERVER['REMOTE_ADDR'])) { $ip = $_SERVER['REMOTE_ADDR']; } }elseif (isset($_SERVER['REMOTE_ADDR'])) { $ip = $_SERVER['REMOTE_ADDR']; } // IP地址合法驗證 $long = sprintf("%u",ip2long($ip)); $ip = $long ? array($ip, $long) : array('0.0.0.0', 0); return $ip[$type]; } /** * @desc php獲取當前訪問的完整url地址 * @return string */ private function sys_GetCurUrl() { $url = 'http://'; if (isset ( $_SERVER ['HTTPS'] ) && $_SERVER ['HTTPS'] == 'on') { $url = 'https://'; } if ($_SERVER ['SERVER_PORT'] != '80') { $url .= $_SERVER ['HTTP_HOST'] . ':' . $_SERVER ['SERVER_PORT'] . $_SERVER ['REQUEST_URI']; } else { $url .= $_SERVER ['HTTP_HOST'] . $_SERVER ['REQUEST_URI']; } return $url; }
非法ip限制訪問,此處的限制一般用在服務器間的接口調用做此限制
// 允許訪問的IP列表 private $ip_allow = array( '111.11.111.111', // 局域網ip '111.11.111.112', // 任務服務器 '111.11.111.113', // 代理IP ); /** * @desc 非法IP限制訪問 * @param array $config * @return bool */ private function illegalip(){ if(!$this->ip_limit){ return true; } $remote_ip = get_client_ip(); if(in_array($remote_ip, $ip_allow)){ return true; } return false; }
參考鏈接:https://www.jianshu.com/p/c6518a8f4040
