Yii2-RESTfulApi實踐

1、了解REST


REST -- Resource REpresentational State Transfer

通俗講:資源在網絡中以某種表現形式進行狀態轉移。

  • (1)、Resource:資源,即數據。
  • (2)、Representational:某種表現形式,比如JSON,XML等。
  • (3)、State Transfer:狀態變化。通過HTTP動詞實現。

REST就是選擇通過http協議和uri,利用client/server model對資源進行CRUD(Create/Read/Update/Delete)增刪改查操作。

REST風格六個限制

  • (1)、客戶-服務器(Client-Server)客戶端服務器分離

    優點:
    提高用戶界面的便攜性(操作簡單)
    通過簡化服務器提高可伸縮性(高性能,低成本)
    允許組件分別優化(可以讓服務端和客戶端分別進行改進和優化)

  • (2)、無狀態(Stateless)

從客戶端的每個請求要包含服務器所需要的所有信息
優點:
提高可見性(可以單獨考慮每個請求)
提高了可靠性(更容易從局部故障中修復)
提高可擴展性(降低了服務器資源使用)

  • (3)、緩存(Cachable)

服務器返回信息必須被標記是否可以緩存,如果緩存,客戶端可能會重用之前的信息發送請求
優點:
減少交互次數
減少交互平均延遲

  • (4)、分層系統(Layered System)

系統組件不需要知道與他交流組件之外的事情。封裝服務,引入中間層。
優點:
限制了系統的復雜性
提高了可擴展性

  • (5)、統一接口(Uniform Interface)

優點:
提高交互的可見性
鼓勵單獨改善組件

  • (6)、支持按需代碼(Code-On-Demand可選)

優點:
提高可擴展性

2、了解RESTful


RESTful(采用REST架構規范的)架構風格規定,數據的元操作,即CRUD(create, read, update和delete,即數據的增刪查改)操作,分別對應于HTTP方法:GET用來獲取資源,POST用來新建資源(也可以用于更新資源),PUT用來更新資源,DELETE用來刪除資源,這樣就統一了數據操作的接口,僅通過HTTP方法,就可以完成對數據的所有增刪查改工作。

HTTP動詞

GET(SELECT):從服務器取出資源(一項或多項)。
POST(CREATE):在服務器新建一個資源。
PUT(UPDATE):在服務器更新資源(客戶端提供改變后的完整資源)。
PATCH(UPDATE):在服務器更新資源(客戶端提供改變的屬性)。
DELETE(DELETE):從服務器刪除資源。
HEAD:獲取資源的元數據。
OPTIONS:獲取信息,關于資源的哪些屬性是客戶端可以改變的。
  • 例如:
GET /zoos:列出所有動物園
POST /zoos:新建一個動物園
GET /zoos/ID:獲取某個指定動物園的信息
PUT /zoos/ID:更新某個指定動物園的信息(提供該動物園的全部信息)
PATCH /zoos/ID:更新某個指定動物園的信息(提供該動物園的部分信息)
DELETE /zoos/ID:刪除某個動物園
GET /zoos/ID/animals:列出某個指定動物園的所有動物
DELETE /zoos/ID/animals/ID:刪除某個指定動物園的指定動物

狀態碼

服務器向用戶返回的狀態碼和提示信息

200: OK。一切正常。
201: 響應 POST 請求時成功創建一個資源。Location header 包含的URL指向新創建的資源。
204: 該請求被成功處理,響應不包含正文內容 (類似 DELETE 請求)。
304: 資源沒有被修改??梢允褂镁彺娴陌姹?。
400: 錯誤的請求??赡芡ㄟ^用戶方面的多種原因引起的,例如在請求體內有無效的JSON 數據,無效的操作參數,等等。
401: 驗證失敗。
403: 已經經過身份驗證的用戶不允許訪問指定的 API 末端。
404: 所請求的資源不存在。
405: 不被允許的方法。 請檢查 Allow header 允許的HTTP方法。
415: 不支持的媒體類型。 所請求的內容類型或版本號是無效的。
422: 數據驗證失敗 (例如,響應一個 POST 請求)。 請檢查響應體內詳細的錯誤消息。
429: 請求過多。 由于限速請求被拒絕。
500: 內部服務器錯誤。 這可能是由于內部程序錯誤引起的。

3、Yii2-RESTful Api


