原文出處:https://jellybool.com/post/programming-with-yii2-user-access-controls
上一篇文章講了用戶的注冊,驗證和登錄,這一篇文章按照約定來說說Yii2之中的用戶和權(quán)限控制。
你可以直接到Github下載源碼,以便可以跟上進(jìn)度,你也可以重頭開始,一步一步按照這個教程來做。
鑒于本教材基于Yii2 Basic,所以對RBAC的詳細(xì)講解我后面再單獨出文章來說說吧,這里主要是簡單地說一說權(quán)限控制
上一篇文章所實現(xiàn)的功能還比較簡單,可以發(fā)一條狀態(tài),但是不知道你注意到?jīng)]有,如果是沒有注冊的用戶也可以使用我們的應(yīng)用(類似小微博)來發(fā)狀態(tài),這是不符合情理的。正確的做法是在用戶沒有注冊,登錄之前,我們甚至都不應(yīng)該給沒有注冊的用戶看到我們創(chuàng)建狀態(tài)的頁面,即是http://localhost:8999/status/create
就不應(yīng)該讓游客看到,更不用說編輯和刪除一條狀態(tài)(status)
了。
什么是權(quán)限控制?個人覺得在一個Web應(yīng)用當(dāng)中,有以下幾種常見的角色和權(quán)限控制:
1. 游客,也就是沒有注冊的用戶,一般這個權(quán)限是最小的,對于一些需要登錄訪問的頁面沒有訪問權(quán)限
2. 用戶,這里的用戶特指注冊用戶,注冊過后的用戶一般可以使用整個web應(yīng)用的主要功能,比如我們這里的發(fā)表一條狀態(tài)(status)
3. 作者,這個不知道確切應(yīng)該使用什么名詞來描述,作者是在用戶注冊之后的一個權(quán)限判斷,比如A發(fā)表的status狀態(tài),B君不能進(jìn)行編輯,刪除等,反之亦然。
4. 管理員,這里的管理員通常會是應(yīng)用的開發(fā)者(所有者,或者應(yīng)該這么說),幾乎可以說是對站點的所有權(quán)限都有
Yii2自帶的權(quán)限控制默認(rèn)只支持兩個角色:
guest(游客,沒有登錄的,用?
表示)
@
表示)在這里我們需要實現(xiàn)的是對這兩種不同的角色指定不同的訪問權(quán)限,就是為他們分配不同的可以訪問的控制器或者方法。
目前我們?nèi)绻苯狱c擊導(dǎo)航欄的Status,我們還是可以在沒有登錄的情況之下進(jìn)行發(fā)表狀態(tài)(status)
,所以我們需要改一下我們的代碼和邏輯,Yii2在這方面的控制做得非常好,其實實現(xiàn)這個我們只需要修改一下StatusController.php
里面的behaviors()
方法而已,在這里面加入一段access
設(shè)置:
public function behaviors()
{
return [
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['post'],
],
],
'access' => [
'class' => AccessControl::className(),
'only' => ['index','create','update','view'],
'rules' => [
// allow authenticated users
[
'allow' => true,
'roles' => ['@'],
],
// everything else is denied
],
],
];
}
加上access這一段之后,我們再次點擊Status,Yii2就會將未登錄的我重定向到登錄頁面。
而且,這個時候,一旦你登入進(jìn)去,Yii會默認(rèn)自動跳轉(zhuǎn)到上一個url,也就是我們剛剛點擊的status/index
。
用戶一旦登錄進(jìn)來之后,我們就可以通過下面這行代碼來獲取用戶的id了:
Yii::$app->user->getId();
一旦用戶的id獲取到,我們可以做的事就很多了。這里我們先來將一條狀態(tài)和用戶聯(lián)系起來,也就是添加用戶與說說的映射關(guān)系。要實現(xiàn)這個目標(biāo)我們需要先修改我們的數(shù)據(jù)表(體驗一下當(dāng)初設(shè)計數(shù)據(jù)表考慮不周全的情況):
./yii migrate/create extend_status_table_for_created_by
Yii Migration Tool (based on Yii v2.0.6)
Create new migration '/Users/jellybool/Desktop/helloYii/migrations/m150806_034325_extend_status_table_for_created_by.php'? (yes|no) [no]:yes
New migration created successfully.
打開對應(yīng)的migration
文件,編輯up()
和down()
方法,如果你想加入數(shù)據(jù)庫的事務(wù)管理功能,你可以使用safeUp()
和safeDown()
方法
public function up()
{
$this->addColumn('{{%status}}','created_by',Schema::TYPE_INTEGER.' NOT NULL');
$this->addForeignKey('fk_status_created_by', '{{%status}}', 'created_by', '{{%user}}', 'id', 'CASCADE', 'CASCADE');
}
public function down()
{
$this->dropForeignKey('fk_status_created_by','{{%status}}');
$this->dropColumn('{{%status}}','created_by');
}
我們需要為status
表添加一個created_by
字段,并且將它跟user
表的id
設(shè)為外鍵關(guān)系。
如果你在status表里面有一條數(shù)據(jù)記錄,你需要先刪除這一條記錄,不然可能會報錯。
執(zhí)行migrate/up
:
./yii migrate/up
Yii Migration Tool (based on Yii v2.0.6)
Total 1 new migration to be applied:
m150806_034325_extend_status_table_for_created_by
Apply the above migration? (yes|no) [no]:yes
*** applying m150806_034325_extend_status_table_for_created_by
> add column created_by integer NOT NULL to table {{%status}} ... done (time: 0.032s)
> add foreign key fk_status_created_by: {{%status}} (created_by) references {{%user}} (id) ... done (time: 0.014s)
*** applied m150806_034325_extend_status_table_for_created_by (time: 0.059s)
數(shù)據(jù)表的外鍵設(shè)置好之后,我們就可以來聲明Status
和User
的關(guān)系了,不過在開始之前需要修改一下User.php
里面的內(nèi)容:
<?php
namespace app\models;
use dektrium\user\models\User as BaseUser;
class User extends BaseUser {
public function register()
{
}
}
直接將原來的User模型的代碼都刪掉,只需要我們上面的代碼就可以了,因為我們使用了Yii2-User, 這里就是使用dektrium\user\models\User
這個模型,然后修改一下我們的config/web.php
,再我們之前的user中加入幾行代碼:
'modules' => [
'user' => [
'class' => 'dektrium\user\Module',
'confirmWithin' => 21600,
// add the following 3 lines
'modelMap' => [
'User' => 'app\models\User',
],
'cost' => 12,
'admins' => ['admin']
],
],
這樣之后,我們的User和Status的對應(yīng)關(guān)系就會建立起來。
然后我們在Status.php寫上以下的說明:
public function getUser()
{
return $this->hasOne(User::className(), ['id' => 'created_by']);
}
這里聲明的映射關(guān)系為hasOne
,也就是說,一條狀態(tài)status(說說)
對應(yīng)一個用戶(User),我們通過['id' => 'created_by']
來指定外鍵映射。
有了Status和User的對應(yīng)關(guān)系之后,我們需要在用戶發(fā)表狀態(tài)的時候?qū)⒂脩舻膇d保存到Status
的created_by
這一個字段中,所以我們需要在StatusController
中的actionCreate
方法中加上一行代碼:
if ($model->load(Yii::$app->request->post())) {
$model->created_by = Yii::$app->user->getId();//add this line
$model->created_at = time();
$model->updated_at = time();
if ($model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
}
}
這里需要確認(rèn)的是,你需要保證create
方法只能是登錄進(jìn)來的用戶才能訪問觸發(fā)。
為了更好地展示一條狀態(tài)stutas
的信息,我們修改一下展示狀態(tài)的視圖文件:status/view.php
?:
<?= DetailView::widget([
'model' => $model,
'attributes' => [
'id',
'user.email', // add this line
'message:ntext',
'created_by', // add this line
'permissions',
'created_at',
'updated_at',
],
]) ?>
上面的user.email
中的user
其實是觸發(fā)Status::getUser()
這個方法。
這樣一刷新之后,我們就可以看到創(chuàng)建這條狀態(tài)的用戶id
和email
了。
上面的一些列設(shè)置和代碼更改,已經(jīng)實現(xiàn)了一小部分的用戶控制:登錄的用戶才能發(fā)表status。然而這還不能滿足我們在日常使用的需求,比如我們現(xiàn)在怎么確定一個用戶能不能對某條狀態(tài)進(jìn)行修改和刪除?或者說,管理員的角色在哪里體現(xiàn)呢?現(xiàn)在貌似都是平等的角色,相同的權(quán)限,對于登錄的用戶來說。
鑒于官方文檔或者很多關(guān)于Yii2 RBAC的資料都是基于Yii2 Advanced Template
,而我們一開始使用的是Yii2 Basic Template
,并且我們也引入Yii2-User,所以這里我們嘗試來自己實現(xiàn)一點點的用戶權(quán)限控制。
首先我們需要在User中定義一些跟角色(role)
相關(guān)的規(guī)定,比如根據(jù)不同的用戶角色來賦予不同的常量:
class User extends BaseUser {
const ROLE_USER = 10;
const ROLE_MODERATOR = 20;
const ROLE_ADMIN = 30;
}
上面的代碼寫在User模型里面,這里定義了三種角色,ROLE_USER
,ROLE_MODERATOR
,ROLE_ADMIN
,USER
可以發(fā)表狀態(tài),MODERATOR
可以修改但是不可以刪除,ADMIN
可以修改和刪除。
然后在helloYii/
目錄之下創(chuàng)建一個components/
目錄,里面新建一個AccessRule.php
文件:
<?php
namespace app\components;
use app\models\User;
class AccessRule extends \yii\filters\AccessRule {
/**
* @inheritdoc
*/
protected function matchRole($user)
{
if (count($this->roles) === 0) {
return true;
}
foreach ($this->roles as $role) {
if ($role === '?') {
if ($user->getIsGuest()) {
return true;
}
} elseif ($role === User::ROLE_USER) {
if (!$user->getIsGuest()) {
return true;
}
// Check if the user is logged in, and the roles match
} elseif (!$user->getIsGuest() && $role === $user->identity->role) {
return true;
}
}
return false;
}
}
這里就直接借用Yii2自帶的\yii\filters\AccessRule
來控制權(quán)限規(guī)則。但是由于Yii2-User在創(chuàng)建user數(shù)據(jù)表的時候并沒有role
這個字段,所以我們需要手動添加,你可以直接在mysql敲命令行,或者也可以通過數(shù)據(jù)庫管理工具來添加。
最后更新一下我們的StatusController.php
文件,這里的behaviors()
方法會做出一些調(diào)整:
<?php
namespace app\controllers;
use Yii;
use app\models\Status;
use app\models\StatusSearch;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;
use yii\filters\AccessControl;
use app\components\AccessRule;
use app\models\User;
/**
* StatusController implements the CRUD actions for Status model.
*/
class StatusController extends Controller
{
public function behaviors()
{
return [
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['post'],
],
],
'access' => [
'class' => AccessControl::className(),
// We will override the default rule config with the new AccessRule class
'ruleConfig' => [
'class' => AccessRule::className(),
],
'only' => ['index','create', 'update', 'delete'],
'rules' => [
[
'actions' => ['index','create'],
'allow' => true,
// Allow users, moderators and admins to create
'roles' => [
User::ROLE_USER,
User::ROLE_MODERATOR,
User::ROLE_ADMIN
],
],
[
'actions' => ['update'],
'allow' => true,
// Allow moderators and admins to update
'roles' => [
User::ROLE_MODERATOR,
User::ROLE_ADMIN
],
],
[
'actions' => ['delete'],
'allow' => true,
// Allow admins to delete
'roles' => [
User::ROLE_ADMIN
],
],
],
],
];
}
我們上面根據(jù)不同等級的用戶賦予不同的訪問權(quán)限,這時候,如果你先logout
出來,再登錄回去,你還是可以看到這些status
,但是一旦你點擊delete(刪除按鈕),你將會看到一個報錯的頁面:
更多建議: