Yii2+Swagger搭建RESTful風格的API項目

在現有的Advanced Template上搭建RESTful API項目的步驟:

本案例前提說明:javascript

  • 本例中不使用\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下php

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設置表):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

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接口公佈出來,以便給他人調用。web

集成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文件生成以下界面:
mongodb

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 '';
}

在線參考文檔
相關文章
相關標籤/搜索