Yii 提供了一整套用來簡化實現 RESTful 風格的 Web Service 服務的 API。 特別是,Yii 支持以下關于 RESTful 風格的 API

  • 支持 Active Record 類的通用API的快速原型
    涉及的響應格式(在默認情況下支持 JSON 和 XML)
  • 支持可選輸出字段的定制對象序列化
  • 適當的格式的數據采集和驗證錯誤
  • 支持 HATEOAS
  • 有適當HTTP動詞檢查的高效的路由
  • 內置OPTIONS和HEAD動詞的支持
  • 認證和授權
  • 數據緩存和HTTP緩存
  • 速率限制

Yii2 中實踐

├─components
│  └─ApiController.php
│
├─models
│      ProjectTeam.php
│      User.php
│      LoginForm.php
│
├─modules
│  └─api
│      │  Module.php
│      │
│      ├─controllers
│            ProjectTeamController.php
│            UserController.php
│      
│                
│          
  • (2)、建立api模塊

修改配置文件(config/web.php)

<?php    
    ......
    'modules' => [
        'api' => [
            'class' => 'api\modules\api\Module',
        ],
    ],
    ......

api模塊文件(modules/api/Module.php)

<?php
/**
 * Restful api 接口模塊
 */
namespace app\modules\api;

use Yii;

class Module extends \yii\base\Module
{
    public $controllerNamespace = 'app\modules\api\controllers';

    public function init()
    {
        parent::init();
        //由于RESTful遵循的是無狀態可將用戶session關閉
        \Yii::$app->user->enableSession = false;
        //關閉登錄失敗跳轉
        \Yii::$app->user->loginUrl = null;
    }
}
  • (3)、配置控制器

Api基類控制器

# components/ApiController.php

<?php
/**
 * Api接口基類
 */
namespace app\components;

use yii\rest\ActiveController;
use Yii;

class ApiController extends ActiveController
{
    public $modelClass = '';
}

// modules/api/controllers/ProjectTeamController.php

<?php
namespace api\modules\api\controllers;

use app\components\ApiController;

class ProjectTeamController extends ApiController
{
    public $modelClass = 'api\models\ProjectTeam';
}
  • (4)、為ProjectTeam配置Url規則
......
'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'enableStrictParsing' =>true,
            'rules' => [
                [
                      'class' => 'yii\rest\UrlRule',
                      'controller' => ['api/project-team'],
                      'pluralize' => false  //不在url鏈接中的project-team后加s 復數
                ],
          ]
]
......
  • (5)、配置請求跟響應(config/web.php)
<?php
......
'components' => [
        'request' => [
            'cookieValidationKey' => 'test',
            'class' => '\yii\web\Request',
            'enableCookieValidation' => false,
            'parsers' => [
                'application/json' => 'yii\web\JsonParser',
            ]
        ],
        'response' => [
            'class' => 'yii\web\Response',
            'on beforeSend' => function ($event) {
                    //restful api
                    $response = $event->sender;
                    $code = $response->getStatusCode();
                    $msg = $response->statusText;
                    if ($code == 404) {
                        !empty($response->data['message']) && $msg = $response->data['message'];
                    }
                    //設置固定返回數據參數
                    $data = [
                        'code' => $code,
                        'msg' => $msg,
                        'data' => $response->data
                    ];
                    $code == 200 && $data['data'] = $response->data;
                    $response->data = $data;
                    $response->format = yii\web\Response::FORMAT_JSON;
            },
        ],
......
]
  • (6)、模擬請求
GET獲取列表
GET /project-team: 逐頁列出所有項目組
HEAD /project-team: 顯示項目組列表的概要信息
POST /project-team: 創建一個新項目組
GET /project-team/2: 返回項目組 2 的詳細信息
PATCH /project-team/2 and PUT /project-team/2: 更新項目組2
DELETE /project-team/2: 刪除項目組2
  • (7)、授權認證

和Web應用不同,RESTful APIs 通常是無狀態的, 也就意味著不應使用sessions 或 cookies, 因此每個請求應附帶某種授權憑證,因為用戶授權狀態可能沒通過sessions 或 cookies維護, 常用的做法是每個請求都發送一個秘密的access token來認證用戶, 由于access token可以唯一識別和認證用戶, API 請求應通過HTTPS來防止man-in-the-middle (MitM) 中間人攻擊.

