ThinkPHP5.1權(quán)限控制之Think-Casbin和狀態(tài)管理PHP-JWT
簡(jiǎn)介
PHP-Casbin 是一個(gè)強(qiáng)大的、高效的開源訪問控制框架,它支持基于各種訪問控制模型的權(quán)限管理。
Think-Casbin 是一個(gè)專為ThinkPHP5.1定制的Casbin的擴(kuò)展包,使開發(fā)者更便捷的在thinkphp項(xiàng)目中使用Casbin。
針對(duì) ThinkPHP6.0 現(xiàn)在推出了更加強(qiáng)大的擴(kuò)展 ThinkPHP 6.0 Authorization.
安裝
- 創(chuàng)建thinkphp項(xiàng)目(如果沒有):
composer create-project topthink/think=5.1.* tp5
- 在
ThinkPHP項(xiàng)目里,安裝JWT擴(kuò)展:
composer require firebase/php-jwt
- 在
ThinkPHP項(xiàng)目里,安裝Think-Casbin擴(kuò)展:
composer require casbin/think-adapter
配置和使用
需求
- 前后端完全分離的網(wǎng)站
- 后臺(tái)接口使用
RESTful API風(fēng)格 - 后臺(tái)使用
JWT進(jìn)行登錄狀態(tài)管理 - 網(wǎng)站有網(wǎng)站管理員、運(yùn)維、游客和會(huì)員四種角色
- 網(wǎng)站管理員root可以訪問任何頁(yè)面
- 運(yùn)維可以devops可以訪問特定的頁(yè)面
- 游客anoymous只能瀏覽部分頁(yè)面
- 會(huì)員vip能夠?yàn)g覽特定的頁(yè)面
- 不同的會(huì)員等級(jí)可以訪問到的頁(yè)面也不相同
配置
生成Think-Casbin配置文件
在ThinkPHP項(xiàng)目里執(zhí)行
php think casbin:publish
這將自動(dòng)創(chuàng)建model配置文件config/casbin-basic-model.conf,和Casbin的配置文件config/casbin.php。
Think-Casbin默認(rèn)配置文件名修改
Think-Casbin的Model CONF的文件名默認(rèn)是config/casbin-basic-model.conf,把它修改為config/casbin.conf
個(gè)人有強(qiáng)迫癥,命名規(guī)范不統(tǒng)一,看著難受
// config/casbin.php
return [
'model' => [
'config_type' => 'file',
# 此處修改為
'config_file_path' => env('config_path') . 'casbin.conf',
'config_text' => '',
],
]
Think-Casbin的Model CONF配置文件修改
[request_definition]
r = sub, obj
[policy_definition]
p = sub, obj
[policy_effect]
e = some(where (p.eft == allow))
[role_definition]
g = _, _
[matchers]
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj)
數(shù)據(jù)庫(kù)連接
Think-Casbin默認(rèn)的使用數(shù)據(jù)庫(kù)保存策略配置
// config/database.php
return [
// 數(shù)據(jù)庫(kù)類型
'type' => 'mysql',
// 服務(wù)器地址
'hostname' => '127.0.0.1',
// 數(shù)據(jù)庫(kù)名
'database' => 'test.tp5.1.local',
// 用戶名
'username' => 'root',
// 密碼
'password' => 'root',
];
生成Think-Casbin的策略表casbin_policy
這一步一定要保證數(shù)據(jù)庫(kù)連接正常,并且數(shù)據(jù)庫(kù)test.tp5.1.loca存在,否則無法生成數(shù)據(jù)表
在ThinkPHP項(xiàng)目中執(zhí)行
php think casbin:migrate

