2 关注者

授权

授权是验证用户是否有足够权限执行某个操作的过程。Yii 提供两种授权方法:访问控制过滤器 (ACF) 和基于角色的访问控制 (RBAC)。

访问控制过滤器

访问控制过滤器 (ACF) 是一种简单的授权方法,它以 yii\filters\AccessControl 的形式实现,最适合那些只需要一些简单访问控制的应用程序。顾名思义,ACF 是一个操作 过滤器,可以在控制器或模块中使用。当用户请求执行操作时,ACF 会检查一个 访问规则 列表,以确定用户是否被允许访问所请求的操作。

以下代码展示了如何在 site 控制器中使用 ACF

use yii\web\Controller;
use yii\filters\AccessControl;

class SiteController extends Controller
{
    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::class,
                'only' => ['login', 'logout', 'signup'],
                'rules' => [
                    [
                        'allow' => true,
                        'actions' => ['login', 'signup'],
                        'roles' => ['?'],
                    ],
                    [
                        'allow' => true,
                        'actions' => ['logout'],
                        'roles' => ['@'],
                    ],
                ],
            ],
        ];
    }
    // ...
}

在上面的代码中,ACF 作为行为附加到 site 控制器。这是使用操作过滤器的典型方法。only 选项指定 ACF 应该只应用于 loginlogoutsignup 操作。site 控制器中的所有其他操作都不受访问控制的限制。rules 选项列出了 访问规则,它表示如下

  • 允许所有访客(尚未进行身份验证)用户访问 loginsignup 操作。roles 选项包含一个问号 ?,它是一个特殊标记,代表“访客用户”。
  • 允许已进行身份验证的用户访问 logout 操作。@ 字符是另一个特殊标记,代表“已进行身份验证的用户”。

ACF 通过从上到下逐一检查访问规则来执行授权检查,直到找到与当前执行上下文匹配的规则。然后使用匹配规则的 allow 值来判断用户是否被授权。如果没有任何规则匹配,则意味着用户没有被授权,ACF 会停止进一步的操作执行。

当 ACF 确定用户没有被授权访问当前操作时,它默认会采取以下措施

你可以通过配置 yii\filters\AccessControl::$denyCallback 属性来定制此行为,如下所示

[
    'class' => AccessControl::class,
    ...
    'denyCallback' => function ($rule, $action) {
        throw new \Exception('You are not allowed to access this page');
    }
]

访问规则 支持许多选项。以下是对支持选项的总结。你也可以扩展 yii\filters\AccessRule 来创建自己的自定义访问规则类。

  • allow:指定这是一个“允许”还是“拒绝”规则。

  • actions: 指定此规则匹配的操作。这应该是一个操作 ID 数组。比较区分大小写。如果此选项为空或未设置,则意味着此规则适用于所有操作。

  • controllers: 指定此规则匹配的控制器。这应该是一个控制器 ID 数组。每个控制器 ID 都会以模块 ID(如果有)作为前缀。比较区分大小写。如果此选项为空或未设置,则意味着此规则适用于所有控制器。

  • roles: 指定此规则匹配的用户角色。识别两种特殊角色,它们通过 yii\web\User::$isGuest 检查。

    • ?: 匹配访客用户(尚未进行身份验证)。
    • @: 匹配经过身份验证的用户。

    使用其他角色名称将触发对 yii\web\User::can() 的调用,这需要启用 RBAC(将在下一小节中介绍)。如果此选项为空或未设置,则意味着此规则适用于所有角色。

  • roleParams: 指定将传递给 yii\web\User::can() 的参数。请参阅下面介绍 RBAC 规则的部分,了解如何使用它。如果此选项为空或未设置,则不会传递任何参数。

  • ips: 指定此规则匹配的 客户端 IP 地址。IP 地址可以在末尾包含通配符 *,以便它匹配具有相同前缀的 IP 地址。例如,'192.168.*' 匹配 '192.168.' 段中的所有 IP 地址。如果此选项为空或未设置,则意味着此规则适用于所有 IP 地址。

  • verbs: 指定此规则匹配的请求方法(例如 GETPOST)。比较不区分大小写。

  • matchCallback: 指定应调用的 PHP 可调用函数,以确定是否应应用此规则。

  • denyCallback: 指定当此规则拒绝访问时应调用的 PHP 可调用函数。

