ThinkPHP5.1权限控制之Think-Casbin和状态管理PHP-JWT

ThinkPHP5.1权限控制之Think-Casbin和状态管理PHP-JWT

简介

PHP-Casbin 是一个强大的、高效的开源访问控制框架,它支持基于各种访问控制模型的权限管理。

Think-Casbin 是一个专为ThinkPHP5.1定制的Casbin的扩展包,使开发者更便捷的在thinkphp项目中使用Casbin。

针对 ThinkPHP6.0 现在推出了更加强大的扩展 ThinkPHP 6.0 Authorization.

安装

  1. 创建thinkphp项目(如果没有):
composer create-project topthink/think=5.1.* tp5
  1. ThinkPHP项目里,安装JWT扩展:
composer require firebase/php-jwt
  1. ThinkPHP项目里,安装Think-Casbin扩展:
composer require casbin/think-adapter

配置和使用

需求

  • 前后端完全分离的网站
  • 后台接口使用RESTful API风格
  • 后台使用JWT进行登录状态管理
  • 网站有网站管理员、运维、游客和会员四种角色
  • 网站管理员root可以访问任何页面
  • 运维可以devops可以访问特定的页面
  • 游客anoymous只能浏览部分页面
  • 会员vip能够浏览特定的页面
  • 不同的会员等级可以访问到的页面也不相同

配置

生成Think-Casbin配置文件

ThinkPHP项目里执行

php think casbin:publish

这将自动创建model配置文件config/casbin-basic-model.conf,和Casbin的配置文件config/casbin.php

Think-Casbin默认配置文件名修改

Think-CasbinModel CONF的文件名默认是config/casbin-basic-model.conf,把它修改为config/casbin.conf

个人有强迫症,命名规范不统一,看着难受

// 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)

数据库连接

Think-Casbin默认的使用数据库保存策略配置

// config/database.php
return [
    // 数据库类型
    'type'            => 'mysql',
    // 服务器地址
    'hostname'        => '127.0.0.1',
    // 数据库名
    'database'        => 'test.tp5.1.local',
    // 用户名
    'username'        => 'root',
    // 密码
    'password'        => 'root',
];

生成Think-Casbin的策略表casbin_policy

这一步一定要保证数据库连接正常,并且数据库test.tp5.1.loca存在,否则无法生成数据表

ThinkPHP项目中执行

php think casbin:migrate
表casbin_policy

生成中间件用于访问控制

thinkphp项目中执行

php think make:middleware Authorization

此时会生成application/http/middleware/Authorizantion.php文件

文件内容如下:

// application/http/middleware/Authorizantion.php
<?php

namespace app\http\middleware;
class Authorization
{
    public function handle($request, \Closure $next)
    {
        
    }
}

配置路由

// route/route.php
<?php

// 游客可以访问的页面
Route::group('anoymous', function(){
   Route::get('/artilces', function(){
       return 'Articles';
   });
    
   Route::get('/articles/:id', function($id){
      return 'Articles' . $id; 
   });
})->allowCrossDomain();


// 登录后可以访问的页面
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)]

给角色组分配权限

// 给role_group_root角色组分配权限
// '/*'表示所有路由
Casbin::addPermissionForUser('role_group_root', '/*');

// 给role_group_vip角色组分配权限
Casbin::addPermissionForUser('role_group_vip', '/authorization/goods');
Casbin::addPermissionForUser('role_group_vip', '/authorization/goods/:id');

// 给role_group_devops角色组分配权限
Casbin::addPermissionForUser('role_group_devops', '/authorization/tools');
表casbin_rule

root角色访问控制验证

$user   = 'root';
$url    = $request->url();
$action = $request->method();

if (true === Casbin::enforce($user, $url)) {
    return $next($request);
} else {
    return \json(['errno' => 2, 'msg' => '权限错误']);
}
  • 访问页面/authorization/goods成功
  • 访问页面/authorization/goods/1成功
  • 访问页面/authorization/tools成功
  • 访问页面/authorizaton/tools/1成功

vip角色访问控制验证

$user   = 'vip';
$url    = $request->url();
$action = $request->method();

if (true === Casbin::enforce($user, $url)) {
    return $next($request);
} else {
    return \json(['errno' => 2, 'msg' => '权限错误']);
}
  • 访问页面/authorization/goods成功
  • 访问页面/authorization/goods/1成功
  • 访问页面/authorization/tools失败
  • 访问页面/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' => '权限错误']);
}
  • 访问页面/authorization/goods失败
  • 访问页面/authorization/goods/1失败
  • 访问页面/authorization/tools成功
  • 访问页面/authorizaton/tools/1失败