以下幾種方式來發送access token

  • 1、(HttpBasicAuth)HTTP 基本認證: access token 當作用戶名發送,應用在access token可安全存在API使用端的場景, 例如,API使用端是運行在一臺服務器上的程序。

  • 2、(QueryParamAuth)請求參數: access token 當作API URL請求參數發送,例如 https://example.com/users?access-token=xxxxxxxx , 由于大多數服務器都會保存請求參數到日志, 這種方式應主要用于JSONP 請求,因為它不能使用HTTP頭來發送access token。

  • 3、(HttpBearerAuth)OAuth 2: 使用者從認證服務器上獲取基于 OAuth2協議的access token,然后通過 HTTP Bearer Tokens 發送到API 服務器。

  • (8)、認證類選擇【QueryParamAuth| HttpBearerAuth】

二者的區別:前者通過get方式傳遞token,后者通過header頭傳遞。get傳遞token的方式有一個風險,以nginx為例,實際請求的地址假設是 /controller/action?token=123,那么nginx的access.log就會把這個訪問地址記錄下來,對于有訪問這個日志文件的管理人員而言,顯而易見我們就把用戶的token暴露了,這是非常不好的一件事,所以我們推薦使用header頭進行傳遞。即我們選擇 yii\filters\auth\HttpBearerAuth 作為接收token并校驗。
建議選擇HttpBearerAuth。

  • (9)、改造User模型
# user 表結構:
CREATE TABLE `user` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
  `username` varchar(30) NOT NULL DEFAULT '' COMMENT '用戶名',
  `password_hash ` varchar(150) NOT NULL DEFAULT '' COMMENT '密碼',
  `createtime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '創建時間',
  `updatetime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '更新時間',
  `api_token` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

# 用戶模型
# models/User.php

<?php
namespace app\models;

use Yii;
use yii\db\ActiveRecord;
use yii\behaviors\TimestampBehavior;
use yii\web\IdentityInterface;

class User extends ActiveRecord implements IdentityInterface
{
    public $authKey;
    public $accessToken;

    public function behaviors()
    {
        return [
            [
                'class' => TimestampBehavior::className(),
                'createdAtAttribute' => 'createtime',
                'updatedAtAttribute' => 'updatetime',
                'attributes' => [
                    ActiveRecord::EVENT_BEFORE_INSERT => ['createtime', 'updatetime'],
                    ActiveRecord::EVENT_BEFORE_UPDATE => ['updatetime'],
                ]
            ]
        ];
    }

    //表名
    public static function tableName()
    {
        return "{{%user}}";
    }

    //規則
    public function rules()
    {
        return [
            ['username', 'required', 'message' => '用戶名不能為空'],
            ['api_token', 'required', 'message' => 'api_token不能為空']
        ];
    }

    /**
     * 生成 "remember me" 認證key
     */
    public function generateAuthKey()
    {
        $this->auth_key = Yii::$app->security->generateRandomString();
    }

    /**
     * 生成 api_token
     */
    public function generateApiToken()
    {
        $this->api_token = Yii::$app->security->generateRandomString() . '_' . time();
    }

    /**
     * 校驗api_token是否有效
     */
    public static function apiTokenIsValid($token)
    {
        if (empty($token)) {
            return false;
        }
        $timestamp = (int) substr($token, strrpos($token, '_') + 1);
        $expire = Yii::$app->params['user.apiTokenExpire'];
        return $timestamp + $expire >= time();
    }

    /**
     * 根據api token 獲取用戶
     * @param $token
     * @return array|null|ActiveRecord
     */
    public static function findByApiToken($token)
    {
        return static::find()->where('api_token = :api_token', [':api_token' => $token])->one();
    }

    /**
     * 根據用戶名查詢用戶
     * @param $username
     * @return array|null|ActiveRecord
     */
    public static function findByUsername($username)
    {
        return static::find()->where('username = :username', [':username' => $username])->one();
    }

    /**
     * @inheritdoc
     */
    public static function findIdentity($id)
    {
        return static::findOne(['id' => $id]);
    }

    /**
     * @inheritdoc
     */
    public static function findIdentityByAccessToken($token, $type = null)
    {
        // 如果token無效的話
        if(!static::apiTokenIsValid($token)) {
            throw new \yii\web\UnauthorizedHttpException("token is invalid.");
        }
        return static::findOne(['api_token' => $token]);
    }

