fleaPHP里的RBAC权限模型

 

 

转自:http://old.fleaphp.org/index.php?q=book/export/html/36


RBAC 是英文(Role-Based Access Control)的缩写,也就是基于角色的访问控制。RBAC 的定义比较晦涩,我就以比较生动的形式来阐述什么是 RBAC。





ATM 机的一天

假设有一台 ATM(自动提款机)放在街边,我们来看看这个 ATM 度过的一天。


  1. 早上,有一个家伙走到 ATM 面前,对着机器说:“芝麻开门,芝麻开门,给我 100 块!”。很显然 ATM不会有任何动作。失望之余,这个家伙踢了 ATM 一脚走了。
  2. 中午,一位漂亮的 Office lady 走到 ATM 机面前,放入她的信用卡,输入密码后,取出了 1200 块钱。当然,这些钱很快就会变成一件衣服或是化妆品。
  3. 下班时分,银行的工作人员来到 ATM 机器面前,放入一张特制的磁卡,然后输入密码。从中查询到 ATM 机器内还有充足的现金,无需补充。所以他很高兴的开着车去下一台 ATM 机器所在地了。

现在我们要开发一台具有同样功能的 ATM 机,应该怎么做呢?

首先,我们的 ATM 机不能让人随便取钱,不然银行会破产的。接下来,ATM 机需要一个让人们放入磁卡并输入密码的设备。人们放入磁卡并输入密码后,ATM 机还要能够判断这张磁卡的卡号和密码是否有效,并且匹配。之后,ATM 机必须判断磁卡的卡号属于哪种型,如果是信用卡,那么则显示查询账户余额和取款的界面。如果是特制的磁卡,则显示 ATM 机内的现金余额。





ATM 与 RBAC

上面的例子显得有点荒诞,但是却是一个典型的基于角色的访问控制。


  1. 对于没有磁卡或者输入了错误密码用户,一律拒绝服务,也就是不允许进行任何其他操作;
  2. 如果输入了正确的密码,必须判断用户输入哪一种型,并提供相应的服务界面;
  3. 如果用户尝试访问自己不能使用的服务,那么要明确告诉用户这是不可能的。

这个流程中,一共出现了两种角色:信用卡用户和管理卡用户。而那些没有磁卡的用户,都属于没有角色一。RBAC 要能够工作,至少需要两个数据:角色信息访问控制表

角色信息通常是指某个用户具有的角色,例如你持有一张信用卡,那么你就具有“信用卡用户”这个角色。如果你持有一张管理卡,那么你就具有“管理卡用户”这个角色。如果你既没有信用卡,又没有管理卡,那么你就没有上述两种角色。

有了角色信息,RBAC 系统还需要一个访问控制表访问控制表(Access Control Table)是一组数据,用于指出哪些角色可以使用哪个功能,哪些角色不能使用哪个功能。例如在 ATM 机中,具有“信用卡用户”角色,就可以使用查询账户余额和取款两项功能;而具有“管理卡用户”角色,就可以使用查询 ATM 机内现金余额的动能。

我们来模拟一次 ATM 机的操作:


  1. 唐雷有一张信用卡,他放入 ATM 机并输入了正确的密码。这时,他被 ATM 机认为具有“信用卡用户”角色。
  2. 根据上面的判断结果,ATM 机显示了一个操作界面,上面有查询账户余额和取款两项操作按钮
  3. 唐雷按下了“查询账户余额”按钮,ATM 机的查询账户余额功能调用
  4. 在查询账户余额功能中,再次检查用户的角色信息,确定他可以使用这个功能
  5. 进行系列操作,然后将唐雷信用卡账户上的余额数字显示到屏幕上。
  6. 唐雷很郁闷他的信用卡又透支了,悻悻然取出卡走人了。这时 ATM 自动清除当前的角色信息,为下一次操作做好准备。

从上面可以看出,RBAC 充当了系统的一道安全屏障。所有的操作都需要进过 RBAC 验证过后才能使用。这样充分保证了系统安全性。





RBAC 概念

在 FleaPHP 的 RBAC 组件中,只有下列几项概念需要理解:



除了上述三个概念,要想 RBAC 系统能够正常工作,还需要用户信息管理器、角色信息管理器和访问控制器三个部件。



