关于“yii2作为微框架”理解

濮阳霄
2023-12-01

看了  https://www.yiiframework.com/doc/guide/2.0/zh-cn/tutorial-yii-as-micro-framework

有必要自己理解一遍该部分知识

mkdir  gxyx_dv2018

cd gxyx_dv2018

mkdir  api

cd  api  用api目录作为后端RESTful API项目的根目录

在api目录创建 composer.json 内容如下(我们用了composer中国全量镜像):

{
  "require": {
    "yiisoft/yii2": "~2.0.0"
  },
  "repositories": [
    {
      "type": "composer",
      "url": "https://packagist.phpcomposer.com"
    }
  ]

}

composer  install 根据composer.json定义安装框架及其依赖项(可能需要先composer global require "fxp/composer-asset-plugin")

mkdir  web

在 web 目录下,用PhpStorm创建index.php(入口文件)内容如下(其实components部分是后来添加的):

 

<?php

defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');

require(__DIR__ . '/../vendor/autoload.php');
require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');

$config = require __DIR__ . '/../config.php';
(new yii\web\Application($config))->run();

在 api 目录下,创建 config.php (配置文件)内容如下:

 

<?php

return [
    'id' => 'gxyx-dv2018',
    'basePath' => __DIR__,
    'controllerNamespace' => 'gxyx\controllers',
    'aliases' => [
        '@gxyx' => __DIR__,
    ],
    'components' => [
        'db' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'sqlite:@gxyx/db_sqlite/database.sqlite',   //sqlite 格式用来演示
        ],
        /*
        'urlManager' => [
            'enablePrettyUrl' => true,
            'enableStrictParsing' => true,
            'showScriptName' => false,
        ],
        */

        'request' => [
            'parsers' => [
                'application/json' => 'yii\web\JsonParser'
            ]
        ]

    ]
];

mkdir  controllers  (api目录下)

在 controllers目录下,创建 SiteController.php 文件,内容如下:(这个 controller,是普通的 yii\web\Controller)

 

<?php

namespace gxyx\controllers;

use yii\web\Controller;

class SiteController extends Controller
{
    public function actionIndex()
    {
        return 'Hello gxyx_dv2018 api !';
    }
}

浏览器访问 http://phpstorm.localhost/gxyx_dv2018/api/web/ ,能够看到 Hello 了

在 config.php 中添加 components 的 db 部分(前面已经写了)

执行以下迁移来创建 post 表

 

vendor/bin/yii migrate/create --appconfig=config.php create_post_table --fields="title:string,body:text"
vendor/bin/yii migrate/up --appconfig=config.php

mkdir  models

在 models 目录下,创建 模型文件 Post.php,内容如下:

 

<?php

namespace gxyx\models;

use yii\db\ActiveRecord;

class Post extends ActiveRecord
{
    public static function tableName()
    {
        return '{{post}}';
    }
}

在 controllers 目录下,创建控制器 PostController.php,内容如下:(这个controller 是 yii\rest\ActiveController)

 

<?php

namespace gxyx\controllers;

use yii\rest\ActiveController;

class PostController extends ActiveController
{
    public $modelClass = 'gxyx\models\Post';

    public function behaviors()
    {
        $behaviors =  parent::behaviors();
        unset($behaviors['rateLimiter']);
        return $behaviors;
    }
}

在 config.php 文件中启用 JSON 输入(前面文件中 components 的 request 下 parsers 部分,API将对json格式输入进行解析)

PhpStorm中,右键点击 db_sqlite/database.sqlite 文件,选择 As Data Source,添加一个 SQLite类型的数据源(可以测试一下Connection),打开这个数据源的schemas,双击打开 post 表,添加一行数据(点DB向上箭头这个按钮保存)

这时,我们可以用  curl -i -H "Accept:application/json" "http://phpstorm.localhost/gxyx_dv2018/api/web/index.php?r=post"  来测试API(查看post列表,返回结果为JSON格式)

curl -i -H "Accept:application/json" "http://phpstorm.localhost/gxyx_dv2018/api/web/index.php?r=post/view&id=1"   查看id为1的记录

如果把头部从 Accept:application/json 改为 Accept:application/xml,则返回的结果就是xml格式的

