最近在做RESTful API認證功能,記錄整個過程,方便以後查看。本文參照了 https://segmentfault.com/a/1190000016368603部分內容,感謝該作者的分享,以上內容根據我的項目實際情況進行了調整。
認證介紹
和Web應用不同,RESTful APIs 通常是無狀態的, 也就意味着不應使用 sessions 或 cookies, 因此每個請求應附帶某種授權憑證,因爲用戶授權狀態可能沒通過 sessions 或 cookies 維護, 常用的做法是每個請求都發送一個祕密的 access token 來認證用戶, 由於 access token 可以唯一識別和認證用戶,API 請求應通過 HTTPS 來防止man-in-the-middle (MitM) 中間人攻擊.
認證方式
- HTTP 基本認證 :access token 當作用戶名發送,應用在access token可安全存在API使用端的場景, 例如,API使用端是運行在一臺服務器上的程序。
- 請求參數: access token 當作API URL請求參數發送,例如 https://example.com/users?acc..., 由於大多數服務器都會保存請求參數到日誌, 這種方式應主要用於JSONP 請求,因爲它不能使用HTTP頭來發送 access token
- OAuth 2 : 使用者從認證服務器上獲取基於 OAuth2 協議的 access token, 然後通過 HTTP Bearer Tokens 發送到 API 服務器。
上方進行簡單介紹,內容來自 Yii Framework 2.0 權威指南
實現步驟
繼續上一篇 的內容(這裏暫時使用默認User數據表,正式環境請分離不同的數據表來進行認證)
需要添加的數據內容
繼上篇的 User 數據表,我們還需要增加一 個 access_token 和 expire_at 的字段,
- 進入項目根目錄打開控制檯輸入以下命令:
./yii migrate/create add_column_access_token_to_user
./yii migrate/create add_column_expire_at_to_user
- 打開 你的項目目錄 /console/migrations/m181224_075747_add_column_access_token_user.php 修改如下內容:
public function up()
{
$ret = $this->db->createCommand("SELECT * FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'user' AND column_name = 'access_token'")->queryOne();//判斷user表是否有'access_token'這個字段
if (empty($ret)) {
$this->addColumn('user', 'access_token', $this->string(255)->defaultValue(NULL)->comment('令牌'));
}
}
public function down()
{
$this->dropColumn('user', 'access_token');
return true;
}
- 打開 你的項目目錄 /console/migrations/m181224_092333_add_column_expire_at_user.php 修改如下內容:
public function up()
{
$ret = $this->db->createCommand("SELECT * FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'user' AND column_name = 'expire_at'")->queryOne();
if (empty($ret)) {
$this->addColumn('user', 'expire_at', $this->integer(11)->defaultValue(NULL)->comment('令牌過期時間'));
}
}
public function down()
{
$this->dropColumn('user', 'expire_at');
return true;
}
- 執行遷移命令
./yii migrate
配置
打開 api\config\main.php
- 配置 user 應用組件:
'user' => [
'identityClass' => 'api\models\User',
'enableAutoLogin' => true,
'enableSession'=>false,
//'identityCookie' => ['name' => '_identity-api', 'httpOnly' => true],
],
- 將 session 組件註釋掉,或刪掉
// 'session' => [
// // this is the name of the session cookie used for login on the backend
// 'name' => 'advanced-api',
// ],
- 編寫 api\models\User.php 實現認證類,繼承 IdentityInterface
將 common\models\User 類拷貝到 api\models\ 目錄下,修改命名空間爲 api\models
<?php
namespace api\models;
use Yii;
use yii\base\NotSupportedException;
use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
use yii\web\IdentityInterface;
...
class User extends ActiveRecord implements IdentityInterface
{
...
...
}
- 將 commonmodelsLoginForm.php 類拷貝到 apimodels* 目錄下,修改命名空間,並重寫 login* 方法:
<?php
namespace api\models;
use Yii;
use yii\base\Model;
...
...
const EXPIRE_TIME = 604800;//令牌過期時間,7天有效
public function login()
{
if ($this->validate()) {
//return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0);
if ($this->getUser()) {
$access_token = $this->_user->generateAccessToken();
$this->_user->expire_at = time() + static::EXPIRE_TIME;
$this->_user->save();
Yii::$app->user->login($this->_user, static::EXPIRE_TIME);
return $access_token;
}
}
return false;
}
- 上方代碼給 User 模型添加了一個 generateAccessToken() 方法,因此我們到 api\models\User.php 中添加此方法
namespace api\models;
use Yii;
use yii\base\NotSupportedException;
use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
use yii\web\IdentityInterface;
use yii\web\UnauthorizedHttpException;
...
...
class User extends ActiveRecord implements IdentityInterface
{
...
...
/**
* 生成accessToken字符串
* @return string
* @throws \yii\base\Exception
*/
public function generateAccessToken()
{
$this->access_token=Yii::$app->security->generateRandomString();
return $this->access_token;
}
}
- 接下來在api\controllers\新加一個控制器 命名爲 UserController 並繼承 yii\rest\ActiveController,編寫登錄 Login 方法,具體代碼如下:
namespace api\controllers;
use api\models\LoginForm;
use yii\rest\ActiveController;
use yii;
class UserController extends ActiveController
{
public $modelClass = 'api\models\User';
public function actions()
{
$action= parent::actions(); // TODO: Change the autogenerated stub
unset($action['index']);
unset($action['create']);
unset($action['update']);
unset($action['delete']);
}
public function actionIndex()
{
//你的代碼
}
/**
* 登陸
* @return array
* @throws \yii\base\Exception
* @throws \yii\base\InvalidConfigException
*/
public function actionLogin()
{
$model = new LoginForm();
if ($model->load(Yii::$app->getRequest()->getBodyParams(), '') && $model->login()) {
return [
'access_token' => $model->login(),
];
} else {
return $model->getFirstErrors();
}
}
}
- 最後新增一條 URL 規則
打開 api\config\main.php 修改 components 屬性,添加下列代碼:
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
['class' => 'yii\rest\UrlRule',
'controller' => 'user',
'extraPatterns'=>[
'POST login'=>'login',
],
],
],
]
使用一個調試工具來進行測試 http://youdomain/users/login 記住是POST 請求發送,假如用POSTMAN有問題的話指定一下 Content-Type:application/x-www-form-urlencoded 。
ok,不出意外的話,相信你已經可以收到一個access_token 了,接下來就是如何使用這個token,如何維持認證狀態,達到不攜帶這個token將無法訪問,返回 401
維持認證狀態
實現認證步驟:
- 在你的 REST 控制器類中配置 authenticator 行爲來指定使用哪種認證方式
- 在你的 user identity class 類中實現 yiiwebIdentityInterface::findIdentityByAccessToken() 方法.
具體實現方式如下:
- 打開之前的 User 控制器( api\controllers\UserController.php ),增加以下內容:
use yii\helpers\ArrayHelper;
use yii\filters\auth\QueryParamAuth;
...//此處省略一些代碼了
public function behaviors()
{
return ArrayHelper::merge(parent::behaviors(), [
'authenticatior' => [
'class' => QueryParamAuth::className(), //實現access token認證
'except' => ['login'], //無需驗證access token的方法,注意區分$noAclLogin
]
]);
}
...
- 實現 findIdentityByAccessToken() 方法:
打開 api\models\User.php 重寫 findIdentityByAccessToken() 方法
...
use yii\web\UnauthorizedHttpException;
...
class User extends ActiveRecord implements IdentityInterface
{
...
...
/**
* {@inheritdoc}
*/
public static function findIdentityByAccessToken($token, $type = null)
{
// throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.');
$user = static::find()->where(['access_token' => $token, 'status' => self::STATUS_ACTIVE])->one();
if (!$user) {
return false;
}
if ($user->expire_at < time()) {
throw new UnauthorizedHttpException('the access - token expired ', -1);
} else {
return $user;
}
}
...
}
- 打開 api\controllers\UserController.php ,增加 Test方法,用於測試令牌驗證
public function actionTest()
{
return ['status'=>'success'];
}
- 修改 api\config\main.php
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
['class' => 'yii\rest\UrlRule',
'controller' => 'user',
//'pluralize' => false, //設置爲false 就可以去掉複數形式了
'extraPatterns'=>[
'GET test'=>'test',
'POST login'=>'login',
],
],
],
]
接下來訪問一下你的域名 http://youdomain/users/test,不攜帶任何參數是不是返回 401了?
ok,這裏介紹兩種訪問方式,一種是URL訪問,另一種是通過header 來進行攜帶
- http://youdomain/users/test?a...
- 傳遞 header 頭信息
Authorization:Bearer YYdpiZna0hJGhjsfqwxUeHEgLDfHEjB-
注意 Bearer 和你的token中間是有 一個空格的,很多同學在這個上面碰了很多次
以上就是基於YII2.0 RESTful 認證的內容。
本文參照了 https://segmentfault.com/a/1190000016368603部分內容,感謝該作者的分享,以上內容根據我的項目實際情況進行了調整。