1. 程式人生 > >Yii2框架之使用Restful自定義Api以及使用者的授權認證

Yii2框架之使用Restful自定義Api以及使用者的授權認證

前言

百度上的許多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