在 Linux 下,直接用 curl 试图往这个SQLite型数据里写入数据会失败,参考了有关文章,让这个数据库放在独立的目录db_sqlite中,然后 sudo chmod 0777 -R  db_sqlite 即可(SQLite写入时要在相同目录下创建临时文件!我们这里用 0777 是为了省事,反正是演示,标准做法是让数据库进程对该目录可写)

curl -i -H "Accept:application/json" -H "Content-Type:application/json" -X POST "http://phpstorm.localhost/gxyx_dv2018/api/web/index.php?r=post/create" -d '{"title": "example title", "body": "Here is the example body"}'

上述操作后,记录新增成功!但是,发现title和body是空的!

为了操作方便,我们使用PhpStorm的REST Client来测试修改功能(选择 Tools 下 Test RESTful Web Service打开此客户端)

因为修改记录要用PUT动词,所以HTTP method选择PUT,Host/port设置为 http://phpstorm.localhost,Path设置为/gxyx_dv2018/api/web/index.php,头部Headers部分,保留 Cache-Control为no-cache,修改 Accept 为 application/json,添加 Content-Type为 application/json,请求参数 Request Parameters 添加 r=post/update,id=4,XDEBUG_SESSION_START=post_update(这个参数是为了调试), 请求主体 Request Body 中设置 Text(点右边按钮输入更方便)为 {   "title":"titleAAA",   "body":"bodyAAA" } ,点击绿色三角形按钮请求,结果没法修改成功

查看源码 yii2/rest/ActiveController.php,发现对于 update,只是在 ActiveController 类的 actions() 中指定了 update 动作 对应 yii\rest\UpdateAction 类

 

'update' => [
    'class' => 'yii\rest\UpdateAction',
    'modelClass' => $this->modelClass,
    'checkAccess' => [$this, 'checkAccess'],
    'scenario' => $this->updateScenario,
],

定位到 yii2/rest/UpdateAction.php 文件中 run() 处,添加代码查看请求是否获得了正确数据 (下面的 // 注释掉的部分)

  $model->scenario = $this->scenario; //$body = Yii::$app->getRequest()->getBodyParams();

 

$model->load(Yii::$app->getRequest()->getBodyParams(), '');
if ($model->save() === false && !$model->hasErrors()) {
    throw new ServerErrorHttpException('Failed to update the object for unknown reason.');
}

发现 $body 获得了正常的结果(请求的JSON主体解析得到的关联数组),但$model->load(...) 没有使$model的$attributes中title和body的值发生变化。

这时,突然想起是不是验证规则缺失? 模型类 Post 是继承自 yii\db\ActiveRecord 的,这一点和普通web应用是一样的,所以,缺失验证规则必然导致有关属性的值不能被保存! Post 类中添加如下代码问题解决。

 

public function rules()
{
    return [
        [['title', 'body'], 'string']
    ];
}

测试了其他方式,就是index和view用GET方法,create用POST方法,update用PUT方法,delete用DELETE方法,具体的定义是在 yii\rest\ActiveController类的verbs() 中定义的

 

protected function verbs()
{
    return [
        'index' => ['GET', 'HEAD'],
        'view' => ['GET', 'HEAD'],
        'create' => ['POST'],
        'update' => ['PUT', 'PATCH'],
        'delete' => ['DELETE'],
    ];
}

仔细观察 yii2/rest目录下的文件,文件不多,就是ActiveController及其基类Controller(从\yii\web\Controller继承),一堆XxxAction类和它们的基类Action(从\yii\base\Action继承),序列化用的Serializer类(从yii\base\Component继承),Url规则定义类UrlRule(从yii\web\CompositeUrlRule继承),跟 yii\web\... 无关的是 Action 和 Serializer,这应该是和一般web应用更值得区分的地方。

问题:create的时候,主体是JSON格式数据,在 $model->load(...)时需要关联数组形式,这个转换是什么时候发生的?

我们发现,$model->load(...) 内的 ... 部分是 Yii::$app->getRequest()->getBodyParams()(同时form名字为空),定位到 yii\web\Application类,发现其 getRequest() 方法就是返回 request 组件,所以,查看config.php中 request 的配置,其中包含了 parsers 参数(对 application/json 数据用 yii\web\JsonParser 解析,默认应该是对 urlencode 的字符串进行解析得到关联数组),定位到 yii\web\Request 类的 getBodyParams()的文档说明,果然,它利用了 parsers 中定义的信息进行工作,返回的是 array,至于 yii\web\JsonParser 如何干活,它基本上就是 Json::decode(...raw body...)

问题:API返回的,总是JSON或者XML,这个转换什么时候完成?

这个奥秘要看 ActiveController 的基类 yii\rest\Controller类,它有一个 afterAction,如下:

 

public function afterAction($action, $result)
{
    $result = parent::afterAction($action, $result);
    return $this->serializeData($result);
}

需要更多细节,追踪 serializeData 方法即可。

问题:要自己写一个新的action,如何操作?

我们先来做一个小实验,将 ViewAction 的 run($id) 部分代码最后的返回语句修改一下:

 

public function run($id)
    {
        $model = $this->findModel($id);
        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id, $model);
        }
