RBAC基於角色的權限訪問控制

RBAC是什麼,能解決什麼難題?

RBAC是Role-Based Access Control的首字母,譯成中文即基於角色的權限訪問控制,說白了也就是用戶經過角色與權限進行關聯[其架構靈感來源於操做系統的GBAC(GROUP-Based Access Control)的權限管理控制]。簡單的來講,一個用戶能夠擁有若干角色,每個角色擁有若干權限。這樣,就構形成「用戶-角色-權限」的受權模型。在這種模型中,用戶與角色之間,角色與權限之間,通常者是多對多的關係。其對應關係以下:
image
在許多的實際應用中,系統不僅是須要用戶完成簡單的註冊,還須要對不一樣級別的用戶對不一樣資源的訪問具備不一樣的操做權限。且在企業開發中,權限管理系統也成了重複開發效率最高的一個模塊之一。而在多套系統中,對應的權限管理只能知足自身系統的管理須要,不管是在數據庫設計、權限訪問和權限管理機制方式上均可能不一樣,這種不致性也就存在以下的憋端: php

  • 維護多套系統,重複造輪子,時間沒用在刀刃上
  • 用戶管理、組織機制等數據重複維護,數據的完整性、一致性很可貴到保障
  • 權限系統設計不一樣,概念理解不一樣,及相應技術差別,系統之間集成存在問題,單點登陸難度大,也複雜的企業系統帶來困難

RBAC是基於不斷實踐以後,提出的一個比較成熟的訪問控制方案。實踐代表,採用基於RBAC模型的權限管理系統具備如下優點:html

  • 因爲角色、權限之間的變化比角色、用戶關係之間的變化相對要慢得多,減少了受權管理的複雜性,下降管理開銷;
  • 並且可以靈活地支持應用系統的安全策略,並對應用系統的變化有很大的伸縮性;
  • 在操做上,權限分配直觀、容易理解,便於使用;分級權限適合分層的用戶級形式;
  • 重用性強。

ThinkPHP中RBAC實現體系

ThinkPHP中RBAC基於Java的Spring的Acegi安全系統做爲參考原型,並作了相應的簡化處理,以適應當前的ThinkPHP結構,提供一個多層、可定製的安全體系來爲應用開發提供安全控制。安全體系中主要有如下幾部分: node

  • 安全攔截器
  • 認證管理器
  • 決策訪問管理器
  • 運行身份管理器

安全攔截器

安全攔截器就比如一道道門,在系統的安全防禦系統中可能存在不少不一樣的安全控制環節,一旦某個環節你未經過安全體系認證,那麼安全攔截器就會實施攔截。算法

認證管理器

防禦體系的第一道門就是認證管理器,認證管理器負責決定你是誰,通常它經過驗證你的主體(一般是一個用戶名)和你的憑證(一般是一個密碼),或者更多的資料來作到。更簡單的說,認證管理器驗證你的身份是否在安全防禦體系受權範圍以內。sql

訪問決策管理

雖然經過了認證管理器的身份驗證,可是並不表明你能夠在系統裏面肆意妄爲,由於你還須要經過訪問決策管理這道門。訪問決策管理器對用戶進行受權,經過考慮你的身份認證信息和與受保護資源關聯的安全屬性決定是是否能夠進入系統的某個模塊,和進行某項操做。例如,安全規則規定只有主管才容許訪問某個模塊,而你並無被授予主管權限,那麼安全攔截器會攔截你的訪問操做。
決策訪問管理器不能單獨運行,必須首先依賴認證管理器進行身份確認,所以,在加載訪問決策過濾器的時候已經包含了認證管理器和決策訪問管理器。
爲了知足應用的不一樣須要,ThinkPHP 在進行訪問決策管理的時候採用兩種模式:登陸模式和即時模式。登陸模式,系統在用戶登陸的時候讀取改用戶所具有的受權信息到 Session,下次再也不從新獲取受權信息。也就是說即便管理員對該用戶進行了權限修改,用戶也必須在下次登陸後才能生效。即時模式就是爲了解決上面的問題,在每次訪問系統的模塊或者操做時候,進行即便驗證該用戶是否具備該模塊和操做的受權,從更高程度上保障了系統的安全。thinkphp

運行身份管理器

運行身份管理器的用處在大多數應用系統中是有限的,例如某個操做和模塊須要多個身份的安全需求,運行身份管理器能夠用另外一個身份替換你目前的身份,從而容許你訪問應用系統內部更深處的受保護對象。這一層安全體系目前的 RBAC 中還沒有實現。數據庫

ThinkPHP中RBAC認證流程