下面是一个示例,展示了如何使用 matchCallback 选项,它允许您编写任意访问检查逻辑。

use yii\filters\AccessControl;

class SiteController extends Controller
{
    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::class,
                'only' => ['special-callback'],
                'rules' => [
                    [
                        'actions' => ['special-callback'],
                        'allow' => true,
                        'matchCallback' => function ($rule, $action) {
                            return date('d-m') === '31-10';
                        }
                    ],
                ],
            ],
        ];
    }

    // Match callback called! This page can be accessed only each October 31st
    public function actionSpecialCallback()
    {
        return $this->render('happy-halloween');
    }
}

基于角色的访问控制 (RBAC)

基于角色的访问控制 (RBAC) 提供了一种简单但功能强大的集中式访问控制。有关将 RBAC 与其他更传统的访问控制方案进行比较的详细信息,请参阅 维基百科

Yii 实现了一个通用层次结构 RBAC,遵循 NIST RBAC 模型。它通过 authManager 应用程序组件 提供 RBAC 功能。

使用 RBAC 包括两部分工作。第一部分是建立 RBAC 授权数据,第二部分是在需要的地方使用授权数据执行访问检查。

为了便于我们接下来进行说明,我们将首先介绍一些基本的 RBAC 概念。

基本概念

角色代表一组权限(例如创建帖子、更新帖子)。一个角色可以分配给一个或多个用户。要检查用户是否具有指定的权限,我们可以检查用户是否被分配了包含该权限的角色。

与每个角色或权限相关联的可能存在一个规则。规则代表在访问检查期间将执行的一段代码,以确定相应的角色或权限是否适用于当前用户。例如,“更新帖子”权限可能有一个规则,检查当前用户是否为帖子创建者。在访问检查期间,如果用户不是帖子创建者,则他/她将被视为没有“更新帖子”权限。

角色和权限都可以组织在层次结构中。特别是,一个角色可能包含其他角色或权限;一个权限可能包含其他权限。Yii 实现了一个偏序层次结构,其中包括更特殊的层次结构。虽然一个角色可以包含一个权限,但反之则不为true

配置 RBAC

在我们开始定义授权数据和执行访问检查之前,我们需要配置 authManager 应用程序组件。Yii 提供两种类型的授权管理器:yii\rbac\PhpManageryii\rbac\DbManager。前者使用 PHP 脚本文件存储授权数据,而后者将授权数据存储在数据库中。如果您的应用程序不需要非常动态的角色和权限管理,您可能会考虑使用前者。

使用 PhpManager

以下代码展示了如何在应用程序配置中使用 yii\rbac\PhpManager 类配置 authManager

return [
    // ...
    'components' => [
        'authManager' => [
            'class' => 'yii\rbac\PhpManager',
        ],
        // ...
    ],
];

现在可以通过 \Yii::$app->authManager 访问 authManager

默认情况下,yii\rbac\PhpManager 将 RBAC 数据存储在 @app/rbac 目录下的文件中。如果需要在线更改权限层次结构,请确保该目录及其中的所有文件都可由 Web 服务器进程写入。

使用 DbManager

以下代码展示了如何在应用程序配置中使用 yii\rbac\DbManager 类配置 authManager

return [
    // ...
    'components' => [
        'authManager' => [
            'class' => 'yii\rbac\DbManager',
            // uncomment if you want to cache RBAC items hierarchy
            // 'cache' => 'cache',
        ],
        // ...
    ],
];

注意:如果您使用的是 yii2-basic-app 模板,则有一个 config/console.php 配置文件,其中需要在 config/web.php 中额外声明 authManager。如果是 yii2-advanced-app,则应仅在 common/config/main.php 中声明 authManager 一次。

DbManager 使用四个数据库表来存储其数据

  • itemTable: 用于存储授权项目的表。默认为 "auth_item"。
  • itemChildTable: 用于存储授权项目层次结构的表。默认为 "auth_item_child"。
  • assignmentTable: 用于存储授权项目分配的表。默认为 "auth_assignment"。
  • ruleTable: 用于存储规则的表。默认为 "auth_rule"。

在您继续之前,您需要在数据库中创建这些表。为此,您可以使用存储在 @yii/rbac/migrations 中的迁移

yii migrate --migrationPath=@yii/rbac/migrations

