百度上的许多Yii2框架的资料都是相互转发,看得我头晕,虽然官方文档上有讲的很全面,但是有些坑还是要自己去踩才会明白的.于是打算记几记录一下,方便以后查阅.
本篇的主要内容是Yii2 RESTful Api的配置以及自定义Api和用户的授权认证.
本文所使用的开发环境是Ubuntu17.10+php7.1+Apache2.4+PhpStorm,用到的测试工具为Postman
本地ip为192.168.1.101.
首先需要安装好Yii2的基础模板basic,具体的安装步骤不是本文的重点内容,这里就不在赘述了.
拿到模板后,先把"app\model\User"这个类删掉,等会儿我们会重新创建一个模型类.
然后新建一张User表:
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(45) DEFAULT NULL,
`password` varchar(30) DEFAULT NULL,
`password_hash` varchar(128) DEFAULT NULL,
`phone` varchar(11) DEFAULT NULL,
`email` varchar(50) DEFAULT NULL,
`access_token` varchar(64) DEFAULT NULL,
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`auth_key` varchar(64) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `phone` (`phone`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
其中说一下access_token,因为RESTful是无状态的,所以每次请求都需要附带token,为了方便,可以把token存到数据库中.
并且后续的用户认证也必须用到这个字段.
config文件下的web.php是整个项目的重要的配置文件,官方文档上配置RESTful这一部分也说得比较清楚:
'components' => [
'request' => [
'cookieValidationKey' => 'BLt7chH5PYCV6zTeMQjZ7ThmB5Rk_rY4',
'parsers' => [
'application/json' => 'yii\web\JsonParser',
]
],
'response' => [
'class' => 'yii\web\Response',
'on beforeSend' => function ($event) {
$response = $event->sender;
$data = $response->data;
if ($data !== null) {
$response->data = [
'code' => isset($data['code']) ? $data['code'] : 200,
'message' => isset($data['message']) ? $data['message'] : null,
'data' => isset($data['data']) ? $data['data'] : $data
];
}
}
],
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
[
'class' => 'yii\rest\UrlRule',
'controller' => 'user',
'extraPatterns' => [
'POST login' => 'login',
]
],
...
],
]
先说request组件里的parser,代表能被服务器解析的数据类型,这里添加JsonParser,使其能够解析Json格式的数据.
然后是response组件,这里主要是自定义返回数据的数据模板,这里代表我想要返回的格式是这样子的:
return [
'code' => 200,
'message' => '操作成功',
'data' => [...]
];
其次是urlManager,第一个属性是开启url美化,第二个属性是开启严格解析模式,第三个是是否显示index.php;当然,还有一个是否启用名词复数形式的属性我没有配置,默认是启用.
比较重要的是下面的rules,这里是配置路由规则的地方.其中,controller是RESTful的控制器,必须继承自ActiveController或其子类.下面的extraPatterns是自定义的路由规则.自定义Api主要是在这个地方配置.
配置看完后再来看看"app\models\User".
首先,要通过Gii来生成User这个模型类,在Ubuntu中生成文件会遇到文件的读写权限问题,这时候需要把整个basic文件夹的权限设置为777:
sudo chmod 777 ~/workspace/basic -R
当然,可能还会遇到404,这是因为Gii的安全规则所引起的,需要在web.php中配置一下:
$config['modules']['gii'] = [
'class' => 'yii\gii\Module',
// uncomment the following to add your IP if you are not connecting from localhost.
'allowedIPs' => ['127.0.0.1', '::1', '192.168.1.*', '192.168.0.*'],
];
在Gii模块中的allowedIPs中加入你自己的本机IP即可.(Gii生成文件的坑还是有点多的0.0)
User模型文件生成好了之后,再来关注一下控制器,在写UserController之前,我先写一个BaseActiveController来作为所有控制器的基类:
_user = User::findIdentityByAccessToken(\Yii::$app->request->headers->get('Authorization'));
}
public function behaviors()
{
$behaviors = parent::behaviors(); // TODO: Change the autogenerated stub
$behaviors['authenticator'] = [
'class' => HttpBearerAuth::className(),
'optional' => ['login']
];
$behaviors['contentNegotiator'] = [
'class' => ContentNegotiator::className(),
'formats' => [
'application/json' => Response::FORMAT_JSON
]
];
return $behaviors;
}
/**
* @param $action
* @return bool
* @throws \yii\web\BadRequestHttpException
*/
public function beforeAction($action)
{
parent::beforeAction($action); // TODO: Change the autogenerated stub
$this->post = \Yii::$app->request->post();
$this->get = \Yii::$app->request->get();
$this->_user = \Yii::$app->user->identity;
$this->_userId = \Yii::$app->user->id;
return $action;
}
}
在这个基类中要做的事情主要有初始化客户端提交过来的数据,像get/post;获取当前用户user/userId;配置认证方式以及对响应格式的设置.
先来说beforeAction()这个方法,它的作用就是初始化post/get以及当前用户user/userId,方便其子类直接调用.初始化完成后在init()方法中执行.
然后是behaviors()中的行为配置.在$behaviors['authenticator]中主要有两个属性,第一个class代表所使用的认证方式,这里采用的是Http Bearer Auth, 还有另外两种Http Basic Auth以及Query Parma Auth.第二个optional中是设置的例外,既不对这个action进行拦截.
写好基础类后UserController直接继承BaseActiveController:
setAttributes($this->post);
if ($model->login()) {
return [
'code' => 200,
'message' => '登陆成功',
'data' => [
'access_token' => $model->user->access_token
]
];
}
return [
'code' => 500,
'message' => $model->errors
];
}
}
上面的代码先不看actionLogin()这个方法,要想完成一个最基础的RESTful Api,还需要指定$modelClass的值.
于是我们就完成了一个最简单的Api,拿Postman测试一下:
如果你得到了如图所示的数据,说明你前面都很成功.
前面已经提到过,自定义Api需要在urlManager中进行配置:
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
[
'class' => 'yii\rest\UrlRule',
'controller' => 'user',
'extraPatterns' => [
'POST login' => 'login',
]
]
],
在UserController中新建actionLogin()方法,并在extraPatterns中注明url中的POST /user/login对应的是UserController中的actionLogin().然后自己在UserController中新建一个actionLogin(),简单的输出一些数据,看看能不能访问:
public function actionLogin() {
return [
'code' => 200,
'message' => 'action login...'
];
}
如果能够正确输出信息,说明你已经成功的添加了一个自己的Api.
当然,还能复写其框架自带的Api,举个栗子,还是用UserController:
public function actions()
{
$actions = parent::actions(); // TODO: Change the autogenerated stub
unset($actions['create']);
return $actions;
}
只需要在actions()中unset()掉你想复写的方法即可.剩下的就和你自定义Api一样的了.
首先,要定义Yii的User组件,还是在web.php中:
'components' => [
'user' => [
'identityClass' => 'app\models\User',
'enableAutoLogin' => true,
'enableSession' => false,
'loginUrl' => null
],
]
identityClass是你自己的User模型类,enableSession设置为false,loginUrl设置为null,具体为什么官方文档上有说.
接下来就是将User模型实现IdentityInterface这个接口:
$username]);
}
public function validatePassword($password) {
return $this->password === $password;
}
public static function findIdentity($id)
{
// TODO: Implement findIdentity() method.
return static::findOne($id);
}
public static function findIdentityByAccessToken($token, $type = null)
{
// TODO: Implement findIdentityByAccessToken() method.
return static::findOne(['access_token' => $token]);
}
public function getId()
{
// TODO: Implement getId() method.
return $this->id;
}
public function getAuthKey()
{
// TODO: Implement getAuthKey() method.
return $this->auth_key;
}
public function validateAuthKey($authKey)
{
// TODO: Implement validateAuthKey() method.
}
/**
* 生成随机的token并加上时间戳
* Generated random accessToken with timestamp
* @throws \yii\base\Exception
*/
public function generateAccessToken() {
$this->access_token = Yii::$app->security->generateRandomString() . '-' . time();
}
/**
* 验证token是否过期
* Validates if accessToken expired
* @param null $token
* @return bool
*/
public static function validateAccessToken($token = null) {
if ($token === null) {
return false;
} else {
$timestamp = (int) substr($token, strrpos($token, '-') + 1);
$expire = Yii::$app->params['user.accessTokenExpire'];
return $timestamp + $expire >= time();
}
}
}
实现了这个接口以后我们只需要实现findIndentity() findIdentityByAccessToken() getId()三个方法即可,自定义方法的作用都在注释上.
然后再来看看LoginForm模型类:
on(self::GET_ACCESS_TOKEN, [$this, 'onGenerateAccessToken']);
}
public function rules()
{
...
}
public function validatePassword($attribute, $params)
{
...
}
/**
* Logs in a user using the provided username and password.
* @return bool whether the user is logged in successfully
*/
public function login()
{
if ($this->validate()) {
//Updates access_token in database
$this->trigger(self::GET_ACCESS_TOKEN);
return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600*24*30 : 0);
}
return false;
}
/**
* Finds user by [[username]]
*
* @return User|null
*/
public function getUser()
{
if ($this->_user === false) {
$this->_user = User::findByUsername($this->username);
}
return $this->_user;
}
/**
* 当登录成功时更新用户的token
* Generated new accessToken when validate successful.
* If accessToken is invalid, generated a new token for it.
* @throws \yii\base\Exception
*/
public function onGenerateAccessToken() {
if (!User::validateAccessToken($this->getUser()->access_token)) {
$this->getUser()->generateAccessToken();
$this->getUser()->save(false);
}
}
}
这个LoginForm用的就是basic模板原来的,然后再加上一个附加时间,作用是当登陆成功后更新用户的token.写好自定义方法onGenerateAccessToken()后在init()中附加,然后在登录成功后触发GET_ACCESS_TOKEN事件来实现token更新.
然后再来完善刚刚自定义的actionLogin()方法:
public function actionLogin() {
$model = new LoginForm();
$model->setAttributes($this->post);
if ($model->login()) {
return [
'code' => 200,
'message' => '登陆成功',
'data' => [
'access_token' => $model->user->access_token
]
];
}
return [
'code' => 500,
'message' => $model->errors
];
}
对了,这里有个很大的坑...,那就是这里的$model不能直接调用load()方法来装载数据,因为它的数据并不是直接从前台表格中提交过来的数据.所以,这里只能用setAttributes()来填充数据,然后基类的post在这里就派上用场啦~
好了,现在可以在Postman中进行测试了,地址是POST http://localhost/users/login:
你会惊喜的发现成功啦,登录成功后已经按照我们先前预定的格式返回了数据,并且还携带了access_token,接下来只需要把token存到localstorage或者其他什么的就ok了,当然这篇文章不涉及前台,我们还是用Postman来进行测试.
我们现在再来访问一下获取所有用户的Api试试看GET http://localhost/users:
你会更惊奇的发现,诶?怎么不行了,我不是登录了嘛...,可是仔细一想,刚才的access_token并没有用到啊.RESTful是无状态的,它怎么知道你是登陆了还是没有登录啊...所以只能靠token来识别啦~
还记得上面我们在BaseActiveController中设置过一个认证方法嘛,HttpBearerAuth这个,所以在Postman的左侧的认证方式选择Bearer Token,把生成的token,复制到Postman中,像这样~
然后点击send~酱酱~,就是这样子啦.
到这里差不多就要结束了,刚学PHP没多久,麻烦各位大佬多多指正.
Github:https://github.com/phw-nightingale/basic.git