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