本案例前提說明:javascript
- 本例中不使用\yii\rest\ActiveController自動建立的API,而是自定義一個API
- 使用Auth2.0的Bearer模式進行身份驗證
- 使用MongoDB做爲數據庫,關於如何在Yii2中使用mongodb,請參考其餘資料
- 本例中將使用Yii2的RESTful Rate Limiting功能對API進行訪問頻率控制
- 本例使用Swagger-UI生成公開的接口文檔
- 本例中,API的請求將使用祕鑰對請求參數進行簽名,簽名參數sign將做爲url的最後一部分,服務端將使用相同的簽名方式進行簽名並匹配sign的值,以肯定訪問是否被僞造
注: 本例中models均放在myapi/models/v1下,也能夠直接將models放在myapi/modules/v1/models下php
return [ ... 'modules' => [ 'v1' => [ 'class' => 'myapi\modules\v1\Module' ], ] ... ];
本例中,數據庫包含如下兩張表external_api_users(API的用戶表)、external_api_settings(Rate Limiting設置表):html
external_api_users數據結構以下:java
{ "_id" : ObjectId("57ac16a3c05b39f9f6bf06a0"), "userName" : "danielfu", "avatar" : "http://www.xxx.com/avatar/default.png", "authTokens" : [ "abcde", // token能夠同時存在多個 "12345" ], "apiKeyInfos" : { "apiKey" : "apikey-123", "publicKey" : "publickey-123", "secreteKey" : "secreteKey-123" // 用來對sign進行簽名 }, "status" : "active", "isDeleted" : false }
external_api_settings數據結構以下:git
{ "_id" : ObjectId("57ac16a81c35b1a5603c9869"), "userID" : "57ac16a3c05b39f9f6bf06a0", // 關聯到external_api_users._id字段 "apiURL" : "/v1/delivery/order-sheet", "rateLimit" : NumberLong(2), // 只能訪問2次 "duration" : NumberLong(10), // rateLimit的限制是10秒以內 "allowance" : NumberLong(1), // 當前在固定時間內剩餘的可訪問次數爲1次 "allowanceLastUpdateTime" : NumberLong(1470896430) // 最後一次訪問時間 }
注意:本例使用的是Mongodb做爲數據庫,所以表結構表示爲json格式github
use yii\mongodb\ActiveRecord; use yii\filters\RateLimitInterface; use yii\web\IdentityInterface; // 要實現Rate Limiting功能,就須要實現 \yii\filters\RateLimitInterface 接口 class ExternalApiUser extends ActiveRecord implements RateLimitInterface, IdentityInterface { ... public function getRateLimit($request, $action) { return \myapi\models\v1\ExternalApiSettings::getRateLimit((string)$this->_id, $action->controller->module->module->requestedRoute); } public function loadAllowance($request, $action) { return \myapi\models\v1\ExternalApiSettings::loadAllowance((string)$this->_id, $action->controller->module->module->requestedRoute); } public function saveAllowance($request, $action, $allowance, $timestamp) { return \myapi\models\v1\ExternalApiSettings::saveAllowance((string)$this->_id, $action->controller->module->module->requestedRoute, $allowance, $timestamp); } ... }
class ExternalApiSettings extends \yii\mongodb\ActiveRecord { ... public static function getRateLimit($userID, $apiUrl) { if (empty($userID) || empty($apiUrl)) { throw new InvalidParamException('Parameter UserID and ApiURL is required!'); } $setting = self::findOne(['userID' => $userID, 'apiURL' => $apiUrl]); if ($setting == null) { $setting = new self(); $setting->userID = $userID; $setting->apiURL = $apiUrl; $setting->rateLimit = \Yii::$app->params['rateLimiting']['rateLimit']; $setting->duration = \Yii::$app->params['rateLimiting']['duration']; $setting->allowance = \Yii::$app->params['rateLimiting']['rateLimit']; $setting->save(); } return [$setting->rateLimit, $setting->duration]; } public static function loadAllowance($userID, $apiUrl) { if (empty($userID) || empty($apiUrl)) { throw new InvalidParamException('Parameter UserID and ApiURL is required!'); } $setting = self::findOne(['userID' => $userID, 'apiURL' => $apiUrl]); if ($setting != null) { return [$setting->allowance, $setting->allowanceLastUpdateTime]; } } public static function saveAllowance($userID, $apiUrl, $allowance, $allowanceLastUpdateTime) { if (empty($userID) || empty($apiUrl)) { throw new InvalidParamException('Parameter UserID and ApiURL is required!'); } $setting = self::findOne(['userID' => $userID, 'apiURL' => $apiUrl]); if ($setting != null) { $setting->allowance = $allowance; $setting->allowanceLastUpdateTime = $allowanceLastUpdateTime; $setting->save(); } } ... }
return [ ... 'components' => [ ... 'user' => [ 'identityClass' => 'myapi\models\v1\ExternalApiUser', 'enableAutoLogin' => true, ] ... ] ... ];
// 特別注意的是須要將\yii\web\ActiveController改成\yii\rest\ActiveController class DeliveryController extends \yii\rest\ActiveController { // $modelClass是\yii\rest\ActiveController必須配置的屬性,可是本例中咱們不須要使用基於ActiveRecord快速生成的API接口,所以對應$modelClass屬性的設置並沒什麼用處 public $modelClass = 'myapi\models\v1\request\delivery\OrderSheetRequest'; /* \yii\rest\ActiveController會對應於$modelClass綁定的ActiveRecord快速生成以下API: GET /deliveries: list all deliveries page by page; HEAD /deliveries: show the overview information of deliveries listing; POST /deliveries: create a new delivery; GET /deliveries/123: return the details of the delivery 123; HEAD /deliveries/123: show the overview information of delivery 123; PATCH /deliveries/123 and PUT /users/123: update the delivery 123; DELETE /deliveries/123: delete the delivery 123; OPTIONS /deliveries: show the supported verbs regarding endpoint /deliveries; OPTIONS /deliveries/123: show the supported verbs regarding endpoint /deliveries/123. */ ... }
class DeliveryController extends \yii\rest\ActiveController { ... public function behaviors() { $behaviors = parent::behaviors(); // 身份驗證模式改成Auth2.0的Bearer模式 $behaviors['authenticator'] = [ 'class' => \yii\filters\auth\HttpBearerAuth::className(), ]; // 開啓RESTful Rate Limiting功能 $behaviors['rateLimiter']['enableRateLimitHeaders'] = true; ... return $behaviors; } ... }
public function actionOrderSheet() { ... }
return [ ... 'components' => [ 'urlManager' => [ 'enablePrettyUrl' => true, 'enableStrictParsing' => true, 'showScriptName' => false, 'rules' => [ // 這一條配置是爲了生成Swagger.json文檔所預留的API,使用的仍是基本的\yii\web\UrlRule [ 'class' => 'yii\web\UrlRule', 'pattern' => 'site/gen-swg', 'route' => 'site/gen-swg' ], /* 這一條配置是配置自定義的RESTful API路由 本例中,咱們的url將會是以下格式: http://www.xxx.com/v1/delivery/order-sheet/sn1001/c0bb9cfe4fdcc5ee0a4237b6601d1df4 其中,sn1001爲shipping-number參數,c0bb9cfe4fdcc5ee0a4237b6601d1df4爲sign參數 */ [ 'class' => 'yii\rest\UrlRule', 'controller' => 'v1/delivery', 'pluralize' => false, // 不須要將delivery自動轉換成deliveries 'tokens' => [ '{shipping-number}' => '<shipping-number:\\w+>', '{sign}' => '<sign:\\w+>' ], 'extraPatterns' => [ 'POST order-sheet/{shipping-number}/{sign}' => 'order-sheet', ], ] ], ], ], ... ];
到這裏爲止,http://www.xxx.com/v1/delivery/order-sheet/sn1001/c0bb9cfe4fdcc5ee0a4237b6601d1df4 已經能夠被請求了,接下來咱們經過Swagger將API接口公佈出來,以便給他人調用。web
"requried": { ... "zircote/swagger-php": "*", // 添加以後應該執行composer update命令安裝該組件 ... }
/** * @SWG\Post(path="/delivery/order-sheet/{shippingNumber}/{sign}", * tags={"Delivery"}, * summary="Sync order sheet result from warehouse to Glitzhome", * description="從倉庫同步發貨結果", * operationId="delivery/order-sheet", * produces={"application/xml", "application/json"}, * @SWG\Parameter( * name="shippingNumber", * in="path", * description="Shipping Number", * required=true, * type="string" * ), * @SWG\Parameter( * name="sign", * in="path", * description="Sign of request parameters", * required=true, * type="string" * ), * @SWG\Parameter( * name="Authorization", * in="header", * description="受權Token,Bearer模式", * required=true, * type="string" * ), * @SWG\Parameter( * in="body", * name="orderSheet", * description="倉庫反饋的Order sheet的結果", * required=true, * type="array", * @SWG\Schema(ref="#/definitions/OrderSheetRequest") * ), * * @SWG\Response(response=200, @SWG\Schema(ref="#/definitions/OrderSheetResponse"), description="successful operation"), * @SWG\Response(response=400,description="Bad request"), * @SWG\Response(response=401,description="Not authorized"), * @SWG\Response(response=404,description="Method not found"), * @SWG\Response(response=405,description="Method not allowed"), * @SWG\Response(response=426,description="Upgrade required"), * @SWG\Response(response=429,description="Rate limit exceeded"), * @SWG\Response(response=499,description="Customized business errors"), * @SWG\Response(response=500,description="Internal Server Error"), * security={ * {"Authorization": {}}, * } * ) * */ public function actionOrderSheet() { ... }
實際使用中,須要經過Swagger Annotation生成完整的swagger.json文件,不然swagger-ui在解析時會出錯而致使沒法生成API文檔。算法
public function actionGenSwg() { $projectRoot = Yii::getAlias('@myapiroot') . '/myapi'; $swagger = \Swagger\scan($projectRoot); $json_file = $projectRoot . '/web/swagger-docs/swagger.json'; $is_write = file_put_contents($json_file, $swagger); if ($is_write == true) { $this->redirect('/swagger-ui/dist/index.html'); } }
... Yii::setAlias('myapiroot', dirname(dirname(__DIR__))); ...
頁面,Swagger-UI將會根據swagger-json文件生成以下界面:
mongodb
咱們本例中使用Rate Limiting進行訪問頻率的限制,假設設置了該API每10秒以內最多訪問2次,若是咱們連續點擊"試一下!"按鈕,則會返回429 Rate limit exceeded錯誤:數據庫
注:因爲代碼是在正式項目中的,所以沒法直接提供完整的源碼,請見諒。
最後附上簽名的算法:
public static function validateSign($parameters, $secretCode) { if (is_array($parameters) && !empty($secretCode)) { // 順序排序 ksort($parameters); // 將 sign 添加到最後 $paramsWithSecret = array_merge($parameters, ["secret" => $secretCode]); // 鏈接成 key1=value&key2=value2....keyN=valueN&secret=secretCode 這樣的格式 $str = implode('&', array_map( function ($v, $k) { return sprintf("%s=%s", $k, json_encode($v)); }, $paramsWithSecret, array_keys($paramsWithSecret) )); // 計算MD5的值 return md5($str); } return ''; }