PHP中基于角色的访问控制

tech2023-10-20  85

There are several different approaches when it comes to managing user permissions, and each have their own positives and negatives. For example, using bit masking is extremely efficient but also limits you to 32 or 64 permissions (the number of bits in a 32- or 64-bit integer). Another approach is to use an access control list (ACL), however you can only assign permissions to objects rather than to specific or meaningful operations.

在管理用户权限方面,有几种不同的方法,每种方法各有其优点和缺点。 例如,使用位掩码非常有效,但也将您限制为32或64位权限(32位或64位整数中的位数)。 另一种方法是使用访问控制列表(ACL),但是您只能将权限分配给对象,而不能分配给特定或有意义的操作。

In this article I will discuss my personal favorite approach: role based access control (RBAC). RBAC is a model in which roles are created for various job functions, and permissions to perform certain operations are then tied to roles. A user can be assigned one or multiple roles which restricts their system access to the permissions for which they have been authorized.

在本文中,我将讨论我个人最喜欢的方法:基于角色的访问控制(RBAC)。 RBAC是一种模型,其中为各种作业功能创建了角色,然后将执行某些操作的权限与角色绑定在一起。 可以为用户分配一个或多个角色,这些角色将其系统访问权限限制为已被授权的权限。

The downside to using RBAC is that if not properly managed, your roles and permissions can easily become a chaotic mess. In a rapidly changing business environment, it can be a job in itself to keep track of assigning appropriate roles to new employees and removing them in a timely manner from former employees or those switching positions. Additionally, identifying new roles for unique job duties and revising or removing requires regular review. Failure to properly manage your roles can open the door to many security risks.

使用RBAC的不利之处在于,如果管理不当,您的角色和权限很容易变得混乱。 在瞬息万变的业务环境中,跟踪为新员工分配合适的角色并及时将其从前员工或更换职位中撤离本身可能是一项工作。 此外,要确定新角色以执行独特的工作职责并进行修订或撤职,需要定期进行审查。 无法正确管理您的角色可能会带来许多安全风险。

I will begin by discussing the necessary database tables, then I’ll create two class files: (Role.php) which will perform a few tasks specific to roles, and (PrivilegedUser.php) that will extend your existing user class. Finally I’ll walk through some examples of how you might integrate the code into your application. Role management and user management go hand in hand, and so in this article I’ll assume that you already have some type of user authentication system in place.

我将首先讨论必要的数据库表,然后创建两个类文件:( Role.php )将执行一些特定于角色的任务,而( PrivilegedUser.php )将扩展现有的用户类。 最后,我将通过一些示例说明如何将代码集成到应用程序中。 角色管理和用户管理是并驾齐驱的,因此在本文中,我将假定您已经具有某种类型的用户身份验证系统。

数据库 (Database)

You need four tables to store role and permission information: the roles table stores a role ID and role name, the permissions table stores a permission ID and description, the role_perm table associates which permissions belong to which roles, and the user_role table associates which roles are assigned to which users.

您需要四个表来存储角色和权限信息: roles表存储角色ID和角色名称, permissions表存储权限ID和描述, role_perm表关联哪些权限属于哪些角色,以及user_role表关联哪些角色分配给哪些用户。

Using this schema, you can have an unlimited number of roles and permissions and each user can be assigned multiple roles.

使用此架构,您可以拥有无​​限数量的角色和权限,并且可以为每个用户分配多个角色。

These are the CREATE TABLE statements for the database:

这些是数据库的CREATE TABLE语句:

CREATE TABLE roles ( role_id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, role_name VARCHAR(50) NOT NULL, PRIMARY KEY (role_id) ); CREATE TABLE permissions ( perm_id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, perm_desc VARCHAR(50) NOT NULL, PRIMARY KEY (perm_id) ); CREATE TABLE role_perm ( role_id INTEGER UNSIGNED NOT NULL, perm_id INTEGER UNSIGNED NOT NULL, FOREIGN KEY (role_id) REFERENCES roles(role_id), FOREIGN KEY (perm_id) REFERENCES permissions(perm_id) ); CREATE TABLE user_role ( user_id INTEGER UNSIGNED NOT NULL, role_id INTEGER UNSIGNED NOT NULL, FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (role_id) REFERENCES roles(role_id) );

Note the final table, user_role, references a users table which I have not defined here. This assumes that user_id is the primary key of your users table.

请注意,最终表user_role引用了我在此处未定义的users表。 假设user_id是users表的主键。

You don’t need to make any modifications to your users table to store role information as that information is stored separately in these new tables. Contrary to some other RBAC systems, a user here is not required to have a role by default; instead, the user simply won’t have any privileges until a role has been specifically assigned. Alternatively, it would be possible in the PrivilegedUser class to detect an empty role and respond with a default unprivileged role when required, or you could opt to write a short SQL script to copy over user IDs and initialize them by assigning a default unprivileged role.

您无需对users表进行任何修改即可存储角色信息,因为该信息分别存储在这些新表中。 与某些其他RBAC系统相反,默认情况下,不需要此处的用户扮演角色; 取而代之的是,在明确指定角色之前,用户根本没有任何特权。 或者,可以在PrivilegedUser类中检测空角色并在需要时使用默认的非特权角色进行响应,或者您可以选择编写简短SQL脚本来复制用户ID,并通过分配默认的非特权角色来对其进行初始化。

角色类别 (Role Class)

The primary focus of the Role class is to return a role object that is populated with each roles corresponding permissions. This will allow you to easily check whether a permission is available without having to perform redundant SQL queries with every request.

Role类的主要焦点是返回一个角色对象,该对象填充有每个角色对应的权限。 这将使您轻松检查权限是否可用,而不必对每个请求都执行冗余SQL查询。

Use the following code to create Role.php:

使用以下代码创建Role.php :

<?php class Role { protected $permissions; protected function __construct() { $this->permissions = array(); } // return a role object with associated permissions public static function getRolePerms($role_id) { $role = new Role(); $sql = "SELECT t2.perm_desc FROM role_perm as t1 JOIN permissions as t2 ON t1.perm_id = t2.perm_id WHERE t1.role_id = :role_id"; $sth = $GLOBALS["DB"]->prepare($sql); $sth->execute(array(":role_id" => $role_id)); while($row = $sth->fetch(PDO::FETCH_ASSOC)) { $role->permissions[$row["perm_desc"]] = true; } return $role; } // check if a permission is set public function hasPerm($permission) { return isset($this->permissions[$permission]); } }

The getRolePerms() method creates a new Role object based on a specific role ID, and then uses a JOIN clause to combine the role_perm and perm_desc tables. For each permission associated with the given role, the description is stored as the key and its value is set to true. The hasPerm() method accepts a permission description and returns the value based on the current object.

getRolePerms()方法基于特定的角色ID创建一个新的Role对象,然后使用JOIN子句组合role_perm和perm_desc表。 对于与给定角色相关联的每个权限,描述都存储为键,并且其值设置为true。 hasPerm()方法接受一个权限描述,并根据当前对象返回该值。

特权用户类别 (Privileged User Class)

By creating a new class that extends your existing user class, you can reuse your existing code logic for managing users and then add some additional methods on top of those which are geared specifically towards working with privileges.

通过创建扩展现有用户类的新类,可以重用现有代码逻辑来管理用户,然后在专门用于特权的方法之上添加一些其他方法。

Use the following code to create the file PrivilegedUser.php:

使用以下代码创建文件PrivilegedUser.php :

<?php class PrivilegedUser extends User { private $roles; public function __construct() { parent::__construct(); } // override User method public static function getByUsername($username) { $sql = "SELECT * FROM users WHERE username = :username"; $sth = $GLOBALS["DB"]->prepare($sql); $sth->execute(array(":username" => $username)); $result = $sth->fetchAll(); if (!empty($result)) { $privUser = new PrivilegedUser(); $privUser->user_id = $result[0]["user_id"]; $privUser->username = $username; $privUser->password = $result[0]["password"]; $privUser->email_addr = $result[0]["email_addr"]; $privUser->initRoles(); return $privUser; } else { return false; } } // populate roles with their associated permissions protected function initRoles() { $this->roles = array(); $sql = "SELECT t1.role_id, t2.role_name FROM user_role as t1 JOIN roles as t2 ON t1.role_id = t2.role_id WHERE t1.user_id = :user_id"; $sth = $GLOBALS["DB"]->prepare($sql); $sth->execute(array(":user_id" => $this->user_id)); while($row = $sth->fetch(PDO::FETCH_ASSOC)) { $this->roles[$row["role_name"]] = Role::getRolePerms($row["role_id"]); } } // check if user has a specific privilege public function hasPrivilege($perm) { foreach ($this->roles as $role) { if ($role->hasPerm($perm)) { return true; } } return false; } }

The first method, getByUsername(), returns an object populated with information about a specific user. A method almost identical to this will likely already exist in your user class, but you need to override it here so that the PrivilegedUser‘s methods can be called with the appropriate object. If you try to invoke a PrivilegedUser method on a User object, you will get an error stating that the method doesn’t exist.

第一种方法getByUsername()返回一个对象,该对象填充有有关特定用户的信息。 与之几乎相同的方法可能已经存在于您的用户类中,但是您需要在此处重写它,以便可以使用适当的对象调用PrivilegedUser的方法。 如果尝试在User对象上调用PrivilegedUser方法,则会收到一条错误消息,指出该方法不存在。

The second method, initRoles(), uses a JOIN to combine the user_role and roles tables to collect the roles associated with the current user’s ID. Each role is then populated with its corresponding permissions with a call to the Role class method previously created, Role::getRolePerms().

第二种方法initRoles()使用JOIN组合user_role和roles表,以收集与当前用户ID关联的角色。 然后,通过调用先前创建的Role类方法Role::getRolePerms()为其每个相应的权限填充Role::getRolePerms() 。

The final method, hasPrivilege(), accepts a permission description and returns true of the user has the permission or false otherwise.

最终方法hasPrivilege()接受许可权描述,并返回具有许可权的用户的true,否则返回false。

With the preceding two classes in place, checking if a user has a specific privilege is as simple as follows:

使用前面的两个类,检查用户是否具有特定特权很简单,如下所示:

<?php require_once "Role.php"; require_once "PrivilegedUser.php"; // connect to database... // ... session_start(); if (isset($_SESSION["loggedin"])) { $u = PrivilegedUser::getByUsername($_SESSION["loggedin"]); } if ($u->hasPrivilege("thisPermission")) { // do something }

Here the username is stored in the active session and a new PrivilegedUser object is created for that user on which the hasPrivilege() method can be called. Depending on the information in your database, your object output will look similar to the following:

此处,用户名存储在活动会话中,并为该用户创建一个新的PrivilegedUser对象,可以在该对象上调用hasPrivilege()方法。 根据数据库中的信息,对象输出将类似于以下内容:

object(PrivilegedUser)#3 (2) { ["roles":"PrivilegedUser":private]=> array(1) { ["Admin"]=> object(Role)#5 (1) { ["permissions":protected]=> array(4) { ["addUser"]=>bool(true) ["editUser"]=>bool(true) ["deleteUser"]=>bool(true) ["editRoles"]=>bool(true) } } } ["fields":"User":private]=> array(4) { ["user_id"]=>string(1) "2" ["username"]=>string(7) "mpsinas" ["password"]=>bool(false) ["email_addr"]=>string(0) "" } }

保持事情井井有条 (Keeping Things Organized)

One of the many benefits of using an OOP approach with RBAC is that it allows you to separate code logic and validation from object specific tasks. For example, you could add the following methods to your Role class to help manage role specific operations such as inserting new roles, deleting roles and so on:

将OOP方法与RBAC一起使用的许多好处之一是,它使您可以将代码逻辑和验证与特定于对象的任务分开。 例如,您可以将以下方法添加到Role类中,以帮助管理特定于角色的操作,例如插入新角色,删除角色等:

// insert a new role public static function insertRole($role_name) { $sql = "INSERT INTO roles (role_name) VALUES (:role_name)"; $sth = $GLOBALS["DB"]->prepare($sql); return $sth->execute(array(":role_name" => $role_name)); } // insert array of roles for specified user id public static function insertUserRoles($user_id, $roles) { $sql = "INSERT INTO user_role (user_id, role_id) VALUES (:user_id, :role_id)"; $sth = $GLOBALS["DB"]->prepare($sql); $sth->bindParam(":user_id", $user_id, PDO::PARAM_STR); $sth->bindParam(":role_id", $role_id, PDO::PARAM_INT); foreach ($roles as $role_id) { $sth->execute(); } return true; } // delete array of roles, and all associations public static function deleteRoles($roles) { $sql = "DELETE t1, t2, t3 FROM roles as t1 JOIN user_role as t2 on t1.role_id = t2.role_id JOIN role_perm as t3 on t1.role_id = t3.role_id WHERE t1.role_id = :role_id"; $sth = $GLOBALS["DB"]->prepare($sql); $sth->bindParam(":role_id", $role_id, PDO::PARAM_INT); foreach ($roles as $role_id) { $sth->execute(); } return true; } // delete ALL roles for specified user id public static function deleteUserRoles($user_id) { $sql = "DELETE FROM user_role WHERE user_id = :user_id"; $sth = $GLOBALS["DB"]->prepare($sql); return $sth->execute(array(":user_id" => $user_id)); }

Likewise, you could add onto your PrivilegedUser class with similar methods:

同样,您可以使用类似的方法将其添加到PrivilegedUser类中:

// check if a user has a specific role public function hasRole($role_name) { return isset($this->roles[$role_name]); } // insert a new role permission association public static function insertPerm($role_id, $perm_id) { $sql = "INSERT INTO role_perm (role_id, perm_id) VALUES (:role_id, :perm_id)"; $sth = $GLOBALS["DB"]->prepare($sql); return $sth->execute(array(":role_id" => $role_id, ":perm_id" => $perm_id)); } // delete ALL role permissions public static function deletePerms() { $sql = "TRUNCATE role_perm"; $sth = $GLOBALS["DB"]->prepare($sql); return $sth->execute(); }

Because permissions are tied directly to the application’s underlying code logic, new permissions should be manually inserted into or deleted from the database as required. Roles on the other hand can be easily created, modified or deleted via an administration interface.

由于权限直接与应用程序的基础代码逻辑绑定在一起,因此应根据需要将新权限手动插入数据库或从数据库中删除。 另一方面,可以通过管理界面轻松创建,修改或删除角色。

The more roles and permissions you have the more difficult things will be to manage; keeping the lists minimal is important, but sometimes the contrary is unavoidable. I can only advise that you use your best judgement and try not to get carried away.

角色和权限越多,管理起来就越困难。 使列表保持最小很重要,但有时不可避免。 我只能建议您使用自己的最佳判断力,并尽量不要气carried。

摘要 (Summary)

You now have an understanding of role based access control and how to implement roles and permissions into an existing application. Furthermore, you’ve learned some tips to help manage your roles and to keep things well organized. As always, I encourage you to experiment and ask questions if you get stuck. We’re all here to learn from each other and I am happy to help when I can!

现在,您将了解基于角色的访问控制以及如何在现有应用程序中实现角色和权限。 此外,您还学到了一些技巧,可帮助您管理角色并保持事物的井井有条。 一如既往,我鼓励您尝试一下,问一下是否被卡住。 我们都在这里互相学习,我们将竭诚为您服务!

Image via PILart / Shutterstock

图片来自PILart / Shutterstock

翻译自: https://www.sitepoint.com/role-based-access-control-in-php/

最新回复(0)