return ['key1' => 'v1', 'key2' => 'vv2'];
        //return $model;
    }

然后 请求 id=1 的 post,返回结果 

{"key1":"v1","key2":"vv2"}

所以,我们只要模仿已有的 XxxAction 类的写法,确保 return 的值即可(当然,控制器内需要类似的一些设置)

 

总的看起来,RESTful API的应用,是控制器层比一般web应用更瘦的应用。接下来,需要解决的问题是身份验证和访问控制。

先解决一下Pretty URL,在 httpd-vhosts.conf 配置vhost如下:

<VirtualHost *:2018>
    DocumentRoot "/home/x201/PhpstormProjects/gxyx_dv2018"
    ServerName gxyx.localhost


    # LogLevel alert rewrite:trace3
</VirtualHost>


<Directory "/home/x201/PhpstormProjects/gxyx_dv2018">
    Options Indexes FollowSymLinks ExecCGI Includes
    AllowOverride All
    Require all granted

</Directory>

因为我们要重写URL,所以,gxyx_dv2018目录必须有选项 Indexes 和 FollowSymLinks,AllowOverride和Require也要设置,然后在gxyx_dv2018目录下放上.htaccess文件,内容如下:

 

<IfModule mod_rewrite.c>
    RewriteEngine On

    # the main rewrite rule for the frontend application
    RewriteCond %{REQUEST_URI} !^/(api/web|api)
    RewriteRule !^frontend/web /frontend/web%{REQUEST_URI} [L]

    # if a directory or a file of the frontend application exists, use the request directly
    RewriteCond %{REQUEST_URI} ^/frontend/web
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    # otherwise forward the request to index.php
    RewriteRule . /frontend/web/index.php [L]

    # the main rewrite rule for the api application
    RewriteCond %{REQUEST_URI} ^/api
    RewriteCond %{REQUEST_URI} !^/api/web
    RewriteRule ^api(.*) /api/web/index.php$1  [L]
    # if a directory or a file of the api application exists, use the request directly
    RewriteCond %{REQUEST_URI} ^/api/web
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    # otherwise forward the request to index.php
    RewriteRule . /api/web/index.php [L]

    RewriteCond %{REQUEST_URI} \.(htaccess|htpasswd|svn|git)
    RewriteRule \.(htaccess|htpasswd|svn|git) - [F]
</IfModule>

对于重写规则,大致如下:不是/api/web,/api开头的,认为是前端请求,所有非frontend/web开头的,都改写为/frontend/web+原始URI。如果是/frontend/web开头,请求的文件名不是文件或目录,直接将当前URI改写为/frontend/web/index.php。是/api开头,但不是/api/web开头,将“api+后续URI”改写为/api/web/index.php+后续URI。如果是/api/web开头,请求的文件名不是文件或目录,直接将当前URI改写为/api/web/index.php。

Yii2配置文件 config.php 中启用urlManager组件,并且配置为:

 

'urlManager' => [
    'enablePrettyUrl' => true,
    'enableStrictParsing' => true,
    'showScriptName' => false,
    'rules' => [
        ['class' => 'yii\rest\UrlRule', 'controller' => 'post'],
    ]
],

这样之后,PhpStorm的REST client中,就可以将 Path 改为 /api/posts(列表和创建都是这个路径,只是HTTP method分别是GET和POST),/api/posts/1(查看、修改和删除都是这个路径,只是HTTP method分别是GET、PUT或PATCH、DELETE)

我们将默认的 SiteController 改造为可以作出RESTful响应的控制器,修改 SiteController.php 如下:

 