FleaPHP 中已经实现了上述三个部件,所以开发者要做的功能就比较简单了。





使用 RBAC

FleaPHP 中提供了 FLEA_Com_RBAC、FLEA_Com_RBAC_UsersManager 和 FLEA_Com_RBAC_RolesManager 三个部件,以及 FLEA_Dispatcher_Auth 调度器。

其中,FLEA_Com_RBAC_UsersManager 提供用户信息存储服务,而 FLEA_Com_RBAC_RolesManager 提供角色信息存储服务。FLEA_Com_RBAC 则和 FLEA_Dispatcher_Auth 结合,一起提供了访问控制能力

下面我们来看看 RBAC 到底怎么工作的。

修改应用程序设置

使用访问控制功能,首先需要修改应用程序设置。让应用程序使用 FLEA_Dispatcher_Auth 调度器,而不是默认的 FLEA_Dispatcher_Simple 调度器。

<?php

require('FLEA/FLEA.php');
set_app_inf('dispatcher', 'FLEA_Dispatcher_Auth');
/**
* ...
* 其他初始化代码
* ...
*/

run();

?>

FLEA_Dispatcher_Auth 调度器和 FLEA_Dispatcher_Simple 调度器的基本功能一样。但在调用控制器动作方法前,FLEA_Dispatcher_Auth 调度器会通过 FLEA_Com_RBAC 组件获取保存在 session 中的用户角色信息,然后再读取控制器访问控制表(ACT)。最后调用 FLEA_Com_RBAC::check() 方法检查用户拥有的角色是否可以访问这个控制器及要调用控制器动作。

验证通过,则控制器动作方法会被调用,否则将显示错误信息,或者调用应用程序设置dispatcherAuthFailedCallback 指定的错误处理程序

准备控制器的 ACT 文件

设置好应用程序后,接下来要做的就是为控制器准备 ACT 文件

ACT 文件控制器文件同名,并且保存在同一个目录下,只是扩展名为 .act.php。例如控制器Controller_Default 的文件名是 Controller/Default.php,那么该控制器的 ACT 文件名就是 Controller/Default.act.php。

ACT 文件内容通常使用下面的格式

<?php

return array(
'allow' => 'POWER_USER, SYSTEM_ADMIN',

'actions' => array(
'remove' => array(
'allow' => 'SYSTEM_ADMIN',
),

'create' => array(
'deny' => 'SYSTEM_ADMIN',
),
),
);

?>

可以看到,ACT 文件只是单纯的返回一个数组。这个数组遵循下面的格式

array(
'allow' => '允许访问该控制器角色名',
'deny' => '禁止访问该控制器角色名',

'actions' => array(
'动作名' => array(
'allow' => '允许访问该动作的角色名',
'deny' => '禁止访问该动作的角色名',
),
// .... 更动作
),
);

在上面的格式中,角色名可以是个,例如“POWER_USER, MANAGER”。只需要用“,”分隔角色名就可以了。

通常,我们只需要为控制器指定 allow 或者 deny 就可以了。但有时候我们要允许个角色都可以访问该控制器,但该控制器中的特定方法只允许上述角色中部分角色可以访问。这时,我们可以通过'动作名' => array('allow' => '角色名', 'deny' => '角色名') 的方式来指定该控制器动作特有的 ACT。

