In-depth analysis of Yii admin permission control and yii permission Control

Source: Internet
Author: User

In-depth analysis of Yii admin permission control and yii permission Control

When it comes to CMS, the most important thing is permission control, especially in complicated scenarios such as multi-user, multi-role, multi-department, and sub-parent-level viewing. Recently, an offline sales Dongdong was developed. This system is divided into administrator, provincial, client, store, sales, department, and sub-department under the department leader, disgusting demands. Our project is developed using the yii framework. yii is still popular in the php session. Although laravel is rampant, some teams in some departments still adopt the yii framework, for example, we.

I was new to the yii framework. At the beginning, this component-oriented framework was quite awkward. At that time, I was planning to write permissions, create permission tables, and associate tables myself. However, after learning to use the yii development documentation, I found that there was a permission Control RBAC, yii-admin can achieve perfect permissions and Menu Control. This blog is divided into two departments. In the first part, I will describe how to set up permission management, including installing yii-admin, creating permission tables, and using permission control menus and access permissions, for more details about this part, refer to http://www.manks.top/tag/rbac.html. it is not difficult to establish and use it. In the second part, I will explain my understanding, including menu optimization, selective highlighting of sub-page navigation, role-based display menu, and permission detection improvement.

1. yii-admin Construction

1. Set up yii-admin

First, you should install a yii mining project because yii-admin is based on the yii framework and has no framework to play! You can download the source code directly on github.

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

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

Of course, you can use composer to install yii, which is best if you have installed yii, you can switch to the project directory and directly execute the following command:

php composer.phar require mdmsoft/yii2-admin "~2.0"php composer.phar update

Then add the yii-admin configuration item to the configuration, the value of note is that if the yii2-admin configuration is global in the common directory, then you will report an error when executing the command console, therefore, permission control should be applied to the web module. We have not used an advanced template for this project, so you can directly write the configuration in the web under config. in php, the configuration is as follows:

First define the alias:

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

Add the admin component to modules:

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

Add the authManager configuration item:

