Yii2框架之使用Restful自定义Api以及用户的授权认证

1 篇文章 0 订阅
1 篇文章 0 订阅

前言

百度上的许多Yii2框架的资料都是相互转发,看得我头晕,虽然官方文档上有讲的很全面,但是有些坑还是要自己去踩才会明白的.于是打算记几记录一下,方便以后查阅.

本篇的主要内容是Yii2 RESTful Api的配置以及自定义Api和用户的授权认证.

本文所使用的开发环境是Ubuntu17.10+php7.1+Apache2.4+PhpStorm,用到的测试工具为Postman

本地ip为192.168.1.101.

1.准备工作

首先需要安装好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存到数据库中.

并且后续的用户认证也必须用到这个字段.

2.配置Restful

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来作为所有控制器的基类:

<?php
/**
 * Created by PhpStorm.
 * User: phw
 * Date: 18-3-9
 * Time: 下午7:28
 */

namespace app\controllers;

use app\models\User;
use yii\filters\auth\HttpBasicAuth;
use yii\filters\auth\HttpBearerAuth;
use yii\filters\ContentNegotiator;
use yii\rest\ActiveController;
use yii\web\Response;

class BaseActiveController extends ActiveController
{

    public $modelClass = 'app\models\User';

    public $post;
    public $get;
    public $_user;
    public $_userId;

    /**
     * @throws \yii\base\InvalidConfigException
     */
    public function init()
    {
        parent::init(); // TODO: Change the autogenerated stub
        $this->_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:

<?php
/**
 * Created by PhpStorm.
 * User: phw
 * Date: 18-3-8
 * Time: 下午3:09
 */

namespace app\controllers;

use app\models\LoginForm;

class UserController extends BaseActiveController
{
    public $modelClass = 'app\models\User';

    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
        ];
    }
}

上面的代码先不看actionLogin()这个方法,要想完成一个最基础的RESTful Api,还需要指定$modelClass的值.

于是我们就完成了一个最简单的Api,拿Postman测试一下:

如果你得到了如图所示的数据,说明你前面都很成功.

2.自定义RESTful Api

前面已经提到过,自定义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一样的了.

3.用户的登录认证
咳咳,接下来才是这篇文章最重要的内容,话说我也是折腾了好久好久好久...

首先,要定义Yii的User组件,还是在web.php中:

'components' => [
    'user' => [
            'identityClass' => 'app\models\User',
            'enableAutoLogin' => true,
            'enableSession' => false,
            'loginUrl' => null
        ],
]

identityClass是你自己的User模型类,enableSession设置为false,loginUrl设置为null,具体为什么官方文档上有说.

接下来就是将User模型实现IdentityInterface这个接口:

<?php

namespace app\models;

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

class User extends ActiveRecord implements IdentityInterface
{
    public static function tableName()
    {
        return 'user';
    }

    public function rules()
    {
       ...
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        ...
    }

    /**
     * 根据用户名查找用户
     * Finds an identity by username
     * @param null $username
     * @return null|static
     */
    public static function findByUsername($username = null) {
        return static::findOne(['username' => $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模型类:

<?php

namespace app\models;

use Yii;
use yii\base\Model;

class LoginForm extends Model
{
    public $username;
    public $password;
    public $rememberMe = true;

    private $_user = false;

    const GET_ACCESS_TOKEN = 'generate_access_token';

    public function init()
    {
        parent::init(); // TODO: Change the autogenerated stub
        $this->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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值