生成中間件用于訪問控制
在thinkphp項(xiàng)目中執(zhí)行
php think make:middleware Authorization
此時(shí)會(huì)生成application/http/middleware/Authorizantion.php文件
文件內(nèi)容如下:
// application/http/middleware/Authorizantion.php
<?php
namespace app\http\middleware;
class Authorization
{
public function handle($request, \Closure $next)
{
}
}
配置路由
// route/route.php
<?php
// 游客可以訪問的頁(yè)面
Route::group('anoymous', function(){
Route::get('/artilces', function(){
return 'Articles';
});
Route::get('/articles/:id', function($id){
return 'Articles' . $id;
});
})->allowCrossDomain();
// 登錄后可以訪問的頁(yè)面
Route::group('authorization', function(){
Route::get('/goods', function(){
return 'Goods';
});
Route::get('/goods/:id', function($id){
return 'Goods' . $id;
});
Route::get('/tools', function(){
return 'Tools';
});
})->allowCrossDomain()->middleware(\app\http\middleware\Authorization::class);
訪問控制中間件配置
// application/http/middleware/Authorization.php
<?php
namespace app\http\middleware;
use Casbin;
class Authorization
{
public function handle($request, \Closure $next)
{
}
}
生成角色名和角色組
// 把root角色添加角色組role_group_root
Casbin::addRoleForUser('root', 'role_group_root');
// 把vip角色添加角色組role_group_vip
Casbin::addRoleForUser('vip', 'role_group_vip');
// 把devops角色添加角色組role_group_devops
Casbin::addRoleForUser('devops', 'role_group_devops');
[圖片上傳失敗...(image-574d3d-1569654454046)]
給角色組分配權(quán)限
// 給role_group_root角色組分配權(quán)限
// '/*'表示所有路由
Casbin::addPermissionForUser('role_group_root', '/*');
// 給role_group_vip角色組分配權(quán)限
Casbin::addPermissionForUser('role_group_vip', '/authorization/goods');
Casbin::addPermissionForUser('role_group_vip', '/authorization/goods/:id');
// 給role_group_devops角色組分配權(quán)限
Casbin::addPermissionForUser('role_group_devops', '/authorization/tools');