分离的迁移 部分中阅读有关使用来自不同命名空间的迁移的更多信息。

现在可以通过 \Yii::$app->authManager 访问 authManager

构建授权数据

构建授权数据就是关于以下任务

  • 定义角色和权限;
  • 建立角色和权限之间的关系;
  • 定义规则;
  • 将规则与角色和权限关联起来;
  • 将角色分配给用户。

根据授权灵活性要求,以上任务可以以不同的方式完成。如果您的权限层次结构旨在仅由开发人员更改,则可以使用迁移或控制台命令。迁移的优点是它可以与其他迁移一起执行。控制台命令的优点是您可以很好地概述代码中的层次结构,而不是将其散布在多个迁移中。

无论哪种方式,您最终都会得到以下 RBAC 层次结构

Simple RBAC hierarchy

如果您需要动态地形成权限层次结构,则需要一个 UI 或一个控制台命令。用于构建层次结构本身的 API 将不会有任何不同。

使用迁移

您可以使用 迁移 来通过 authManager 提供的 API 初始化和修改层次结构。

使用 ./yii migrate/create init_rbac 创建新的迁移,然后实现创建层次结构

<?php
use yii\db\Migration;

class m170124_084304_init_rbac extends Migration
{
    public function up()
    {
        $auth = Yii::$app->authManager;

        // add "createPost" permission
        $createPost = $auth->createPermission('createPost');
        $createPost->description = 'Create a post';
        $auth->add($createPost);

        // add "updatePost" permission
        $updatePost = $auth->createPermission('updatePost');
        $updatePost->description = 'Update post';
        $auth->add($updatePost);

        // add "author" role and give this role the "createPost" permission
        $author = $auth->createRole('author');
        $auth->add($author);
        $auth->addChild($author, $createPost);

        // add "admin" role and give this role the "updatePost" permission
        // as well as the permissions of the "author" role
        $admin = $auth->createRole('admin');
        $auth->add($admin);
        $auth->addChild($admin, $updatePost);
        $auth->addChild($admin, $author);

        // Assign roles to users. 1 and 2 are IDs returned by IdentityInterface::getId()
        // usually implemented in your User model.
        $auth->assign($author, 2);
        $auth->assign($admin, 1);
    }
    
    public function down()
    {
        $auth = Yii::$app->authManager;

        $auth->removeAll();
    }
}

如果您不想硬编码哪些用户具有某些角色,请不要在迁移中放置 ->assign() 调用。相反,请创建 UI 或控制台命令来管理分配。

可以通过使用 yii migrate 来应用迁移。

使用控制台命令

如果您的权限层次结构根本不更改,并且您有固定数量的用户,则可以创建一个 -控制台命令,该命令将通过 authManager 提供的 API 一次初始化授权数据

<?php
namespace app\commands;

use Yii;
use yii\console\Controller;

class RbacController extends Controller
{
    public function actionInit()
    {
        $auth = Yii::$app->authManager;
        $auth->removeAll();
        
        // add "createPost" permission
        $createPost = $auth->createPermission('createPost');
        $createPost->description = 'Create a post';
        $auth->add($createPost);

        // add "updatePost" permission
        $updatePost = $auth->createPermission('updatePost');
        $updatePost->description = 'Update post';
        $auth->add($updatePost);

        // add "author" role and give this role the "createPost" permission
        $author = $auth->createRole('author');
        $auth->add($author);
        $auth->addChild($author, $createPost);

        // add "admin" role and give this role the "updatePost" permission
        // as well as the permissions of the "author" role
        $admin = $auth->createRole('admin');
        $auth->add($admin);
        $auth->addChild($admin, $updatePost);
        $auth->addChild($admin, $author);

        // Assign roles to users. 1 and 2 are IDs returned by IdentityInterface::getId()
        // usually implemented in your User model.
        $auth->assign($author, 2);
        $auth->assign($admin, 1);
    }
}

注意:如果您使用的是高级模板,则需要将 RbacController 放入 console/controllers 目录中,并将命名空间更改为 console\controllers

上面的命令可以通过以下方式从控制台执行

yii rbac/init

如果您不想硬编码哪些用户具有某些角色,请不要将 ->assign() 调用放入命令中。相反,请创建 UI 或控制台命令来管理分配。

将角色分配给用户