<?php

namespace gxyx\controllers;

use yii\rest\ActiveController;

class SiteController extends ActiveController
{
    public $modelClass = 'gxyx\models\Post';  //该属性必须设置!这里借用Post

    public function behaviors()
    {
        $behaviors =  parent::behaviors();
        unset($behaviors['rateLimiter']);
        return $behaviors;
    }

    public function actions()
    {
        return [
            'index' => [
                'class' => 'gxyx\controllers\SiteIndexAction',
                'modelClass' => $this->modelClass,
                'checkAccess' => [$this, 'checkAccess'],
            ]
        ];
    }

}

注意:必须设定 $modelClass(模型类代表着“资源”,它一般继承自yii\base\Model或者它的子类如yii\db\ActiveRecord,如果不继承任何类,它返回所有公开成员),并且不能为null值,另外,behaviors中如果不 unset 掉 rateLimiter,那就必须先通过用户验证的,我们这里将它去掉了。我们覆盖了基类 ActiveController 类的actions方法,设定对 index 动作作出响应的是自定义的 SiteIndexAction(同样放在controllers目录下,用前缀Site来识别对应的控制器)。

SiteIndexAction.php文件如下:

 

<?php

namespace gxyx\controllers;

use yii\rest\Action;

class SiteIndexAction extends Action
{
    public function run()
    {
        return ['help' => 'This is a API for gxyx web APP'];
    }
}

(2021.1.28注:如果某个action局限于当前controller,并且不复杂,那么没必要单独写一个XxxAction类来响应它,直接在controller内部写public function actionXxx(...) { ... } 返回数组或值即可。对于预定义的index/view/create/update/delete/options这些动作,因为yii\rest\ActiveController已经指定用预定义的一些XxxAction类来处理,没必要自己再去写一个public function actionXxx(...) { ... },即使写了也是无效的,如果期望覆盖,只能重写actions()方法。对于非预定义的动作,即自定义动作,如果直接用controller内部的方法public function actionXxx(...) { ... } 实现,那么必须在应用路由配置中用 extraPatterns 添加自定义动作)

作了上述工作后,还差一步,就是请求路径为 /api/sites 时,能够路由到 SiteIndexAction 给出的响应。修改配置文件config.php的路由部分(只对index进行响应):

 

'urlManager' => [
    'enablePrettyUrl' => true,
    'enableStrictParsing' => true,
    'showScriptName' => false,
    'rules' => [
        ['class' => 'yii\rest\UrlRule', 'controller' => 'post'],
        ['class' => 'yii\rest\UrlRule', 'controller' => 'site', 'only' => ['index']]
    ]
],

概括一下流程:路由设置中控制器site只响应index动作,当请求路径为 /api/sites 时,控制器site查询actions,发现应该用SiteIndexAction类负责响应 index 动作,SiteIndexAction类的run方法中,只是简单回复一个字符串进行响应。

 

是时候来理解一下 yii2 对 RESTful API支持的那些特性了

“资源”差不多相当于模型,可以覆盖模型的fields()和extraFields(),fields方法可以增加、删除、重命名、重定义需要返回的字段(对基本模型类,fields默认返回模型的所有属性作为字段;对AR类,返回从数据库计算而得的属性作为字段),AR类的extraFields方法返回数据表关联的关系(一般指对应的值是一个对象的字段,而基本模型类中extraFields返回空值)。在请求时,用参数fields和expand来对应fields()和extraFields(),如http://localhost/users?fields=id,email&expand=profile

资源类实现对HATEOAS支持的方法是实现yii\web\Linkable接口(实现getLinks()方法)。

动作响应中,可以返回一个资源“集合”:集合要么是数组,要么是数据提供者data provider(这个更常见,因为支持排序和分页)。响应的HTTP头部包含了页码信息。

yii\rest\Controller提供的功能主要靠过滤器实现,如内容协商、HTTP verb验证、用户认证、频率限制。跨域资源共享CORS实现时,需要在behaviors中先取消验证行为,添加CORS过滤器后再添加上验证行为。

当控制器继承自yii\rest\ActiveController类时,默认支持了一系列动作(actions中可配置),但同时必须设定modelClass属性。覆盖checkAccess方法,可以完成权限检查(更复杂的权限管理可以结合RBAC)。