root角色訪問控制驗(yàn)證
$user = 'root';
$url = $request->url();
$action = $request->method();
if (true === Casbin::enforce($user, $url)) {
return $next($request);
} else {
return \json(['errno' => 2, 'msg' => '權(quán)限錯(cuò)誤']);
}
- 訪問頁(yè)面
/authorization/goods成功 - 訪問頁(yè)面
/authorization/goods/1成功 - 訪問頁(yè)面
/authorization/tools成功 - 訪問頁(yè)面
/authorizaton/tools/1成功
vip角色訪問控制驗(yàn)證
$user = 'vip';
$url = $request->url();
$action = $request->method();
if (true === Casbin::enforce($user, $url)) {
return $next($request);
} else {
return \json(['errno' => 2, 'msg' => '權(quán)限錯(cuò)誤']);
}
- 訪問頁(yè)面
/authorization/goods成功 - 訪問頁(yè)面
/authorization/goods/1成功 - 訪問頁(yè)面
/authorization/tools失敗 - 訪問頁(yè)面
/authorizaton/tools/1失敗
devops角色訪問控制
$user = 'devops';
$url = $request->url();
$action = $request->method();
if (true === Casbin::enforce($user, $url)) {
return $next($request);
} else {
return \json(['errno' => 2, 'msg' => '權(quán)限錯(cuò)誤']);
}
- 訪問頁(yè)面
/authorization/goods失敗 - 訪問頁(yè)面
/authorization/goods/1失敗 - 訪問頁(yè)面
/authorization/tools成功 - 訪問頁(yè)面
/authorizaton/tools/1失敗
添加登錄和JWT登錄狀態(tài)管理
添加登錄路由
// route/route.php
*// 游客可以訪問的頁(yè)面*
// 游客可以訪問的頁(yè)面
Route::group('', function () {
Route::get('/artilces', function () {
return 'Articles';
});
Route::get('/articles/:id', function ($id) {
return 'Articles' . $id;
});
// 添加這一行
Route::post('/login', 'index/index/login');
})->allowCrossDomain();
模擬實(shí)現(xiàn)登錄
// application/index/controller/Index.php
<?php
namespace app\index\controller;
use \Firebase\JWT\JWT;
class Index {
public function login() {
$user_info = [
'user_name' => '小明',
'user_phone' => '1888888888',
'role' => 'vip',
];
$jwt = [
// 簽發(fā)時(shí)間
'iat' => time(),
// 生效時(shí)間
'nbf' => (time() + 10),
// 過期時(shí)間 3天
'exp' => (time() + 60 * 60 * 24 * 3),
'data' => $user_info,
];
$jwt_token = JWT::encode($user_info, 'jwt_key');
return \json([
'errno' => 0,
'msg' => '登錄成功',
'data' => [
'jwt_token' => $jwt_token
]
]);
}
}
訪問控制修改
// application/http/middleware/Authorization.php
<?php
namespace app\http\middleware;
use Casbin;
use \Firebase\JWT\JWT;
class Authorization {
public function handle($request, \Closure $next) {
$jwt_token = request()->header('Authorization');
if (!isset($jwt_token)) {
return \json(['errno' => 2, 'msg' => '用戶未登錄']);
}
$user_info = JWT::decode($jwt_token, 'jwt_key', ['HS256']);
try {
$user_info = JWT::decode($jwt_token, 'jwt_key', ['HS256']);
} catch (\Throwable $th) {
return \json(['errno' => 2, 'msg' => '非法token或token已過期']);
}
$role = $user_info->role;
$url = $request->url();
$action = $request->method();
if (true === Casbin::enforce($role, $url)) {
return $next($request);
} else {
return \json(['errno' => 2, 'msg' => '權(quán)限錯(cuò)誤']);
}
}
}
心得體會(huì)
Casbin
Casbin是什么?
Casbin可以做到:
- 支持自定義請(qǐng)求的格式,默認(rèn)的請(qǐng)求格式為
{subject, object, action}。- 具有訪問控制模型model和策略policy兩個(gè)核心概念。
- 支持RBAC中的多層角色繼承,不止主體可以有角色,資源也可以具有角色。
- 支持超級(jí)用戶,如
root或Administrator,超級(jí)用戶可以不受授權(quán)策略的約束訪問任意資源。- 支持多種內(nèi)置的操作符,如
keyMatch,方便對(duì)路徑式的資源進(jìn)行管理,如/foo/bar可以映射到/foo*Casbin不能做到:
- 身份認(rèn)證 authentication(即驗(yàn)證用戶的用戶名、密碼),casbin只負(fù)責(zé)訪問控制。應(yīng)該有其他專門的組件負(fù)責(zé)身份認(rèn)證,然后由casbin進(jìn)行訪問控制,二者是相互配合的關(guān)系。
- 管理用戶列表或角色列表。 Casbin 認(rèn)為由項(xiàng)目自身來管理用戶、角色列表更為合適, 用戶通常有他們的密碼,但是 Casbin 的設(shè)計(jì)思想并不是把它作為一個(gè)存儲(chǔ)密碼的容器。 而是存儲(chǔ)RBAC方案中用戶和角色之間的映射關(guān)系。
PHP-Casbin是什么?
PHP-Casbin是基于casbin的一種實(shí)現(xiàn)
Think-Casbin是什么?
Think-Casbin是基于ThinkPHP和php-casbin實(shí)現(xiàn)
Casbin是如何實(shí)現(xiàn)訪問控制的?
在 Casbin 中, 訪問控制模型被抽象為基于 PERM (Policy, Effect, Request, Matcher) 的一個(gè)文件。 因此,切換或升級(jí)項(xiàng)目的授權(quán)機(jī)制與修改配置一樣簡(jiǎn)單。 您可以通過組合可用的模型來定制您自己的訪問控制模型。 例如,您可以在一個(gè)model中獲得RBAC角色和ABAC屬性,并共享一組policy規(guī)則。
Policy:策略 Effect:作用范圍 Request:請(qǐng)求 Matcher:匹配器
Model CONFI的作用
casbin支持ACL(Access Control list, 訪問控制列表)、RBAC(Role-based Access Control, 基于角色的訪問控制)、ABAC(Attribute-based Access Control, 基于屬性的訪問控制)等多種類型的訪問控制
通過Model CONFI的語法規(guī)則,進(jìn)行簡(jiǎn)單的配置即可制定訪問控制的驗(yàn)證規(guī)則,方便項(xiàng)目遷移和開發(fā)
Model CONFI文件的說明
### 請(qǐng)求的定義
[request_definition]
# sub訪問的角色
# obj訪問的接口
# 在實(shí)際進(jìn)行權(quán)限驗(yàn)證的時(shí)候,會(huì)把sub、obj作為實(shí)參,傳遞到驗(yàn)證函數(shù)中與策略表中策略進(jìn)行匹配
r = sub, obj
### 策略的定義
[policy_definition]
# sub允許訪問的角色或角色組
# obj允許訪問的接口
# 在實(shí)際開發(fā)中,會(huì)根據(jù)此處的配置格式向策略表中添加策略和查詢策略
p = sub, obj
### 策略的作用范圍
[policy_effect]
# some表示任意一個(gè)條件成立即可
# p.eft是策略匹配后的結(jié)果
# 此處的含義是任意一個(gè)策略匹配被允許就生效
e = some(where (p.eft == allow))
### 角色的定義
[role_definition]
# _,_表示角色的繼承關(guān)系,前者繼承后者
g = _, _
### 匹配器
[matchers]
# g(r.sub, p.sub)表示請(qǐng)求傳遞的角色與策略表中的角色(可以存在繼承關(guān)系)進(jìn)行匹配
# keyMatch2(r.obj, p.obj)是內(nèi)置的一個(gè)函數(shù),表示請(qǐng)求的接口與策略表的接口進(jìn)行匹配
# 此處的含義是當(dāng)角色和接口都能匹配成功返回true,否則返回false
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj)
需要注意:
官方文檔中所給的示例是基于ACL(Access Control List,訪問控制列表)的,因此在Model CONF文件中會(huì)多出一個(gè)字段act,這里我們是基于ThinkPHP5.1的,在路由階段,已經(jīng)實(shí)現(xiàn)對(duì)訪問方法的驗(yàn)證,因此不需要再對(duì)訪問方法進(jìn)行驗(yàn)證了。
策略表
官方文檔中默認(rèn)使用CSV文件進(jìn)行存儲(chǔ)策略的,而Think-Casbin默認(rèn)的是使用數(shù)據(jù)表存儲(chǔ)策略的。
策略表會(huì)根據(jù)Model CONF中policy_defnition定義的格式進(jìn)行存儲(chǔ)策略