作者可以创建帖子,管理员可以更新帖子并执行作者可以执行的所有操作。

如果您的应用程序允许用户注册,则需要在注册新用户后将其分配角色。例如,为了让所有注册的用户在您的高级项目模板中成为作者,您需要修改 frontend\models\SignupForm::signup() 如下所示

public function signup()
{
    if ($this->validate()) {
        $user = new User();
        $user->username = $this->username;
        $user->email = $this->email;
        $user->setPassword($this->password);
        $user->generateAuthKey();
        $user->save(false);

        // the following three lines were added:
        $auth = \Yii::$app->authManager;
        $authorRole = $auth->getRole('author');
        $auth->assign($authorRole, $user->getId());

        return $user;
    }

    return null;
}

对于需要使用动态更新的授权数据的复杂访问控制的应用程序,可能需要使用 authManager 提供的 API 开发特殊的用户界面(即管理面板)。

使用规则

如前所述,规则为角色和权限添加了额外的约束。规则是扩展自 yii\rbac\Rule 的类。它必须实现 execute() 方法。在我们之前创建的层次结构中,作者无法编辑自己的帖子。让我们修复它。首先,我们需要一个规则来验证用户是否是帖子作者

namespace app\rbac;

use yii\rbac\Rule;
use app\models\Post;

/**
 * Checks if authorID matches user passed via params
 */
class AuthorRule extends Rule
{
    public $name = 'isAuthor';

    /**
     * @param string|int $user the user ID.
     * @param Item $item the role or permission that this rule is associated with
     * @param array $params parameters passed to ManagerInterface::checkAccess().
     * @return bool a value indicating whether the rule permits the role or permission it is associated with.
     */
    public function execute($user, $item, $params)
    {
        return isset($params['post']) ? $params['post']->createdBy == $user : false;
    }
}

上面的规则检查 post 是否由 $user 创建。我们将在之前使用的命令中创建一个特殊的权限 updateOwnPost

$auth = Yii::$app->authManager;

// add the rule
$rule = new \app\rbac\AuthorRule;
$auth->add($rule);

// add the "updateOwnPost" permission and associate the rule with it.
$updateOwnPost = $auth->createPermission('updateOwnPost');
$updateOwnPost->description = 'Update own post';
$updateOwnPost->ruleName = $rule->name;
$auth->add($updateOwnPost);

// "updateOwnPost" will be used from "updatePost"
$auth->addChild($updateOwnPost, $updatePost);

// allow "author" to update their own posts
$auth->addChild($author, $updateOwnPost);

现在我们有了以下层次结构

RBAC hierarchy with a rule

访问检查

准备好授权数据后,访问检查就像调用 yii\rbac\ManagerInterface::checkAccess() 方法一样简单。由于大多数访问检查都是关于当前用户的,为了方便起见,Yii 提供了一个快捷方法 yii\web\User::can(),它可以像以下这样使用

if (\Yii::$app->user->can('createPost')) {
    // create post
}

如果当前用户是 Jane,ID=1,我们从 createPost 开始,并尝试到达 Jane

Access check

为了检查用户是否可以更新帖子,我们需要传递一个额外的参数,该参数是之前描述的 AuthorRule 所需的

if (\Yii::$app->user->can('updatePost', ['post' => $post])) {
    // update post
}

以下是如果当前用户是 John 的情况

Access check

我们从 updatePost 开始,并通过 updateOwnPost。为了通过访问检查,AuthorRule 应该从其 execute() 方法中返回 true。该方法从 can() 方法调用中接收其 $params,因此其值为 ['post' => $post]。如果一切正常,我们将到达分配给 John 的 author

Jane 的情况比较简单,因为她是管理员。

Access check

在你的控制器中,有几种方法可以实现授权。如果你想要细粒度的权限来区分添加和删除的访问权限,那么你需要检查每个操作的访问权限。你可以在每个操作方法中使用上面的条件,也可以使用 yii\filters\AccessControl