It should be emphasized that the authManager component in yii has two methods: PhpManager and DbManager. The two methods are different. PhpManager stores the permission relationship in the file and DbManager, save the permission relationship to the database. We use the method of saving 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/*',]],

Do not put an error in the unknown, as shown in:

2. Configure the database permission table

In this step, you do not need to write it yourself. Switch to the yii2 directory through the command line. Execute the following command to create the table required by rbac. However, you need to create a database named yii2basic yourself. If you want to execute the command, you need to put the configuration file you Just configured on the console. in php, also write a copy. If the execution fails, you can generate a data table script and execute it by yourself.

yii migrate --migrationPath=@yii/rbac/migrationsyii migrate --migrationPath=@mdm/admin/migrations

If the execution is successful, five tables are generated and a user table is required. You can add

Menu // menu table

Auth_rule // rule table

Auth_item_child // permission of the role, parent role, and child permission name

Auth_item // role and permission table. type = 1 indicates the role and type = 2 indicates the permission.

Auth_assignment // role-to-user relationship table

If all are successful, access index. php? R = admin, you can see the permission control visualization page. If an error occurs, check the cause of the error carefully. Basically, the configuration is incorrect. After the configuration is complete, access to other pages will have no permissions. Then you can modify allowActions in as access, which is useful when developing APIs or some shared modules, because these pages do not require permission control.

The permission control page is shown in the following figure:

3. Implement Menu Control

To control the menu, you need to use the menu tables in the tables you just created. The navigation on the left should be controlled by permission according to our design, and the navigation that is written to death cannot achieve the goal, scalable rows are not strong, so menu control must be supported.

It should be noted that if your layout is used in your background framework, you need to specify it by yourself. Our project is to have our own layout, the admin component is added as follows:

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

Then we operate the menu list. Add a menu item and open the layout file. In fact, the logic for obtaining the menu has been written. In MenuHelper, add the namespace mdm \ admin \ components \ MenuHelper. Then, cancel the original navigation, add the following code to implement permission-user-navigation control.

echo Nav::widget(["encodeLabels" => false,"options" => ["class" => "sidebar-menu"],"items" => MenuHelper::getAssignedMenu(Yii::$app->user->id),]);

Now, let's take a look at this page:

Ii. yii-admin optimization and rewriting

In the process of use, the navigation permission Control Implemented by yii-admin far cannot meet our needs. In addition, the development of such components, each operation is completely independent. For example, check permissions, take menus, and obtain user information. Execute SQL for each operation. The following shows the normal check permissions and obtain the SQL Execution Process of the menu. In fact, this process is extremely time-consuming. When there are a large number of users, menus, and a large amount of data in the permission table, using our own SQL detection tool, we can see that this process has executed more than 20 SQL statements:

As shown in the figure, the permission check involves 14 SQL queries, and the menu involves five SQL queries. Once so many SQL statements are executed online, there is no concurrency. The yii-admin component provides convenient permission control and Menu Control, but we are not sure about the performance. Check the source code and you will know that this component is a highly decoupled component. Each component can be used independently. Therefore, each operation must have its own database source, to put it bluntly, you need to execute SQL every time to get the desired value. In the middle, you seldom use SQL statements such as table connection queries. In fact, the functions of 10 SQL statements are coupled with the Internet, one SQL statement.

People like me cannot tolerate so many irrelevant SQL statements, So I modified the yii-admin permission check section on the root cause, the modification method is self-defined by myself. It is not necessarily true or suitable for all scenarios. I will share it with you below.

1. Menu Optimization

By viewing the menu generation process, we will execute more than five SQL statements. This is okay. I have not optimized the SQL statements, the reason is that our menu is to correspond to different roles and parent-child relationships. On the basis of the original menu, I added a type to distinguish between those roles to see this menu, which role corresponds to the hierarchical relationship displayed in a menu. In this way, different menus are displayed for administrators and provincial users. Even if you configure the same permissions, different levels of users will see different menus.

Our optimization is to generate data from the cache menu. We have customized this menu. We didn't use the Nav: widget configured at the beginning to present it, but our own cyclic hierarchical relationship, this is troublesome, but it is good to extract none of the logic we need in the menu. For example, if the label of the menu is automatically generated, the label of the menu can be extracted every time, such as the sub-page, different controllers can highlight the left navigation. The following code is mixed with php and html, which will be extracted in the future.

<ul class="nav nav-list"><?php $idx = ;$request_url = '/' . $mod_id . '/' . $con_id . '/' . $act_id . '/';foreach ($menus_new['list'] as $label => $menu): ?><?phpif (empty($menu['label']) && empty($menu['url'][])) {continue;}?><?php if(!isset($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-<?php echo $menu['icon'] ?>"></i><span class="menu-text"> <?php echo $menu['label'] ?> </span></a><b class="arrow"></b></li><?php else:?><li class="<?phpif (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(!isset($menu['items'])):?><li class="<?phpif (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="<?phpif (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="arrow"></b></li><?php endforeach ?></ul> </li><?php endif?><?php endforeach ?></ul></li><?php endif?><?php $idx++; ?><?php endforeach ?></ul>

In this navigation, I modified the multi-edition to summarize the solutions that suit us. breadcrumb controls the display of breadcrumb and I will leave php if I have time. I am introducing menu optimization. Now I have completed the first step. The menu display. When it comes to optimization, I use the cache menu data policy, the above $ menus_new ['LIST'] is cached. The policy is as follows:

This policy uses the role to cache data, that is, using the permissions of each role plus uid and the environment to configure MD5 to generate the key. Considering that many users cache data, the overhead is too large, in addition, many users have the same permissions and special permissions can be taken for special purposes. This eliminates the need to store a lot of duplicate data. The environment configuration distinguishes between online data and test data for debugging.

Expiration mechanism: More importantly, the cache expiration mechanism. When the cache has a menu or permission change, the cache must be updated. Here we introduce the version concept, the minimum overhead of cache changes can be achieved. For example, if the menu changes, all navigation should be modified. Here we add a navigation version variable to redis, each time you read data into the cache, the system first checks whether the version is consistent with the version stored in the cache. if the version is consistent, the navigation remains unchanged. if the version is inconsistent, the menu is changed and the navigation has expired, you need to obtain the cache again. In this way, as long as one person updates the navigation, other people will access the latest navigation (unified role) The next time they come in ). This global redis variable will automatically add 1 when navigation changes and permission changes to ensure version changes. If there are four types of roles and tens of thousands of users, the actual data is modified only four times (there will actually be more than this. For example, if the permissions of the same role are different, the corresponding redis key will be different and it needs to retrieve the cache by itself ). The Code 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'])? $ Menus_new ['version']: ''; // you can determine the version number of the menu to timely update 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;} // you can set the update key to update redisfunction 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 and updating keys is as follows: /*** get menu one user by the id * @ param $ user_id * @ return key string */public static function getMenuKeyByUserId ($ user_id) {if (empty ($ user_id )) {return false;} $ list = (new \ yii \ db \ Query ()-> select ('**')-> from ('**') -> 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 the menu Update Status and 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-> user-> id, 'version _ key' => $ version_key, 'version _ val' => $ version_val]); writeLog ($ log, 'Update _ menu_version '); Yii: $ app-> redis-> set ($ version_key, $ version_val );}

2. Highlight, icon, and display of the navigation

By default, the navigation highlight is directly matched by the module, Controller, and method. In this way, A requirement cannot be met. For example, you can download the highlight under controller B on the page of Controller, this kind of things cannot be implemented, so we need to modify their highlighting mechanism. Instead of using the highlighted logic, we realized a new logic. First, add the page url to the menu data. data is a json data, as shown below:

{"icon": "fa fa-home", "visible": true, "openurl":"/web/site/index/"}

In this way, we can use openurl to know which navigation item is highlighted. On the page, we can directly determine whether the requested url is not in this openurl. However, this method has some disadvantages, the highlighted page must be added to the navigation to be highlighted. If there are too many pages, this method is not very good, but I didn't think of a better solution, if anyone has a good way to write it in the comments, thank you very much.