路由规则和URL美化我们前面已经处理,主要是熟悉yii\rest\UrlRule的配置项。

响应格式处理包括内容协商(behaviors中配置过滤器)、数据序列化(配置$serializer成员)、JSON输出控制(配置应用组件response)。

认证:可以用HTTP基本认证(access-token作为用户名+密码)、access-token作为请求参数、OAuth2,还可以复合它们或者创建新的认证方式。

限流:user identity class应实现yii\filters\RateLimitInterface接口,即实现其中的3个方法(保存有关信息和取回有关信息)

 

我们开始来处理验证部分

首先创建user表,类似前面创建一个migration,配置文件config.php,名称 create_user_table,不指定字段,创建migration文件后,在safeUp方法中写入如下代码:

 

public function safeUp()
{
    $this->createTable('user', [
        'id' => $this->primaryKey(),
        'username' => $this->string(20),
        'password' => $this->string(64),
        'name' => $this->string(32),
        'auth_key' => $this->string(128),
        'access_token' => $this->string(128),
        'sex' => $this->string(4),
        'disabled' => $this->tinyInteger(1),
        'first_time' => $this->integer(11),
        'last_time' => $this->integer(11)
    ]);
}

然后up一下,在sqlite中创建了user表。

因为user表是空的,而且考虑到今后需要做一些维护工作,所以,将配置文件中的db部分独立为一个文件db.php:

 

<?php

return [
    'class' => 'yii\db\Connection',
    'dsn' => 'sqlite:@gxyx/db_sqlite/database.sqlite',   //sqlite 格式用来演示
];

然后,config.php对应的地方改为

'db' => require(__DIR__ . '/db.php'),

再创建一个控制台命令用的配置文件console.php:

 

<?php

return [
    'id' => 'gxyx-dv2018-console',
    'basePath' => __DIR__,
    'controllerNamespace' => 'gxyx\commands',
    'aliases' => [
        '@gxyx' => __DIR__,
    ],
    'components' => [
        'db' => require(__DIR__ . '/db.php'),
//*
        'urlManager' => [
            'enablePrettyUrl' => true,
            'enableStrictParsing' => true,
            'showScriptName' => false,
        ],
//*/
    ]
];

在api目录下建立控制台应用的入口文件yii.php(前面的配置文件也都在api目录下)

 

<?php

defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');

require(__DIR__ . '/vendor/autoload.php');
require(__DIR__ . '/vendor/yiisoft/yii2/Yii.php');

$config = require(__DIR__ . '/console.php');

$application = new yii\console\Application($config);
$exitCode = $application->run();
exit($exitCode);

再在api目录下新建目录commands,并在其中建立控制器文件AdminController.php:

 

<?php

namespace gxyx\commands;

use Yii;
use yii\console\Controller;
use gxyx\models\User;

class AdminController extends Controller
{
    public function actionIndex()
    {
        echo self::outLine('这是测试信息, This is a test message');
    }

    public function actionGetTable($name, $limit = 20)
    {
        $command = Yii::$app->db->createCommand("SELECT * FROM `$name` ORDER BY id DESC limit $limit");
        $rows = $command->queryAll();
        if ($rows) {
            foreach ($rows as $row) {
                echo self::outLine(implode("\t", $row));
            }
        }
    }

    public function actionResetAdmin($username = 'admin', $password = '123456')
    {
        $user = User::findByUsername($username);
        if ($user) {
            //already exist, update it
            $user->password = $password;
            $result = $user->save();
        } else {
            //not exist, create new one
            $user = new User;
            $user->username = $username;
            $user->password = $password;
            $user->name = '超级管理员';
            $user->sex = '男';
            $user->disabled = 0;
            $user->first_time = $user->last_time = time();
            $result = $user->save();
        }
        echo self::outLine($result ? '成功,完毕!' : '失败,待查!');
    }

    public static function out($message)
    {
        return strpos(PHP_OS, 'WIN') === false ? $message : self::gbk($message);
    }

    public static function outLine($message)
    {
        return self::out($message) . PHP_EOL;
    }

    public static function gbk($message)
    {
        return mb_convert_encoding($message, 'GBK', 'UTF-8');
    }

}

来试验一下控制台命令(api目录下):输入 php  yii.php  admin/reset-admin,就可以新建admin用户