    /**
     * @inheritdoc
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @inheritdoc
     */
    public function getAuthKey()
    {
        return $this->authKey;
    }

    /**
     * @inheritdoc
     */
    public function validateAuthKey($authKey)
    {
        return $this->authKey === $authKey;
    }

    /**
     * 為model的password_hash字段生成密碼的hash值
     *
     * @param string $password
     */
    public function setPassword($password)
    {
        $this->password_hash = Yii::$app->security->generatePasswordHash($password);
    }

    /**
     * Validates password
     *
     * @param string $password password to validate
     * @return bool if password provided is valid for current user
     */
    public function validatePassword($password, $password_hash)
    {
        return Yii::$app->security->validatePassword($password, $password_hash);
    }
}
  • (10)、配置登錄認證
# 配置基類控制器
# components/ApiController.php
<?php
/**
 * Api接口基類
 */
namespace app\components;

use yii\rest\ActiveController;
use Yii;

class ApiController extends ActiveController
{
    public $modelClass = '';
    
    public function behaviors()
    {
        $behaviors = parent::behaviors();
        $behaviors['authenticator'] = [
            'class' => HttpBearerAuth::className(),
            'optional' => [
                    'login',  //認證排除登錄接口
                    'reg' //認證排除測試注冊用戶
                ]
        ];
        return $behaviors;
    }

    public function actions()
    {
        $actions =  parent::actions();
        return $actions;
    }

}

# 配置登錄表單
# models/LoginForm.php

<?php
namespace api\models;
use Yii;
use yii\base\Model;
use common\models\User;
/**
 * Login form
 */
class LoginForm extends Model
{
    public $username;
    public $password;
    private $_user;
    const GET_API_TOKEN = 'generate_api_token';
    public function init ()
    {
        parent::init();
        $this->on(self::GET_API_TOKEN, [$this, 'onGenerateApiToken']);
    }
    /**
     * @inheritdoc
     * 對客戶端表單數據進行驗證的rule
     */
    public function rules()
    {
        return [
            [['username', 'password'], 'required'],
            ['password', 'validatePassword'],
        ];
    }
    /**
     * 自定義的密碼認證方法
     */
    public function validatePassword($attribute, $params)
    {
        if (!$this->hasErrors()) {
            $this->_user = $this->getUser();
            if (!$this->_user || !$this->_user->validatePassword($this->password)) {
                $this->addError($attribute, '用戶名或密碼錯誤.');
            }
        }
    }
    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'username' => '用戶名',
            'password' => '密碼',
        ];
    }
    /**
     * Logs in a user using the provided username and password.
     *
     * @return boolean whether the user is logged in successfully
     */
    public function login()
    {
        if ($this->validate()) {
            $this->trigger(self::GET_API_TOKEN);
            return $this->_user;
        } else {
            return null;
        }
    }
    /**
     * 根據用戶名獲取用戶的認證信息
     *
     * @return User|null
     */
    protected function getUser()
    {
        if ($this->_user === null) {
            $this->_user = User::findByUsername($this->username);
        }
        return $this->_user;
    }
    /**
     * 登錄校驗成功后,為用戶生成新的token
     * 如果token失效,則重新生成token
     */
    public function onGenerateApiToken ()
    {
        if (!User::apiTokenIsValid($this->_user->api_token)) {
            $this->_user->generateApiToken();
            $this->_user->save(false);
        }
    }
}

# 配置用戶控制器
# modules/api/controllers/UserController.php

<?php
namespace app\modules\api\controllers;

use app\components\ApiController;
use Yii;

class UserController extends ApiController
{
    public $modelClass = 'app\models\User';
     
    //注冊-測試用
    public function actionReg()
    {
        $user = new User();
        $user->generateAuthKey();
        $user->setPassword('123456');
        $user->username = 'test';
        $user->save(false);
        return [
            'code' => 0
        ];
  }    

    //登錄
    public function actionLogin ()
    {
        $model = new LoginForm;
        $model->setAttributes(Yii::$app->request->post());
        if ($user = $model->login()) {
            return $user->api_token;
        } else {
            return $model->errors;
        }
    }
}

# 配置url路由
# config/web.php

<?php
......
'components' => [
......
'urlManager' => [
   'enablePrettyUrl' => true,
   'showScriptName' => false,
   'enableStrictParsing' =>true,
   'rules' => [
      [
        'class' => 'yii\rest\UrlRule',
        'controller' => ['api/project-team'],
        'pluralize' => false  //不在url鏈接中的project-team后加s 復數
      ],
      [
        'controller' => ['api/user'],
        'class' => 'yii\rest\UrlRule',
        'pluralize' => false,
        'extraPatterns' => [
            'POST login' => 'login',
            'GET  reg' => 'reg'
        ],
      ],
    ],
]
......
],
'params' => [
       // token 有效期默認1天,可以按照自己的項目需求配置
      'user.apiTokenExpire' => 1*24*3600,      
]
......
  • (11)、登錄獲取token
POST  /api/user/login

{
    "code": 200,
    "msg": "OK",
    "data": "6GA0kFMIJt7Wm4zae-6BRo5bDcCgsRhl_1503122875"
}

將獲取到的token保存起來,用于請求其他接口的認證
yii\filters\auth\HttpBearerAuth 類是從header頭的Authorization這個key中進行獲取的,其格式如下

Authorization: Bearer your-token
  • (12)、獲取用戶信息
# modules/api/controllers/UserController.php

    //獲取用戶信息
    public function actionInfo()
    {
        $user = $this->authenticate(Yii::$app->user, Yii::$app->request, Yii::$app->response);
        return $user;
    }

# 配置用戶信息路由
# config/web.php

......
[
        'controller' => ['api/user'],
        'extraPatterns' => [
            'POST login' => 'login',
            'GET  reg' => 'reg',
            //獲取用戶信息
            'GET info' => 'info',
        ],
]
......

請求

獲取用戶信息
  • (13)、接口速率限制
# 用戶表添加字段

allowance:剩余的允許的請求數量
allowance_updated_at:相應的UNIX時間戳數

alter table `user` add `allowance` int(11) DEFAULT '0';
alter table `user` add  `allowance_updated_at` int(11) DEFAULT '0';

# 修改User模型

<?php
/**
 * 用戶模型
 */
namespace app\models;

use Yii;
use yii\db\ActiveRecord;
use yii\behaviors\TimestampBehavior;
use yii\web\IdentityInterface;
use yii\filters\RateLimitInterface;

class User extends ActiveRecord implements IdentityInterface, RateLimitInterface
{
    public $authKey;
    public $accessToken;

    public function behaviors()
    {
        return [
            [
                'class' => TimestampBehavior::className(),
                'createdAtAttribute' => 'createtime',
                'updatedAtAttribute' => 'updatetime',
                'attributes' => [
                    ActiveRecord::EVENT_BEFORE_INSERT => ['createtime', 'updatetime'],
                    ActiveRecord::EVENT_BEFORE_UPDATE => ['updatetime'],
                ]
            ]
        ];
    }

    # 速度控制  2秒內訪問3次,注意,數組的第一個不要設置1,設置1會出問題,一定要
    #大于2,譬如下面  2秒內只能訪問三次
    # 文檔標注:返回允許的請求的最大數目及時間,例如,[100, 600] 表示在600秒內最多100次的API調用。
    public  function getRateLimit($request, $action){
        return [3, 2];
    }

    # 文檔標注: 返回剩余的允許的請求和相應的UNIX時間戳數 當最后一次速率限制檢查時。
    public  function loadAllowance($request, $action){
        //return [1,strtotime(date("Y-m-d H:i:s"))];
        //echo $this->allowance;exit;
        return [$this->allowance, $this->allowance_updated_at];
    }

    # allowance 對應user 表的allowance字段  int類型
    # allowance_updated_at 對應user allowance_updated_at  int類型
    # 文檔標注:保存允許剩余的請求數和當前的UNIX時間戳。
    public  function saveAllowance($request, $action, $allowance, $timestamp){
        $this->allowance = $allowance;
        $this->allowance_updated_at = $timestamp;
        $this->save();
    }

    //表名
    public static function tableName()
    {
        return "{{%user}}";
    }

    //規則
    public function rules()
    {
        return [
            ['username', 'required', 'message' => '用戶名不能為空'],
            ['display_name', 'required', 'message' => '顯示名不能為空'],
            ['dn', 'required', 'message' => '顯示名不能為空'],
            ['api_token', 'required', 'message' => 'api_token不能為空']
        ];
    }

    /**
     * 生成 "remember me" 認證key
     */
    public function generateAuthKey()
    {
        $this->auth_key = Yii::$app->security->generateRandomString();
    }

