• Yii2+Swagger搭建RESTful风格的API项目


    在现有的Advanced Template上搭建RESTful API项目的步骤:

    本案例前提说明:

    • 本例中不使用yii estActiveController自动创建的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' => 'myapimodulesv1Module'
            ],
        ]
        ...
    ];
    

    创建数据库以及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 yiimongodbActiveRecord;
    use yiifiltersRateLimitInterface;
    use yiiwebIdentityInterface;
    
    // 要实现Rate Limiting功能,就需要实现 yiifiltersRateLimitInterface 接口
    class ExternalApiUser extends ActiveRecord implements RateLimitInterface, IdentityInterface 
    {
        ...
        
        public function getRateLimit($request, $action)
        {
            return myapimodelsv1ExternalApiSettings::getRateLimit((string)$this->_id, $action->controller->module->module->requestedRoute);
        }
    
        public function loadAllowance($request, $action)
        {
            return myapimodelsv1ExternalApiSettings::loadAllowance((string)$this->_id, $action->controller->module->module->requestedRoute);
        }
    
        public function saveAllowance($request, $action, $allowance, $timestamp)
        {
            return myapimodelsv1ExternalApiSettings::saveAllowance((string)$this->_id, $action->controller->module->module->requestedRoute, $allowance, $timestamp);
        }
        
        ...
    }
    
    Step 2 - 创建ExternalApiSettings类:
    class ExternalApiSettings extends yiimongodbActiveRecord
    {
        ...
        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 - 在 myapiconfigmain.php 文件中配置用户身份认证类为刚才创建的ExternalApiUser类:
    return [
        ...
        'components' => [
            ...
            'user' => [
                'identityClass' => 'myapimodelsv1ExternalApiUser',
                'enableAutoLogin' => true,
            ]
            ...
        ]
        ...
    ];
    

    创建RESTful API:

    Step 1 - 在myapi/modules/v1/controllers下创建controller,名为DeliveryController:
    // 特别注意的是需要将yiiwebActiveController改为yii
    estActiveController
    class DeliveryController extends yii
    estActiveController
    {
        // $modelClass是yii
    estActiveController必须配置的属性,但是本例中我们不需要使用基于ActiveRecord快速生成的API接口,因此对应$modelClass属性的设置并没什么用处
        public $modelClass = 'myapimodelsv1
    equestdeliveryOrderSheetRequest';
        
        /* 
        yii
    estActiveController会对应于$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
    estActiveController
    {
        ...
        public function behaviors()
        {
            $behaviors = parent::behaviors();
            
            // 身份验证模式改为Auth2.0的Bearer模式
            $behaviors['authenticator'] = [
                'class' => yiifiltersauthHttpBearerAuth::className(),
            ];
    
            // 开启RESTful Rate Limiting功能
            $behaviors['rateLimiter']['enableRateLimitHeaders'] = true;
    
            ...
            
            return $behaviors;
        }
        ...
    }
    
    Step 3 - 创建自定义action,名为actionOrderSheet:
    public function actionOrderSheet()
    {
        ...
    }
    
    Step 4 - 在 myapiconfigmain.php 文件中配置自定义路由:
    return [
        ...
        'components' => [
                'urlManager' => [
                'enablePrettyUrl' => true,
                'enableStrictParsing' => true,
                'showScriptName' => false,
                'rules' => [
                    // 这一条配置是为了生成Swagger.json文档所预留的API,使用的还是基本的yiiwebUrlRule
                    [
                        'class' => 'yiiwebUrlRule',
                        '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
    estUrlRule',
                        '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方法,部分代码如下:

    /**
         * @SWGPost(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"},
         *   @SWGParameter(
         *     name="shippingNumber",
         *     in="path",
         *     description="Shipping Number",
         *     required=true,
         *     type="string"
         *   ),
         *   @SWGParameter(
         *     name="sign",
         *     in="path",
         *     description="Sign of request parameters",
         *     required=true,
         *     type="string"
         *   ),
         *   @SWGParameter(
         *     name="Authorization",
         *     in="header",
         *     description="授权Token,Bearer模式",
         *     required=true,
         *     type="string"
         *   ),
         *   @SWGParameter(
         *     in="body",
         *     name="orderSheet",
         *     description="仓库反馈的Order sheet的结果",
         *     required=true,
         *     type="array",
         *     @SWGSchema(ref="#/definitions/OrderSheetRequest")
         *   ),
         *
         *   @SWGResponse(response=200, @SWGSchema(ref="#/definitions/OrderSheetResponse"), description="successful operation"),
         *   @SWGResponse(response=400,description="Bad request"),
         *   @SWGResponse(response=401,description="Not authorized"),
         *   @SWGResponse(response=404,description="Method not found"),
         *   @SWGResponse(response=405,description="Method not allowed"),
         *   @SWGResponse(response=426,description="Upgrade required"),
         *   @SWGResponse(response=429,description="Rate limit exceeded"),
         *   @SWGResponse(response=499,description="Customized business errors"),
         *   @SWGResponse(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 = Swaggerscan($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 '';
    }
    

    在线参考文档
  • 相关阅读:
    System.Net.Http.HttpClient POST 未能创建 SSL/TLS 安全通道
    SQL Server用户权限查询
    IIS 7 Deploy .Net Framework 4.8 Application
    System.Net.Http.HttpClient 模拟登录并抓取弹窗数据
    HtmlAgilityPack Sample
    嵌套 struct & class 的遍历
    SQL循环插入测试数据
    windows文本转语音 通过java 调用python 生成exe可执行文件一条龙
    Centos8 删除了yum.repos.d 下面的文件
    nacos 配置
  • 原文地址:https://www.cnblogs.com/wushangjue/p/5788117.html
Copyright © 2020-2023  润新知