添加登录和JWT登录状态管理

添加登录路由

// route/route.php
*// 游客可以访问的页面*

// 游客可以访问的页面
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();

模拟实现登录

// 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 = [
            // 签发时间
            'iat'  => time(),
            // 生效时间
            'nbf'  => (time() + 10),
            // 过期时间  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' => '权限错误']);
        }
    }
}

心得体会

Casbin

Casbin是什么?

Casbin可以做到:

  1. 支持自定义请求的格式,默认的请求格式为{subject, object, action}
  2. 具有访问控制模型model和策略policy两个核心概念。
  3. 支持RBAC中的多层角色继承,不止主体可以有角色,资源也可以具有角色。
  4. 支持超级用户,如 rootAdministrator,超级用户可以不受授权策略的约束访问任意资源。
  5. 支持多种内置的操作符,如 keyMatch,方便对路径式的资源进行管理,如 /foo/bar 可以映射到 /foo*

Casbin不能做到:

  1. 身份认证 authentication(即验证用户的用户名、密码),casbin只负责访问控制。应该有其他专门的组件负责身份认证,然后由casbin进行访问控制,二者是相互配合的关系。
  2. 管理用户列表或角色列表。 Casbin 认为由项目自身来管理用户、角色列表更为合适, 用户通常有他们的密码,但是 Casbin 的设计思想并不是把它作为一个存储密码的容器。 而是存储RBAC方案中用户和角色之间的映射关系。

PHP-Casbin是什么?

PHP-Casbin是基于casbin的一种实现

Think-Casbin是什么?

Think-Casbin是基于ThinkPHPphp-casbin实现

Casbin是如何实现访问控制的?

在 Casbin 中, 访问控制模型被抽象为基于 PERM (Policy, Effect, Request, Matcher) 的一个文件。 因此,切换或升级项目的授权机制与修改配置一样简单。 您可以通过组合可用的模型来定制您自己的访问控制模型。 例如,您可以在一个model中获得RBAC角色和ABAC属性,并共享一组policy规则。

Policy:策略 Effect:作用范围 Request:请求 Matcher:匹配器

Model CONFI的作用

casbin支持ACL(Access Control list, 访问控制列表)RBAC(Role-based Access Control, 基于角色的访问控制)ABAC(Attribute-based Access Control, 基于属性的访问控制)等多种类型的访问控制

通过Model CONFI的语法规则,进行简单的配置即可制定访问控制的验证规则,方便项目迁移和开发

Model CONFI文件的说明

### 请求的定义
[request_definition]
# sub访问的角色
# obj访问的接口
# 在实际进行权限验证的时候,会把sub、obj作为实参,传递到验证函数中与策略表中策略进行匹配
r = sub, obj

### 策略的定义
[policy_definition]
# sub允许访问的角色或角色组
# obj允许访问的接口
# 在实际开发中,会根据此处的配置格式向策略表中添加策略和查询策略
p = sub, obj

### 策略的作用范围
[policy_effect]
# some表示任意一个条件成立即可
# p.eft是策略匹配后的结果
# 此处的含义是任意一个策略匹配被允许就生效
e = some(where (p.eft == allow))

### 角色的定义
[role_definition]
# _,_表示角色的继承关系,前者继承后者
g = _, _

### 匹配器
[matchers]
# g(r.sub, p.sub)表示请求传递的角色与策略表中的角色(可以存在继承关系)进行匹配
# keyMatch2(r.obj, p.obj)是内置的一个函数,表示请求的接口与策略表的接口进行匹配
# 此处的含义是当角色和接口都能匹配成功返回true,否则返回false
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj)

需要注意:

官方文档中所给的示例是基于ACL(Access Control List,访问控制列表)的,因此在Model CONF文件中会多出一个字段act,这里我们是基于ThinkPHP5.1的,在路由阶段,已经实现对访问方法的验证,因此不需要再对访问方法进行验证了。

策略表

官方文档中默认使用CSV文件进行存储策略的,而Think-Casbin默认的是使用数据表存储策略的。

策略表会根据Model CONFpolicy_defnition定义的格式进行存储策略

策略表
  • 此处的p可以忽略,除非你想用更复杂的访问控制,需要自行查询文档
  • 此处的role_group_vip对应policy_denfition中的sub
  • 此处的/authorization对应policy_denfition中的obj

