> 原文出處:https://jellybool.com/post/programming-with-yii2-user-access-controls
上一篇文章講了[用戶的注冊,驗證和登錄](https://jellybool.com/post/programming-with-yii2-integrating-user-registration),這一篇文章按照約定來說說Yii2之中的用戶和權限控制。
你可以直接到[Github](https://github.com/JellyBool/helloYii)下載源碼,以便可以跟上進度,你也可以重頭開始,一步一步按照這個教程來做。
> 鑒于本教材基于Yii2 Basic,所以對RBAC的詳細講解我后面再單獨出文章來說說吧,這里主要是簡單地說一說權限控制

上一篇文章所實現的功能還比較簡單,可以發一條狀態,但是不知道你注意到沒有,如果是沒有注冊的用戶也可以使用我們的應用(類似小微博)來發狀態,這是不符合情理的。正確的做法是在用戶沒有注冊,登錄之前,我們甚至都不應該給沒有注冊的用戶看到我們創建狀態的頁面,即是`http://localhost:8999/status/create`就不應該讓游客看到,更不用說編輯和刪除一條狀態`(status)`了。
## 權限控制
什么是權限控制?個人覺得在一個Web應用當中,有以下幾種常見的角色和權限控制:
~~~
1. 游客,也就是沒有注冊的用戶,一般這個權限是最小的,對于一些需要登錄訪問的頁面沒有訪問權限
2. 用戶,這里的用戶特指注冊用戶,注冊過后的用戶一般可以使用整個web應用的主要功能,比如我們這里的發表一條狀態(status)
3. 作者,這個不知道確切應該使用什么名詞來描述,作者是在用戶注冊之后的一個權限判斷,比如A發表的status狀態,B君不能進行編輯,刪除等,反之亦然。
4. 管理員,這里的管理員通常會是應用的開發者(所有者,或者應該這么說),幾乎可以說是對站點的所有權限都有
~~~
Yii2自帶的權限控制默認只支持兩個角色:
1. guest(游客,沒有登錄的,用`?`表示)
2. authenticated (登錄了的,用`@`表示)
在這里我們需要實現的是對這兩種不同的角色指定不同的訪問權限,就是為他們分配不同的可以訪問的控制器或者方法。
目前我們如果直接點擊導航欄的Status,我們還是可以在沒有登錄的情況之下進行發表狀態`(status)`,所以我們需要改一下我們的代碼和邏輯,Yii2在這方面的控制做得非常好,其實實現這個我們只需要修改一下`StatusController.php`里面的`behaviors()`方法而已,在這里面加入一段`access`設置:
~~~
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就會將未登錄的我重定向到登錄頁面。
而且,這個時候,一旦你登入進去,Yii會默認自動跳轉到上一個url,也就是我們剛剛點擊的`status/index`。
## 添加映射關系
用戶一旦登錄進來之后,我們就可以通過下面這行代碼來獲取用戶的id了:
~~~
Yii::$app->user->getId();
~~~
一旦用戶的id獲取到,我們可以做的事就很多了。這里我們先來將一條狀態和用戶聯系起來,也就是添加用戶與說說的映射關系。要實現這個目標我們需要先修改我們的數據表(體驗一下當初設計數據表考慮不周全的情況):
~~~
./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.
~~~
打開對應的`migration`文件,編輯`up()`和`down()`方法,如果你想加入數據庫的事務管理功能,你可以使用`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`設為外鍵關系。
> 如果你在status表里面有一條數據記錄,你需要先刪除這一條記錄,不然可能會報錯。
執行`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)
~~~
數據表的外鍵設置好之后,我們就可以來聲明`Status`和`User`的關系了,不過在開始之前需要修改一下`User.php`里面的內容:
~~~
<?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的對應關系就會建立起來。
然后我們在Status.php寫上以下的說明:
~~~
public function getUser()
{
return $this->hasOne(User::className(), ['id' => 'created_by']);
}
~~~
這里聲明的映射關系為`hasOne`,也就是說,一條狀態`status(說說)`對應一個用戶(User),我們通過`['id' => 'created_by']`來指定外鍵映射。
有了Status和User的對應關系之后,我們需要在用戶發表狀態的時候將用戶的id保存到`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]);
}
}
~~~
這里需要確認的是,你需要保證`create`方法只能是登錄進來的用戶才能訪問觸發。
為了更好地展示一條狀態`stutas`的信息,我們修改一下展示狀態的視圖文件:`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`其實是觸發`Status::getUser()`這個方法。
這樣一刷新之后,我們就可以看到創建這條狀態的用戶`id`和`email`了。

