序
权限管理是无线运营系统中的核心模块,通过访问控制策略的配置,来约定人与资源的访问关系。
本文着重讲解如何通过PHP来构建一个灵活、通用、安全的权限管理系统。
关于权限
首先我们来聊聊权限。
权限系统一直以来是我们应用系统不可缺少的一个部分,若每个应用系统都重新对系统的权限进行设计,以满足不同系统用户的需求,将会浪费我们不少宝贵时间,所以花时间来设计一个相对通用的权限系统是很有意义的。
系统目标:对应用系统的所有对象资源和数据资源进行权限控制,比如 应用系统的功能菜单、各个界面的按钮、数据显示的列以及各种行级数据 进行权限的操控。
权限模型
设计初期,我们学习了Amazon的 IAM ,经过对比分析,最终我们选用 RBAC3模型 来指导系统的设计工作。
RBAC认为权限授权实际上是Who、What、How的问题。在RBAC模型中,who、what、how构成了访问权限三元组,也就是“Who对What(Which)进行How的操作”。
Who:权限的拥用者或主体(如Principal、User、Group、Role、Actor等等)
What:权限针对的对象或资源(Resource、Class)。
How:具体的权限(Privilege,正向授权与负向授权)。
Operator:操作。表明对What的How操作。也就是Privilege+Resource
Role:角色,一定数量的权限的集合。权限分配的单位与载体,目的是隔离User与Privilege的逻辑关系.
PHP架构之道
「如何用PHP构建我们的权限中心」
接下来我们将从 编码规范、依赖管理、数据源架构、数据处理、单元测试 等方面来体验一把PHP的神奇之旅。
编码规范
好的编码规范可以改善软件的可读性,可以促进团队成长,可以减少Bug,可以降低维护成本,可以。。。(这么X,我们必须要推广)
PHP社区一直百花齐放,拥有大量的函数库、框架和组件,因而PHP代码遵循或尽量接近同一个代码风格就非常重要。
框架互操作组(即PHP标准组)发布了一系列推荐风格。
权限中心的目录结构:
-- /tuniu/rbac
|-- src
| |-- App //应用建模层
| | |-- City.php
| | |-- Cms.php
| | |-- Menu.php
| |-- App.php
| |-- Auth.php
| |-- Orm //ActiveRecord层
| | |-- App
| | | |-- Resource
| | | | |-- Map.php
| | | |-- Resource.php
| | | |-- User.php
| | |-- App.php
| | |-- Log.php
| | |-- Role
| | | |-- User.php
| | |-- Role.php
| | |-- Rule.php
| | |-- User.php
| |-- Orm.php
| |-- Utils.php
|-- tests //测试
| |-- bootstrap.php
| |-- fixtures
| | |-- null.yml
| | |-- rbac
| | | |-- app.yml
| | | |-- auth.yml
| | | |-- role.yml
| |-- rbac
| | |-- appTest.php
| | |-- authTest.php
| | |-- roleTest.php
|-- vendor
|-- composer.json
|-- composer.lock
|-- phpunit.xml
|-- README.md
PSR-2,权限应用资源类:
namespace Tuniu\Rbac\Orm\App;
use Tuniu\Rbac\Orm;
use Tuniu\Rbac\Orm\App;
use Tuniu\Rbac\Orm\App\Resource\Map;
use Tuniu\Rbac\Orm\Rule;
use Tuniu\Rbac\Orm\User;
class Resource extends Orm {}
PSR-4,命名空间的约定:
\<NamespaceName>(\<SubNamespaceNames>)*\<ClassName>
类名 | 文件路径 |
---|---|
\Tuniu\Rbac\Orm\App\Resource |
/tuniu/rbac/src/Orm/App/Resource.php |
\Tuniu\Rbac\App\Cms |
/tuniu/rbac/src/App/Cms.php |
依赖管理
Composer 是PHP中用来管理依赖(dependency)关系的工具。你可以在自己的项目中声明所依赖的外部工具库(libraries),Composer会帮你安装这些依赖的库文件。
权限中心的依赖声明:
{
"name": "tuniu/rbac",
"require": { //声明依赖关系
"php": ">=5.3.0",
"squizlabs/php_codesniffer": "2.*", //PHP_CodeSniffer检查代码规范
"php-activerecord/php-activerecord": "dev-master" //ActiveRecord
},
"require-dev": { //声明开发依赖
"phpunit/phpunit": "~4.6",
"phpunit/dbunit": ">=1.2"
},
"autoload": {
"psr-4": {
"Tuniu\\Rbac\\": "src/" //命名空间
}
}
}
检查代码规范,执行单元测试。
$./vendor/bin/phpcs --config-set default_standard PSR2
$./vendor/bin/phpcs src
$./vendor/bin/phunit
^^^^^^ 眼涩,眼酸,眼疲劳,怎么办。。。
骚年,如果舒服了,就使劲往下滑动吧。。。
争渡,争渡,惊起一滩鸥鹭。
数据源架构
基于权限中心各表的关系(各种关联,各种回调),采用传统的模式,必将把精力耗在无尽的循环中。
于是乎,开始寻觅一种数据源的架构模式,Active Record很靠谱的出现了。
Active Record(中文名:活动记录)是一种领域模型模式,特点是一个模型类对应关系型数据库中的一个表,而模型类的一个实例对应表中的一行记录。
PHP ActiveRecord 是一个基于ActiveRecord设计模式开发的开源PHP/ORM库。它旨在大大简化与数据库的交互和减少手写SQL语句。它不同于其他的ORM,你不需要使用任何的代码生成器,也不费劲去手写、维护模型层的表映射文件。这个库的灵感来自Ruby on Rails,因此它也借鉴Ruby on Rails的想法和实现。(谁用谁知道)
下面介绍下这个小伙伴给编程带来的快乐:
-
Validation(数据验证)
validates_presence_of
validates_inclusion_of
场景(角色表数据约定)
/**
* 1:约定角色类型的范围
* 2:约定角色状态的范围
*/
public static $validates_inclusion_of = array(
array('f_type', 'in' => array('role', 'group', 'department', 'member')),
array('f_status', 'in' => array(1,2))
);
/**
* 设定角色名称、角色状态、角色类型、角色描述不能为空
*/
public static $validates_presence_of = array(
array('f_name'),
array('f_status'),
array('f_type'),
array('f_desc')
);
//录入数据不满足约定条件,就无法保存,再也不用担心那些脏脏的数据。
-
Callback(回调)
before_save
before_create
before_update
before_destroy
after_save
after_create
after_update
after_destroy
场景1(角色表操作记录)
//定义回调函数
public static $before_save = array('setMisc');
//每当角色表保存之前,都默默的把数据格式好,好开心。。。
public function setMisc()
{
//创建时间,创建人
$this->is_new_record() && ($this->f_create_at = date("Y-m-d H:i:s"));
$this->is_new_record() && ($this->f_create_by = $this->op);
//更新时间,更新人
$this->f_update_by = $this->op; //操作人
$this->f_update_at = date("Y-m-d H:i:s"); //操作时间
}
场景2(新增资源,推送资源映射表):
//定义回调函数
public static $after_save = array('syncRelations');
//每当资源保存之后,自动把资源数据中的映射字段值,推送给资源映射表。
public function syncRelations()
{
//获取资源的映射字段
$mapFiledValue = $this->getMapFiledValue();
$mapFiledValue
? $this->addMap($mapFiledValue)
: Map::removeAllByResourceId($this->id);
}
场景3(角色删除):
//定义回调函数
public static $after_destroy = array('deleteRelations');
//每当角色删除时,自动把系统中角色的成员和角色的资源规则清空,而且是事务的。
public function deleteRelations()
{
UserRelations::removeAllByRoleId($this->id);
RuleRelatinos::removeAllByRoleId($this->id);
}
-
Association(关联)
has_many
场景(新增角色用户)
//定义角色表和角色用户表的关系
public static $has_many = array(
array(
'relations',
'foreign_key' => 'f_role_id',
'class_name' => "\\Tuniu\\Rbac\\Orm\\Role\\User",
)
);
//新增角色用户,默默的把role_id传递给了角色用户表,此处如果用SQL,简直不忍直视。
Role::first()->create_relation(
array(
'f_user_id' => $uid
)
);
事务(一致性与安全性)
权限系统中数据一致性和数据安全性的重要性是不言而喻,不用事务会被BS的。
在此我们郑重承诺,权限系统中每一次数据增删改请求,都是事务处理的。
比如角色保存:
self::transaction(
function () use ($params, &$role) {
$role->f_name = $params['name'];
$role->f_status = $params['status'];
$role->f_type = $params['type'];
$role->f_desc = $params['desc'];
if ($role->is_invalid()) {
throw new \Exception('角色相关操作失败', '900202');
}
$role->save();
}
);
篇幅有限,这个小伙伴的战斗力场景远甚于此。
坦白的说,用AR是一种编程享受。
单元测试(持续交付)
一切都如此的完美,没有测试,又如何可以证明这件事情的完美,又如何可以保障交付的质量。
PHPUnit 是一个轻量级的PHP测试框架。它是在PHP5下面对JUnit3系列版本的完整移植,是xUnit测试框架家族的一员(它们都基于模式先锋Kent Beck的设计)。
单元测试是一种提高软件质量非常有效的方法,但很重要的是我们要去实践和体会。
简单的介绍下权限管理中的角色行为测试用例:
角色行为测试
数据集(YAML)
t_rbac_user:
-
f_id: 1
f_name: "zhaoyang2"
f_create_at: "2013-10-10 17:04:05"
t_rbac_role_user:
-
f_id: 1
f_user_id: 1
f_role_id: 1
t_rbac_role:
-
f_id: 1
f_name: "运营研发部-1"
f_status: 1,
f_type: "department"
f_desc: "我们是运营研发部-1"
f_create_at: "2013-10-10 17:04:05"
f_create_by: "zhaoyang2"
f_update_by: "zhaoyang2"
测试代码:
//预设数据集
public function getDataSet()
{
return new \PHPUnit_Extensions_Database_DataSet_YamlDataSet(
fixture('rbac/role.yml')
);
}
/**
* 权限操作-异常验证
* @expectedException Exception
* @expectedExceptionMessage 您无权运营当前数据
*/
public function testRoleDeleteException()
{
Role::first()->remove();
}
/**
* 权限操作-删除验证
*/
public function testRoleDelete()
{
Role::first()->remove("zhaoyang2");
//验证数据行
$this->assertEquals(1, $this->getConnection()->getRowCount(Role::$table_name));
$this->assertEquals(1, $this->getConnection()->getRowCount(UserRelation::$table_name));
}
鉴权用例和应用管理用例,远比这个复杂,感兴趣的同学可以去 Fork 一把,了解下PHPUNIT的魅力。
PHPUNIT很强大,想合理运用的话,没有任何捷径,开始写测试用例吧。。
结束语
其实说架构算上下,就是和大家分享下权限中心的PHP之道。
高效便捷的使用PHP服务我们的工作。
多交流,多分享,书写更好的PHP代码,享受编程和技术所带来的快乐。