为了便于开发,FleaPHP 预定义了几个角色,分别是:


  • RBAC_EVERYONE:表示任何用户(不管该用户是否具有角色信息
  • RBAC_HAS_ROLE:表示具有任何角色的用户(该用户必须有角色信息
  • RBAC_NO_ROLE:表示不具有任何角色的用户
  • RBAC_NULL:表示该设置没有值

 特别注意,上述四个预定义角色并不是字符串,而是常量。因此必须以 'allow' => RBAC_EVERYONE 这样方式使用。并且不能和其他角色混用,例如 'allow' => RBAC_EVERYONE . ', POWER_USER' 就是错误的。

验证规则

在验证时,首先从 session 中取出用户的角色信息。取出来的角色信息是一个数组数组中每一个项为用户具有的一个角色。例如:

$userRoles = array(
'POWER_USER',
'MANAGER',
);

然后取出控制器的 ACT,再按照下列规则进行验证:


  1. 如果 ACT 的 allow 为 RBAC_EVERYONE,则进行下列检查:
    1. 如果 ACT 的 deny 为 RBAC_NULL,则表示表示允许任何角色访问,验证结果为 true;
    2. 如果 ACT 的 deny 为 RBAC_NO_ROLE,则表示用户只要具有角色信息,就可以访问。因此如果用户的角色信息为空白,则验证结果为 false,否则验证结果为true;
    3. 如果 ACT 的 deny 为 RBAC_HAS_ROLE,则表示用户只要具有角色信息,就不允许访问。因此如果用户的角色信息为空白,则验证结果为 true,否则验证结果为false;
    4. 如果 ACT 的 deny 为 RBAC_EVERYONE,则表示这个 ACT 存在冲突(因为 allow 和 deny 都为 RBAC_EVERYONE);
    5. 检查用户角色名是否出现在 deny 指定的角色名中,如果有,则验证结果为 false,否则验证结果为 true。

  1. 如果 ACT 的 allow 为 RBAC_HAS_ROLE,则表示用户只要具有角色信息,就可以访问。因此如果用户的角色信息为空白,则验证结果为 false,否则验证结果为 true;

  1. 如果 ACT 的 allow 为 RBAC_NO_ROLE,则表示用户只要具有角色信息,就不允许访问。因此如果用户的角色信息为空白,则验证结果为 true,否则验证结果为false;

  1. 如果 ACT 的 allow 为 RBAC_NULL,则进行下列检查:
    1. 如果 ACT 的 deny 为 RBAC_NULL,则表示 ACT 既没有设置允许访问的角色,也没有设置拒绝访问的角色,这时候假定为允许访问,所以验证结果为 true;
    2. 如果 ACT 的 deny 为 RBAC_NO_ROLE,则表示用户只要具有角色信息,就可以访问。因此如果用户的角色信息为空白,则验证结果为 false,否则验证结果为true;
    3. 如果 ACT 的 deny 为 RBAC_HAS_ROLE,则表示用户只要具有角色信息,就不允许访问。因此如果用户的角色信息为空白,则验证结果为 true,否则验证结果为false;
    4. 如果 ACT 的 deny 为 RBAC_EVERYONE,则表示拒绝任何角色访问,验证结果为 false;
    5. 5) 检查用户用户角色名是否出现在 deny 指定的角色名中,如果有,则验证结果为 false,否则验证结果为 true。

  1. 验证进行到这里时,ACT 的 allow 必然是角色名,因此只要用户具有的角色名在 allow 指定的角色名中,验证结果就为true,否则验证结果为false。

ACT 示例

之所有进行这么复杂的验证,是考虑到样的验证需求。看看下面几个例子:


  • 只要具有角色,就允许访问:

array(
'allow' => RBAC_HAS_ROLE
);

  • 只要具有角色,就不允许访问:

array(
'deny' => RBAC_HAS_ROLE
);

  • 用户具有角色,并且没有 POWER_USER 角色时,允许访问:

array(
'allow' => RBAC_HAS_ROLE,
'deny' => 'POWER_USER'
)

  • 用户只要没有 MANAGER 角色,就允许访问:

array(
'deny' => 'MANAGER',
)

  • 用户具有 POWER_USER 和 MANAGER 角色时允许访问,但具有 SYSTEM_ADMIN 角色时拒绝访问。对于这个 ACT 定义,如果用户的角色为 ‚POWER_USER, GUEST‘,则允许访问。如果用户的角色为 ‚POWER_USER, SYSTEM_ADMIN‘,则不允许访问。因为 deny 的优先级总是大于 allow。

array(
'allow' => 'POWER_USER, MANAGER',
'deny' => 'SYSTEM_ADMIN',
)

上面虽然只说了对控制器的验证,但对控制器动作的验证规则是完全相同的。只是只有当用户被允许访问控制器时,才会对要访问的控制器动作进行验证。这种机制提供非常高的灵活性,例如:

<?php

return array(
'allow' => 'POWER_USER, SYSTEM_ADMIN',

'actions' => array(
'remove' => array(
'allow' => 'SYSTEM_ADMIN',
),

'create' => array(
'deny' => 'SYSTEM_ADMIN',
),
),
);

?>

用户只要具有 POWER_USER 和 SYSTEM_ADMIN 两个角色之一,就可以访问这个控制器。但只有当用户具有 SYSTEM_ADMIN 角色时,才允许使用控制器的 remove 动作。反之,如果用户具有 SYSTEM_ADMIN 角色,就不允许使用控制器的 create 动作。

 特别注意,不管是 allow 还是 deny,只要用户具有的角色有其中之一符合条件就会判定该规则有效。例如 'allow' => 'POWER_USER, SYSTEM_ADMIN' 只要用户具有 POWER_USER 和 SYSTEM_ADMIN 两个角色之一,就算作允许访问。而不需要用户同时具有 POWER_USER 和 SYSTEM_ADMIN 角色。

角色信息存储服务

准备好控制器的 ACT 后,我们还需要使用角色信息存储服务,来管理应用程序中会用到的角色信息

首先,我们要建立如下的数据表(假定使用 MySQL)。这个数据表很简单,每行记录存储一个角色。

CREATE TABLE `roles` (
`role_id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`rolename` VARCHAR( 32 ) NOT NULL ,
`created` INT NULL ,
`updated` INT NULL
);

然后我们在应用程序中就可以从 FLEA_Com_RBAC_RolesManager 派生一个对象来管理角色信息了:

load_class('FLEA_Com_RBAC_RolesManager');
class MyRolesManager extends FLEA_Com_RBAC_RolesManager
{
var $tableName = 'roles';
var $primaryKey = 'role_id';
}

$rolesManager =& get_singleton('MyRolesManager');
/* @var $rolesManager MyRolesManager */
$role = array('rolename' => 'SYSTEM_ADMIN');
$rolesManager->create($role);
$role = array('rolename' => 'POWER_USER');
$rolesManager->create($role);
$role = array('rolename' => 'MANAGER');
$rolesManager->create($role);

事实上,FLEA_Com_RBAC_RolesManager 是一个表数据入口对象,所以可以直接使用 create()、find() 等方法来添加、查询角色信息

用户信息存储服务

只有角色信息,RBAC 还无法工作。我们还需要用户信息存储服务。这些服务由 FLEA_Com_RBAC_UsersManager 来提供。首先建立存储用户信息数据表(仍然假定使用 MySQL):

CREATE TABLE `users` (
`user_id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`username` VARCHAR( 32 ) NOT NULL ,
`password` VARCHAR( 64 ) NOT NULL ,
`email` VARCHAR( 128 ) NOT NULL ,
`created` INT NULL ,
`updated` INT NULL
);

 特别注意,存储用户信息数据表中,至少需要用户名、密码和电子邮件地址三个字段

然后我们就可以很方便的存储和查询用户信息了:

load_class('FLEA_Com_RBAC_UsersManager');
class MyUsersManager extends FLEA_Com_RBAC_UsersManager
{
var $tableName = 'users';
var $primaryKey = 'user_id';
}

$usersManager =& get_singleton('MyUsersManager');
/* @var $usersManager MyUsersManager */

$user = array(
'username' => 'dualface',
'password' => '12345678',
'email' => 'dualface@gmail.com',
);
$usersManager->create($user);

上面的代码会建立一个用户名为 dualface 的用户,而密码明文是 12345678。你也许会奇怪,难道密码不需要加密存储吗?

实际上,FLEA_Com_RBAC_UsersManager 为你自动完成了该项工作。FLEA_Com_RBAC_UsersManager 在建立一个用户或者更新一个用户时,对于密码字段都是特别处理的。所以程序只需要提供密码明文就行了,而不需要自己加密。不过同时也要注意在更新用户时,不要更新密码字段。而是应该使用FLEA_Com_RBAC_UsersManager::changePassword() 方法来修改用户账户的密码

FLEA_Com_RBAC_UsersManager 采用什么加密方法存储密码,是由 FLEA_Com_RBAC_UsersManager::$encodeMethod 变量决定的。默认为 PWD_CRYPT,即使用 crypt() 函数加密。可用的加密方式还有 PWD_MD5(存储用 md5() 函数编码后的密码)和 PWD_CLEARTEXT(存储密码明文)。由于不同的加密方式,生成的密码长度都不同,所以建立用户信息数据表时,密码字段的长度为 64,而不是常用的 32。

 特别注意,crypt() 函数加密密码超过了 32 个字符,因此一定要确保用户信息表的密码字段有足够的长度。

如果用户信息表的用户名、密码和电子邮件地址三个字段不是默认的 username、password 和 email,那么要分别通过FLEA_Com_RBAC_UsersManager::$usernameField、FLEA_Com_RBAC_UsersManager::$passwordField 和 FLEA_Com_RBAC_UsersManager::$emailField 变量来指定。

FLEA_Com_RBAC_UsersManager 虽然也是一个表数据入口,但是提供了 findByUserId()、findByUsername、findByEmail 等便利的方法。请参考 API 文档中的 FLEA_Com_RBAC_UsersManager ,了解所有便利方法

用户指定角色

有了用户和角色信息,我们还需要将用户信息和角色信息关联起来。这仍然是通过 FLEA_Com_RBAC_UsersManager 来完成。

首先也是建立一个数据表,用来存储用户和角色之间的关联关系

CREATE TABLE `roles_users` (
`user_id` INT NOT NULL ,
`role_id` INT NOT NULL ,
PRIMARY KEY ( `user_id` , `role_id` )
);

接下来修改我们的 FLEA_Com_RBAC_UsersManager 继承

load_class('FLEA_Com_RBAC_UsersManager');

class MyUsersManager extends FLEA_Com_RBAC_UsersManager
{
var $tableName = 'users';
var $primaryKey = 'user_id';
var $rolesFields = 'roles';

var $manyToMany = array(
'tableClass' => 'MyRolesManager',
'mappingName' => 'roles',
'joinTable' => 'roles_users',
);
}

新增加的一个 MANY_TO_MANY 用于将用户和角色关联起来。**注意 MANY_TO_MANY 关联的 mappingName 选项一定要和 $rolesFields 变量的值一样。否则用 FLEA_Com_RBAC_UsersManager::fetchRoles() 无法取得用户的角色信息

不过只是这样的定义,还不能实际使用。我们看一段实际使用代码

<?php
require('FLEA/FLEA.php');
// 假定数据连接信息保存在 APP/Config/DSN.php 文件
register_app_inf('APP/Config/DSN.php');
// 由于没有调用 run() 来启动 MVC 模式,所以需要自行初始化 FleaPHP
__FLEA_PREPARE();

load_class('FLEA_Com_RBAC_RolesManager');
class MyRolesManager extends FLEA_Com_RBAC_RolesManager
{
var $tableName = 'roles';
var $primaryKey = 'role_id';
}

load_class('FLEA_Com_RBAC_UsersManager');
class MyUsersManager extends FLEA_Com_RBAC_UsersManager
{
var $tableName = 'users';
var $primaryKey = 'user_id';

var $manyToMany = array(
'tableClass' => 'MyRolesManager',
'mappingName' => 'roles',
'joinTable' => 'roles_users',
);
}

$usersManager =& get_singleton('MyUsersManager');
/* @var $usersManager MyUsersManager */

// 取出用户
$user = $usersManager->findByUsername('dualface');
// 清空现有的角色信息
$user[$usersManager->rolesField] = array();

// 取出 POWER_USER 角色
$rolesManager =& get_singleton('MyRolesManager');
/* @var rolesManager MyRolesManager */
$role = $rolesManager->find(array('rolename' => 'POWER_USER'));
// 指定给用户
$user[$usersManager->rolesField][] = $role[$rolesManager->primaryKey];

// 取出 MANAGER 角色
$role = $rolesManager->find(array('rolename' => 'MANAGER'));
// 指定给用户
$user[$usersManager->rolesField][] = $role[$rolesManager->primaryKey];

// 保存修改后的用户信息
$usersManager->update($user);

// 重新从数据库读取用户信息,确定为用户指定的角色信息已经保存到数据
$user = $usersManager->findByUsername('dualface');
dump($user);

运行这段代码,可以看到为用户指定的角色信息,确实保存到数据库了:

通常,我们会在应用程序中提供一个管理界面,用于管理用户信息,并且可以为用户指定角色。这样的管理界面似下图:

上图中的权限实际上就是可以给用户指定的角色,只不过换了一个称谓而已。

用户登录

由于要使用 RBAC 组件进行访问控制,所以我们的用户登录部分要写几行代码常见的登录代码如下:

<?php
require('FLEA/FLEA.php');
// 假定数据连接信息保存在 APP/Config/DSN.php 文件
register_app_inf('APP/Config/DSN.php');
// 由于没有调用 run() 来启动 MVC 模式,所以需要自行初始化 FleaPHP
__FLEA_PREPARE();

load_class('FLEA_Com_RBAC_RolesManager');
class MyRolesManager extends FLEA_Com_RBAC_RolesManager
{
var $tableName = 'roles';
var $primaryKey = 'role_id';
}

load_class('FLEA_Com_RBAC_UsersManager');
class MyUsersManager extends FLEA_Com_RBAC_UsersManager
{
var $tableName = 'users';
var $primaryKey = 'user_id';

var $manyToMany = array(
'tableClass' => 'MyRolesManager',
'mappingName' => 'roles',
'joinTable' => 'roles_users',
);
}

/**
* 模拟登录
*/
login('dualface', '12345678');

/**
* 处理用户登录
*/
function login($username, $password) {
$usersManager =& get_singleton('MyUsersManager');
/* @var $usersManager MyUsersManager */

// 验证用户名和密码是否正确
$user = $usersManager->findByUsername($username);
if (!$user || !$usersManager->checkPassword($password, $user[$usersManager->passwordField])) {
echo "Username invalid or password mismatch.";
exit;
}

// 获取用户角色信息
$roles = $usersManager->fetchRoles($user);

// 获得 FLEA_Com_RBAC 组件实例
$rbac =& get_singleton('FLEA_Com_RBAC');
/* @var $rbac FLEA_Com_RBAC */

// 为了降低服务器负担,我们只在 session 中存储用户ID和用户
$sessionUser = array(
'USERID' => $user[$usersManager->primaryKey],
'USERNAME' => $user[$usersManager->usernameField],
);

// 将用户ID、用户名和角色信息保存到 session
$rbac->setUser($sessionUser, $roles);

// 登录成功
echo "Login successed, contents of session:";
dump($_SESSION);
}

?>

上面的代码中第一次出现了对 FLEA_Com_RBAC 的使用。通常我们只需要用到 FLEA_Com_RBAC::setUser()方法。这个方法用户信息和对应的角色信息保存到 session。为了节约服务器资源,我们要尽量减少保存在 session 中的内容

运行这个脚本,可以看到如下的输出

大家在开发自己的应用程序时,基本上可以把 login() 函数内容照搬过去。

用户注销

处理用户注销非常简单,通常用 session_destroy() 销毁 session 数据就可以了。如果只想清除用户登录信息,而不影响 session 中的其他信息,可以用下面两行代码

$rbac =& get_singleton('FLEA_Com_RBAC');
$rbac->clearUser();




实现访问控制

实际上,做完上面几个步骤,我们的 RBAC 已经可以工作了。你可以尝试登录系统,然后访问那些受到保护的控制器。然后再从系统注销后,重新访问受保护的控制器

目前,FleaPHP 的 RBAC 在处理 ACT 上,还不够灵活。每个控制器的 ACT 都是从文件载入的,而不是从数据库。但有聪明的开发者已经想出了变通的做法。

那就是把从数据库获取控制器 ACT 的代码写在控制器的 .act.php 文件中,例如:

APP\Controller\MyController.act.php

<?php

$modelACT =& get_singleton('Model_ControllerACTProvider');
/* @var $modelACT Model_ControllerACTProvider */
return $modelACT->getACT('MyController');

?>

当然,我们还要实现一个 Model_ControllerACTProvider 表数据入口:

<?php

load_class('FLEA_Db_TableDataGateway');

class Model_ControllerACTProvider extends FLEA_Db_TableDataGateway
{
var $tableName = 'controller_acts';
var $primaryKey = 'controller_name';

function getACT($controllerName) {
$row = parent::find(array($this->primaryKey => strtoupper($controllerName)));
return unserialize($row['act']);
}

function setACT($controllerName, $ACT) {
$row = array(
$this->primaryKey => strtoupper($controllerName),
'act' => serialize($ACT)
);
return parent::create($row);
}
}

?>

以及一个数据表:

CREATE TABLE `controller_acts` (
`controller_name` VARCHAR( 32 ) NOT NULL ,
`act` TEXT NOT NULL ,
`created` INT NULL ,
`updated` INT NULL ,
PRIMARY KEY ( `controller_name` )
)

这样一来,我们就可以在数据库中保存控制器的 ACT 了。