The icon and visibility control can be implemented by using the getAssignedMenu callback method in MenuHelper. You can pass in the callback method when calling this method, and I will directly write the anonymous method, added to this method, as shown below:

$ 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 hidden logic of the menu if (empty ($ return ['visable']) {return false ;} $ return ['icon '] = isset ($ data ['icon'])? $ Data ['icon ']: ''; // logic for opening the control menu $ return ['openurl'] = isset ($ data ['openurl'])? $ Data ['openurl']: ''; $ items & $ return ['items '] = $ items; return $ return ;};

3. Permission rewriting Detection

As I have already said, the yii-admin permission check takes too much time to execute too many SQL statements. Therefore, I plan to rewrite his permission check method. You can see it by reading the source code, they are called through the can method in the user, and then implemented through beforeAction in mdm \ admin \ components \ AccessControl. Let's take a look:

/*** @ Inheritdoc */public function beforeAction ($ action) {$ actionId = $ action-> getUniqueId (); $ user = $ this-> getUser (); // Reserved System check Permission Logic. Once the override check permission fails, call the system check permission method 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, that is to say, the/admin/menu/update permission is visible to/admin/menu/* And/admin, therefore, we can see that the $ user-> can call will be performed using do-while, which increases the complexity of the check and increases the number of executed SQL statements in batches, none of the parent-level checks are all new function calls, so this is the most disgusting thing. If you are interested, you can look at this process, when you call this function, you will find that the number of SQL statements executed is not average.

The following is my rewrite method. An SQL statement is compatible with permissions, roles, batch checks, and permission checks for Unlogged users. The specific implementation is as follows:

/*** Permission judgment method (do not use this method first, the system method is used, the efficiency is very low, and will be reused after time rewriting) * @ param string/array $ permission_name permission value (URL or permission name)/an array can be input for batch detection * @ param int $ user ID, if the value is not set, the current Login user * @ return boolen * @ author zhaoyafei */public static function permissionCheck ($ permission_name, $ user = 0) will be taken) {// check whether you have logged on to 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;} // true cannot be returned for administrator permissions. Administrator type = 1 is assigned to a person not authorized by the Administrator) // anonymous method, handling the Administrator's returned values/* $ setAdminSet = function ($ param) use ($ permission_name) {$ paramtmp = $ permission_name; if (is_array ($ paramtmp )) {if (count ($ paramtmp) = 1) {return true;} $ paramtmp = array_flip ($ paramtmp); foreach ($ paramtmp as $ key => & $ value) {$ value = true ;}} else {$ paramtmp = true;} return $ paramtmp ;}; * // check whether it is an administrator. Permission/* if (empty ($ user) {$ user = Yii ::$ app-> user-> id; $ user_type = Yii :: $ app-> user-> identity-> 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) -> bindValue (": id", $ user)-> queryOne (); if (empty ($ user_info) {return false ;} if ($ user_info ['type'] = TYPE_ADMIN) {retur N $ setAdminSet ($ permission_name) ;}} * // obtain the permission based on the user $ 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_name = xc. parentLEFT JOIN xm_auth_item_child xc1 ON xc. child = xc1.parentWHERE xa. user_id =: user_id "; $ permission = Yii: $ app-> db-> createCommand ($ SQL)-> bindValue (": user_id ", $ user) -> queryAll (); if (empty ($ permissi On) {return false;} // combined 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 method, process sub-url generation $ getUrlList = function ($ url) {if (! Strstr ($ url, '/') {return [$ url] ;}$ url = '/'. trim ($ url, '/'); $ params = 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 a 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_url, $ param_str) {$ param_str [] = $ chold_url ;}} return $ param_str ;}; // Concatenates the check data, compatibility with single pass and transmission groups $ check_list = []; if (is_array ($ permission_name) {foreach ($ permission_name as $ key => $ value) {$ check_list [$ value] = $ getUrlList ($ value) ;}} else {$ check_list [$ permission_name] = $ getUrlList ($ permission_name );} if (empty ($ check_list) {return false;} // check whether the batchcompute operation has the permission $ ret = []; foreach ($ check_list as $ key => $ value) {$ ret [$ key] = false; foreach ($ value as $ k => $ v) {if (in_array ($ v, $ permission_list )) {$ ret [$ key] = true; break ;}}// compatible with one-dimensional array if (count ($ ret) = 1) {$ ret = array_values ($ ret); return $ ret [0];} return $ ret ;}

Note that the commented out part is the administrator's permission check. If the Administrator is an administrator, all permissions will be automatically returned, but this is not good because there will be multiple administrators in actual situations, in this way, the Administrator may not have all the permissions. If the Administrator is not a super administrator, he or she should be cautious when using the permissions. It is best to use the permission check in a unified manner. If you feel that the SQL statement is too slow to be executed, you can add a cache. The cache expiration time is similar to the menu expiration time. When the user's permissions are changed, the cache is updated with the new cache when the menu is modified. One of the two solutions is to combine this method with a ticket. The ticket only executes a permission query. The check phase can be written as a method separately.

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.