## 探尋RBAC
上面的一些列設置和代碼更改,已經實現了一小部分的用戶控制:登錄的用戶才能發表status。然而這還不能滿足我們在日常使用的需求,比如我們現在怎么確定一個用戶能不能對某條狀態進行修改和刪除?或者說,管理員的角色在哪里體現呢?現在貌似都是平等的角色,相同的權限,對于登錄的用戶來說。
鑒于官方文檔或者很多關于Yii2 RBAC的資料都是基于`Yii2 Advanced Template`,而我們一開始使用的是`Yii2 Basic Template`,并且我們也引入Yii2-User,所以這里我們嘗試來自己實現一點點的用戶權限控制。
首先我們需要在User中定義一些跟`角色(role)`相關的規定,比如根據不同的用戶角色來賦予不同的常量:
~~~
class User extends BaseUser {
const ROLE_USER = 10;
const ROLE_MODERATOR = 20;
const ROLE_ADMIN = 30;
}
~~~
上面的代碼寫在User模型里面,這里定義了三種角色,`ROLE_USER`,`ROLE_MODERATOR`,`ROLE_ADMIN`,`USER`可以發表狀態,`MODERATOR`可以修改但是不可以刪除,`ADMIN`可以修改和刪除。
然后在`helloYii/`目錄之下創建一個`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`來控制權限規則。但是由于Yii2-User在創建user數據表的時候并沒有`role`這個字段,所以我們需要手動添加,你可以直接在mysql敲命令行,或者也可以通過數據庫管理工具來添加。
最后更新一下我們的`StatusController.php`文件,這里的`behaviors()`方法會做出一些調整:
~~~
<?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
],
],
],
],
];
}
~~~
我們上面根據不同等級的用戶賦予不同的訪問權限,這時候,如果你先`logout`出來,再登錄回去,你還是可以看到這些`status`,但是一旦你點擊**delete(刪除按鈕)**,你將會看到一個報錯的頁面:

我們手動創建的role是成功,但是我們怎么給一個注冊的用戶默認的權限呢,我們這里就是想實現在新用戶注冊的時候賦予用戶ROLE_USER的角色和權限。由于Yii2-User是在`vendor\dektrium\yii2-user\models\RegistrationForm.php`這個文件里面進行創建新的用戶的,我門這里只要修改一個小地方,找到`register()`方法:
~~~
public function register()
{
if ($this->validate()) {
$user = $this->module->manager->createUser([
'email' => $this->email,
'username' => $this->username,
'password' => $this->password,
'role'=>10, // add this line User::ROLE_USER;
]);
return $user->register();
}
return false;
}
~~~
添加`'role'=>10`就可以了。
如果你想證明一下我們的權限是否正確,你可以手動修改數據庫中的role字段的數值,然后在進行修改和刪除等操作,看看是否可以正確運行。
權限控制其實可以說是Yii2的一大特色和亮點,在這里可能并沒有說得很清晰,只是簡單地實現了一些規則,有機會借助`Yii2 Advanced Template`來實現一下。
源碼會放在 Github:[](https://github.com/JellyBool/helloYii)[https://github.com/JellyBool/helloYii](https://github.com/JellyBool/helloYii)
## 下一節
下一節嘗試集成一個編輯器和做一下url的美化,內容應該會比較簡單