    /**
     * 生成 api_token
     */
    public function generateApiToken()
    {
        $this->api_token = Yii::$app->security->generateRandomString() . '_' . time();
    }

    /**
     * 校驗api_token是否有效
     */
    public static function apiTokenIsValid($token)
    {
        if (empty($token)) {
            return false;
        }
        $timestamp = (int) substr($token, strrpos($token, '_') + 1);
        $expire = Yii::$app->params['user.apiTokenExpire'];
        return $timestamp + $expire >= time();
    }

    /**
     * 根據api token 獲取用戶
     * @param $token
     * @return array|null|ActiveRecord
     */
    public static function findByApiToken($token)
    {
        return static::find()->where('api_token = :api_token', [':api_token' => $token])->one();
    }

    /**
     * 根據用戶名查詢用戶
     * @param $username
     * @return array|null|ActiveRecord
     */
    public static function findByUsername($username)
    {
        return static::find()->where('username = :username', [':username' => $username])->one();
    }

    /**
     * @inheritdoc
     */
    public static function findIdentity($id)
    {
        return static::findOne(['id' => $id]);
    }

    /**
     * @inheritdoc
     */
    public static function findIdentityByAccessToken($token, $type = null)
    {
        // 如果token無效的話
        if(!static::apiTokenIsValid($token)) {
            throw new \yii\web\UnauthorizedHttpException("token is invalid.");
        }
        return static::findOne(['api_token' => $token]);
    }

    /**
     * @inheritdoc
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @inheritdoc
     */
    public function getAuthKey()
    {
        return $this->authKey;
    }

    /**
     * @inheritdoc
     */
    public function validateAuthKey($authKey)
    {
        return $this->authKey === $authKey;
    }

    /**
     * 為model的password_hash字段生成密碼的hash值
     *
     * @param string $password
     */
    public function setPassword($password)
    {
        $this->password_hash = Yii::$app->security->generatePasswordHash($password);
    }

    /**
     * Validates password
     *
     * @param string $password password to validate
     * @return bool if password provided is valid for current user
     */
    public function validatePassword($password, $password_hash)
    {
        return Yii::$app->security->validatePassword($password, $password_hash);
    }
}

# 修改Api基類
# components/ApiController.php

use yii\filters\RateLimiter;
use yii\filters\auth\HttpBearerAuth;

public function behaviors()
{
        $behaviors = parent::behaviors();

        $behaviors['authenticator'] = [
            'class' => HttpBearerAuth::className(),
            'optional' => [
                    'login',
                    'reg'
            ],
        ];

        # rate limit部分,速度的設置是在
        #   app\models\User::getRateLimit($request, $action)
        /*  官方文檔:
            當速率限制被激活,默認情況下每個響應將包含以下HTTP頭發送 目前的速率限制信息:
            X-Rate-Limit-Limit: 同一個時間段所允許的請求的最大數目;
            X-Rate-Limit-Remaining: 在當前時間段內剩余的請求的數量;
            X-Rate-Limit-Reset: 為了得到最大請求數所等待的秒數。
            你可以禁用這些頭信息通過配置 yii\filters\RateLimiter::enableRateLimitHeaders 為false, 就像在上面的代碼示例所示。
        */
        $behaviors['rateLimiter'] = [
            'class' => RateLimiter::className(),
            'enableRateLimitHeaders' => true,
        ];

        return $behaviors;
}

# 請求過多響應如下:
{
  "name": "Too Many Requests",
  "message": "Rate limit exceeded.",
  "code": 0,
  "status": 429,
  "type": "yii\\web\\TooManyRequestsHttpException"
}

QA:

  • (1)、跨域請求

# 修改Api基類控制器
use yii\filters\RateLimiter;
use yii\filters\auth\HttpBearerAuth;
use yii\filters\Cors;

    public function behaviors()
    {
        $behaviors = parent::behaviors();

        unset($behaviors['authenticator']);
        /*
        取消默認authenticator認證,以確保 cors 被首先處理。然后,我們在實施自己的認證程序之前,強制 cors 允許憑據。
        */

        //設置跨域
        $behaviors['corsFilter'] = [
            'class' => Cors::className(),
            'cors' => [
                'Origin' => ['*'],
                'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
                'Access-Control-Request-Headers' => ['*'],
                'Access-Control-Allow-Credentials' => true,
            ],
        ];

        $behaviors['authenticator'] = [
            'class' => HttpBearerAuth::className(),
            'optional' => [
                    'login',
                    'reg'
            ],
        ];

        $behaviors['rateLimiter'] = [
            'class' => RateLimiter::className(),
            'enableRateLimitHeaders' => true,
        ];

        return $behaviors;
    }
  • (2)、ajax跨域請求提示404

