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

配置路由

// route/route.php
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

生成角色名和角色组

// 把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');
ThinkPHP5.1权限控制之Think-Casbin和状态管理PHP-JWT_第1张图片
表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
 '小明',
            '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
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定义的格式进行存储策略

ThinkPHP5.1权限控制之Think-Casbin和状态管理PHP-JWT_第2张图片
策略表
  • 此处的p可以忽略,除非你想用更复杂的访问控制,需要自行查询文档
  • 此处的role_group_vip对应policy_denfition中的sub
  • 此处的/authorization对应policy_denfition中的obj

角色管理

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

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

ThinkPHP5.1权限控制之Think-Casbin和状态管理PHP-JWT_第3张图片
策略表
  • 此处的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官方文档

你可能感兴趣的:(ThinkPHP5.1权限控制之Think-Casbin和状态管理PHP-JWT)