

上一篇文章所实现的功能还比较简单，可以发一条状态，但是不知道你注意到没有，如果是没有注册的用户也可以使用我们的应用(类似小微博)来发状态，这是不符合情理的。正确的做法是在用户没有注册，登录之前，我们甚至都不应该给没有注册的用户看到我们创建状态的页面，即是`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`来实现一下。

## 下一节

下一节尝试集成一个编辑器和做一下url的美化，内容应该会比较简单


















