看了 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控制器内的动作,都需要验证
(待续)