本案例前提说明:
注: 本例中models均放在myapi/models/v1下,也可以直接将models放在myapi/modules/v1/models下
return [ ... 'modules' => [ 'v1' => [ 'class' => 'myapi/modules/v1/Module' ], ] ... ];
本例中,数据库包含以下两张表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格式
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接口公布出来,以便给他人调用。
"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文件生成如下界面:
我们本例中使用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 ''; }