- 此處的
p可以忽略,除非你想用更復(fù)雜的訪問控制,需要自行查詢文檔 - 此處的
role_group_vip對(duì)應(yīng)policy_denfition中的sub - 此處的
/authorization對(duì)應(yīng)policy_denfition中的obj
角色管理
Casbin有默認(rèn)的角色管理,也可以使用第三方的角色管理,這里Casbin的角色管理已經(jīng)足夠我們使用了。
Think-Casbin默認(rèn)把角色管理也放到了策略表中

- 此處的
g也可以忽略,除非你想用更復(fù)雜的角色管理,需要自行查詢文檔 - 此處的
vip對(duì)應(yīng)role_denfition中的第一個(gè)_ - 此處的
role_group_vip對(duì)應(yīng)role_denfition中的第二個(gè)_ -
vip屬于role_group_vip,擁有role_group_vip中的所有權(quán)限 - 默認(rèn)的角色管理,最高繼承層數(shù)是10層
JWT
什么是JWT?
全稱JSON Web Token,基于JSON的開放標(biāo)準(zhǔn)((RFC 7519) ,以token的方式代替?zhèn)鹘y(tǒng)的Cookie-Session模式,用于各服務(wù)器、客戶端傳遞信息簽名驗(yàn)證。
JWT的優(yōu)點(diǎn)
1:服務(wù)端不需要保存?zhèn)鹘y(tǒng)會(huì)話信息,沒有跨域傳輸問題,減小服務(wù)器開銷。
2:jwt構(gòu)成簡(jiǎn)單,占用很少的字節(jié),便于傳輸。
3:json格式通用,不同語言之間都可以使用。
firebase/JWT的編碼
$token = [
// 簽發(fā)者 可選
'iss' => 'http://www.example_iis.com',
// 在哪個(gè)域名下生效 可選
'aud' => 'http://www.example_aud.com',
//簽發(fā)時(shí)間,單位s
'iat' => time(),
// 生效時(shí)間,單位s
'nbf' => time(),
//過期時(shí)間,單位s
'exp' => $time+7200,
// 自定義信息,不要定義敏感信息
'data' => [
'userid' => 1,
'username' => '李小龍'
];
// 進(jìn)行編碼和解碼用的密鑰,需要妥善保存
$key = md5('example_jwt');
// 進(jìn)行JWT編碼,默認(rèn)使用`SHA256`進(jìn)行編碼,返回一個(gè)字符串
$jwt_token = JWT::encode($token, $key);
firebase/JWT的解碼
// 從請(qǐng)求頭中獲取jwt_token,我這里定義的請(qǐng)求頭是Authorization
$jwt_token = $_SERVER['Authorization'];
if(!isset($jwt_token)){
// 未傳遞jwt_token
}
// 進(jìn)行編碼和解碼用的密鑰,與編碼時(shí)的一致
$key = md5('example_jwt');
// 需要捕獲異常,可以根據(jù)不同的報(bào)錯(cuò)信息進(jìn)行相應(yīng)的處理
try {
$user_info = JWT::decode($jwt_token, $key, ['HS256']);
} catch (\Throwable $th) {
return \json(['errno' => 2, 'msg' => '非法token或token已過期']);
}
權(quán)限管理API
獲取用戶具有的角色:
Casbin::getRolesForUser("alice");
獲取具有角色的用戶:
Casbin::getUsersForRole("data1_admin");
確定用戶是否具有角色:
Casbin::hasRoleForUser("alice", "data1_admin");
為用戶添加角色。 如果用戶已經(jīng)擁有該角色(aka不受影響),則返回false:
Casbin::addRoleForUser("alice", "data2_admin");
刪除用戶的角色。 如果用戶沒有該角色(aka不受影響),則返回false:
Casbin::deleteRoleForUser("alice", "data1_admin");
刪除用戶的所有角色。 如果用戶沒有任何角色(aka不受影響),則返回false:
Casbin::deleteRolesForUser("alice");
刪除一個(gè)用戶。 如果用戶不存在,則返回false(也就是說不受影響):
Casbin::deleteUser("alice");
刪除一個(gè)角色:
Casbin::deleteRole("data2_admin");
刪除權(quán)限。 如果權(quán)限不存在,則返回false(aka不受影響):
Casbin::deletePermission("read");
為用戶或角色添加權(quán)限。 如果用戶或角色已經(jīng)擁有該權(quán)限(aka不受影響),則返回false:
Casbin::addPermissionForUser("bob", "read");
刪除用戶或角色的權(quán)限。 如果用戶或角色沒有權(quán)限(aka不受影響),則返回false:
Casbin::deletePermissionForUser("bob", "read");
刪除用戶或角色的權(quán)限。 如果用戶或角色沒有任何權(quán)限(aka不受影響),則返回false:
Casbin::deletePermissionsForUser("bob");
獲取用戶或角色的權(quán)限:
Casbin::getPermissionsForUser("bob");
確定用戶是否具有權(quán)限:
Casbin::hasPermissionForUser("alice", []string{"read"});
獲取用戶具有的隱式角色。 與GetRolesForUser() 相比,該函數(shù)除了直接角色外還檢索間接角色:
例如:
g, alice, role:admin
g, role:admin, role:userGetRolesForUser("alice") 只能獲取到: ["role:admin"].
But GetImplicitRolesForUser("alice") 卻能獲取到: ["role:admin", "role:user"].
Casbin::getImplicitRolesForUser("alice");
獲取用戶或角色的隱式權(quán)限。與getPermissionsForuser()相比,此函數(shù)檢索繼承角色的權(quán)限
p, admin, data1, read
p, alice, data2, read
g, alice, adminGetPermissionsForUser("alice") 只能獲取到: [["alice", "data2", "read"]].
But GetImplicitPermissionsForUser("alice") 卻能獲取到: [["admin", "data1", "read"], ["alice", "data2", "read"]].
Casbin::getImplicitPermissionsForUser("alice");