對應上面的安全體系,ThinkPHP 的 RBAC 認證的過程大體以下:數組

  1. 判斷當前模塊的當前操做是否須要認證
  2. 若是須要認證而且還沒有登陸,跳到認證網關,若是已經登陸 執行5
  3. 經過委託認證進行用戶身份認證
  4. 獲取用戶的決策訪問列表
  5. 判斷當前用戶是否具備訪問權限

權限管理的具體實現過程

RBAC相關的數據庫介紹

在ThinkPHP完整包,包含了RBAC處理類RBAC.class.php文件,位於Extend/Library/ORG/Util。打開該文件,其中就包含了使用RBAC必備的4張表,SQL語句以下(複製後請替換表前綴):安全

// 配置文件增長設置
// ADMIN_AUTH_KEY 管理員的認證鍵名
// USER_AUTH_ON 是否須要認證
// USER_AUTH_TYPE 認證類型 1 登陸認證 2 實時認證 見AccessDecision函數。
// USER_AUTH_KEY 認證識別號
// REQUIRE_AUTH_MODULE  須要認證模塊
// NOT_AUTH_MODULE 無需認證模塊
// USER_AUTH_GATEWAY 認證網關
// RBAC_DB_DSN  數據庫鏈接DSN
// RBAC_ROLE_TABLE 角色表名稱
// RBAC_USER_TABLE 用戶表名稱
// RBAC_ACCESS_TABLE 權限表名稱
// RBAC_NODE_TABLE 節點表名稱
CREATE TABLE IF NOT EXISTS `think_access` (
  `role_id` smallint(6) unsigned NOT NULL,
  `node_id` smallint(6) unsigned NOT NULL,
  `level` tinyint(1) NOT NULL,
  `module` varchar(50) DEFAULT NULL,
  KEY `groupId` (`role_id`),
  KEY `nodeId` (`node_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `think_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,
  PRIMARY KEY (`id`),
  KEY `level` (`level`),
  KEY `pid` (`pid`),
  KEY `status` (`status`),
  KEY `name` (`name`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `think_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,
  PRIMARY KEY (`id`),
  KEY `pid` (`pid`),
  KEY `status` (`status`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 ;

CREATE TABLE IF NOT EXISTS `think_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;

user用戶表,這個根據業務本身定義session

字段名 字段類型 做用
id INT 用戶ID(惟一識別號)
username VARCHAR(16) 用戶名
password VARCHAR(32) 密碼
email VARCHAR(100) 用戶郵箱
create_time TIMESTAMP 建立時間(時間戳)
logintime TIMESTAMP 最近一次登陸時間(時間戳)
loginip VARCHAR(15) 最近登陸的IP地址
status TINYINT(1) 啓用狀態:0:表示禁用;1:表示啓用
remark VARCHAR(255) 備註信息

role角色表

字段名 字段類型 做用
id INT 角色ID
name VARCHAR(20) 角色名稱
pid SMALLINT(6) 父角色對應ID
status TINYINT(1) 啓用狀態(同上)
remark VARCHAR(255) 備註信息

node節點表(功能模塊節點)

字段名 字段類型 做用
id SMALLINT(6) 節點ID
name VARCHAR(20) 節點名稱(英文名,對應應用控制器、應用、方法名)
title VARCHAR(50) 節點中文名(方便看懂)
status TINYINT(1) 啓用狀態(同上)
remark VARCHAR(255) 備註信息
sort SMALLINT(6) 排序值(默認值爲50)
pid SMALLINT(6) 父節點ID(如:方法pid對應相應的控制器)
level TINYINT(1) 節點類型:1:表示應用(模塊);2:表示控制器;3:表示方法

role_user用戶角色關係表

字段名 字段類型 做用
user_id INT 用戶ID
role_id SMALLINT(6) 角色ID

access權限表

字段名 字段類型 做用
role_id SMALLINT(6) 角色ID
node_id SMALLINT(6) 節點ID
level TINYINT(1) 冗餘節點表界別?
module VARCHAR(50) 模塊說明?

ThinkPHP的RBAC處理類

  1 class Rbac {
  2     // 認證方法,$map參數根據用戶表自定義,只要能校驗用戶名密碼就行。
  3     static public function authenticate($map,$model='') {
  4         if(empty($model)) $model =  C('USER_AUTH_MODEL');
  5         //使用給定的Map進行認證
  6         return M($model)->where($map)->find();
  7     }
  8  
  9     //用於檢測用戶權限的方法,並保存到Session中
 10     static function saveAccessList($authId=null) {
 11         if(null===$authId)   $authId = $_SESSION[C('USER_AUTH_KEY')];
 12         // 若是使用普通權限模式,保存當前用戶的訪問權限列表
 13         // 對管理員開發全部權限
 14         if(C('USER_AUTH_TYPE') !=2 && !$_SESSION[C('ADMIN_AUTH_KEY')] )
 15             $_SESSION['_ACCESS_LIST']    =    self::getAccessList($authId);
 16         return ;
 17     }
 18  
 19     // 取得模塊的所屬記錄訪問權限列表 返回有權限的記錄ID數組
 20     static function getRecordAccessList($authId=null,$module='') {
 21         if(null===$authId)   $authId = $_SESSION[C('USER_AUTH_KEY')];
 22         if(empty($module))  $module    =    CONTROLLER_NAME;
 23         //獲取權限訪問列表
 24         $accessList = self::getModuleAccessList($authId,$module);
 25         return $accessList;
 26     }
 27  
 28     //檢查當前操做是否須要認證
 29     static function checkAccess() {
 30         //若是項目要求認證,而且當前模塊須要認證,則進行權限認證
 31         if( C('USER_AUTH_ON') ){
 32             $_module    =    array();
 33             $_action    =    array();
 34             if("" != C('REQUIRE_AUTH_MODULE')) {
 35                 //須要認證的模塊
 36                 $_module['yes'] = explode(',',strtoupper(C('REQUIRE_AUTH_MODULE')));
 37             }else {
 38                 //無需認證的模塊
 39                 $_module['no'] = explode(',',strtoupper(C('NOT_AUTH_MODULE')));
 40             }
 41             //檢查當前模塊是否須要認證
 42             if((!empty($_module['no']) && !in_array(strtoupper(CONTROLLER_NAME),$_module['no'])) || (!empty($_module['yes']) && in_array(strtoupper(CONTROLLER_NAME),$_module['yes']))) {
 43                 if("" != C('REQUIRE_AUTH_ACTION')) {
 44                     //須要認證的操做
 45                     $_action['yes'] = explode(',',strtoupper(C('REQUIRE_AUTH_ACTION')));
 46                 }else {
 47                     //無需認證的操做
 48                     $_action['no'] = explode(',',strtoupper(C('NOT_AUTH_ACTION')));
 49                 }
 50                 //檢查當前操做是否須要認證
 51                 if((!empty($_action['no']) && !in_array(strtoupper(ACTION_NAME),$_action['no'])) || (!empty($_action['yes']) && in_array(strtoupper(ACTION_NAME),$_action['yes']))) {
 52                     return true;
 53                 }else {
 54                     return false;
 55                 }
 56             }else {
 57                 return false;
 58             }
 59         }
 60         return false;
 61     }
 62  
 63     // 登陸檢查
 64     static public function checkLogin() {
 65         //檢查當前操做是否須要認證
 66         if(self::checkAccess()) {
 67             //檢查認證識別號
 68             if(!$_SESSION[C('USER_AUTH_KEY')]) {
 69                 if(C('GUEST_AUTH_ON')) {
 70                     // 開啓遊客受權訪問
 71                     if(!isset($_SESSION['_ACCESS_LIST']))
 72                         // 保存遊客權限
 73                         self::saveAccessList(C('GUEST_AUTH_ID'));
 74                 }else{
 75                     // 禁止遊客訪問跳轉到認證網關
 76                     redirect(PHP_FILE.C('USER_AUTH_GATEWAY'));
 77                 }
 78             }
 79         }
 80         return true;
 81     }
 82  
 83     //權限認證的過濾器方法
 84     static public function AccessDecision($appName=MODULE_NAME) {
 85         //檢查是否須要認證
 86         if(self::checkAccess()) {
 87             //存在認證識別號,則進行進一步的訪問決策
 88             $accessGuid   =   md5($appName.CONTROLLER_NAME.ACTION_NAME);
 89             if(empty($_SESSION[C('ADMIN_AUTH_KEY')])) {
 90                 if(C('USER_AUTH_TYPE')==2) {
 91                     //增強驗證和即時驗證模式 更加安全 後臺權限修改能夠即時生效
 92                     //經過數據庫進行訪問檢查
 93                     $accessList = self::getAccessList($_SESSION[C('USER_AUTH_KEY')]);
 94                 }else {
 95                     // 若是是管理員或者當前操做已經認證過,無需再次認證
 96                     if( $_SESSION[$accessGuid]) {
 97                         return true;
 98                     }
 99                     //登陸驗證模式,比較登陸後保存的權限訪問列表
100                     $accessList = $_SESSION['_ACCESS_LIST'];
101                 }
102                 //判斷是否爲組件化模式,若是是,驗證其全模塊名
103                 if(!isset($accessList[strtoupper($appName)][strtoupper(CONTROLLER_NAME)][strtoupper(ACTION_NAME)])) {
104                     $_SESSION[$accessGuid]  =   false;
105                     return false;
106                 }
107                 else {
108                     $_SESSION[$accessGuid]    =    true;
109                 }
110             }else{
111                 //管理員無需認證
112                 return true;
113             }
114         }
115         return true;
116     }
117  
118     /**
119      +----------------------------------------------------------
120      * 取得當前認證號的全部權限列表,看起來有點暈,仔細閱讀。
121      +----------------------------------------------------------
122      * @param integer $authId 用戶ID
123      +----------------------------------------------------------
124      * @access public
125      +----------------------------------------------------------
126      */
127     static public function getAccessList($authId) {
128         // Db方式權限數據
129         $db     =   Db::getInstance(C('RBAC_DB_DSN'));
130         $table = array('role'=>C('RBAC_ROLE_TABLE'),'user'=>C('RBAC_USER_TABLE'),'access'=>C('RBAC_ACCESS_TABLE'),'node'=>C('RBAC_NODE_TABLE'));
131         $sql    =   "select node.id,node.name from ".
132                     $table['role']." as role,".
133                     $table['user']." as user,".
134                     $table['access']." as access ,".
135                     $table['node']." as node ".
136                     "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";
137         $apps =   $db->query($sql);
138         $access =  array();
139         foreach($apps as $key=>$app) {
140             $appId    =    $app['id'];
141             $appName     =     $app['name'];
142             // 讀取項目的模塊權限
143             $access[strtoupper($appName)]   =  array();
144             $sql    =   "select node.id,node.name from ".
145                     $table['role']." as role,".
146                     $table['user']." as user,".
147                     $table['access']." as access ,".
148                     $table['node']." as node ".
149                     "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";
150             $modules =   $db->query($sql);
151             // 判斷是否存在公共模塊的權限
152             $publicAction  = array();
153             foreach($modules as $key=>$module) {
154                 $moduleId     =     $module['id'];
155                 $moduleName = $module['name'];
156                 if('PUBLIC'== strtoupper($moduleName)) {
157                 $sql    =   "select node.id,node.name from ".
158                     $table['role']." as role,".
159                     $table['user']." as user,".
160                     $table['access']." as access ,".
161                     $table['node']." as node ".
162                     "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";
163                     $rs =   $db->query($sql);
164                     foreach ($rs as $a){
165                         $publicAction[$a['name']]     =     $a['id'];
166                     }
167                     unset($modules[$key]);
168                     break;
169                 }
170             }
171             // 依次讀取模塊的操做權限
172             foreach($modules as $key=>$module) {
173                 $moduleId     =     $module['id'];
174                 $moduleName = $module['name'];
175                 $sql    =   "select node.id,node.name from ".
176                     $table['role']." as role,".
177                     $table['user']." as user,".
178                     $table['access']." as access ,".
179                     $table['node']." as node ".
180                     "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";
181                 $rs =   $db->query($sql);
182                 $action = array();
183                 foreach ($rs as $a){
184                     $action[$a['name']]     =     $a['id'];
185                 }
186                 // 和公共模塊的操做權限合併
187                 $action += $publicAction;
188                 $access[strtoupper($appName)][strtoupper($moduleName)]   =  array_change_key_case($action,CASE_UPPER);
189             }
190         }
191         return $access;
192     }
193  
194     // 讀取模塊所屬的記錄訪問權限
195     static public function getModuleAccessList($authId,$module) {
196         // Db方式
197         $db     =   Db::getInstance(C('RBAC_DB_DSN'));
198         $table = array('role'=>C('RBAC_ROLE_TABLE'),'user'=>C('RBAC_USER_TABLE'),'access'=>C('RBAC_ACCESS_TABLE'));
199         $sql    =   "select access.node_id from ".
200                     $table['role']." as role,".
201                     $table['user']." as user,".
202                     $table['access']." as access ".
203                     "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.module='{$module}' and access.status=1";
204         $rs =   $db->query($sql);
205         $access    =    array();
206         foreach ($rs as $node){
207             $access[]    =    $node['node_id'];
208         }
209         return $access;
210     }
211 }

實際使用

登陸校驗

    // 用戶登陸頁面,若是已經登錄過,直接跳轉主頁
    public function login() {
        //RBAC類裏面也包含了checkLogin函數,用哪一個隨意。
        if(!isset($_SESSION[C('USER_AUTH_KEY')])) {
            $this->display();
        }else{
            $this->redirect('Index/index');
        }
    }

    // 用戶登出,清空相關session參數
    public function logout() {
        if(isset($_SESSION[C('USER_AUTH_KEY')])) {
            unset($_SESSION[C('USER_AUTH_KEY')]);
            unset($_SESSION);
            session_destroy();
            $this->success('登出成功!',__URL__.'/login/');
        }else {
            $this->error('已經登出!');
        }
    }

    // 檢查用戶是否登陸
    protected function checkUser() {
        //RBAC類裏面也包含了checkLogin函數,用哪一個隨意。
        if(!isset($_SESSION[C('USER_AUTH_KEY')])) {
            $this->error('沒有登陸','Public/login');
        }
    }

    // 登陸檢測
    public function checkLogin() {
        if(empty($_POST['account'])) {
            $this->error('賬號錯誤!');
        }elseif (empty($_POST['password'])){
            $this->error('密碼必須!');
        }elseif (empty($_POST['verify'])){
            $this->error('驗證碼必須!');
        }
        //生成認證條件
        $map            =   array();
        // 首先使用帳號密碼校驗是否正確,這裏的account等參數和數據庫用戶表設計相關。
        $map['account']    = $_POST['account'];
        $map["status"]    =    array('gt',0);
        //先檢測驗證碼
        if(session('verify') != md5($_POST['verify'])) {
            $this->error('驗證碼錯誤!');
        }
        //開始使用RBAC校驗。
        import ( '@.ORG.Util.RBAC' );
        //這裏會使用配置文件USER_AUTH_MODEL設置的model來校驗,去用戶表查看用戶密碼是否正確。
        $authInfo = RBAC::authenticate($map);
        //使用用戶名、密碼和狀態的方式進行認證
        if(false === $authInfo) {
            $this->error('賬號不存在或已禁用!');
        }else {
            //這裏的密碼比對根據本身的密碼加鹽算法進行修改,默認是用MD5
            if($authInfo['password'] != md5($_POST['password'])) {
                $this->error('密碼錯誤!');
            }
            //校驗沒問題,設置好session會話參數,相似authInfo的參數和用戶表的設計有關。
            $_SESSION[C('USER_AUTH_KEY')]    =    $authInfo['id'];
            $_SESSION['email']    =    $authInfo['email'];
            $_SESSION['loginUserName']        =    $authInfo['nickname'];
            $_SESSION['lastLoginTime']        =    $authInfo['last_login_time'];
            $_SESSION['login_count']    =    $authInfo['login_count'];
            //不必定是admin就是管理員,這個也看系統設計,見配置文件ADMIN_AUTH_KEY
            if($authInfo['account']=='admin') {
                $_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);
 
            // 用於檢測用戶權限的方法,並保存到Session中
            RBAC::saveAccessList();
            $this->success('登陸成功!',__APP__.'/Index/index');
 
        }
    }

 

自動校驗權限狀態

在Controller或者Action中初始化函數校驗是否登陸,而後繼承便可

 1     function _initialize() {
 2         import('@.ORG.Util.Cookie');
 3         // 用戶權限檢查,只檢查須要校驗的模塊
 4         if (C('USER_AUTH_ON') && !in_array(MODULE_NAME, explode(',', C('NOT_AUTH_MODULE')))) {
 5             import('@.ORG.Util.RBAC');
 6             //判斷是否有權限,見源碼。
 7             if (!RBAC::AccessDecision()) {
 8                 //檢查認證識別號
 9                 if (!$_SESSION [C('USER_AUTH_KEY')]) {
10                     //跳轉到認證網關
11                     redirect(PHP_FILE . C('USER_AUTH_GATEWAY'));
12                 }
13                 // 沒有權限 拋出錯誤
14                 if (C('RBAC_ERROR_PAGE')) {
15                     // 定義權限錯誤頁面
16                     redirect(C('RBAC_ERROR_PAGE'));
17                 } else {
18                     //開啓遊客驗證,跳轉登陸界面
19                     if (C('GUEST_AUTH_ON')) {
20                         $this->assign('jumpUrl', PHP_FILE . C('USER_AUTH_GATEWAY'));
21                     }
22                     // 提示錯誤信息
23                     $this->error(L('_VALID_ACCESS_'));
24                 }
25             }
26         }
27     }

 

總的來講,讀完源碼,再實際使用一遍,絕對搞定~

 

thinkphp demo下載地址

連接:http://pan.baidu.com/s/1ge5pkll 密碼:qds8

參考

http://www.lyblog.net/detail/552.html
http://www.thinkphp.cn/extend/235.html





附件列表

相關文章
相關標籤/搜索