原則條件
REST 指的是一組架構約束條件和原則。滿足這些約束條件和原則的應用程序或設計就是 RESTful。
Web 應用程序最重要的 REST 原則是,客戶端和服務器之間的交互在請求之間是無狀態的。從客戶端到服務器的每個請求都必須包含理解請求所必需的信息。如果服務器在請求之間的任何時間點重啟,客戶端不會得到通知。此外,無狀態請求可以由任何可用服務器回答,這十分適合雲計算之類的環境。客戶端可以緩存數據以改進性能。
在服務器端,應用程序狀態和功能可以分為各種資源。資源是一個有趣的概念實體,它向客戶端公開。資源的例子有:應用程序對象、數據庫記錄、算法等等。每個資源都使用 URI (Universal Resource Identifier) 得到一個唯一的地址。所有資源都共享統一的接口,以便在客戶端和服務器之間傳輸狀態。使用的是標准的 HTTP 方法,比如 GET、PUT、POST 和 DELETE。Hypermedia 是應用程序狀態的引擎,資源表示通過超鏈接互聯。
REST這個詞,是Roy Thomas Fielding在他2000年的博士論文中提出的
但是規則是很早之前的人設計的,但是it這個行業日新月異,業務結構復雜,變化性大,所有,你可以選擇遵守,也是適當改變,方便開發,比如現在移動化發展快速,app接口,微信網頁接口等
第一點要做的就是HTTPS 自2017年1月1號,開始新的ios都需要是https才能訪問,版本好像是10.0系列開始,並且滿足 蘋果ATS安全
如果您的APP如果仍采用HTTP傳輸,那么,在Apple Store中您的APP將不再能被用戶下載使用 ,所以,呵呵
場景分析和理論
首先說下幾種運用場景:
1,后台服務端->app客戶端 ios 安卓
一般來說單向信任加密解密就可以,客戶端向服務器請求進行數據加密,發送服務器端進行解密,然后返回數據,
但是請求返回的數據是不加密的,信任是單向的,有時候也需要加密就是雙向加密,效率來說就比較麻煩和文件返回用
base64進行傳輸,處理效率也比較差,特別是服務器端還有專門的文件服務器的,代碼層級處理起來就比較麻煩,效率也
很差,做過ios和java的同學應該比較了解,直接作為文件傳輸,php文件上傳的時候,會先把文件上傳系統的臨時文件目錄
而不是先去驗證權限在上傳,全局變量$_FILE php在上傳到其他文件服務器,或者移動到文件目錄,如果客戶端
沒有做驗證,或者允許大文件上傳,就會存在temp文件夾爆炸,服務器宕機的危險,對於中小項目來說,不是被人惡意攻擊
問題不大,這種情況下需要使用base64傳輸就可以先驗證在判斷是不是在做處理,然后就是在服務器驗證時候加上針對每
個接入端標識存儲,線上好處理具體問題來自哪些方面的接口,可以比較好的定位
2,后台服務端->html5 微信 瀏覽器
因為現在狠多為了一種開發多處使用,必須現在流行的html5混合app開發簡單方便,如果不是做游戲,性能一般app足夠
使用,不要被原生性能更好的屁話左右,為了適配機器app需要做無數處理,麻煩的很,而且對於小公司人力成本是巨大的,
而且不同語言的數據對接本身就是時間消耗,精力消耗,各種未知bug的調試,特別是第一次做個的人,有些東西在不同語言
顯示方式根本不一樣,比如 一個數組(php)
key=>0,name=>z key在ios解析的時候key不顯示,安卓key顯示NULL,呵呵,所以一些細節很麻煩,但是也並不是原生的
不好,只是就現在的技術潮流來說是這樣,比如如果一些ios新特性,在app的混合開發框架肯定不會那么及時的更新api接口
,比如hbuilder,做的HTML5+,整體性能也肯定沒有object-c好,swift個人沒什么了解,所以不清楚,所以有利有弊,需要技
術經理,公司,開發人員來衡量。
htnl5頁面在微信和瀏覽器里面需要的快速,如果每次都需要驗證數據加密,機密,效率首先需要考慮,而且js處理session
和cookies,在頁面不太好處理,很多現在很多都是純js去渲染數據,如果使用php來混合編寫也是可以的,使用php來模擬
數據加密,請求接口,這樣寫比較容易,但是無法再混合app中使用,如果你只是單獨開發手機瀏覽器和微信里面來使用php
混寫是沒有問題的,如果是頁面需要純js處理數據的話么就需要注意我說的上面的問題
數據認證的話,可以簡單做用戶登錄,比如微信自動等,根據name和password,salt計算一個唯一值作為token去數據庫校驗,
接口請求的時候js發送請求的時候校驗即可
3,文件服務器 特殊處理
很多項目到了后期都需要考慮單獨的文件服務器的問題,比如我前面博客說道的,掛在nfs文件服務,或者文件服務器接口
文件服務器處理起來比較麻煩,但是對於大的項目來說也是必要的,所有服務都需要接口化,在接口認證里面進行權限和
資源分配,這就是SOA的過程,比如使用文件接口,在富文本編輯器上傳文件的時候,就需要改造富文本編輯器的文件上傳
所以有點麻煩,uedit修改文件上傳路勁,支持api文件接口 我這篇博客就有介紹,文件服務器也有好處,就是可以規避一部
分文件安全問題
4,后台服務端->游戲客戶端
這個和html有些相似,又有些不同,一個是為了高效數據傳輸,保持socket穩定,或者數據傳輸的速度,還有就是保證
數據不被篡改,比如游戲作弊,考慮的東西比較多,還有比如一局游戲某個用戶斷線了,多久T掉,保證游戲繼續進行下去,接口里面
需要處理的東西很多,因為游戲需要更新的狀態很多,合理的設計表結構也是無比重要,比如一個buff就增加一個字段的話,一個玩家狀態
對應就有幾百個字段,傳輸數據當然就會大,一次更新那么數據,速度和性能就得再次考慮
(后面更新)
API接口理論設計實踐
1, 加密解密
使用https的 公鑰 私鑰 加密解密,但是其實也是發送數據不加密,只是通過簽名pkf crt來加密和驗證簽名的正確性,
很多借口采用的就是這種設計,但是數據安全性,我不是很理解,發送的是明文數據,木馬程序可以很輕松的監聽,
這種單向信任的明文數據發送驗證借口,在瀏覽器訪問的時候是有自帶https的公鑰,但是php curl的是可以不帶證書訪問的,
所有依然是接口里面是明文發送出去的,(如果這段有錯,請反饋)
2接口理論
現在普遍采用3des加密,只是因為方便,現在都出都有php ios java的通用demo方便,不方便的地方也有很多,發送的就是加密的數據,
雖然是對稱加密解密,單向信任,但是也是根據單個接口的賬號密碼進行二次驗證,如果你key和sercet,被別人知道,也是可以解密出加密發送的數據,如
果想使用接口獲取數據,還是得有該用的用戶名和秘密的,當然現在流行的手機號碼+手機短信驗證碼也是可以,但是服務器對應實現才可以,而且
最好ios和安卓開發框架需要支持session,cookies,https等為好,后面會有說明
推薦框架,但是需要支持這個多個協議
3,數據傳輸格式
json xml 數據流 二進制 等等,但是數據解析方便來說json還是最方便的。建議json,主要是怕麻煩
4,支持請求方法POST GET 文件上傳
麻煩一點就是文件上傳,base64上傳,或者直接文件上傳,但是臨時文件會出現上面說的系統臨時文件爆了情況
5,兼容舊代碼進行模擬登陸session cookies ,權限兼容
其他很多系統都需要去舊的rabc的里面去獲取數據,模擬登錄的時候,就會涉及到權限問題,當然你也可以單獨剝離出來
這些功能,時間花的也比較長,特別是需要在客戶端也要實現一定的權限的時候就麻煩了,你單獨剝離出來,你又要去模擬
權限,當然,最好辦法就是,接口對應的賬號里面也去實現一套RBAC,這樣后續開發人員就輕松了,看起來有點蛋疼,但是是比較
實際的問題,這個問題如果在設計接口的時候沒有就考慮進去的話,就在對接一些舊功能就忒麻煩,特別是沒有獨立方法化的代碼時候,
耦合度大,改起來麻煩的要死
6,目前接口優缺點。另一種接口設計方式
這個接口是所有終端同一個加密的秘鑰,也就說一個終端秘鑰丟失,就是可以獲得其他終端發送的數據進行解密,獲得發送數據,
所以中小項目,或者某個公司內部項目使用還不錯,但是大項目api化的數據安全性就不是很好
另一種接口設計,就是每個終端都是自己使用的8位key和32位的 value 然后加密還是下面的加密方法,但是在吧user_key直接明文也帶過
來先把數據庫的這個用戶的key 和value ,先解密,后吧對比解密數據里面的value 對比一樣就通過,去處理數據,就像一般的
用戶名密碼登錄校驗一樣,或者你想更安全就是md5('value .key') 去對比,全部不明文發送過來的數據
如果循環數據庫的key 和value去解密,效率太低,app接口需要就速度和效率
7,一些細節問題
$_REQUEST['header'] 為什么要這樣獲取數據,因為在ios和安卓,字典必須是要key和vaule的,所以兼容
urldecode建議不要使用這對函數 使用
rawurldecode($str);
rawurlencode($str);
會出現+ 和%2D 空格,多種語言交互的時候這樣的問題很多,所以要注意,特別是對接接口的時候,簽名的時候
簽名加密算法:sha1 長度40 (因為語言可能導致生產長度不一樣)
foreach (unserialize($res['contract']['idcards_file_ids']) as $k => $v) { $rr['user_idcard_data'][$k]['file_paths'] = app_standard_path_new($file_path['file_path']); $rr['user_idcard_data'][$k]['file_id'] = $v['file_id']; $rr['user_idcard_data'][$k]['name'] = $v['name']; } }
如果把 $rr['user_idcard_data'] json傳給app端,他就是個字典不是數組,對於ios和安卓來說
"customer_idcard_file": { "1": { "file_paths": "http://app.xinyzx.com/Uploads/personal_app_ios/201704/11/58ec42d23c090.jpeg", "file_id": "717892", "name": "work_card" } },
$rr['user_idcard_data'] = array_values($rr['user_idcard_data']);
需要處理成以下格式
"file_id":[ { "file_paths":"58ec42d22f310.jpeg", "file_id":"717891", "name":"bank_card1" }, { "file_paths":"58ec42d23c090.jpeg", "file_id":"717892", "name":"work_card" } ]
demo實例代碼,測試代碼
本代碼基於tp3.1.2 目前不提供完整測試類,后面有時間在更新
目前只支持 key 8位 secket 32位,支持更多位數的后續跟新
Crypt3Des.class.php 加密算法 3DES
<?php class Crypt3Des { private $key = "Symetric"; private $iv = "Symetric"; /** * 構造,傳遞二個已經進行base64_encode的KEY與IV * * @param string $key * @param string $iv */ function __construct($key, $iv) { if (empty($key) || empty($iv)) { echo 'key and iv is not valid'; exit(); } $this->key = $key; $this->iv = $iv; } /** *加密 * @param $value * @return */ public function encrypt($value) { $td = mcrypt_module_open(MCRYPT_3DES, '', MCRYPT_MODE_ECB, ''); $iv = base64_decode($this->iv); $value = $this->PaddingPKCS7($value); $key = base64_decode($this->key); mcrypt_generic_init($td, $key, $iv); $ret = base64_encode(mcrypt_generic($td, $value)); mcrypt_generic_deinit($td); mcrypt_module_close($td); return $ret; } /** *解密 * @param $value * @return */ public function decrypt($value) { $td = mcrypt_module_open(MCRYPT_3DES, '', MCRYPT_MODE_ECB, ''); $iv = base64_decode($this->iv); $key = base64_decode($this->key); mcrypt_generic_init($td, $key, $iv); $ret = trim(mdecrypt_generic($td, base64_decode($value))); $ret = $this->UnPaddingPKCS7($ret); mcrypt_generic_deinit($td); mcrypt_module_close($td); return $ret; } private function PaddingPKCS7($data) { $block_size = mcrypt_get_block_size('tripledes', 'cbc'); $padding_char = $block_size - (strlen($data) % $block_size); $data .= str_repeat(chr($padding_char), $padding_char); return $data; } private function UnPaddingPKCS7($text) { $pad = ord($text{strlen($text) - 1}); if ($pad > strlen($text)) { return false; } if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) { return false; } return substr($text, 0, - 1 * $pad); } }
BaseAction.class.php
<?php //API父類 class BaseAction extends Action { protected $user_id; protected $appkey; protected $secret; protected $appIDname; //接入用戶來源標示,可能在用戶數據錄入的時候會用到 public function _initialize() { myLog($_REQUEST, 'api_log'); if (empty($_REQUEST) && empty($_FILES)) { $this->response(array('code' => 0, 'msg' => '發送數據不能為空'), 'json', 200); } $data = $_REQUEST['header']; if (empty($data)) { $data = urldecode(file_get_contents('php://input')); //兼容php發送數據接收 和 php模擬測試,正式不一定 if (empty($data)) { $this->response(array('code' => 0, 'msg' => '發送數據不能為空'), 'json', 200); } } $data = $this->crypt3des()->decrypt($data); myLog($data, 'api_log'); $_POST = json_decode($data, true); //api接口日志記錄--打印發送數據 myLog($_POST, 'api_log'); $this->origin = $this->check_sign($_POST); } function crypt3des() { import('App.ORG.Crypt3Des'); $crypt3des = new Crypt3Des(base64_encode(C('crypt_key')), base64_encode(C('crypt_iv'))); return $crypt3des; } function _empty() { $this->response(array('code' => 0, 'msg' => '_empty,非法操作'), 'json', 200); } protected function check_sign($data, $expires = 300) { $params = array(); $params['timestamp'] = $data['timestamp']; if (time() - strtotime($params['timestamp']) > $expires) { $this->response(array('code' => 0, 'msg' => '簽名已過期'), 'json', 200); } //數據庫查詢校驗相關用戶數據 $where['app_api_name'] = $data['appkey']; $res = M('app_api_partner')->where($where)->find(); if (empty($res)) { $this->response(array('code' => 0, 'msg' => 'appkey錯誤'), 'json', 200); } if ($res['api_status'] !== '0') { $this->response(array('code' => 0, 'msg' => '目前api賬戶不可用'), 'json', 200); } if ($res['app_api_key'] !== $data['secret']) { $this->response(array('code' => 0, 'msg' => '通信密鑰錯誤'), 'json', 200); } $params['appkey'] = $res['app_api_name']; $params['secret'] = $res['app_api_key']; $sign = $this->data_auth_sign($params); // $this->response(array('msg' => $params, 'code' => 0), 'json', 200); if ($sign !== $data['sign']) { $this->response(array('code' => 0, 'msg' => '簽名錯誤'), 'json', 200); } else { myLog('簽名解析正確', 'api_log'); $this->appIDname = $res['app_api_mark']; //返回接入的使用的名稱 }
} private function data_auth_sign($data) { if (!is_array($data)) { $data = (array) $data; } ksort($data); $param = array(); foreach ($data as $key => $val) { $param[] = $key . "=" . $val; } $param = join("&", $param); $sign = sha1($param); return $sign; } }
C 獲取系統配置數據
myLog 打印系統日志
function myLog($str, $flag = 'default') { //if( APP_DEBUG != true )return ''; is_array($str) && $str = print_r($str, true); $dir = SYSTEM_ROOT . '/Uploads/logs/' . $flag . '/'; !is_dir($dir) && @mkdir($dir, 0755, true); $file = $dir . date('Ymd') . '.log.txt'; $fp = fopen($file, 'a'); if (flock($fp, LOCK_EX)) { $content = "[" . date('Y-m-d H:i:s') . "]\r\n"; $content .= $str . "\r\n\r\n"; fwrite($fp, $content); flock($fp, LOCK_UN); fclose($fp); return true; } else { fclose($fp); return false; } }
隨機生成一個8位隨機字符串
function get_rand_str($len) { $chars = array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ); $charsLen = count($chars) - 1; shuffle($chars); $output = ""; for ($i = 0; $i < $len; $i++) { $output .= $chars[mt_rand(0, $charsLen)]; } return $output; }
集成這個父控制就可以寫你自己的代碼了,下面是一個實力和一個接口測試的demo
<?php //客戶信息相關api接口類 class CustomerAction extends BaseAction { public function test() { $this->response(array('code' => 1, 'msg' => '測試成功', 'data' => $data), 'json', 200); } }
測試接口的demo
import('app.ORG.Crypt3Des'); $crypt3des = new Crypt3Des(base64_encode(C('crypt_key')), base64_encode(C('crypt_iv'))); $data['appkey'] = $_data['appkey'] = '11111111'; // 用於簽名參數 對應C的取到的值 $data['secret'] = $_data['secret'] = md5('11111111'); //用於簽名參數 $data['timestamp'] = $_data['timestamp'] = date('YmdHis', time()); //用於簽名參數 $data['sign'] = $this->data_auth_sign($_data); $data = json_encode($data); $crypt_data = (urlencode($crypt3des->encrypt($data))); $url = "http://127.0.0.1/app.php/customer/test"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_VERBOSE, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $crypt_data); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $return_data = curl_exec($ch); print_r($return_data); echo '<hr />'; print_r(json_decode($return_data, true)); if ($error = curl_error($ch)) { die($error); } // $rinfo = curl_getinfo($ch); // P($rinfo); curl_close($ch);
2017年6月24日23:17:10
這里補充一點,這個接口有個比較大的問題,就是沒有版本控制,比如在接口加個V參數。
最新碰到一個問題就是,有個比較大的改版,因為ios發布不通過,導致安卓可以升級但是ios不能,所以整體升級就只能直接進行
解決發難,就是根據V參數的版本號去訪問不同的比如,在訪問控制器的ZxAction.class.php 的test方法的時候,實際訪問的是Zxv3Action.class.php,或者 V3_zxAction.class.php,
在基礎控制器里面處理一下,不然在大版本更新,如果某些接口是不能升級的話,就很需要這個參數進行處理對應的接口
/* 新增版本控制參數v很重要,根據版本控制,比如 * * 訪問 m=test&a=zx * 里面有$_POST['v'] =v1 * 實際訪問的就是 m=test&a=v1_zx */ if (!empty($_POST['v'])) { $module = 'App_admin://' . MODULE_NAME; $function = $_POST['v'] . '_' . ACTION_NAME; A("$module")->$function(); 這里實例化的時候,會再次經過_initialize方法,有問題 die; }
這個如果寫在父控制器就會出現無限循環,出現問題
需要在自控制器里面加入這個,因為在初期沒有考慮到這個,就只能補充這個,唉,經驗不足
public function __construct() { parent::__construct(); if (!empty($_POST['v'])) { $function = trim($_POST['v']) . '_' . ACTION_NAME; $this->$function(); die; } }
其實還可以在父控制使用二級域名來控制,進行版本控制