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阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 1. 微服务架构介绍 1.1 什么是微服务架构? 形像一点来说,微服务架构就像搭积木,每个微服务都是一个零件,并使...
    静修佛缘阅读 6,532评论 0 39
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,359评论 6 343
  • 本文目录:一、单体应用 VS 微服务二、微服务常见安全认证方案三、JWT介绍四、OAuth 2.0 介绍五、思考总...
    挨踢的懒猫阅读 17,871评论 5 29
  • API定义规范 本规范设计基于如下使用场景: 请求频率不是非常高:如果产品的使用周期内请求频率非常高,建议使用双通...
    有涯逐无涯阅读 2,442评论 0 6