输入 php   yii.php  admin/get-table user就可以查看user表的记录,把user换成posts,也可以查看posts表的记录

创建UserController,并配置其 behaviors,使用杂合验证模式

 

<?php

namespace gxyx\controllers;

use yii\rest\ActiveController;
use yii\filters\auth\CompositeAuth;
use yii\filters\auth\HttpBasicAuth;
use yii\filters\auth\HttpBearerAuth;
use yii\filters\auth\QueryParamAuth;

class UserController extends ActiveController
{
    public $modelClass = 'gxyx\models\User';

    public function behaviors()
    {
        $behaviors = parent::behaviors();
        $behaviors['authenticator'] = [
            'class' => CompositeAuth::class,
            'authMethods' => [
                HttpBasicAuth::class,
                HttpBearerAuth::class,
                QueryParamAuth::class,
            ],
        ];
        return $behaviors;
    }

}

在配置文件中配置一下user组件(必须指明web用户所用的身份验证类,我们是模型类User):

 

'user' => [
    'identityClass' => 'gxyx\models\User',
    'enableSession' => false,
    'loginUrl' => null,
],

光是这样还不行,request组件中配置一下 cookie 验证秘钥://(2021.01.30注:restful API通常是无状态的,所以,不使用cookie和session。在上面user组件中已经禁用session,所以,不需要配置验证cookie的验证密钥,但同时'enableCookieValidation' => false,因为该值默认为true启用cookie验证)

 

'request' => [
    'parsers' => [
        'application/json' => 'yii\web\JsonParser'
    ],
    //'cookieValidationKey' => 'rrxTfcGINZVlYL6oOjdmSYmx-cjcvpdN',
    'enableCookieValidation' => false,
]

这时,我们从REST client试着访问一下GET  /api/users,发现 Unauthorized 了。用控制台命令查看好admin的access-token,然后在请求参数中添加 access-token=xxxxx,再GET   /api/users,结果就出来了。当然,连access-token等敏感信息也出来了(这个问题前面的fields方法可以处理)

上面的是实现了 QueryParamAuth,即访问令牌查询参数验证的方式

(2018.7.1 注)其实实现HttpBearerAuth和访问令牌查询参数验证基本一样,就是access-token不是作为查询参数传递,而是在头部添加一点信息——在REST client中添加一项到Headers :Authorization: Bearer xxxx ,其中xxxx部分就是access-token

接下来实现HttpBasicAuth,这个略显复杂。为了让验证不用在每个控制器都添加behaviors,我们来建立一个自己的控制器基类AuthActiveController.php:

 

<?php

namespace gxyx\controllers;

use yii\rest\ActiveController;
use yii\filters\auth\CompositeAuth;
use yii\filters\auth\HttpBasicAuth;
use yii\filters\auth\QueryParamAuth;
use gxyx\models\User;

class AuthActiveController extends ActiveController
{
    public function behaviors()
    {
        $behaviors = parent::behaviors();
        $behaviors['authenticator'] = [
            'class' => CompositeAuth::class,
            'authMethods' => [
                [
                    'class' => HttpBasicAuth::class,
                    'auth' => function ($username, $password) {
                        return User::findIdentityByUsernamePassword($username, $password);
                    }
                ],

                //HttpBearerAuth::class,
                QueryParamAuth::class,
            ],
        ];
        return $behaviors;
    }
}

 

User模型添加一个方法:

 

public static function findIdentityByUsernamePassword($username, $password)
{
    $identity = self::findByUsername($username);
    return $identity->validatePassword($password) ? $identity : null;
}

 

然后,让UserController从该基类派生class UserController extends AuthActiveController,可以去掉不需要的use和behaviors

应该注意到,HttpBasicAuth的authenticate方法开头是这样的:

 

