ThinkPHP5.1权限控制之Think-Casbin和状态管理PHP-JWT
简介
PHP-Casbin 是一个强大的、高效的开源访问控制框架,它支持基于各种访问控制模型的权限管理。
Think-Casbin 是一个专为ThinkPHP5.1定制的Casbin的扩展包,使开发者更便捷的在thinkphp项目中使用Casbin。
针对 ThinkPHP6.0 现在推出了更加强大的扩展 ThinkPHP 6.0 Authorization.
安装
- 创建thinkphp项目(如果没有):
composer create-project topthink/think=5.1.* tp5
- 在
ThinkPHP
项目里,安装JWT
扩展:
composer require firebase/php-jwt
- 在
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-Casbin
的Model 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
生成中间件用于访问控制
在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');
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可以做到:
- 支持自定义请求的格式,默认的请求格式为
{subject, object, action}
。- 具有访问控制模型model和策略policy两个核心概念。
- 支持RBAC中的多层角色继承,不止主体可以有角色,资源也可以具有角色。
- 支持超级用户,如
root
或Administrator
,超级用户可以不受授权策略的约束访问任意资源。- 支持多种内置的操作符,如
keyMatch
,方便对路径式的资源进行管理,如/foo/bar
可以映射到/foo*
Casbin不能做到:
- 身份认证 authentication(即验证用户的用户名、密码),casbin只负责访问控制。应该有其他专门的组件负责身份认证,然后由casbin进行访问控制,二者是相互配合的关系。
- 管理用户列表或角色列表。 Casbin 认为由项目自身来管理用户、角色列表更为合适, 用户通常有他们的密码,但是 Casbin 的设计思想并不是把它作为一个存储密码的容器。 而是存储RBAC方案中用户和角色之间的映射关系。
PHP-Casbin是什么?
PHP-Casbin
是基于casbin
的一种实现
Think-Casbin是什么?
Think-Casbin
是基于ThinkPHP
和php-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 CONF
中policy_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:userGetRolesForUser("alice") 只能获取到: ["role:admin"].
But GetImplicitRolesForUser("alice") 却能获取到: ["role:admin", "role:user"].
Casbin::getImplicitRolesForUser("alice");
获取用户或角色的隐式权限。与getPermissionsForuser()相比,此函数检索继承角色的权限
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");