瀏覽器自動在跨域的 GET 請求發送之前發送一個 OPTIONS 請求,以判斷服務端是否允許這一域訪問。

# 一般規則是當您要求您的瀏覽器執行一個HTTP的動詞,例如PUT,DELETE或POST時,您可以在瀏覽器的網絡標簽中檢查該列表。到任何網址,它可能首先向該相同的網址發送OPTIONS請求。

# 例如獲取用戶信息接口  修改為:

[
        'controller' => ['api/user'],
        'class' => 'yii\rest\UrlRule',
        'pluralize' => false,
        'extraPatterns' => [
            'POST login' => 'login',
            'GET  reg' => 'reg',
            //支持OPTIONS請求
            'GET,OPTIONS info' => 'info',
        ],
],
  • (3)、ajax請求接口時出現 401授權驗證問題

# 修改Api基類控制器

use yii\filters\RateLimiter;
use yii\filters\auth\HttpBearerAuth;
use yii\filters\Cors;

    public function behaviors()
    {
        $behaviors = parent::behaviors();

        unset($behaviors['authenticator']);

        $behaviors['corsFilter'] = [
            'class' => Cors::className(),
            'cors' => [
                'Origin' => ['*'],
                'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
                'Access-Control-Request-Headers' => ['*'],
                'Access-Control-Allow-Credentials' => true,
            ],
        ];

        $behaviors['authenticator'] = [
            'class' => HttpBearerAuth::className(),
            'optional' => [
                    'login',
                    'reg'
            ],
            'except'=> ['options'] //認證排除OPTIONS請求
        ];

        $behaviors['rateLimiter'] = [
            'class' => RateLimiter::className(),
            'enableRateLimitHeaders' => true,
        ];

        return $behaviors;
    }


    public function actions()
    {
        $actions =  parent::actions();
        
        //設置固定options控制器
        $actions['options'] = [
            'class' => 'yii\rest\OptionsAction',
            // optional:
            'collectionOptions' => ['GET', 'POST', 'HEAD', 'OPTIONS'],
            'resourceOptions' => ['GET', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
        ];
        return $actions;
    }

優化的路由配置文件、Api基類

# config/url-rules.php

<?php
/**
 * 路由規則
 */
//基礎路由
$baseRuleConfigs = [
    
];

//api接口路由
$apiRuleConfigs = [
    //例如用戶接口
    [
        'controller' => ['api/user'],
        'extraPatterns' => [
            'POST login' => 'login',
            'GET  reg' => 'reg',
            //獲取
            'GET info' => 'info',
        ],
    ],
    //例如項目組
    [
         'controller' => ['api/project-team']
    ],
  
];

/**
 * 基礎的api url規則配置
 */
$apiUrls = array_map(function($unit)
{
    $urlRule = $unit;
    //防止默認options控制器被屏蔽
    if(isset($unit['only'])&&!empty($unit['only'])&&!in_array('options', $unit['only'])){
        $urlRule['only'][] = 'options';
    }
    if(isset($unit['except'])&&!empty($unit['except'])&&in_array('options', $unit['except'])){
        $urlRule['except'] = array_merge(array_diff($unit['except'], ['options']));
    }
    //由于ajax設置請求頭后,會有一次options請求,默認為所有路由添加支持options請求
    if(isset($unit['extraPatterns'])&&!empty($unit['extraPatterns'])){
        foreach ($unit['extraPatterns'] as $key => $val)
        {
            if(!is_numeric(strpos($key, 'OPTIONS'))){
                //判斷是否有空格符
                if(is_numeric(strpos($key, ' '))){
                    //存在
                    $tmp = explode(' ', $key);
                    $k = str_replace($tmp[0], 'OPTIONS', $key);
                    $urlRule['extraPatterns'][$k] = 'options';
                } else {
                    //不存在
                    $urlRule['extraPatterns']['OPTIONS'] = 'options';
                }
            }
        }
    }
    if(isset($unit['patterns'])&&!empty($unit['patterns'])) {
        foreach ($unit['patterns'] as $key => $val) {
            if (!is_numeric(strpos($key, 'OPTIONS'))) {
                //判斷是否有空格符
                if (is_numeric(strpos($key, ' '))) {
                    //存在
                    $tmp = explode(' ', $key);
                    $k = str_replace($tmp[0], 'OPTIONS', $key);
                    $urlRule['patterns'][$k] = 'options';
                } else {
                    //不存在
                    $urlRule['patterns']['OPTIONS'] = 'options';
                }
            }
        }
    }

    $config = [
        'class' => 'yii\rest\UrlRule',
        'pluralize' => false
    ];
    return array_merge($config, $urlRule);
}, $apiRuleConfigs);

//合并整個項目路由
return array_merge($baseRuleConfigs, $apiUrls);

//路由規則配置
'rules' => require(__DIR__ . '/url-rules.php')

<?php
/**
 * Api接口基類
 */
namespace app\components;

use yii\rest\ActiveController;
use yii\filters\auth\HttpBearerAuth;
use yii\filters\RateLimiter;
use yii\filters\Cors;

class ApiController extends ActiveController
{
    public $modelClass = '';
    public $optional = [
//        'options'
    ];
    //重寫動作
    public $rewriteActions = [
        'update',
        'delete',
        'view',
        'create',
        'index',
//        'options' //默認支持OPTIONS請求
    ];

    public function behaviors()
    {
        $behaviors = parent::behaviors();

        unset($behaviors['authenticator']);

        $behaviors['corsFilter'] = [
            'class' => Cors::className(),
            'cors' => [
                'Origin' => ['*'],
                'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
                'Access-Control-Request-Headers' => ['*'],
                'Access-Control-Allow-Credentials' => true,
            ],
        ];

        $behaviors['authenticator'] = [
            'class' => HttpBearerAuth::className(),
            'optional' => $this->optional,
            'except'=> ['options'] //認證排除OPTIONS請求
        ];

        # rate limit部分,速度的設置是在
        #   app\models\User::getRateLimit($request, $action)
        /*  官方文檔:
            當速率限制被激活,默認情況下每個響應將包含以下HTTP頭發送 目前的速率限制信息:
            X-Rate-Limit-Limit: 同一個時間段所允許的請求的最大數目;
            X-Rate-Limit-Remaining: 在當前時間段內剩余的請求的數量;
            X-Rate-Limit-Reset: 為了得到最大請求數所等待的秒數。
            你可以禁用這些頭信息通過配置 yii\filters\RateLimiter::enableRateLimitHeaders 為false, 就像在上面的代碼示例所示。
        */
        $behaviors['rateLimiter'] = [
            'class' => RateLimiter::className(),
            'enableRateLimitHeaders' => true,
        ];
        return $behaviors;
    }

    public function actions()
    {
        $actions =  parent::actions();
        //判斷是否需要重寫的控制器
        if(!empty($this->rewriteActions)){
            foreach ($this->rewriteActions as $actionKey)
            {
                if(isset($actions[$actionKey])&&$actionKey!='options') unset($actions[$actionKey]);
            }
        }
        //設置固定options控制器
        $actions['options'] = [
            'class' => 'yii\rest\OptionsAction',
            // optional:
            'collectionOptions' => ['GET', 'POST', 'HEAD', 'OPTIONS'],
            'resourceOptions' => ['GET', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
        ];
        return $actions;
    }

}

參考:

1、Yii2文檔
2、restful_api
3、CORS跨域

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,775評論 18 139
  • 1. 微服務架構介紹 1.1 什么是微服務架構? 形像一點來說,微服務架構就像搭積木,每個微服務都是一個零件,并使...
    靜修佛緣閱讀 6,658評論 0 39
  • Spring Boot 參考指南 介紹 轉載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,887評論 6 342
  • 本文目錄:一、單體應用 VS 微服務二、微服務常見安全認證方案三、JWT介紹四、OAuth 2.0 介紹五、思考總...
    挨踢的懶貓閱讀 17,995評論 5 29
  • API定義規范 本規范設計基于如下使用場景: 請求頻率不是非常高:如果產品的使用周期內請求頻率非常高,建議使用雙通...
    有涯逐無涯閱讀 2,571評論 0 6