在現有的Advanced Template上搭建RESTful API項目的步驟:
本案例前提說明:
- 本例中不使用\yii\rest\ActiveController自動創建的API,而是自定義一個API
- 使用Auth2.0的Bearer模式進行身份驗證
- 使用MongoDB作為數據庫,關於如何在Yii2中使用mongodb,請參考其他資料
- 本例中將使用Yii2的RESTful Rate Limiting功能對API進行訪問頻率控制
- 本例使用Swagger-UI生成公開的接口文檔
- 本例中,API的請求將使用秘鑰對請求參數進行簽名,簽名參數sign將作為url的最后一部分,服務端將使用相同的簽名方式進行簽名並匹配sign的值,以確定訪問是否被偽造
創建新項目myapi以及模塊v1的步驟:
Step 1 - 添加一個新的API項目,名為myapi: 參考教程
Step 2 - 創建一個名為v1的Module,創建之后項目結構如下:
注: 本例中models均放在myapi/models/v1下,也可以直接將models放在myapi/modules/v1/models下
Step 3 - 將創建的Module v1 添加到配置文件myapi/config/main.php中:
return [
...
'modules' => [
'v1' => [
'class' => 'myapi\modules\v1\Module'
],
]
...
];
創建數據庫以及ActiveRecord:
本例中,數據庫包含以下兩張表external_api_users(API的用戶表)、external_api_settings(Rate Limiting設置表):
external_api_users數據結構如下:
{
"_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數據結構如下:
{
"_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格式
Step 1 - 創建ExternalApiUser類:
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);
}
...
}
Step 2 - 創建ExternalApiSettings類:
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();
}
}
...
}
Step 3 - 在 \myapi\config\main.php 文件中配置用戶身份認證類為剛才創建的ExternalApiUser類:
return [
...
'components' => [
...
'user' => [
'identityClass' => 'myapi\models\v1\ExternalApiUser',
'enableAutoLogin' => true,
]
...
]
...
];
創建RESTful API:
Step 1 - 在myapi/modules/v1/controllers下創建controller,名為DeliveryController:
// 特別注意的是需要將\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.
*/
...
}
Step 2 - 將DeliveryController的身份驗證模式改為Auth2.0的Bearer模式,並開啟RESTful Rate Limiting功能:
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;
}
...
}
Step 3 - 創建自定義action,名為actionOrderSheet:
public function actionOrderSheet()
{
...
}
Step 4 - 在 \myapi\config\main.php 文件中配置自定義路由:
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接口公布出來,以便給他人調用。
集成Swagger:
Step 1 - 從https://github.com/swagger-api/swagger-ui/releases 下載Swagger-UI,並放到項目web目錄下,同時可以創建一個swagger-docs目錄用以存放swagger.json文件:
Step 2 - 在composer.json的required節點中添加zircote/swagger-php配置:
"requried": {
...
"zircote/swagger-php": "*", // 添加之后應該執行composer update命令安裝該組件
...
}
Step 3 - 用Annotation語法標注actionOrderSheet方法,部分代碼如下:
/**
* @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文檔。
Step 4 - 在SiteController中增加actionGenSwg方法,用來解析Swagger Annotation並生成swagger.json文件:
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');
}
}
Step 5 - 在文件 /myapi/config/bootstrap.php 中定義 ‘@myapiroot’:
...
Yii::setAlias('myapiroot', dirname(dirname(__DIR__)));
...
通過Swagger-UI查看並測試API:
Step 1 - 在瀏覽器中打開 http://www.xxx.com/site/gen-swg
頁面,Swagger-UI將會根據swagger-json文件生成如下界面:
Step 2 - 在參數位置按要求填寫參數,點擊"試一下!"按鈕:
Step 3 - 返回調用結果:
我們本例中使用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 '';
}