public function behaviors()
{
    return [
        'access' => [
            'class' => AccessControl::class,
            'rules' => [
                [
                    'allow' => true,
                    'actions' => ['index'],
                    'roles' => ['managePost'],
                ],
                [
                    'allow' => true,
                    'actions' => ['view'],
                    'roles' => ['viewPost'],
                ],
                [
                    'allow' => true,
                    'actions' => ['create'],
                    'roles' => ['createPost'],
                ],
                [
                    'allow' => true,
                    'actions' => ['update'],
                    'roles' => ['updatePost'],
                ],
                [
                    'allow' => true,
                    'actions' => ['delete'],
                    'roles' => ['deletePost'],
                ],
            ],
        ],
    ];
}

如果所有 CRUD 操作都一起管理,那么使用一个单独的权限,例如 managePost,并在 yii\web\Controller::beforeAction() 中检查它是一个好主意。

在上面的例子中,没有为访问操作指定的角色传递参数,但对于 updatePost 权限,我们需要传递一个 post 参数才能正常工作。你可以通过在访问规则上指定 roleParams 来将参数传递给 yii\web\User::can()

[
    'allow' => true,
    'actions' => ['update'],
    'roles' => ['updatePost'],
    'roleParams' => function() {
        return ['post' => Post::findOne(['id' => Yii::$app->request->get('id')])];
    },
],

在上面的例子中,roleParams 是一个闭包,它会在检查访问规则时被评估,所以模型只有在需要时才会被加载。如果创建角色参数是一个简单的操作,你也可以直接指定一个数组,如下所示

[
    'allow' => true,
    'actions' => ['update'],
    'roles' => ['updatePost'],
    'roleParams' => ['postId' => Yii::$app->request->get('id')],
],

使用默认角色

默认角色是隐式分配给所有用户的角色。不需要调用 yii\rbac\ManagerInterface::assign(),授权数据也不包含其分配信息。

默认角色通常与一个规则相关联,该规则决定角色是否适用于正在检查的用户。

默认角色通常用于已经有一些角色分配的应用程序。例如,一个应用程序可能在其用户表中有一个 "group" 列来表示每个用户属于哪个权限组。如果每个权限组可以映射到一个 RBAC 角色,你可以使用默认角色功能自动将每个用户分配到一个 RBAC 角色。让我们用一个例子来说明如何做到这一点。

假设在用户表中,你有一个 group 列,使用 1 代表管理员组,2 代表作者组。你计划使用两个 RBAC 角色 adminauthor 来分别代表这两个组的权限。你可以按照如下方式设置 RBAC 数据,首先创建一个类

namespace app\rbac;

use Yii;
use yii\rbac\Rule;

/**
 * Checks if user group matches
 */
class UserGroupRule extends Rule
{
    public $name = 'userGroup';

    public function execute($user, $item, $params)
    {
        if (!Yii::$app->user->isGuest) {
            $group = Yii::$app->user->identity->group;
            if ($item->name === 'admin') {
                return $group == 1;
            } elseif ($item->name === 'author') {
                return $group == 1 || $group == 2;
            }
        }
        return false;
    }
}

然后,创建你自己的命令/迁移,如 上一节所述

$auth = Yii::$app->authManager;

$rule = new \app\rbac\UserGroupRule;
$auth->add($rule);

$author = $auth->createRole('author');
$author->ruleName = $rule->name;
$auth->add($author);
// ... add permissions as children of $author ...

$admin = $auth->createRole('admin');
$admin->ruleName = $rule->name;
$auth->add($admin);
$auth->addChild($admin, $author);
// ... add permissions as children of $admin ...

请注意,在上面,因为 "author" 被添加为 "admin" 的子级,当你实现规则类的 execute() 方法时,也需要遵守这种层次结构。这就是为什么当角色名称为 "author" 时,execute() 方法将在用户组为 1 或 2(表示用户在 "admin" 组或 "author" 组中)时返回 true

接下来,通过在 yii\rbac\BaseManager::$defaultRoles 中列出两个角色来配置 authManager

return [
    // ...
    'components' => [
        'authManager' => [
            'class' => 'yii\rbac\PhpManager',
            'defaultRoles' => ['admin', 'author'],
        ],
        // ...
    ],
];

现在,如果你执行访问检查,adminauthor 两个角色都将通过评估与它们关联的规则来进行检查。如果规则返回 true,则表示该角色适用于当前用户。根据上面的规则实现,这意味着如果用户的 group 值为 1,则 admin 角色将适用于该用户;如果 group 值为 2,则 author 角色将适用于该用户。

发现错别字或认为此页面需要改进?
在 github 上编辑它 !