關於RBAC的原理講解在網上可以找到很多,推薦:編程浪子的RBAC講解,本篇博客就不再累述RBAC的原理到底是什么樣的.
傳統的權限控制有ACL和RBAC方式,ACL的耦合度很高,擴展性不佳,RBAC很好的解耦合,將權限控制的整個過程涉及的數據表大致分為5張表格:
- user表
- role表
- access表(存儲資源數據)
- user_role表
- role_access表
下面是使用原生PHP來實現RBAC權限控制,是一個很簡單的例子,因為重在理解這個原理,所以要修改的地方有很多,但是,如果跟着代碼一步一步來,那你肯定對RBAC有更深的理解,同時也會發現很多問題,比如多對多的數據表該怎么設計。。。。。
話不多說,先看一下代碼吧,代碼已經提交到github:https://github.com/searchingbeyond/RBAC-IN-PHP
先看一下目錄結構:
然后我會按照便於理解原理的順序來展示代碼:
/RBAC/backend/index.php 后台管理的主頁面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <div> <table> <tr> <td><a href="./UserList.php">用戶列表</a></td> <td><a href="./RoleList.php">角色管理</a></td> <td><a href="./AccessList.php">權限管理</a></td> </tr> <tr> <td><a href="./AddUser.php">添加用戶</a></td> <td><a href="./AddRole.php">添加角色</a></td> <td><a href="./AddAccess.php">增加資源</a></td> </tr> </table> </div> </body> </html>
/RBAC/backend/AddUser.php 添加用戶
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>創建用戶</title> </head> <body> <div> <table> <form action="" method="post"> <caption>用戶信息</caption> <tr> <td>用戶ID:</td> <td><input type="text" name="user_id" style="width:400px"></td> </tr> <tr> <td>用戶名:</td> <td><input type="text" name="user_name" style="width:400px"></td> </tr> <tr> <td colspan="2"> <input type="submit" name="adduser" value="添加"> <input type="button" value="返回首頁" onclick="location.href='./index.php'"> </td> </tr> </form> </table> </div> </body> </html> <?php if( isset($_POST['adduser']) ){ $user_id = $_POST['user_id']; $user_name = $_POST['user_name']; $pdo = new PDO("mysql:host=127.0.0.1;dbname=rbac","root","123456"); $stmt = $pdo->prepare("insert into user (user_id,user_name) values ( :user_id, :user_name )"); $stmt->execute(array(":user_id"=>$user_id,":user_name"=>$user_name)); } ?>
/RBAC/backend/UserList.php 顯示用戶列表
<?php $pdo = new PDO("mysql:host=127.0.0.1;dbname=rbac","root","123456"); $stmt = $pdo->prepare("select * from user"); $stmt->execute(); $user_arr = $stmt->fetchAll(PDO::FETCH_ASSOC); ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>用戶管理</title> </head> <body> <div> <table> <caption>用戶列表</caption> <tr> <td>用戶名</td> <td>狀態</td> <td>操作</td> </tr> <?php if( count($user_arr) ): ?> <?php foreach($user_arr as $user): ?> <tr> <td><?php echo $user['user_name']; ?></td> <td><?php echo $user['user_status']?"正常":"禁用"; ?></td> <td> <a href="EditUser.php?user_id=<?php echo $user['user_id']?>">角色設置</a> </td> </tr> <?php endforeach; ?> <?php endif; ?> </table> </div> </body> </html>
/RBAC/backend/EditUser.php 用於設置用戶的角色
<?php if( isset($_GET['user_id']) ){ $pdo = new PDO("mysql:host=127.0.0.1;dbname=rbac","root","123456"); //查詢用戶信息 $stmt = $pdo->prepare("select * from user where user_id = :user_id"); $stmt->execute(array("user_id" => $_GET['user_id'])); $user_info = $stmt->fetch(PDO::FETCH_ASSOC); //print_r($user_info); //查詢用戶的角色 $stmt = $pdo->prepare("select * from user_role where user_id = :user_id"); $stmt->execute(array(":user_id" => $_GET['user_id'])); //這里只留下role_id $user_role_info = array_column( $stmt->fetchAll(PDO::FETCH_ASSOC),"role_id" ); //print_r($user_role_info); //查詢所有角色 $stmt = $pdo->prepare("select * from role"); $stmt->execute(); $role_arr = $stmt->fetchAll(PDO::FETCH_ASSOC); //print_r($role_arr); } //用來判斷復選框已選中 function checked($i,$arr){ if( in_array($i,$arr) ){ echo "checked"; } } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <div> <form action="" method='post'> <table> <caption>用戶信息</caption> <tr> <td>用戶名:</td> <td> <input type="hidden" name="user_id" value="<?php echo $user_info['user_id'] ?>"> <?php echo $user_info['user_name']; ?> </td> </tr> <tr> <td>角色:</td> <?php if( count($role_arr) ):?> <?php foreach($role_arr as $role): ?> <td> <div> <input type="checkbox" <?php checked($role['role_id'],$user_role_info);?> name="role[]" value="<?php echo $role['role_id'];?>"><?php echo $role['role_name'] ?> </div> </td> <?php endforeach; ?> <?php endif; ?> </tr> <tr> <td colspan="5"> <input type="submit" name="editUser"> <input type="button" value="返回主頁" onclick="location.href='./index.php'"> <input type="button" value="返回用戶列表" onclick="location.href='./UserList.php'"> </td> </tr> </table> </form> </div> </body> </html> <?php if( isset($_POST['editUser'])){ //獲取傳遞的role復選框數組,當將全部角色都撤銷時,傳遞的post數據中將不再有role,所以將其設為空數組。 $edit_role = isset($_POST['role'])?$_POST['role']:array(); $user_id = $_POST['user_id']; //增加的角色: $add_role = array_diff($edit_role,$user_role_info); //刪除的角色 $sub_role = array_diff($user_role_info,$edit_role); //執行刪除角色 $stmt = $pdo->prepare("delete from user_role where user_id = :user_id and role_id = :role_id"); foreach($sub_role as $role_id){ $stmt->execute(array(":user_id"=>$user_id,":role_id"=>$role_id)); } //執行增加角色 $stmt = $pdo->prepare("insert into user_role (user_id,role_id) values(:user_id,:role_id)"); foreach($add_role as $role_id){ $stmt->execute(array(":user_id"=>$user_id,":role_id"=>$role_id)); } // echo "<script>location.href='editUser.php?user_id=$user_id</script>"; echo "<script>location.replace(location.href);</script>"; } ?>
其實看到這里,大概就知道RBAC是怎么簡單實現的了,下面幾個文件和上面的文件很相似;
/RBAC/backend/AddRole.php 添加一個角色
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>創建角色</title> </head> <body> <div id="container"> <table> <form action="" method="post"> <caption>角色信息</caption> <tr> <td>角色名:</td> <td><input type="text" name="role_name" style="width:300px"></td> </tr> <tr> <td colspan="2"> <input type="submit" name="addrole" value="添加"> <input type="button" value="返回首頁" onclick="location.href='./index.php'"> </td> </tr> </form> </table> </div> </body> </html> <?php if( isset($_POST['addrole']) ){ $role_name = $_POST['role_name']; $pdo = new PDO("mysql:host=127.0.0.1;dbname=rbac","root","123456"); $stmt = $pdo->prepare("insert into role (role_name) values ( :role_name )"); $stmt->execute(array(":role_name"=>$role_name)); } ?>
/RBAC/backend/RoleList.php 顯示角色的列表
<?php $pdo = new PDO("mysql:host=127.0.0.1;dbname=rbac","root","123456"); $stmt = $pdo->prepare("select * from role"); $stmt->execute(); $role_arr = $stmt->fetchAll(PDO::FETCH_ASSOC); ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>角色管理</title> </head> <body> <div> <table> <caption>角色列表</caption> <tr> <td>角色名</td> <td>操作</td> </tr> <?php if( count($role_arr) ): ?> <?php foreach($role_arr as $role): ?> <tr> <td><?php echo $role['role_name']; ?></td> <td> <a href="EditRole.php?role_id=<?php echo $role['role_id']?>">權限設置</a> </td> </tr> <?php endforeach; ?> <?php endif; ?> </table> </div> </body> </html>
/RBAC/backend/EditRole.php 編輯角色可以獲得哪些資源(權限access)
<?php if( isset($_GET['role_id']) ){ $pdo = new PDO("mysql:host=127.0.0.1;dbname=rbac","root","123456"); //查詢角色信息 $stmt = $pdo->prepare("select * from role where role_id = :role_id"); $stmt->execute(array("role_id" => $_GET['role_id'])); $role_info = $stmt->fetch(PDO::FETCH_ASSOC); //print_r($user_info); //查詢當前角色擁有的權限 $stmt = $pdo->prepare("select * from role_access where role_id = :role_id"); $stmt->execute(array(":role_id" => $_GET['role_id'])); //這里只留下access_id $role_access_info = array_column( $stmt->fetchAll(PDO::FETCH_ASSOC),"access_id" ); //print_r($user_role_info); //查詢所有的資源信息 $stmt = $pdo->prepare("select * from access"); $stmt->execute(); $access_arr = $stmt->fetchAll(PDO::FETCH_ASSOC); //print_r($role_arr); } //用來判斷復選框已選中 function checked($i,$arr){ if( in_array($i,$arr) ){ echo "checked"; } } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>編輯角色</title> </head> <body> <div style="width:400px;margin:0 auto"> <form action="" method='post'> <table> <caption>角色信息</caption> <tr> <td>角色名:</td> <td> <input type="hidden" name="role_id" value="<?php echo $role_info['role_id'] ?>"> <?php echo $role_info['role_name']; ?> </td> </tr> <tr> <td>權限:</td> <?php if( count($access_arr) ):?> <td> <?php foreach($access_arr as $access): ?> <div> <input type="checkbox" <?php checked($access['access_id'],$role_access_info);?> name="access[]" value="<?php echo $access['access_id']?>"><?php echo $access['title'] ?> </div> <?php endforeach; ?> </td> <?php endif; ?> </tr> <tr> <td colspan="5"> <input type="submit" name="editRole"> <input type="button" value="返回主頁" onclick="location.href='./index.php'"> <input type="button" value="返回角色列表" onclick="location.href='./RoleList.php'"> </td> </tr> </table> </form> </div> </body> </html> <?php if( isset($_POST['editRole'])){ //獲取傳遞的role復選框數組,當將全部角色都撤銷時,傳遞的post數據中將不再有role,所以將其設為空數組。 $access = isset($_POST['access'])?$_POST['access']:array(); $role_id = $_POST['role_id']; //增加的角色: $add_access = array_diff($access,$role_access_info); //刪除的角色 $sub_access = array_diff($role_access_info,$access); //執行刪除角色 $stmt = $pdo->prepare("delete from role_access where role_id = :role_id and access_id = :access_id"); foreach($sub_access as $access_id){ $stmt->execute(array(":role_id"=>$role_id,":access_id"=>$access_id )); } //執行增加角色 $stmt = $pdo->prepare("insert into role_access (role_id,access_id) values(:role_id,:access_id)"); foreach($add_access as $access_id){ $stmt->execute(array(":role_id"=>$role_id,":access_id"=>$access_id )); } echo "<script>location.replace(location.href);</script>"; } ?>
/RBAC/backend/AddAccess.php 添加一個權限(資源)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>創建訪問資源</title> </head> <body> <div> <table> <form action="" method="post"> <caption>資源信息</caption> <tr> <td>資源名:</td> <td><input type="text" name="title" style="width:400px"></td> </tr> <tr> <td>URLs:</td> <td> <textarea style="margin-top:20px;width:400px;height:200px" name="urls"></textarea> </td> </tr> <tr> <td colspan="2"> <input type="submit" name="addaccess" value="添加"> <input type="button" value="返回首頁" onclick="location.href='./index.php'"> </td> </tr> </form> </table> </div> </body> </html> <?php if( isset($_POST['addaccess']) ){ $title = $_POST['title']; $urls = json_encode( explode(",",$_POST['urls']) ); $pdo = new PDO("mysql:host=127.0.0.1;dbname=rbac","root","123456"); $stmt = $pdo->prepare("insert into access (title,urls) values ( :title, :urls )"); $stmt->execute(array(":title"=>$title,":urls"=>$urls)); } ?>
/RBAC/backend/AccessList.php 資源(權限)列表
<?php $pdo = new PDO("mysql:host=127.0.0.1;dbname=rbac","root","123456"); $stmt = $pdo->prepare("select * from access"); $stmt->execute(); $access_arr = $stmt->fetchAll(PDO::FETCH_ASSOC); function print_json($data){ $arr = json_decode($data,true); foreach($arr as $v ){ echo $v."<br>"; } } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>資源管理</title> </head> <body> <div id="container"> <table> <caption>資源列表</caption> <tr> <td>資源ID</td> <td>名稱</td> <td>urls</td> <td>操作</td> </tr> <?php if( count($access_arr) ): ?> <?php foreach($access_arr as $access): ?> <tr> <td><?php echo $access['access_id']; ?></td> <td><?php echo $access['title']; ?></td> <td><?php print_json($access['urls']); ?></td> <td> <a href="EditAccess.php?access_id=<?php echo $access['access_id']?>">資源設置</a> </td> </tr> <?php endforeach; ?> <?php endif; ?> </table> </div> </body> </html>
到這里,基本的權限系統就已經建立好了,但是缺少實例,而且還有檢測權限的操作。
/RBAC/frontend/index.php 前台首頁
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>首頁</title> </head> <body> <h2><a href="./pageone.php">pageone.php</a></h2> <h2><a href="./pagetwo.php">pagetwo.php</a></h2> <h2><a href="./pagethree.php">pagethree.php</a></h2> </body> </html>
/RBAC/frontend/pageone.php 測試頁面1
<?php include "checkPermission.php" ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>pageOne</title> </head> <body> <h1>pageOne</h1> <input type="button" onclick="history.back()" value="返回"> </body> </html>
/RBAC/frontend/pagetwo.php 測試頁面2
<?php include "checkPermission.php" ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>pageTwo</title> </head> <body> <h1>pageTwo</h1> <input type="button" onclick="history.back()" value="返回"> </body> </html>
/RBAC/frontend/pagethree.php 測試頁面3
<?php include "checkPermission.php" ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>pageThree</title> </head> <body> <h1>pageThree</h1> <input type="button" onclick="history.back()" value="返回"> </body> </html>
/RBAC/frontend/checkPermission.php 檢測權限
<?php //此處為了測試,不進行檢查 // 真正使用的時候,應該使用下面一個php代碼 $user_id="666"; $user_name="beyond"; $pdo = new PDO("mysql:host=127.0.0.1;dbname=rbac","root","123456"); $stmt = $pdo->prepare("select count(*) from user where user_id=:user_id and user_name=:user_name;"); $stmt->execute(array(":user_id"=>$user_id,":user_name"=>$user_name)); $flag = $stmt->fetch(PDO::FETCH_ASSOC); !$flag && die("無此用戶"); $stmt = $pdo->prepare("select * from user left join user_role on user.user_id = user_role.user_id right join role_access on user_role.role_id = role_access.role_id left join access on access.access_id = role_access.access_id where user.user_id = :user_id "); $stmt->execute(array(":user_id"=>$user_id)); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); //urls用來保存當前用戶所擁有的權限url //可以保存在緩存中,提高效率 $urls = array(); foreach( $data as $v ){ $urls[] = json_decode($v['urls'],true)[0]; } //判斷對當前url是否有權限 if ( ! in_array( $_SERVER['SCRIPT_NAME'], $urls ) ){ include "error.html"; exit; } ?> <?php /* 真正使用的時候,應該使用這個 //因為重點不是驗證登錄,所以這里簡單的驗證登錄 if( $_SESSION['user_id'] && $_SESSION['user_name'] ){ $pdo = new PDO("mysql:host=127.0.0.1;dbname=rbac","root","123456"); $stmt = $pdo->prepare("select count(*) from user where user_id=:user_id and user_name=:user_name;"); $stmt->execute(array(":user_id"=>$_SESSION['user_id'],":user_name"=>$_SESSION['user_name'])); $flag = $stmt->fetch(PDO::FETCH_ASSOC); !$flag && die("無此用戶"); $stmt = $pdo->prepare("select * from user left join user_role on user.user_id = user_role.user_id right join role_access on user_role.role_id = role_access.role_id left join access on access.access_id = role_access.access_id where user.user_id = :user_id "); $stmt->execute(array("user_id"=>$_SESSION['user_id'])); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); $urls = array(); foreach( $data as $v ){ $urls[] = json_decode($v['urls'],true)[0]; } if ( ! in_array( $_SERVER['SCRIPT_NAME'], $urls ) ){ include "error.html"; exit; } } */ ?>
/RBAC/frontend/error.html 沒有權限時顯示
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>無權限</title> </head> <body> <h2>你沒有權限查看該頁面,請聯系管理員開通</h2> </body> </html>