角色管理

Casbin有默认的角色管理,也可以使用第三方的角色管理,这里Casbin的角色管理已经足够我们使用了。

Think-Casbin默认把角色管理也放到了策略表中

策略表
  • 此处的g也可以忽略,除非你想用更复杂的角色管理,需要自行查询文档
  • 此处的vip对应role_denfition中的第一个_
  • 此处的role_group_vip对应role_denfition中的第二个_
  • vip属于role_group_vip,拥有role_group_vip中的所有权限
  • 默认的角色管理,最高继承层数是10层

JWT

什么是JWT?

全称JSON Web Token,基于JSON的开放标准((RFC 7519) ,以token的方式代替传统的Cookie-Session模式,用于各服务器、客户端传递信息签名验证。

JWT的优点

1:服务端不需要保存传统会话信息,没有跨域传输问题,减小服务器开销。

2:jwt构成简单,占用很少的字节,便于传输。

3:json格式通用,不同语言之间都可以使用。

firebase/JWT的编码

$token = [
            // 签发者 可选
            'iss' => 'http://www.example_iis.com',
            // 在哪个域名下生效 可选
            'aud' => 'http://www.example_aud.com',
             //签发时间,单位s
            'iat' => time(),
            // 生效时间,单位s
            'nbf' => time(),
            //过期时间,单位s
            'exp' => $time+7200, 
                 // 自定义信息,不要定义敏感信息
                'data' => [
                    'userid' => 1,
                    'username' => '李小龙'
            ];
 // 进行编码和解码用的密钥,需要妥善保存
 $key = md5('example_jwt');
 
 // 进行JWT编码,默认使用`SHA256`进行编码,返回一个字符串
 $jwt_token = JWT::encode($token, $key);

firebase/JWT的解码

// 从请求头中获取jwt_token,我这里定义的请求头是Authorization
$jwt_token = $_SERVER['Authorization'];
if(!isset($jwt_token)){
    // 未传递jwt_token
}
// 进行编码和解码用的密钥,与编码时的一致
$key = md5('example_jwt');

// 需要捕获异常,可以根据不同的报错信息进行相应的处理
try {
    $user_info = JWT::decode($jwt_token, $key, ['HS256']);
} catch (\Throwable $th) {
    return \json(['errno' => 2, 'msg' => '非法token或token已过期']);
}

权限管理API

获取用户具有的角色:

Casbin::getRolesForUser("alice");

获取具有角色的用户:

Casbin::getUsersForRole("data1_admin");

确定用户是否具有角色:

Casbin::hasRoleForUser("alice", "data1_admin");

为用户添加角色。 如果用户已经拥有该角色(aka不受影响),则返回false:

Casbin::addRoleForUser("alice", "data2_admin");

删除用户的角色。 如果用户没有该角色(aka不受影响),则返回false:

Casbin::deleteRoleForUser("alice", "data1_admin");

删除用户的所有角色。 如果用户没有任何角色(aka不受影响),则返回false:

Casbin::deleteRolesForUser("alice");

删除一个用户。 如果用户不存在,则返回false(也就是说不受影响):

Casbin::deleteUser("alice");

删除一个角色:

Casbin::deleteRole("data2_admin");

删除权限。 如果权限不存在,则返回false(aka不受影响):

Casbin::deletePermission("read");

为用户或角色添加权限。 如果用户或角色已经拥有该权限(aka不受影响),则返回false:

Casbin::addPermissionForUser("bob", "read");

删除用户或角色的权限。 如果用户或角色没有权限(aka不受影响),则返回false:

Casbin::deletePermissionForUser("bob", "read");

删除用户或角色的权限。 如果用户或角色没有任何权限(aka不受影响),则返回false:

Casbin::deletePermissionsForUser("bob");

获取用户或角色的权限:

Casbin::getPermissionsForUser("bob");

确定用户是否具有权限:

Casbin::hasPermissionForUser("alice", []string{"read"});

获取用户具有的隐式角色。 与GetRolesForUser() 相比,该函数除了直接角色外还检索间接角色:

例如:

g, alice, role:admin
g, role:admin, role:user

GetRolesForUser("alice") 只能获取到: ["role:admin"].
But GetImplicitRolesForUser("alice") 却能获取到: ["role:admin", "role:user"].

Casbin::getImplicitRolesForUser("alice");

获取用户或角色的隐式权限。与getPermissionsForuser()相比,此函数检索继承角色的权限

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官方文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容