💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
决定某些控制器方法在某些情况下可以访问,某些情况下不能访问的这种控制行为叫做权限控制。ThinkPHP提供了基于RBAC和基于AUTH的两种权限。 # RBAC ## 概念 基于角色的访问控制(Role-Based Access Control)。它作为传统访问控制(自主访问,强制访问)的有前景的代替受到广泛的关注。 在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。 在一个组织中,角色是为了完成各种工作而创造,用户则依据它的责任和资格来被指派相应的角色,用户可以很容易地从一个角色被指派到另一个角色。角色可依新的需求和系统的合并而赋予新的权限,而权限也可根据需要而从某角色中回收。角色与角色的关系可以建立起来以囊括更广泛的客观情况。 ## 作用 区分不同角色的可见资源、可操作资源。如老板可以控制最大、财务可以看见并处理财务相关的报表。研发人员可以看见整个系统的组成模块、导航和配置之类的。哪些操作谁该直接处理,这些操作的权限就应该给哪个角色。 ## 组成 ### 安全拦截器 RBAC::checkAccess 方法返回当前请求的操纵是否需要认证。 ### 认证管理器 RBAC::authenticate 识别不同的身份,你的用户名、密码、权限是否在授权范围内。 ### 决策访问管理器 RBAC::AccessDecision 必须同认证管理器一同使用 分为即时模式和登录模式。即时模式指修改了权限后,下次操作访问时权限检测立即生效,登录模式则必须退出后再登录权限列表才会是新的。其实就是一个是每次读表查询出数据,一个是读取之前登录时获取的权限列表的session缓存。 ### 运行身份管理器 单身份、多身份管理B/S。单身份指一个用户只有一个角色,多身份指一个用户具有多个身份,并且某个操作权限要求极高必须多个身份的权限才能使用。所以多身份管理一般不存在。 ## 原理 1. 判断当前的操作(项目【应用】,模块、动作(操作)是否需要认证)对应节点(Node表里和配置里和AUTH相关的配置如`NOT_AUTH_MODULE`、`REQUIRE_AUTH_MODULE`、`NOT_AUTH_ACTION`、`REQUIRE_AUTH_ACTION`也就是需要认证的模块、不需要认证的模块、需要认证的操作和不需要认证的操作啦)。 2. 如果需要认证(判断用户是否登录,如果没登录-->跳至委托认证管理器验证身份,判断用户是否有权限访问如果没权限--直接跳至无权访问页面) 3. 委托认证来验证用户身份 4. 获取该用户的权限列表 5. 判断用户是否有权限访问 ![document/2015-09-14/55f5a0d77bf0c](http://box.kancloud.cn/document_2015-09-14_55f5a0d77bf0c.png) ## 如何使用 为了方便大家更好的理解,我在参考了[http://www.thinkphp.cn/code/714.html](http://www.thinkphp.cn/code/714.html) 源码后,将年久失修的ThinkPHP RBAC示列美化增强了一下。我会结合示列给大家讲解如何正确使用ThinkPHP的RBAC。 ### 确认类库文件存在 ThinkPHP中使用RBAC挺简单,只要你的第三方类库里有RBAC类文件,![document/2015-09-09/55efc743ce24f](http://box.kancloud.cn/document_2015-09-09_55efc743ce24f.png)。那么在你要使用RBAC的类的控制器里写上命名空间 `use Org\Util\Rbac;` 就可以使用RBAC::checkAccess 的这些方法了。 当然这只是类文件的准备,要使用RBAC我们得准备5张表。 ### 建表 user(用户)、role(角色)、node(节点)、role_user(角色用户关联表)、access(角色权限关联表) #### user用户表 除了必备的 id、account、password 之外,其他字段和权限无关。 #### role角色表 id、name、status、remark备注 是必要的, 其他的也可以不要。有可能角色存在上下级关系,我觉得很少会用到。 #### role_user 角色关联表 role_id、user_id 只用来关联的 #### node 节点 id、name、title、status、remark 这个是rbac 最终认证的最小单位, 其实最小单位是 APP_NAME.Controller_NAME.ACTION_NAME。 #### access 权限关联表 role_id、node_id、level、pid 用于存放每个节点可以操作的节点id 以上表的建表sql RBAC类里注释已经帮我们准备好了,注意原文件access表的少了一个pid字段。 下面是建表sql: ~~~ CREATE TABLE IF NOT EXISTS `rbac_access` ( `role_id` smallint(6) unsigned NOT NULL, `node_id` smallint(6) unsigned NOT NULL, `level` tinyint(1) NOT NULL, `pid` smallint(6) NOT NULL, `module` varchar(50) DEFAULT NULL, KEY `groupId` (`role_id`), KEY `nodeId` (`node_id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- -- 转存表中的数据 `rbac_access` -- INSERT INTO `rbac_access` (`role_id`, `node_id`, `level`, `pid`, `module`) VALUES (2, 1, 1, 0, NULL), (2, 40, 2, 1, NULL), (2, 30, 2, 1, NULL), (3, 1, 1, 0, NULL), (2, 69, 2, 1, NULL), (2, 50, 3, 40, NULL), (3, 50, 3, 40, NULL), (1, 50, 3, 40, NULL), (3, 7, 2, 1, NULL), (3, 39, 3, 30, NULL), (2, 39, 3, 30, NULL), (2, 49, 3, 30, NULL), (4, 1, 1, 0, NULL), (4, 2, 2, 1, NULL), (4, 3, 2, 1, NULL), (4, 4, 2, 1, NULL), (4, 5, 2, 1, NULL), (4, 6, 2, 1, NULL), (4, 7, 2, 1, NULL), (4, 11, 2, 1, NULL), (5, 25, 1, 0, NULL), (5, 51, 2, 25, NULL), (1, 1, 1, 0, NULL), (1, 39, 3, 30, NULL), (1, 69, 2, 1, NULL), (1, 30, 2, 1, NULL), (1, 40, 2, 1, NULL), (1, 49, 3, 30, NULL), (3, 69, 2, 1, NULL), (3, 30, 2, 1, NULL), (3, 40, 2, 1, NULL), (1, 37, 3, 30, NULL), (1, 36, 3, 30, NULL), (1, 35, 3, 30, NULL), (1, 34, 3, 30, NULL), (1, 33, 3, 30, NULL), (1, 32, 3, 30, NULL), (1, 31, 3, 30, NULL), (2, 32, 3, 30, NULL), (2, 31, 3, 30, NULL), (7, 1, 1, 0, NULL), (7, 30, 2, 1, NULL), (7, 40, 2, 1, NULL), (7, 69, 2, 1, NULL), (7, 50, 3, 40, NULL), (7, 39, 3, 30, NULL), (7, 49, 3, 30, NULL); CREATE TABLE IF NOT EXISTS `rbac_form` ( `id` smallint(4) unsigned NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `content` varchar(255) NOT NULL, `create_time` int(11) unsigned NOT NULL, `status` tinyint(1) unsigned NOT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 ; CREATE TABLE IF NOT EXISTS `rbac_group` ( `id` smallint(3) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(25) NOT NULL, `title` varchar(50) NOT NULL, `create_time` int(11) unsigned NOT NULL, `update_time` int(11) unsigned NOT NULL DEFAULT '0', `status` tinyint(1) unsigned NOT NULL DEFAULT '0', `sort` smallint(3) unsigned NOT NULL DEFAULT '0', `show` tinyint(1) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=6 ; -- -- 转存表中的数据 `rbac_group` -- INSERT INTO `rbac_group` (`id`, `name`, `title`, `create_time`, `update_time`, `status`, `sort`, `show`) VALUES (2, 'App', '应用中心', 1222841259, 0, 1, 0, 0); CREATE TABLE IF NOT EXISTS `rbac_node` ( `id` smallint(6) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(20) NOT NULL, `title` varchar(50) DEFAULT NULL, `status` tinyint(1) DEFAULT '0', `remark` varchar(255) DEFAULT NULL, `sort` smallint(6) unsigned DEFAULT NULL, `pid` smallint(6) unsigned NOT NULL, `level` tinyint(1) unsigned NOT NULL, `type` tinyint(1) NOT NULL DEFAULT '0', `group_id` tinyint(3) unsigned DEFAULT '0', PRIMARY KEY (`id`), KEY `level` (`level`), KEY `pid` (`pid`), KEY `status` (`status`), KEY `name` (`name`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=83 ; -- -- 转存表中的数据 `rbac_node` -- INSERT INTO `rbac_node` (`id`, `name`, `title`, `status`, `remark`, `sort`, `pid`, `level`, `type`, `group_id`) VALUES (49, 'read', '查看', 1, '', NULL, 30, 3, 0, 0), (40, 'Index', '默认模块', 1, '', 1, 1, 2, 0, 0), (39, 'index', '列表', 1, '', NULL, 30, 3, 0, 0), (37, 'resume', '恢复', 1, '', NULL, 30, 3, 0, 0), (36, 'forbid', '禁用', 1, '', NULL, 30, 3, 0, 0), (35, 'foreverdelete', '删除', 1, '', NULL, 30, 3, 0, 0), (34, 'update', '更新', 1, '', NULL, 30, 3, 0, 0), (33, 'edit', '编辑', 1, '', NULL, 30, 3, 0, 0), (32, 'insert', '写入', 1, '', NULL, 30, 3, 0, 0), (31, 'add', '新增', 1, '', NULL, 30, 3, 0, 0), (30, 'Public', '公共模块', 1, '', 2, 1, 2, 0, 0), (69, 'Form', '数据管理', 1, '', 1, 1, 2, 0, 2), (7, 'User', '后台用户', 1, '', 4, 1, 2, 0, 2), (6, 'Role', '角色管理', 1, '', 3, 1, 2, 0, 2), (2, 'Node', '节点管理', 1, '', 2, 1, 2, 0, 2), (1, 'App', 'Rbac后台管理', 1, '', NULL, 0, 1, 0, 0), (50, 'main', '空白首页', 1, '', NULL, 40, 3, 0, 0); CREATE TABLE IF NOT EXISTS `rbac_role` ( `id` smallint(6) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(20) NOT NULL, `pid` smallint(6) DEFAULT NULL, `status` tinyint(1) unsigned DEFAULT NULL, `remark` varchar(255) DEFAULT NULL, `ename` varchar(5) DEFAULT NULL, `create_time` int(11) unsigned NOT NULL, `update_time` int(11) unsigned NOT NULL, PRIMARY KEY (`id`), KEY `parentId` (`pid`), KEY `ename` (`ename`), KEY `status` (`status`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=8 ; -- -- 转存表中的数据 `rbac_role` -- INSERT INTO `rbac_role` (`id`, `name`, `pid`, `status`, `remark`, `ename`, `create_time`, `update_time`) VALUES (1, '领导组', 0, 1, '', '', 1208784792, 1254325558), (2, '员工组', 0, 1, '', '', 1215496283, 1254325566), (7, '演示组', 0, 1, '', NULL, 1254325787, 0); CREATE TABLE IF NOT EXISTS `rbac_role_user` ( `role_id` mediumint(9) unsigned DEFAULT NULL, `user_id` char(32) DEFAULT NULL, KEY `group_id` (`role_id`), KEY `user_id` (`user_id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- -- 转存表中的数据 `rbac_role_user` -- INSERT INTO `rbac_role_user` (`role_id`, `user_id`) VALUES (4, '27'), (4, '26'), (4, '30'), (5, '31'), (3, '22'), (3, '1'), (1, '4'), (2, '3'), (7, '2'); CREATE TABLE IF NOT EXISTS `rbac_user` ( `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT, `account` varchar(64) NOT NULL, `nickname` varchar(50) NOT NULL, `password` char(32) NOT NULL, `bind_account` varchar(50) NOT NULL, `last_login_time` int(11) unsigned DEFAULT '0', `last_login_ip` varchar(40) DEFAULT NULL, `login_count` mediumint(8) unsigned DEFAULT '0', `verify` varchar(32) DEFAULT NULL, `email` varchar(50) NOT NULL, `remark` varchar(255) NOT NULL, `create_time` int(11) unsigned NOT NULL, `update_time` int(11) unsigned NOT NULL, `status` tinyint(1) DEFAULT '0', `type_id` tinyint(2) unsigned DEFAULT '0', `info` text NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `account` (`account`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=35 ; -- -- 转存表中的数据 `rbac_user` -- INSERT INTO `rbac_user` (`id`, `account`, `nickname`, `password`, `bind_account`, `last_login_time`, `last_login_ip`, `login_count`, `verify`, `email`, `remark`, `create_time`, `update_time`, `status`, `type_id`, `info`) VALUES (1, 'admin', '管理员', '21232f297a57a5a743894a0e4a801fc3', '', 1326335612, '127.0.0.1', 888, '8888', 'liu21st@gmail.com', '备注信息', 1222907803, 1326266696, 1, 0, ''), (2, 'demo', '演示', 'fe01ce2a7fbac8fafaed7c982a04e229', '', 1254326091, '127.0.0.1', 90, '8888', '', '', 1239783735, 1254325770, 1, 0, ''), (3, 'member', '员工', 'aa08769cdcb26674c6706093503ff0a3', '', 1326266720, '127.0.0.1', 17, '', '', '', 1253514375, 1254325728, 1, 0, ''), (4, 'leader', '领导', 'c444858e0aaeb727da73d2eae62321ad', '', 1254325906, '127.0.0.1', 15, '', '', '领导', 1253514575, 1254325705, 1, 0, ''); ~~~ 这sql可以见我的examples 里rbac应用下的rabc.sql。 ![document/2015-09-09/55efcf6e00519](http://box.kancloud.cn/document_2015-09-09_55efcf6e00519.png) 为了不区分不同应用,我将表前缀定为rbac_。 各个表的关系图: ![document/2015-09-14/55f5ac519d89c](http://box.kancloud.cn/document_2015-09-14_55f5ac519d89c.png) ### 更改配置 将RBAC类注释里的配置拿到Home的conf.php中, ~~~ 'USER_AUTH_ON' => true, 'USER_AUTH_TYPE' => 2, // 默认认证类型 1 登录认证 2 实时认证 'USER_AUTH_KEY' => 'authId', // 用户认证SESSION标记 'ADMIN_AUTH_KEY' => 'administrator', 'USER_AUTH_MODEL' => 'User', // 默认验证数据表模型 'AUTH_PWD_ENCODER' => 'md5', // 用户认证密码加密方式 'USER_AUTH_GATEWAY' => '/Public/login',// 默认认证网关 'NOT_AUTH_MODULE' => 'Public', // 默认无需认证模块 'REQUIRE_AUTH_MODULE' => '', // 默认需要认证模块 'NOT_AUTH_ACTION' => '', // 默认无需认证操作 'REQUIRE_AUTH_ACTION' => '', // 默认需要认证操作 'GUEST_AUTH_ON' => false, // 是否开启游客授权访问 'GUEST_AUTH_ID' => 0, // 游客的用户ID 'DB_LIKE_FIELDS' => 'title|remark', 'RBAC_ROLE_TABLE' => 'rbac_role', 'RBAC_USER_TABLE' => 'rbac_role_user', 'RBAC_ACCESS_TABLE' => 'rbac_access', 'RBAC_NODE_TABLE' => 'rbac_node', ~~~ 需要注意的是,用户表如果你业务里不是User需要修改写,其他4张表也按实际情况来写真实表名。 ### 添加登录后权限获取缓存 ![document/2015-09-09/55efd5f152cf9](http://box.kancloud.cn/document_2015-09-09_55efd5f152cf9.png) ThinkPHP的RBAC 权限列表依赖于SESSION。并且权限是在登录后获取访问权限列表的。 我们看下登录方法,到底做了什么。 ~~~ //登录页面 public function login(){ if(IS_POST){ if(empty($_POST['username'])) { $this->error('帐号错误!'); }elseif (empty($_POST['password'])){ $this->error('密码必须!'); } //生成认证条件 $map = array(); // 支持使用绑定帐号登录 $map['account'] = $_POST['username']; $map["status"] = array('gt',0); //使用用户名、密码和状态的方式进行认证 $authInfo = RBAC::authenticate($map); if(false === $authInfo) { $this->error('帐号不存在或已禁用!'); }else { if($authInfo['password'] != md5($_POST['password'])) { $this->error('密码错误!'); } $_SESSION[C('USER_AUTH_KEY')] = $authInfo['id']; if($authInfo['username'] == C('ADMIN_AUTH_KEY')) { $_SESSION['administrator'] = true; } //保存登录信息 $User = M('User'); $ip = get_client_ip(); $time = time(); $data = array(); $data['id'] = $authInfo['id']; $data['last_login_time'] = $time; $data['login_count'] = array('exp','login_count+1'); $data['last_login_ip'] = $ip; $User->save($data); // 缓存访问权限 RBAC::saveAccessList(); $this->success('登录成功!', U('/')); } }else{ $this->display(); } } ~~~ 1. 显示用认证管理器获取user表里存在的对应用户信息。 //生成认证条件 ~~~ $map = array(); // 支持使用绑定帐号登录 $map['account'] = $_POST['username']; $map["status"] = array('gt',0); //使用用户名、密码和状态的方式进行认证 $authInfo = RBAC::authenticate($map); ~~~ 2. 然后判断账号是否存在、密码是否正确。身份存在的话将用户表id赋值给`$_SESSION[C('USER_AUTH_KEY')]`,并判断是否是超级管理员 。 ~~~ if($authInfo['username'] == C('ADMIN_AUTH_KEY')) { $_SESSION['administrator'] = true; } ~~~ 3. 最关键的一步,缓存权限,如果是即时模式,缓存无效,最好写上,方便认证模式切换。 ~~~ // 缓存访问权限 RBAC::saveAccessList(); ~~~ 看下saveAccessList方法: ~~~ //用于检测用户权限的方法,并保存到Session中 static function saveAccessList($authId=null) { if(null===$authId) $authId = $_SESSION[C('USER_AUTH_KEY')]; // 如果使用普通权限模式,保存当前用户的访问权限列表 // 对管理员开发所有权限 if(C('USER_AUTH_TYPE') !=2 && !$_SESSION[C('ADMIN_AUTH_KEY')] ) $_SESSION['_ACCESS_LIST'] = self::getAccessList($authId); return ; } ~~~ 非即时模式、并且不是超级管理员时才更新session。 我们再看下getAccessList方法。 ~~~ static public function getAccessList($authId) { // Db方式权限数据 $db = Db::getInstance(C('RBAC_DB_DSN')); $table = array('role'=>C('RBAC_ROLE_TABLE'),'user'=>C('RBAC_USER_TABLE'),'access'=>C('RBAC_ACCESS_TABLE'),'node'=>C('RBAC_NODE_TABLE')); $sql = "select node.id,node.name from ". $table['role']." as role,". $table['user']." as user,". $table['access']." as access ,". $table['node']." as node ". "where user.user_id='{$authId}' and user.role_id=role.id and ( access.role_id=role.id or (access.role_id=role.pid and role.pid!=0 ) ) and role.status=1 and access.node_id=node.id and node.level=1 and node.status=1"; $apps = $db->query($sql); $access = array(); foreach($apps as $key=>$app) { $appId = $app['id']; $appName = $app['name']; // 读取项目的模块权限 $access[strtoupper($appName)] = array(); $sql = "select node.id,node.name from ". $table['role']." as role,". $table['user']." as user,". $table['access']." as access ,". $table['node']." as node ". "where user.user_id='{$authId}' and user.role_id=role.id and ( access.role_id=role.id or (access.role_id=role.pid and role.pid!=0 ) ) and role.status=1 and access.node_id=node.id and node.level=2 and node.pid={$appId} and node.status=1"; $modules = $db->query($sql); // 判断是否存在公共模块的权限 $publicAction = array(); foreach($modules as $key=>$module) { $moduleId = $module['id']; $moduleName = $module['name']; if('PUBLIC'== strtoupper($moduleName)) { $sql = "select node.id,node.name from ". $table['role']." as role,". $table['user']." as user,". $table['access']." as access ,". $table['node']." as node ". "where user.user_id='{$authId}' and user.role_id=role.id and ( access.role_id=role.id or (access.role_id=role.pid and role.pid!=0 ) ) and role.status=1 and access.node_id=node.id and node.level=3 and node.pid={$moduleId} and node.status=1"; $rs = $db->query($sql); foreach ($rs as $a){ $publicAction[$a['name']] = $a['id']; } unset($modules[$key]); break; } } // 依次读取模块的操作权限 foreach($modules as $key=>$module) { $moduleId = $module['id']; $moduleName = $module['name']; $sql = "select node.id,node.name from ". $table['role']." as role,". $table['user']." as user,". $table['access']." as access ,". $table['node']." as node ". "where user.user_id='{$authId}' and user.role_id=role.id and ( access.role_id=role.id or (access.role_id=role.pid and role.pid!=0 ) ) and role.status=1 and access.node_id=node.id and node.level=3 and node.pid={$moduleId} and node.status=1"; $rs = $db->query($sql); $action = array(); foreach ($rs as $a){ $action[$a['name']] = $a['id']; } // 和公共模块的操作权限合并 $action += $publicAction; $access[strtoupper($appName)][strtoupper($moduleName)] = array_change_key_case($action,CASE_UPPER); } } return $access; } ~~~ 其实过程就是先查出为当前用户授权过的应用列表(或叫项目),然后再遍历项目获取授权过的模块,最后遍历模块获取授权过的操作。 最后得到的数组类似: ![2015-08-02/55be24d99842e](http://box.kancloud.cn/2015-08-02_55be24d99842e.png) 最里层的都是节点id。这样一个多维数组生成后,缓存到session里。 ### 将要进行权限判断的控制器继承Common控制器,自动绑上初始化进行**模块/控制器/操作**对应的权限判断 ~~~ <?php namespace Home\Controller; use Think\Controller; use Think\Page; use Org\Util\Rbac; //如果不需要权限验证的控制器不要继承它 class CommonController extends Controller{ //对权限的验证和判断 public function _initialize() { define('__URL__', __CONTROLLER__); // 用户权限检查 if (C('USER_AUTH_ON') && !in_array(MODULE_NAME, explode(',', C('NOT_AUTH_MODULE')))) { if (!RBAC::AccessDecision()) { //检查认证识别后 if (!$_SESSION [C('USER_AUTH_KEY')]) { //跳转到认证网关 redirect(PHP_FILE . C('USER_AUTH_GATEWAY')); } // 没有权限 抛出错误 if (C('RBAC_ERROR_PAGE')) { // 定义权限错误页面 redirect(C('RBAC_ERROR_PAGE')); } else { if (C('GUEST_AUTH_ON')) { $this->assign('jumpUrl', PHP_FILE . C('USER_AUTH_GATEWAY')); } // 提示错误信息 $this->error(L('_VALID_ACCESS_')); } } } } ~~~ 用RBAC类进行权限判断: `if (C('USER_AUTH_ON') && !in_array(MODULE_NAME, explode(',', C('NOT_AUTH_MODULE')))) {` 当开启RBAC判断并且模块不在无需认证模块中时执行权限判断。 用RBAC::AccessDecision() 决策访问管理器进行判断。 没通过判断,进行错误引导。 ~~~ if (!$_SESSION [C('USER_AUTH_KEY')]) { //跳转到认证网关 redirect(PHP_FILE . C('USER_AUTH_GATEWAY')); } ~~~ 如果没有认证的session表示没登录,跳到登录页面进行授权。 ~~~ // 没有权限 抛出错误 if (C('RBAC_ERROR_PAGE')) { // 定义权限错误页面 redirect(C('RBAC_ERROR_PAGE')); } else { if (C('GUEST_AUTH_ON')) { $this->assign('jumpUrl', PHP_FILE . C('USER_AUTH_GATEWAY')); } // 提示错误信息 $this->error(L('_VALID_ACCESS_')); } ~~~ 登录了没权限,如果定义了权限报错页面,去那。没定义的话如果开启游客模式,跳到登录网关。没有开游客访问模式直接提示无权限。 所以这段代码关键处在于判断是否有权限,我们看AccessDescision方法。 ~~~ //权限认证的过滤器方法 static public function AccessDecision($appName=MODULE_NAME) { //检查是否需要认证 if(self::checkAccess()) { //存在认证识别号,则进行进一步的访问决策 $accessGuid = md5($appName.CONTROLLER_NAME.ACTION_NAME); if(empty($_SESSION[C('ADMIN_AUTH_KEY')])) { if(C('USER_AUTH_TYPE')==2) { //加强验证和即时验证模式 更加安全 后台权限修改可以即时生效 //通过数据库进行访问检查 $accessList = self::getAccessList($_SESSION[C('USER_AUTH_KEY')]); }else { // 如果是管理员或者当前操作已经认证过,无需再次认证 if( $_SESSION[$accessGuid]) { return true; } //登录验证模式,比较登录后保存的权限访问列表 $accessList = $_SESSION['_ACCESS_LIST']; } //判断是否为组件化模式,如果是,验证其全模块名 if(!isset($accessList[strtoupper($appName)][strtoupper(CONTROLLER_NAME)][strtoupper(ACTION_NAME)])) { $_SESSION[$accessGuid] = false; return false; } else { $_SESSION[$accessGuid] = true; } }else{ //管理员无需认证 return true; } } return true; } ~~~ `self::checkAccess()`先判断当前模块下控制器下操作是否需要判断。如果不需要咱直接通过返回true。 需要的话,先通过md5 将 `$appName.CONTROLLER_NAME.ACTION_NAME` 加密生成一个guid。 然后先通过session里ADMIN_AUTH_KEY 判断是不是超级管理员,超级管理员直接返回true,具有所有权限。 不是超级管理员。 ~~~ if(C('USER_AUTH_TYPE')==2) { //加强验证和即时验证模式 更加安全 后台权限修改可以即时生效 //通过数据库进行访问检查 $accessList = self::getAccessList($_SESSION[C('USER_AUTH_KEY')]); }else { // 如果是管理员或者当前操作已经认证过,无需再次认证 if( $_SESSION[$accessGuid]) { return true; } //登录验证模式,比较登录后保存的权限访问列表 $accessList = $_SESSION['_ACCESS_LIST']; //判断是否为组件化模式,如果是,验证其全模块名 } if(!isset($accessList[strtoupper($appName)][strtoupper(CONTROLLER_NAME)][strtoupper(ACTION_NAME)])) { $_SESSION[$accessGuid] = false; return false; } else { $_SESSION[$accessGuid] = true; } ~~~ 看当前认证模式是否是即时,即时就重新获取,非即时的如果当前guid在session存在说明认证过了,不存在读取访问认证权限缓存列表。然后就是简单的数组判断看授权列表里有无当前[模块][控制器][操作]的节点。 有就将session里guid 设为true,没有就设为false。 老杨觉的这里写的有问题,应该用isset判断一下,如果$_SESSION[$accessGuid]赋过值,返回该session。没赋过值,返回比较结果。 退出时清除整个session。 整个RBAC 认证流程就结束了。 ### RBAC附加功能的实现 其实RBAC的难点在于数据库的理解和节点添加、角色授权。 从数据的先后顺序上来说先是添加节点,然后新增用户,新增角色,最后给角色授权。 #### 添加节点 这里注意的是节点是权限判断的基础数据,由于程序上的设计,必须保证上下级顺序才能对应查询时生成对应的多为数组供以判断。所以其实大家只要记住先添加模块再添加控制器最后添加操作即可。 我们先看一下老版的rbac示列: ![document/2015-09-09/55f000e3bcf34](http://box.kancloud.cn/document_2015-09-09_55f000e3bcf34.png) ![document/2015-09-09/55f000ff546ff](http://box.kancloud.cn/document_2015-09-09_55f000ff546ff.png) 点相应列表里的名称 发布该节点的子节点。 老杨改后的呢: ![document/2015-09-09/55f001470a27a](http://box.kancloud.cn/document_2015-09-09_55f001470a27a.png) ![document/2015-09-09/55f001746a37a](http://box.kancloud.cn/document_2015-09-09_55f001746a37a.png) 看出来没,少了一个分组,多了一个上级下啦,用以修改当前节点的上级pid。 如果是老示列,想移动一个节点。只能删除了旧节点,去指定上级下新增元属性一样的节点了。 因为只是示列,老杨也没去实现排序和搜索,其实大家都知道怎么实现。 这里我只特别说下这个下拉树的实现。其他添加和老版的示列一样,就是添加数据,带个上下级pid而已。 在示列的NodeController 中, 写了一个 _before_add 和_before_edit 用于编辑和显示页面获取上级列表树: ~~~ // 获取配置类型 public function _before_add() { $model = M("Node"); $list = $model->where('status=1')->select(); $node_tree = D('Tree')->toFormatTree($list); $this->assign('node_tree', $node_tree); $this->assign('pid', I('pid', 1)); } public function _before_edit() { $this->_before_add(); } ~~~ 主要诀窍就在这个TreeModel里。 我们看把所有开启状态的节点查出来的列表怎么通过toFormatTree变成了有层级先后顺序显示的下拉。 ~~~ public function toFormatTree($list, $title = 'title', $pk = 'id', $pid = 'pid', $root = 0) { $list = list_to_tree($list, $pk, $pid, '_child', $root); $this->formatTree = array(); $this->_toFormatTree($list, 0, $title); return $this->formatTree; } ~~~ 首先将列表转换为无限级tree。这个需求很常见。用了以前老板tp了扩展函数里的list_to_tree: ~~~ /** * 把返回的数据集转换成Tree * @access public * @param array $list 要转换的数据集 * @param string $pid parent标记字段 * @param string $level level标记字段 * @return array */ function list_to_tree($list, $pk = 'id', $pid = 'pid', $child = '_child', $root = 0) { // 创建Tree $tree = array(); if (is_array($list)) { // 创建基于主键的数组引用 $refer = array(); foreach ($list as $key => $data) { $refer[$data[$pk]] = & $list[$key]; } foreach ($list as $key => $data) { // 判断是否存在parent $parentId = $data[$pid]; if ($root == $parentId) { $tree[] = & $list[$key]; } else { if (isset($refer[$parentId])) { $parent = & $refer[$parentId]; $parent[$child][] = & $list[$key]; } } } } return $tree; } ~~~ 转换成树以后,调用_toFormatTree进行格式转换。 ~~~ /** * 将格式数组转换为树 * * @param array $list * @param integer $level 进行递归时传递用的参数 */ private $formatTree; //用于树型数组完成递归格式的全局变量 private function _toFormatTree($list, $level = 0, $title = 'title') { foreach ($list as $key => $val) { $tmp_str = str_repeat(" ", $level * 1); $tmp_str.="└"; $val['level'] = $level; $val['title_show'] = $level == 0 ? $val[$title] : $tmp_str . $val[$title]; if (!array_key_exists('_child', $val)) { array_push($this->formatTree, $val); } else { $tmp_ary = $val['_child']; unset($val['_child']); array_push($this->formatTree, $val); $this->_toFormatTree($tmp_ary, $level + 1, $title); //进行下一层递归 } } return; } ~~~ 遍历每条数据,根据level算出前面要补多少空格。如果没有_child子树就添加到formatTree属性里。有的话递归调用_toFormatTree,传入时讲level+1了。这样子级的前导空格始终比父级多一个。 #### 添加角色 ![document/2015-09-09/55f04ed4230df](http://box.kancloud.cn/document_2015-09-09_55f04ed4230df.png) 只是美化了一下。 #### 添加用户 老版的是这样的: ![document/2015-09-09/55f04e0869d61](http://box.kancloud.cn/document_2015-09-09_55f04e0869d61.png) 我修改之后多了选择角色。 ![document/2015-09-09/55f04db9db861](http://box.kancloud.cn/document_2015-09-09_55f04db9db861.png) 并且在添加的时候已经自动加入了role_user表。 编辑时也先删除了该user的所有role,保存最后编辑的那一个。 老版的角色绑定多个用户目前来说完全用不到: ![document/2015-09-09/55f04e7ec6f17](http://box.kancloud.cn/document_2015-09-09_55f04e7ec6f17.png) #### 角色授权优化 老版的角色授权很麻烦,完成一个节点授权得经过3步。 先授权应用: ![document/2015-09-09/55f04f49373f7](http://box.kancloud.cn/document_2015-09-09_55f04f49373f7.png) 再授权模块: ![document/2015-09-09/55f04fa04177a](http://box.kancloud.cn/document_2015-09-09_55f04fa04177a.png) 最后再选择模块,授权子级操作: ![document/2015-09-09/55f04fcc8dde3](http://box.kancloud.cn/document_2015-09-09_55f04fcc8dde3.png) 而老杨修改了只需要一步: ![document/2015-09-09/55f0504a3e59d](http://box.kancloud.cn/document_2015-09-09_55f0504a3e59d.png) 实现方法就是,显示把模块和对应操作遍历出来,并且获取对应角色的权限列表后,输出json,给前端进行匹配勾选。将勾选的节点id保存在rule数组里传递。 保存时: ~~~ public function saveAccessList(){ $groupId = I('groupID'); $model = D('Role'); $apps = $model->getGroupAppList($groupId); $moduleList = $model->getGroupModuleList($groupId, $apps[0]['id']); foreach ($moduleList as $key => $module) { $model->delGroupAction($groupId, $module['id']); } $res = $model->setGroupActions($groupId, I('rule')); if($res) $this->success('更新成功'); else $this->error('更新失败'); } ~~~ 先按模块删除权限列表。然后再保存该角色的可用操作节点列表。 getGroupModuleList和delGroupAction及setGroupModules这些都是官网老rbac示列里的方法。 ~~~ function getGroupModuleList($groupId,$appId) { $table = $this->tablePrefix.'access'; $rs = $this->db->query('select b.id,b.title,b.name from '.$table.' as a ,'.$this->tablePrefix.'node as b where a.node_id=b.id and b.pid='.$appId.' and a.role_id='.$groupId.' '); return $rs; } function setGroupModules($groupId,$moduleIdList) { if(empty($moduleIdList)) { return true; } if(is_array($moduleIdList)) { $moduleIdList = implode(',',$moduleIdList); } $where = 'a.id ='.$groupId.' AND b.id in('.$moduleIdList.')'; $rs = $this->db->execute('INSERT INTO '.$this->tablePrefix.'access (role_id,node_id,pid,level) SELECT a.id, b.id,b.pid,b.level FROM '.$this->tablePrefix.'role a, '.$this->tablePrefix.'node b WHERE '.$where); if($result===false) { return false; }else { return true; } } function delGroupAction($groupId,$moduleId) { $table = $this->tablePrefix.'access'; $result = $this->db->execute('delete from '.$table.' where level=3 and pid='.$moduleId.' and role_id='.$groupId); if($result===false) { return false; }else { return true; } } ~~~ 这样一个角色授权在一个页面内就可选择并保存完毕了。不用来回切模块、操作。 真正授权的苦力活就是添加节点了。其实那里可以优化用前端树组件做到一个页面ajax管理树节点。大家自己深入吧。 详情可以去示列里看rbac的源码。我就不赘述了。 # AUTH Auth 类已经在ThinkPHP代码仓库中存在很久了,但是因为一直没有出过它的教程, 很少人知道它, 它其实比RBAC更方便 。 RBAC是按节点进行认证的,如果要控制比节点更细的权限就有点困难了,比如页面上面的操作按钮, 我想判断用户权限来显示这个按钮, 如果没有权限就不会显示这个按钮; 再比如我想按积分进行权限认证, 积分在0-100时能干什么, 在101-200时能干什么。 这些权限认证用RABC都很困难。 下面介绍 Auth权限认证, 它几乎是全能的, 除了能进行节点认证, 上面说的RABC很难认证的两种情况,它都能实现。 Auth权限认证是按规则进行认证。我先说说它的原理。 在数据库中我们有 规则表(think_auth_rule) ,用户组表(think_auth_group), 用户组明显表(think_auth_group_access) 我们在规则表中定义权限规则 , 在用户组表中定义每个用户组有哪些权限规则,在用户组明显表中 定义用户所属的用户组。 下面举例说明。 我们要判断用户是否有显示一个操作按钮的权限, 首先定义一个规则, 在规则表中添加一个名为 show_button 的规则。 然后在用户组表添加一个用户组,定义这个用户组有show_button 的权限规则(think_auth_group表中rules字段存得时规则ID,多个以逗号隔开), 然后在用户组明细表定义 UID 为1 的用户 属于刚才这个的这个用户组。 ok,表数据定义好后, 判断权限很简单 ~~~ import('ORG.Util.Auth');//加载类库 $auth=new Auth(); if($auth->check('show_button',1)){// 第一个参数是规则名称,第二个参数是用户UID //有显示操作按钮的权限 }else{ //没有显示操作按钮的权限 } ~~~ Auth类同样可以做像RBAC一样的对节点进行认证。 我们只要将规则名称,定义为节点名称就行了。 和RABC一样 在公共控制器CommonAction 中定义_initialize 方法, ~~~ <?php class CommonAction extends Action{ public function _initialize(){ import('ORG.Util.Auth');//加载类库 $auth=new Auth(); if(!$auth->check(MODULE_NAME.'-'.ACTION_NAME,session('uid'))){ $this->error('你没有权限'); } } } ~~~ 这时候我们可以在数据库中添加的节点规则, 格式为: “控制器名称-方法名称” Auth 类 还可以多个规则一起认证 如: `$auth->check('rule1,rule2',uid); ` 表示 认证用户只要有rule1的权限或rule2的权限,只要有一个规则的权限,认证返回结果就为true 即认证通过。 默认多个权限的关系是 “or” 关系,也就是说多个权限中,只要有个权限通过则通过。 我们也可以定义为 “and” 关系 `$auth->check('rule1,rule2',uid,'and'); ` 第三个参数指定为"and" 表示多个规则以and关系进行认证, 这时候多个规则同时通过认证 才有权限。只要一个规则没有权限则就会返回false。 Auth认证,一个用户可以属于多个用户组。 比如我们对 show_button这个规则进行认证, 用户A 同时属于 用户组1 和用户组2 两个用户组 , 用户组1 没有show_button 规则权限, 但如果用户组2 有show_button 规则权限,则一样会权限认证通过。 `$auth->getGroups(uid)` 通过上面代码,可以获得用户所属的所有用户组,方便我们在网站上面显示。 Auth类还可以按用户属性进行判断权限, 比如 按照用户积分进行判断, 假设我们的用户表 (think_members) 有字段 score 记录了用户积分。 我在规则表添加规则时,定义规则表的condition 字段,condition字段是规则条件, 默认为空 表示没有附加条件, 用户组中只有规则 就通过认证。 如果定义了 condition字段, 用户组中有规则 不一定能通过认证, 程序还会判断是否满足 附加条件。 比如我们添加几条规则: name字段: grade1 , condition字段: {score}<100 name字段: grade2, condition字段: {score}>100 and {score}<200 name 字段: grade3, condition字段 : {score}>200 and {score}<300 这里 {score} 表示 think_members 表 中字段 score的值。 那么这时候 $auth->check('grade1',uid) 是判断用户积分是不是0-100 $auth->check('grade2',uid) 判断用户积分是不是在100-200 $auth->check('grade3',uid) 判断用户积分是不是在200-300 Auth 类认证的使用方法 大致如上,是否有点相见恨晚的感觉? ---------------------------------------------------- 在使用Auth类前需要配置config.php ~~~ 'AUTH_CONFIG'=>array( 'AUTH_ON' => true, //认证开关 'AUTH_TYPE' => 1, // 认证方式,1为时时认证;2为登录认证。 'AUTH_GROUP' => 'think_auth_group', //用户组数据表名 'AUTH_GROUP_ACCESS' => 'think_auth_group_access', //用户组明细表 'AUTH_RULE' => 'think_auth_rule', //权限规则表 'AUTH_USER' => 'think_members'//用户信息表 ) ~~~ 需要导入数据库 ~~~ -- ---------------------------- -- think_auth_rule,规则表, -- id:主键,name:规则唯一标识, title:规则中文名称 status 状态:为1正常,为0禁用,condition:规则表达式,为空表示存在就验证,不为空表示按照条件验证 -- ---------------------------- DROP TABLE IF EXISTS `think_auth_rule`; CREATE TABLE `think_auth_rule` ( `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, `name` char(80) NOT NULL DEFAULT '', `title` char(20) NOT NULL DEFAULT '', `status` tinyint(1) NOT NULL DEFAULT '1', `condition` char(100) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- ---------------------------- -- think_auth_group 用户组表, -- id:主键, title:用户组中文名称, rules:用户组拥有的规则id, 多个规则","隔开,status 状态:为1正常,为0禁用 -- ---------------------------- DROP TABLE IF EXISTS `think_auth_group`; CREATE TABLE `think_auth_group` ( `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, `title` char(100) NOT NULL DEFAULT '', `status` tinyint(1) NOT NULL DEFAULT '1', `rules` char(80) NOT NULL DEFAULT '', PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- ---------------------------- -- think_auth_group_access 用户组明细表 -- uid:用户id,group_id:用户组id -- ---------------------------- DROP TABLE IF EXISTS `think_auth_group_access`; CREATE TABLE `think_auth_group_access` ( `uid` mediumint(8) unsigned NOT NULL, `group_id` mediumint(8) unsigned NOT NULL, UNIQUE KEY `uid_group_id` (`uid`,`group_id`), KEY `uid` (`uid`), KEY `group_id` (`group_id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; ~~~ 最后需要下载Auth类文件到你的项目中,大家可以下载我上传的附件, Auth类在ThinkPHP代码仓库中的位置在: https://github.com/liu21st/extend/blob/master/Extend/Library/ORG/Util/Auth.class.php 【目前Auth认证还没用示例, 欢迎广大TPer 贡献示例!!!】 ## auth demo 本文转自ThinkPHP官网的[Auth权限认证暴力来袭,有图有码有种子,绝对暴力!](http://www.thinkphp.cn/code/714.html) auth的相关介绍说明,官方网站上已经有很多相关文章了,我就不再重复啰嗦了.在这里,我直接上demo以及简要的说下注意事项及一点点建议.废话不多说,直接进入主题. 一、auth认证原理 auth类通过认证用户uid所在的角色组是否拥有对应的权限.如图所示 ![2015-07-26/55b4a67537019](http://box.kancloud.cn/2015-07-26_55b4a67537019.png) 二、准备工作 1、建表(主要对新增的字段或表作说明,如没有改变过的表,这里不做说明) 1) tk_auth_group 用户组 ~~~ CREATE TABLE `tk_auth_group` ( `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, `title` char(100) NOT NULL DEFAULT '', `status` tinyint(1) NOT NULL DEFAULT '1', `rules` char(80) NOT NULL DEFAULT '', `describe` char(50) NOT NULL DEFAULT '', #新增,对用户组作简单的说明 PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; ~~~ 2) tk_auth_rule 规则表 ~~~ CREATE TABLE `tk_auth_rule` ( `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, `name` char(80) NOT NULL DEFAULT '', `title` char(20) NOT NULL DEFAULT '', `type` tinyint(1) NOT NULL DEFAULT '1', `status` tinyint(1) NOT NULL DEFAULT '1', `condition` char(100) NOT NULL DEFAULT '', `mid` tinyint(3) unsigned NOT NULL DEFAULT '0', #新增,外键,和tk_modules的id对应,对规则分类处理,方便管理 PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; ~~~ 3) tk_modules 模块表 ~~~ CREATE TABLE `tk_modules` ( `id` tinyint(3) unsigned NOT NULL AUTO_INCREMENT, `moduleName` varchar(20) NOT NULL DEFAULT '', #模块名称 PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ~~~ 4) tk_members 用户表 ~~~ CREATE TABLE `tk_members` ( `uid` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(20) NOT NULL DEFAULT '', `password` char(32) NOT NULL DEFAULT '', `score` mediumint(8) unsigned NOT NULL, #用户积分 PRIMARY KEY (`uid`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; ~~~ 2、配置文件 1) 数据库项配置(本人采用的是pdo方式,如果你的环境不支持pdo,则手动改为其它方式) ~~~ //数据库配置 //pdo类型,采用dsn方式连接 'DB_TYPE'=>'pdo', 'DB_USER'=>'root', 'DB_PWD'=>'123456', 'DB_PREFIX'=>'tk_', 'DB_PORT'=>'3306', 'DB_DSN'=>'mysql:host=localhost;dbname=auth;charset=utf8', ~~~ 2) 角色组设置,设置超级管理员组,直接跳过认证 ~~~ //超级管理员id,拥有全部权限,只要用户uid在这个角色组里的,就跳出认证.可以设置多个值,如array('1','2','3') 'ADMINISTRATOR'=>array('1'), ~~~ 3、注意事项 1)运行环境:要求php版本5.3以上 2)对原auth类作了小小的改动.原文件155行左右 ~~~ $user_groups = M() ->table($this->_config['AUTH_GROUP_ACCESS'] . ' a') ->where("a.uid='$uid' and g.status='1'") ->join($this->_config['AUTH_GROUP']." g on a.group_id=g.id") ->field('id,rules')->select(); //原field方法的参数为field('rules'),增加id主要是获取用户组的id,便于验证超级管理员. $groups[$uid]=$user_groups?:array(); ~~~ 建议官方在下次更新能添加id,或增加一个参数,便于用户灵活设置 三、安装说明 使用说明请看压缩包的readme.md文件.另外代码我都作了详细的说明,大家一看就懂.下面上几张暴力图. ![2015-07-26/55b4a79ea4a6b](http://box.kancloud.cn/2015-07-26_55b4a79ea4a6b.png) ![2015-07-26/55b4a7ac8f57d](http://box.kancloud.cn/2015-07-26_55b4a7ac8f57d.png) ![2015-07-26/55b4a7b74a7ca](http://box.kancloud.cn/2015-07-26_55b4a7b74a7ca.png) ![2015-07-26/55b4a7c534c29](http://box.kancloud.cn/2015-07-26_55b4a7c534c29.png) ![2015-07-26/55b4a7d2d217d](http://box.kancloud.cn/2015-07-26_55b4a7d2d217d.png) 四、结语 本demo提供给大家互相学习,希望通过它对auth认证有一个全面的了解.其实基于tp使用auth权限认证没有那么复杂,那么难.希望通过能在此基础上扩展出再多的功能来. 补充: 用户密码都是admin 附件 [auth.zip](http://www.thinkphp.cn/code/download/id/714.html)