ThinkPHP5.1權(quán)限控制之Think-Casbin和狀態(tài)管理PHP-JWT

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.

安裝

  1. 創(chuàng)建thinkphp項(xiàng)目(如果沒有):
composer create-project topthink/think=5.1.* tp5
  1. ThinkPHP項(xiàng)目里,安裝JWT擴(kuò)展:
composer require firebase/php-jwt
  1. 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-CasbinModel 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
表casbin_policy

生成中間件用于訪問控制

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');
表casbin_rule

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可以做到:

  1. 支持自定義請(qǐng)求的格式,默認(rèn)的請(qǐng)求格式為{subject, object, action}
  2. 具有訪問控制模型model和策略policy兩個(gè)核心概念。
  3. 支持RBAC中的多層角色繼承,不止主體可以有角色,資源也可以具有角色。
  4. 支持超級(jí)用戶,如 rootAdministrator,超級(jí)用戶可以不受授權(quán)策略的約束訪問任意資源。
  5. 支持多種內(nèi)置的操作符,如 keyMatch,方便對(duì)路徑式的資源進(jìn)行管理,如 /foo/bar 可以映射到 /foo*

Casbin不能做到:

  1. 身份認(rèn)證 authentication(即驗(yàn)證用戶的用戶名、密碼),casbin只負(fù)責(zé)訪問控制。應(yīng)該有其他專門的組件負(fù)責(zé)身份認(rèn)證,然后由casbin進(jìn)行訪問控制,二者是相互配合的關(guān)系。
  2. 管理用戶列表或角色列表。 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是基于ThinkPHPphp-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 CONFpolicy_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:user

GetRolesForUser("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, admin

GetPermissionsForUser("alice") 只能獲取到: [["alice", "data2", "read"]].
But GetImplicitPermissionsForUser("alice") 卻能獲取到: [["admin", "data1", "read"], ["alice", "data2", "read"]].

Casbin::getImplicitPermissionsForUser("alice");

參考文檔

ThinkPHP5.1官方文檔

PHP-Casbin官方文檔

Think-Casbin官方文檔

PHP-JWT官方文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容