public function authenticate($user, $request, $response)
{
    list($username, $password) = $request->getAuthCredentials();

而Request的getAuthCredentials方法开头是:

 

public function getAuthCredentials()
{
    $username = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null;
    $password = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null;

这时,我们可以发现,从网页访问 http://gxyx.localhost:2018/api/users/,浏览器会弹出一个验证窗口,要求输入用户名和密码,输入admin和123456,就可以显示内容了。

但在REST client中怎么测试呢?一开始尝试添加username=admin和password=123456这样的请求参数,但没用(通过调试,请求参数不会在$_SERVER中添加所需信息)。参考了以下文章,知道了HTTP基本验证是要在头部发送一定的信息,并且验证信息是用户名和密码的base64编码串

https://blog.csdn.net/sxb0841901116/article/details/23140097

http://www.ietf.org/rfc/rfc2617.txt

http://php.net/manual/zh/features.http-auth.php

好了,在命令行输入  echo  admin:123456  |  base64,得到字符串 admin:123456 经过base64编码后的字符串xxxx,然后,在REST client中添加一项到Headers :Authorization: Basic xxxx  ,再 GET  /api/users 就成功了。此时,两种验证方法都是可行的。

(2021.01.30注:似乎直接用username,password也是可以的,即在头部添加Authorization: Basic admin 123456)

API请求的跨域

 

class AuthActiveController extends ActiveController
{
    public function behaviors()
    {
        $behaviors = parent::behaviors();
        $behaviors['corsFilter'] = [
            'class' => Cors::class,
            'cors' => [
                'Origin' => ['*'],
                'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
                'Access-Control-Request-Headers' => ['*'],
                'Access-Control-Allow-Origin' => ['*'],         // 允许跨域
                'Access-Control-Allow-Credentials' => true,     // 允许 cookie 跨域
                'Access-Control-Max-Age' => 86400,
                'Access-Control-Expose-Headers' => [],
            ]
        ];
        $behaviors['authenticator'] = [
            'class' => CompositeAuth::class,
            'authMethods' => [
                [
                    'class' => HttpBasicAuth::class,
                    'auth' => function ($username, $password) {
                        return User::findIdentityByUsernamePassword($username, $password);
                    }
                ],

                HttpBearerAuth::class,
                QueryParamAuth::class,
            ],
        ];

        return $behaviors;
    }
}

我们来完成登录获得access-token的功能

在配置文件config.php的urlManager部分,添加路由规则,使得POST  login时对应user控制器的login action(下面的配置中pluralize为false,所以,请求的时候用user单数,POST  /api/user/login,请求体为{"username":"admin", "password":"123456"})

 

'urlManager' => [
    'enablePrettyUrl' => true,
    'enableStrictParsing' => true,
    'showScriptName' => false,
    'rules' => [
        ['class' => 'yii\rest\UrlRule', 'controller' => ['post', 'user']],
        ['class' => 'yii\rest\UrlRule', 'controller' => 'site', 'only' => ['index']],
        [
            'class' => 'yii\rest\UrlRule',
            'controller' => 'user',
            'pluralize' => false,
            'extraPatterns' => [
                'POST login' => 'login'
            ]
        ]
    ]
],

前面的SiteController,我们是在actions中指明index动作对应自定义的SiteIndexAction,然后自定义SiteIndexAction类完成定制,这里,我们在UserController中直接添加actionLogin方法

 

public function actionLogin()
{
    $modelClass = $this->modelClass;
    try {
        $body = Yii::$app->getRequest()->getBodyParams();
    } catch (InvalidConfigException $e) {
        return ['result' => 1, 'access-token' => '', 'message' => 'Invalid config!'];
    } catch (BadRequestHttpException $e) {
        return ['result' => 1, 'access-token' => '', 'message' => 'Wrong JSON request body!'];
    }
    if (array_key_exists('username', $body) && array_key_exists('password', $body)) {
        $user = $modelClass::findIdentityByUsernamePassword($body['username'], $body['password']);
        if ($user) {
            return ['result' => 0, 'access-token' => $user->access_token, 'message' => 'Success'];
        }
    }
    return ['result' => 1, 'access-token' => '', 'message' => 'Invalid username or password'];
}

因为UserController从AuthActiveController继承,默认所有动作都需要验证才能访问,而登录往往是在获得访问令牌之前进行的,所以,对于登录动作,我们去掉验证:在UserController中添加behaviors方法,重写此方法

 

public function behaviors()
{
    $behaviors =  parent::behaviors();
    // TODO: 登录请求不做验证!
    $currentAction = Yii::$app->controller->action->id;
    if (in_array($currentAction, ['login'])) {
        unset($behaviors['authenticator']);
    }
    return $behaviors;
}

这时,我们可以试验,对于login,不需要验证就能访问,而其余user控制器内的动作,都需要验证

(待续)

 类似资料: