An in-depth analysis of the rights control _php example of Yii admin

Source: Internet
Author: User
Tags aliases anonymous key string php session php and php foreach redis yii

When it comes to CMS, the most needed thing is permission control, especially some complex scenes, multiple users, multiple roles, multiple departments, child parent view and so on. Recently in the development of an offline sales of Dongdong, this system is divided into the administrator side, the province of the end, the client, the store end, sales end, department end, the department boss under the molecular department, etc., disgusting demand. Our project uses the YII framework development, YII in the PHP session is still more popular, although said Laravel now rampant, but some departments of some teams or the use of YII framework, such as us.

I was just touching the framework of Yii, and at first it was awkward for this component-oriented framework. At that time to write their own permissions, to create their own permission table, association table, etc., but learning to use YII development documents, found that there is a privilege control RBAC, with the help of yii-admin can achieve the perfect permissions, Menu control. This blog is divided into two departments, the first part I will describe how to build permissions management includes: Installation Yii-admin, create a permission table, the use of permissions to control the menu and access rights and other basic operations, this part of the general say, want to see more detailed steps can refer to this more detailed explanation: http:// Www.manks.top/tag/rbac.html, after all, build and use is not difficult, just follow the steps. The second part I will explain my own understanding, including: Menu optimization, sub-page navigation selective highlighting, role display menu, permission detection improvements.

First, the construction of yii-admin related

1. Build Yii-admin

First you should install a yii mine, because Yii-admin is based on the YII framework, no frame to play hair Ah! You can download the source directly on the GitHub

Yii2:https://github.com/yiisoft/yii2

Yii2-admin:https://github.com/mdmsoft/yii2-admin

Of course you can use composer to install, so it's best if you install Yii, you can switch to the project directory and execute the following command directly:

PHP Composer.phar require mdmsoft/yii2-admin "~2.0"
php composer.phar Update

Then the configuration to add yii-admin configuration items, the value of the note is that if the yii2-admin configuration in the common directory is the global effective, then you execute the command console when the error, so you should control the rights of the Web module, we do not use the advanced template, this project, So you can simply write the configuration in the web.php below config, configured as follows:

Define aliases First:

' Aliases ' => [
' @mdm/admin ' => ' @vendor/mdmsoft/yii2-admin ',
],

To add the admin component to the modules:

' Admin ' => [
' class ' => ' Mdm\admin\module ',
' layout ' => ' @app/views/layouts/main_nifty ',/ Yii2-admin navigation menu
],

Add Add AuthManager Configuration entry:

It is to be emphasized that the AuthManager components in Yii have Phpmanager and Dbmanager two ways, which are distinguished, Phpmanager Save the permission relationship in a file, Dbmanager, and save the permission relationship in the database. We adopt a way of keeping it in the database.

' AuthManager ' => [
' class ' => ' Yii\rbac\dbmanager ',//or use ' Yii\rbac\dbmanager '
],

Add as Access:

' As Access ' => [
' class ' => ' Mdm\admin\components\accesscontrol ',
' allowactions ' => [
//Add or Remove allowed actions to this list
//' admin/* ',
//' * ',
' site/* ',
' api/* ',
]

What needs to be said is that the unknown should not be misplaced, as shown in the following figure:

2. Configuration Database Permission table

This step does not have to write on its own, the command line to switch to the YII2 directory, to execute the following command, create RBAC required tables, but the database needs to create its own name is: Yii2basic, if you want to execute the command, You just have to configure the configuration file in the console.php also write a copy, if the implementation is not successful, you can generate a datasheet script to take out their own implementation.

Yii migrate--migrationpath= @yii/rbac/migrations
yii migrate--migrationpath= @mdm/admin/migrations

If execution succeeds in generating 5 tables, you will need a user table that you can add yourself

Menus//Menu tables

Auth_rule//Rules table

Auth_item_child//role-appropriate permissions, parent role, child permission name

Auth_item//Roles, permission tables, type=1 represent roles, type=2 Express permissions

Auth_assignment//Role and User correspondence table

If all successful, then access to Index.php?r=admin can see the control of the permissions of the visual page, if the error, you seriously look at the cause of the error, is basically configured incorrectly. When configured, you will not have permission to access other pages, and then you can modify the allowactions in as access, which is useful when developing APIs or shared modules because these pages do not require permission to be controlled.

The Rights control page is as follows:

3, the Menu control

To carry out the menu control, you need to use the table just created the menu table, the left side of the navigation according to our design should be able to control through the permissions, write dead navigation can not achieve the goal, extensible line is not strong, so the menu control must support.

Note that if you use your layout in the background frame, you need to specify that our project is, we have our own layout, add the admin component when added:

' Layout ' => ' @app/views/layouts/main_nifty ',

Then we operate the menu list. Add a menu item, and then open the layout file, in fact, the logic to get the menu has been written, in Menuhelper, add namespace Mdm\admin\components\menuhelper; Then log off the original navigation, add the following code, basically you can achieve permissions-user-navigation control.

Echo Nav::widget (
[
"Encodelabels" => false,
"Options" => ["Class" => "Sidebar-menu"],
" Items "=> menuhelper::getassignedmenu (Yii:: $app->user->id),
]
);

All right, finally, take a look at this page:

Second, yii-admin optimization and rewriting

In the use of the process, yii-admin implementation of navigation rights control is far from satisfying our needs, and, this component trial development, each operation is completely independent, for example, check permissions, access to the menu, access to user information, Each operation needs to execute SQL to perform the following normal check permissions and get the SQL execution process of the menu. In fact, this process is extremely time-consuming, when the user volume is more, the menu is relatively large, the data in the permission table is very many times is not to do so, using our own SQL detection tools can be seen, the process executed 20 of the many SQL statements:

As can be seen in the figure, the permission check involves 14 of SQL queries, the menu involves 5 SQL queries, so many SQL execution once the line thing has nothing to say. Yii-admin This component provides convenient permission control, menu control, but the performance above we disagree. See the source you know, this component in my opinion is a relatively high decoupling component, each of the components can be used separately, which requires that each operation must have its own independent database source, it is necessary to execute SQL every time to fetch the desired value, in the middle of the use of a table query such as SQL, In fact, 10 SQL to do the function, in the coupling of the internet situation, a SQL is done.

People like me can't tolerate so many unrelated SQL execution, so I have modified the Yii-admin permission section on the root of the source, the modification is my own thinking, not necessarily right, not necessarily suitable for all the scenes, the following is written to share with you.

1, the menu optimization

We can roughly execute more than 5 SQL by looking at the menu's build process. This is OK, I did not do the optimization on the SQL, because our menu is to correspond to different roles and child parent relationship, on the basis of the original I added a type to distinguish is that the role can see this menu, What kind of role does one level correspond to the level of a menu display? This way, the administrator, save the user, the customer will render a different menu. Users at different levels will see different menus even if they are configured with the same permissions.

Our optimization is to cache the generated data for the menu, our menu is customized, not in the initial configuration of the nav::widget to present, but our own cycle level relationship, so although the trouble, but can be very good to extract the menu we need not a logic, such as: The automatic generation of bread crumbs, You can extract the menu each time the label, and then such as child pages, different controllers have left navigation highlighting, the following is code, PHP and HTML mixed, will be slowly extracted.

<ul class= "Nav nav-list" > <?php $idx =; $request _url = '/'. $mod _id. '/' . $con _id. '/' . $act _id.
'/'; foreach ($menus _new[' list '] as $label => $menu):?> <?php if (Empty ($menu [' label ']) && empty ($menu [' url '] ] {Continue}?> <?php if (!isset ($menu [' Items ')):?> <li class= "<?php if (isset ($menu [' OpenURL ']) &A mp;& strstr ($menu [' OpenURL '], $request _url)) {echo ' active '; $breadcrumb [] = $menu [' label '];?> ' > <a href= "<?php echo $menu [' URL '] []?>" > <i class= "Menu-icon fa fa-<?php echo $menu [' icon ']?>" ></i> &L T;span class= "Menu-text" > <?php echo $menu [' label ']?> </span> </a> <b class= "Arrow" ></b&
Gt </li> <?php else:?> <li class= "<?php if Isset ($menu [' OpenURL ']) && strstr ($menu [' OpenURL '], $request _url)) {echo ' open '; $breadcrumb [] = $menu [' label '];}?> "> <a href=" index.html "data-target=" # multi-cols-<?php echo $idx?> "Class= "Dropdown-toggle" > <i class= "Menu-icon fa fa-<?php echo $menu [' icon ']?>" ></i> <span class= " Menu-text "> <?php echo $menu [' label ']?> </span> <b class=" arrow fa Fa-angle-down "></b> </ a> <b class= "Arrow" ></b> <ul id= "multi-cols-<?php echo $idx?>" class= "submenu" > <?php foreach ($menu [' Items '] as $label => $menu):?> <?php if (empty ($menu) | |!is_array ($MENU)) {continue;} if (!is Set ($menu [' Items ']):?> <li class= "<?php if Isset ($menu [' OpenURL ']) && strstr ($menu [' OpenURL '], $ Request_url) {echo ' active '; $breadcrumb [] = $menu [' label '];}?> "> <a href=" <?php echo $menu [' URL '] []?> "> <i class=" menu-icon fa fa-caret-right ></i> <?php echo $menu [' label ']?> </a> <b class= " Arrow "></b> </li> <?php else:?> <li class=" <?php if (isset ($menu [' OpenURL ']) && Strstr ($menu [' OpenURL '], $request _url)) {echo' Open ';
$breadcrumb [] = $menu [' label ']; }?> "> <a href=" # "class=" Dropdown-toggle "> <i class=" menu-icon fa fa-caret-right "></i> PHP echo $menu [' label ']?> <b class= "arrow fa Fa-angle-down" ></b> </a> <b class= "Arrow" ></  b> <ul class= "submenu" > <?php foreach ($menu [' Items '] as $label => $url):?> <?php if (empty ($url) | | !is_array ($url)) {continue}?> <li class= "<?php if Isset ($url [' OpenURL ']) && strstr ($url [' OpenURL '] , $request _url)) {echo ' active '; $breadcrumb [] = $url [' label '];}?> "> <a href=" <?php echo $url [' URL '] []? ;" > <i class= "menu-icon fa fa-caret-right" ></i> <?php echo $url [' label ']?> </a> <b class= "ar  Row "></b> </li> <?php endforeach?> </ul> </li> <?php endif?> <?php Endforeach ?> </ul> </li> <?php endif?> <?php $idx + +?> <?php endforeach?> </ul>

This navigation is my own changed a lot of version to sum up for our own scheme, which breadcrumb is to control the display of bread crumbs, there is time I will draw away from PHP. I introduced the menu optimization, now only completed the first step, the menu display, when it comes to optimization I use the cache menu data strategy, is the cache above the $menus_new[' list ', strategy as follows:

This policy uses role caching data, is to use the permissions of each role plus the UID and environment configuration MD5 after the generation of key, taking into account the user more than each user is cached words too much overhead, and the user the same permissions of the more, special permissions can be special treatment, so that save a lot of duplication of data, The environment configuration is to differentiate the online data and test data, which is convenient for us to debug.

Expiration mechanism: More important is the cache expiration mechanism, the cache has but when the menu or permissions change to update the cache, here we introduced the concept of a version, can do the minimum cost of cache changes. Like menu changes, all navigation should be modified, here we add a navigation version of the variable in the Redis, each read into the cache will first determine whether this version of the cache and its own storage version is consistent, if the consistent proof that the navigation has not changed, if the inconsistency that the menu has been modified, the navigation has expired, The same role needs to be cached again, as long as one person updates the navigation, and the next time they come in, they will have access to the latest navigation (unified role). This global Redis variable automatically adds 1 to the navigation changes and permission changes. Guaranteed version of the change, so if there are 4 types of roles, tens of thousands of users, the actual data modification occurs only 4 times (actually more than this, than the same as a role of different permissions, then his corresponding Redis key is not the same, It needs to fetch its own cache). The specific code implementation is as follows:

$user _id = Yii:: $app->user->id;
$breadcrumb = [];
$menus _new[' list '] = Menuhelper::getassignedmenu ($user _id);
$redis _key = Menuhelper::getmenukeybyuserid ($user _id);
$redis _menu = Yii:: $app->redis->get ($redis _key);
$redis _varsion = GetVersion (); if (!empty ($redis _menu)) {$menus _new = Json_decode ($redis _menu, true) $old _version = isset ($menus _new[' version '])? $me
nus_new[' version ']: '; Determine the version number of the menu to facilitate timely updating of the cache if (!isset ($menus _new[' list ') | | | empty ($old _version) | | intval ($old _version)!= $redis _varsion) {$
Menus_new = GetMenu ($user _id, $redis _varsion, $redis _key);  $log = Json_encode ([' user_id ' => $user _id, ' varsion ' => $redis _varsion, ' Redis_key ' => $redis _key, ' value ' =>
$menus _new]);
Writelog ($log, ' update_menu ');
} else {$menus _new = GetMenu ($user _id, $redis _varsion, $redis _key);} function GetMenu ($user _id, $varsion, $redis _key)
{$menus _new[' list '] = Menuhelper::getassignedmenu ($user _id); $menus _new[' version '] = $varsion; Yii:: $app->redis->set ($rEdis_key, Json_encode ($menus _new));
Yii:: $app->redis->expire ($redis _key, 300);
return $menus _new; //Set Update key to make it easy to update Redis function getversion () {$version _key = Yii:: $app->params[' Redis_key ' [' Menu_prefix ']. MD5 ( Yii:: $app->params[' Redis_key '] [' menu_version '].
Yii:: $app->db->dsn);
$version _val = Yii:: $app->redis->get ($version _key); return empty ($version _val)?
1: $version _val; The logic for generating key and updating key is as follows:/** * Get menu one user by the ID * @param $user _id * @return key string/public static function get Menukeybyuserid ($user _id) {if empty ($user _id)) {return false;} $list = (new \yii\db\query ())->select (' * * ')->fro
M (' * * * ')->where ([' user_id ' => $user _id])->all ();
if (empty ($list)) {return false;} $role _str = '; foreach ($list as $key => $value) {$role _str. = $value [' Item_name '];} $redis _key = Yii:: $app->params[' key ']. MD5 ($role _str.
Yii:: $app->db->dsn);
return $redis _key; /** * Modify Menu update status, update Redis */public static function UpdatemenUversion () {$version _key = Yii:: $app->params[' key '. MD5 (yii:: $app->params[' key ').
Yii:: $app->db->dsn);
$version _val = Yii:: $app->redis->get ($version _key); if (Empty ($version _val)) {$version _val = ' 1 ';} else {$version _val++} $log = Json_encode ([' user_id ' => Yii:: $app-&
Gt;user->id, ' Version_key ' => $version _key, ' version_val ' => $version _val]);
Writelog ($log, ' update_menu_version ');
Yii:: $app->redis->set ($version _key, $version _val); }

2, the navigation of the highlight, icon, whether to show

The default navigation highlighting is in accordance with the module, controller, method to make a direct match, so there is a demand can not meet, such as: A controller under the page Download B controller highlighted below, this thing can not be achieved, so to modify their highlighting mechanism. Instead of using his highlight logic, we have implemented a new logic of our own. I first add the URL of the page to be highlighted to the menu's data, which is a JSON, as follows:

{"icon": "FA fa-home", "visible": True, "OpenURL": "/web/site/index/"}

So we can know which navigation is highlighted by OpenURL, in the page directly to determine the current request URL in the OpenURL can be, but this has shortcomings, you must have to add the highlighted page to highlight the navigation inside, if the page too much this way is not good, But I did not think of a better way to solve it, if any great God has a good way to write in the comments, thank you very much.

The control of icons and visibility can be implemented with the help of the Getassignedmenu callback method in Menuhelper, and you can pass in the callback method when calling the method, and the anonymous method I write directly is added to the method, as follows:

$user _type = Yii:: $app->user->identity->type;
$customer _id = Yii:: $app->user->identity->customer_id;
$callback _func = function ($menu) use ($user _type, $customer _id) {
$data = Json_decode ($menu [' Data '], true);
$items = $menu [' Children '];
$return = ['
label ' => $menu [' name '],
' url ' => [$menu [' Route ']],
];
$return [' visible '] = isset ($data [' visible '])? $data [' Visible ']: ';
The menu-Hidden logical
if (Empty ($return [' visible '])) {return
false;
}
$return [' icon '] = isset ($data [' icon '])? $data [' Icon ']: ';
Control menu Open Logic
$return [' openurl '] = isset ($data [' OpenURL '])? $data [' OpenURL ']: ';
$items && $return [' items '] = $items;
return $return;
};

3. Override Permission Detection

Just already said, yii-admin permission detection execution too time-consuming, execute SQL too much, so I intend to rewrite his permission check method, through the reading source can see, they check is through the user can method call, and then through the mdm\admin\components\ AccessControl in the Beforeaction implementation, we can look at:

/**
* @inheritdoc
/Public Function beforeaction ($action)
{
$actionId = $action-> Getuniqueid ();
$user = $this->getuser ();
The logic to reserve the System check permission, once the Override check permission fails, the method that invokes the System check permission
if ($user->can ('/'. $actionId)) {return
true;
}
$obj = $action->controller;
Do {
if ($user->can ('/'. LTrim ($obj->getuniqueid (). '/*, '/')} {return
true;
}
$obj = $obj->module;
} while ($obj!== null);
$this->denyaccess ($user);
}

Because full permission checks include child parent checks, which means that/admin/menu/update permissions are visible to/admin/menu/* and/admin/* and/*, we see that $user->can calls use the Do- While to proceed, so as to increase the complexity of the check, execute the SQL will increase in batches, you want Ah, not a parent of the check is a new function call, so the most disgusting is this, interested students can go to see his process, when you call this function detection when you will find , the execution of SQL is not generally much.

Here is my rewrite method, a SQL that is compatible with permissions, roles, bulk checks, and permission checks of the logged-in user, as follows:

/** * Permission to judge methods (do not use this method, the system method, inefficient, and so have time to rewrite and then use) * @param string/array $permission _name Permission value (URL or permission name)/batch detection can be passed into the array * @param int $user User ID, no value will take the current login user * @return Boolen * @author Zhaoyafei/public static function Permissioncheck ($permission _na  Me, $user = 0) {//Check to see if (Yii:: $app->user->isguest) {yii:: $app->response->redirect ('/site/login ');} if (Empty ($permission _name))
{return false;} if (empty ($user)) {$user = Yii:: $app->user->id;}//admin rights cannot return true directly, there will be a person with administrator type = 1 to non-administrator privileges (with pits)//anonymous methods, Handling administrator return values/* $setAdminSet = function ($param) use ($permission _name) {$paramtmp = $permission _name; if Is_array ($paramt MP) {if (count ($paramtmp) = = 1) {return true;} $paramtmp = Array_flip ($paramtmp); foreach ($paramtmp as $key => &am p; $value) {$value = true;}}
else {$paramtmp = true;} return $paramtmp; };*///Check whether administrators, administrators have permissions/*if (empty ($user)) {$user = Yii:: $app->user->id; $user _type = yii:: $app->user->ide
ntity->type; if ($user _type = = Type_admin) {return $setAdminSet ($permission _name);} else {$user _sql = "Select type from xm_user WHERE ID =: id"; $user _info = Yii:: $app->db->createcommand ($user _sql)-&
Gt;bindvalue (": id", $user)->queryone (); 
if (Empty ($user _info)) {return false;} if ($user _info[' type '] = = type_admin) {return $setAdminSet ($permission _name);}
}*///According to the user to take permissions $permission _list = []; $sql = "Select Xc.child, Xc1.child as role_name from xm_auth_assignment xa INNER JOIN xm_auth_item_child XC on Xa.item_na
me = Xc.parent left JOIN xm_auth_item_child xc1 on xc.child = xc1.parent WHERE xa.user_id =: user_id ";
$permission = Yii:: $app->db->createcommand ($sql)->bindvalue (": user_id", $user)->queryall (); if (empty ($permission)) {return false;}//Combo permission list foreach ($permission as $key => $value) {if!empty ($value [' child '] ) &&!in_array ($value [' Child '], $permission _list)) {$permission _list[] = $value [' Child '];} if (!empty ($value Role_name '] &&!in_array ($value [' Role_naMe '], $permission _list)) {$permission _list[] = $value [' Role_name '];} Anonymous methods, processing child URL Generation $getUrlList = function ($url) {if (!strstr ($url, '/')} {return [$url];} $url = '/'. Trim ($url, '/'); $p
Arams = explode ('/', $url);
$param _arr = [];
$param _str = []; if (!empty ($params) && Is_array ($params)) {foreach ($params as $key => $value) {if (!empty ($value)) {$param _
Arr[] = $value;  }} if (!empty ($param _arr)) {$tmp _str = '; $param _str[] = $url; $count = count ($param _arr);//Generate child-parent relationship for ($i = $count -1; $i >= 0; $i-) {$tmp _str = '/'. $param _arr[$i]. $tmp _str; $chold _url = Str_replace ($tmp _str, '/* ', $url); if (!in_array ($chold _u
RL, $param _str)) {$param _str[] = $chold _url}}
return $param _str;
};
Stitching inspection data, compatible with conveys and transmission group $check _list = []; if (Is_array ($permission _name)) {foreach ($permission _name as $key => $value) {$check _list[$value] = $getUrlList ($val
UE); } else {$check _list[$permission _name] = $getUrlList ($permission _name);} if (Empty ($check _list)) {RETurn false;
///Bulk Check for permissions $ret = []; foreach ($check _list as $key => $value) {$ret [$key] = false; foreach ($value as $k => $v) {if In_array ($v, $permi
Ssion_list)) {$ret [$key] = true; break;}}
//Compatible one-dimensional array if (count ($ret) = = 1) {$ret = Array_values ($ret); return $ret [0];} return $ret; }

The

needs to be explained that comments out of the section is the administrator's permissions check, if the administrator will automatically return all the permissions, but this is not very good, because the actual situation will be a variety of administrators, so the administrator does not necessarily have all the permissions, if this is not a super administrator can not use, so use the time to be cautious , it is best to use permission checks uniformly. If you feel that SQL execution is too slow to add a cache, the cache expiration time and the menu expiration are similar, when the user's permissions are changed and the menu changes with the new cache. The 21 solution is to Cheng this method, using the simple only a query that executes one permission, and the phase of the inspection can be written separately as the method provided.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.