harry / auth-rbac-abac
基于 ThinkPHP 8 的 Auth + RBAC + ABAC 权限控制组件(PHP 8.4+)。可插拔 ORM(think-orm 8 / PDO)与缓存(Redis / APCu / array),多守卫、门面、组合判定门。
Package info
pkg:composer/harry/auth-rbac-abac
Requires (Dev)
- phpunit/phpunit: ^11.0
Suggests
- topthink/think-orm: 仅当你注入 ThinkPHP 8 的 think\orm\Connection / think\facade\Db 时才需要(^8.0)。
README
基于 ThinkPHP 8 的 Auth(认证)+ RBAC(角色权限)+ ABAC(属性策略) 权限控制组件,要求 PHP 8.4+。
- ✅ 统一的 Auth、RBAC、ABAC 三大能力,可通过组合门
can()/enforce()一起使用 - ✅ 多守卫配置:
web、api、admin、rbac、abac… - ✅ 不下载任何 ORM:注入
think-orm 8连接(think\orm\Connection/think\facade\Db)或原生 PDO - ✅ 不下载任何 Redis 客户端:注入 Redis 连接对象并指定库序号
- ✅ 用户数据完全外部传入:组件不查询、不依赖你的用户表,只通过注入的
UserProviderInterface取数据 - ✅ 不签发任何令牌:身份持久化(session)由外部框架负责
- ✅ 可插拔 缓存:
redis/apcu/array,通过DriverRegistry注册新驱动 - ✅ 面向前端 的权限数据导出:菜单树(嵌套
children)+ 按钮权限码 - ✅ ThinkPHP 8 集成包:开箱即用的
ServiceProvider/Middleware/ModelUserProvider - ✅ **门面(Facade)**静态调用,并兼容 Laravel 容器解析(不影响 ThinkPHP 主体)
设计模式落点
| 设计模式 | 落地位置 | 作用 |
|---|---|---|
| Strategy(策略) | CacheInterface、ConnectionInterface、QueryInterface |
缓存 / ORM 可互换策略 |
| Adapter(适配器) | PdoConnection、ThinkphpConnection、ThinkphpQuery、ArrayCache、ApcuCache、RedisCache |
把外部 API 适配到内部契约 |
| Registry + Factory | Support\DriverRegistry、Database\ConnectionResolver、Cache\CacheResolver |
通过别名注册 / 创建驱动,便于扩展 |
| Facade(外观) | src\Facades\* |
静态门面对外暴露能力(同时兼容 Laravel 容器解析) |
| Composite(组合) | Manager |
聚合 auth / rbac / abac 三个子管理器 |
| Chain of Responsibility | Manager::can() / enforce()、AuthRbacMiddleware |
RBAC → ABAC 顺序判定,任一拒绝即整体拒绝 |
| Specification(规格) | Abac\Evaluator + Abac\Policy(readonly) |
用策略对象表达"什么条件下允许/拒绝" |
| Template Method(模板方法) | Cache\CacheKey |
集中所有缓存键命名模板 |
| Value Object(值对象) | Abac\Policy |
不可变策略载体 |
| Service Locator | ThinkPHP\AuthRbacServiceProvider |
把 Manager 注入 ThinkPHP 容器 |
| Callback(回调) | Auth\CallbackUserProvider、ThinkPHP\ModelUserProvider |
把外部用户查询逻辑包成 UserProviderInterface |
环境要求
- PHP >= 8.4
ext-pdo、ext-json- 可选:
topthink/think-orm(^8.0,仅当你注入 ThinkPHP 8 连接时需要)
安装
composer require harry/auth-rbac-abac
发布前请把
composer.json的 vendor 前缀harry改成你自己的 Packagist 账号。
1. 配置(你唯一需要改的东西)
组件完全由「配置数组」驱动。它不会自行创建数据库或 Redis 连接 —— 由你注入。
1.1 ThinkPHP 8 接入(首选)
use AuthRbac\Manager; use think\facade\Db; // think-orm 8 的 Db 管理器 $config = [ 'connections' => [ // 直接把 think\facade\Db 注入;适配器内部走 Db::name() 路线。 'db' => ['orm' => 'thinkorm', 'connection' => Db::class, 'prefix' => ''], // 或:传入 Db::connect('alias') 已经返回的 think\orm\Connection // 'db' => ['orm' => 'thinkorm', 'connection' => Db::connect('database.auth')], ], 'cache' => [ 'driver' => 'redis', 'connection' => $redis, // 你注入的 Redis 对象 'database' => 1, // <-- 使用哪个 Redis 库序号 'prefix' => 'authrbac:', ], 'guards' => [ // Auth:用户数据【必须外部传入】,组件不查用户表 'web' => [ 'driver' => 'auth', 'id_column' => 'id', 'provider' => $userProvider, // UserProviderInterface 实例 或 回调数组(见 1.4) ], 'rbac' => [ 'driver' => 'rbac', 'connection' => 'db', 'cache' => true, 'cache_ttl' => 600, 'tables' => [ 'roles' => 'roles', 'permissions' => 'permissions', 'role_user' => 'role_user', 'permission_role' => 'permission_role', 'permission_user' => 'permission_user', ], ], 'abac' => [ 'driver' => 'abac', 'connection' => 'db', 'cache' => true, 'table' => 'abac_policies', ], ], 'default' => 'web', ]; $manager = new Manager($config);
1.2 PDO 接入(零框架依赖)
'connections' => [ 'db' => ['orm' => 'pdo', 'connection' => new \PDO('sqlite:app.db'), 'prefix' => ''], ],
可选驱动:
connections.*.orm:pdo|thinkorm(think-orm 8)cache.driver:redis|apcu|array
1.3 直接注入连接对象
守卫的 connection 既可以是 connections 里的键名,也可以是连同 orm 类型一起直接注入的对象:
'rbac' => [ 'driver' => 'rbac', 'orm' => 'thinkorm', 'connection' => Db::connect(), // think-orm 8 连接 'tables' => [/* ... */], ],
1.4 Auth 用户数据外部注入(关键)
组件不查询、不依赖你的用户表。每个 auth 守卫的 provider 必须是:
- 一个实现了
AuthRbac\Contracts\UserProviderInterface的对象(包装你自己的用户体系,如 ThinkPHP 模型,或任意用户服务);或 - 一个包含三个闭包的数组,组件会自动包成
CallbackUserProvider:
'web' => [ 'driver' => 'auth', 'id_column' => 'id', 'provider' => [ 'find_by_id' => fn ($id) => $userService->find($id), 'find_by_credentials' => fn (array $creds) => $userService->findByEmail($creds['email'] ?? ''), 'validate_credentials' => fn (array $creds, array $user) => password_verify($creds['password'], $user['password']), ], ],
接口方法:
| 方法 | 作用 |
|---|---|
findById($id) |
按主键找回用户(从 session 恢复身份时) |
findByCredentials($credentials) |
按凭据(如 email + password)解析用户 |
validateCredentials($credentials, $user) |
校验凭据(如密码哈希) |
如果你直接使用 ThinkPHP 8 的模型,请使用 ThinkPHP\ModelUserProvider(见 §11.3)。
2. 数据库表结构
完整的建表脚本随包提供,带字段、索引与中文 COMMENT 说明:
database/auth_rbac.mysql.sql—— MySQL 8.0+(InnoDB + utf8mb4,含 ENUM / JSON / COMMENT)database/auth_rbac.sqlite.sql—— SQLite(等价实现,ENUM / JSON 以 CHECK / TEXT 表达)
permissions(权限 / 节点)表是本组件面向前端的关键表,主要字段:
| 字段 | 说明 |
|---|---|
slug |
权限标识(唯一,按钮权限码 / 接口码,如 post:edit) |
type |
节点类型:menu = 菜单,button = 按钮,api = 接口,data = 数据 |
parent_id |
父节点 ID(0 为顶级,用于菜单树 / 按钮挂在菜单下) |
path / icon / component |
前端路由路径 / 图标 / 组件路径 |
sort |
排序权重 |
status |
1=启用 0=禁用 |
meta |
前端扩展元信息(JSON,如 {title, hidden}) |
角色 / 关联表 / ABAC 策略表的完整 DDL 与 COMMENT 见上述 SQL 文件。
3. Auth(认证)
if ($manager->attempt(['email' => 'ada@example.com', 'password' => 'secret'])) { $userId = $manager->id(); // 1 $user = $manager->user(); // 完整记录(来自外部 provider) } $manager->login($userRecord); // 设置已解析好的身份 $manager->loginUsingId(1); // 通过 id 从外部提供者恢复会话身份 $manager->check(); // true $manager->logout();
组件只做「认证 + 权限」,不签发 JWT / token,身份持久化(session 等)由你的框架负责。
4. RBAC(角色与权限)
$rbac = $manager->rbac(); $adminRole = $rbac->createRole('Administrator'); $editPost = $rbac->createPermission('Edit Post', 'post:edit'); $rbac->assignRole(1, $adminRole); $rbac->givePermissionTo($adminRole, $editPost); $rbac->hasRole(1, 'administrator'); // true $rbac->hasPermission(1, 'post:edit'); // true $rbac->can(1, 'post:edit'); // true $rbac->syncRoles(1, [$adminRole, $editorRole]); $rbac->removeRole(1, $editorRole); // 直接给用户授权(绕过角色) $rbac->givePermissionToUser(1, $editPost);
角色 / 权限的 slug 默认由名称转换而来(
Str::slug会将Edit Post转为edit.post)。传入显式的slug参数即可自定义。
创建菜单 / 按钮节点时,可一并写入前端字段(type / parent_id / path / icon / meta 等):
$postMenu = $rbac->createPermission('文章管理', 'post', [ 'type' => 'menu', 'parent_id' => 0, 'path' => '/post', 'icon' => 'el-icon-document', 'sort' => 2, 'meta' => ['title' => '文章管理'], ]); $rbac->createPermission('编辑文章', 'post:edit', [ 'type' => 'button', 'parent_id' => $postMenu, 'sort' => 2, 'meta' => ['title' => '编辑'], ]);
5. 面向前端的数据导出
把当前用户可访问的权限整理成前端可直接使用的结构:
$data = $rbac->exportForFrontend($userId); /* [ 'user_id' => 1, 'roles' => ['editor'], 'codes' => ['post', 'post:create', 'post:edit', 'post:publish', ...], // 权限码 'menus' => [ ['id'=>..., 'slug'=>'post', 'type'=>'menu', 'path'=>'/post', 'children'=>[ ['slug'=>'post:create', 'type'=>'button', ...], ['slug'=>'post:edit', 'type'=>'button', ...], ['slug'=>'post:publish', 'type'=>'button', ...], ]], ], 'buttons' => [ 'post:edit' => ['slug'=>'post:edit', 'type'=>'button', 'meta'=>['title'=>'编辑'], ...], ], ] */
codes:所有权限码集合,前端用v-if="codes.includes('post:edit')"之类的指令直接控制按钮显隐。menus:嵌套节点树,meta已解码为数组,菜单节点下直接挂按钮子节点。- 如需自行组装树,也可分别调用
getPermissionNodes($userId)与buildMenuTree($nodes)。
6. ABAC(基于属性的策略)
策略以 JSON 形式存储其条件,在内存中求值。
$abac = $manager->abac(); $abac->createPolicy([ 'name' => '所有者可编辑', 'effect' => 'allow', 'action' => 'post:edit', 'conditions' => [ ['attribute' => 'resource.owner_id', 'operator' => 'eq', 'value_attr' => 'subject.id'], ], ]); $abac->createPolicy([ 'name' => '管理员放行', 'effect' => 'allow', 'action' => '*', 'conditions' => [ ['attribute' => 'subject.roles', 'operator' => 'contains', 'value' => 'administrator'], ], ]); $abac->createPolicy([ 'name' => '禁止非工作时间发布', 'effect' => 'deny', 'action' => 'post:publish', 'conditions' => [ ['attribute' => 'environment.hour', 'operator' => 'lt', 'value' => 9], ], ]); $subject = ['id' => 1, 'roles' => ['editor'], 'permissions' => ['post:edit']]; $resource = ['owner_id' => 1]; $env = ['hour' => 12]; $abac->can($subject, 'post:edit', $resource, $env); // true(所有者) $abac->enforce($subject, 'post:edit', $resource, $env); // 被拒绝时抛出 AbacException
属性解析
attribute 是相对于请求命名空间的点号路径:
| 路径 | 解析自 |
|---|---|
subject.role |
$subject['role'] |
resource.owner_id |
$resource['owner_id'] |
environment.hour |
$environment['hour'] |
action |
操作字符串本身 |
用 value_attr 代替 value,可将左值与另一个属性比较(例如 resource.owner_id 是否等于 subject.id)。
操作符
eq、neq、gt、gte、lt、lte、in、not_in、contains、not_contains、matches(正则)、between、is_null、true、false。
组合方式(combine)
combine: 'all'(默认,且 AND)或 'any'(或 OR),作用于一条策略内的多个条件。
判定顺序
一条请求只有在至少命中一条匹配的 allow 策略、且没有命中任何匹配的 deny 策略时才被允许(deny 优先)。若没有任何策略命中 → 拒绝。
7. 让 Auth + RBAC + ABAC 一起工作
// 由用户的 RBAC 身份自动构造 ABAC 主体: $subject = $manager->contextFor($userId); // {id, roles[], permissions[]} // 组合门:RBAC 是否授权 且 ABAC 策略是否允许 if ($manager->can($userId, 'post:edit', 'post:edit', $resource, $environment)) { // 允许 } // 或在被拒绝时抛出异常: $manager->enforce($userId, 'post:edit', 'post:edit', $resource, $environment);
你也可以让三种策略完全独立,自行组合:
$allowed = $manager->rbac()->can($userId, 'post:edit') && $manager->abac()->can($subject, 'post:edit', $resource);
8. 门面(Facade)—— 便捷调用
| 门面 | 代理对象 | 典型用法 |
|---|---|---|
AuthRbac\Facades\AuthRbac |
Manager |
组合判定 / 身份状态 / 子管理器入口 |
AuthRbac\Facades\Auth |
AuthManager |
attempt / login / logout / id / user |
AuthRbac\Facades\Rbac |
RbacManager |
角色 / 权限的创建、分配、判定 |
AuthRbac\Facades\Abac |
AbacManager |
策略创建、属性策略判定 |
框架无关场景(最便捷)
use AuthRbac\Facades\AuthRbac; use AuthRbac\Facades\Rbac; use AuthRbac\Facades\Abac; use AuthRbac\Facades\Auth; AuthRbac::setRootManager($manager); AuthRbac::can(1, 'post:edit', 'post:edit', $resource, $environment); AuthRbac::enforce(1, 'post:edit', 'post:edit', $resource, $environment); Auth::attempt(['email' => 'ada@example.com', 'password' => 'secret']); $id = Auth::id(); Rbac::createRole('editor'); Rbac::assignRole(1, $roleId); Rbac::hasPermission(1, 'post:edit'); Abac::createPolicy([/* ... */]); Abac::can($subject, 'post:publish', $resource, $env);
Facade 同时支持 Laravel 容器解析:调用 Facade::setFacadeApplication($container) 后,访问器会从容器中 make(),不影响 ThinkPHP 8 的正常使用。
9. 自定义驱动(Registry + Factory)
ConnectionResolver::extend() 与 CacheResolver::extend() 接受一个「由配置数组构造实例」的闭包:
use AuthRbac\Database\ConnectionResolver; use AuthRbac\Contracts\ConnectionInterface; ConnectionResolver::extend('my-orm', function (array $cfg): ConnectionInterface { return new MyOrmConnection($cfg['connection']); }); // 之后即可在配置中写 'orm' => 'my-orm' use AuthRbac\Cache\CacheResolver; CacheResolver::extend('memcached', function (array $cfg) { return new MyMemcachedCache($cfg['connection']); });
两个注册表都是单例 + 共享的,扩展会全局生效;测试场景可用 ConnectionResolver::flush() / CacheResolver::flush() 重置。
10. 缓存键模板(Template Method)
Cache\CacheKey 集中管理所有缓存键:
use AuthRbac\Cache\CacheKey; CacheKey::rbacUserPermissions($userId); // "authrbac:rbac:user:1:permissions" CacheKey::abacPolicies(); // "authrbac:abac:policies" CacheKey::prefix('custom:foo'); // "authrbac:custom:foo"
升级或多租户场景只需调整 CacheKey::NAMESPACE,无需改业务代码。
11. ThinkPHP 8 集成包
src/ThinkPHP/ 下提供开箱即用的三个组件,仅当项目运行在 ThinkPHP 8 上才需要。
11.1 服务提供者
在 config/service.php 注册:
use AuthRbac\ThinkPHP\AuthRbacServiceProvider; return [ AuthRbacServiceProvider::class, ];
它会:
- 把
Manager单例绑定到 ThinkPHP 容器(authrbac.manager); - 把
Facade的解析器指向容器,使AuthRbac::can(...)在控制器中开箱即用; - 注册中间件别名
authrbac→AuthRbacMiddleware。
11.2 路由中间件
use think\facade\Route; // 仅 RBAC Route::post('post/:id', 'Post/update')->middleware('authrbac:post:edit'); // RBAC + ABAC(action 名作为第二段),资源来自 query+body,user 来自 session Route::group('admin', function () { Route::get('users', 'Admin\\User@index'); Route::post('users', 'Admin\\User@save')->middleware('authrbac:user:create,user:create'); })->middleware('authrbac:admin:access'); // user_id 来自请求头 X-User-Id,from=body,deny=401 Route::delete('post/:id', 'Post@delete') ->middleware('authrbac:post:delete,post:delete,user_id=header:X-User-Id,from=body,deny=401');
环境属性通过 X-Authrbac-Env-<Key> 头传入,例如:
X-Authrbac-Env-hour: 18 X-Authrbac-Env-ip: "10.0.0.1"
11.3 基于 ThinkPHP 模型的用户提供者
ModelUserProvider 让你零模板代码接入任意 think\Model:
use AuthRbac\ThinkPHP\ModelUserProvider; 'web' => [ 'driver' => 'auth', 'id_column' => 'id', 'provider' => new ModelUserProvider( modelClass: \app\model\User::class, credentialField: 'email', // 用作凭据匹配的字段 passwordField: 'password', // 数据库中存放密码哈希的字段 hidden: ['password'], // 返回记录前剔除 ), ],
构造期会校验模型类必须是 think\Model 的子类,错误即抛 InvalidArgumentException。
12. 测试
phpunit.xml.dist 已拆分为两个 test suite:
php vendor/bin/phpunit --testsuite unit # 纯 CPU 测试,无需数据库 php vendor/bin/phpunit # unit + integration(需 MySQL)
| Suite | 覆盖 | 备注 |
|---|---|---|
unit |
Cache\CacheKey、Support\DriverRegistry、Abac\Evaluator |
任何环境可跑 |
integration |
PermissionTest(PDO + MySQL) |
通过 AUTHRBAC_TEST_DB_* 环境变量配置凭据 |
端到端冒烟:
scripts/demo.php(PDO + MySQL)与scripts/test-cache.php(RedisCache 适配器,使用内嵌 FakeRedis)。
13. 目录结构
auth-rbac-abac/
├── src/
│ ├── Manager.php # 顶层组合根(Composite + Chain of Responsibility)
│ ├── Auth/ # 认证守卫
│ ├── Rbac/ # 角色权限引擎
│ ├── Abac/ # 属性策略引擎(Specification + Value Object)
│ ├── Cache/ # 策略 + 适配器 + 键模板(Template Method)
│ ├── Config/ # 点号语法配置仓库
│ ├── Contracts/ # 全部对外契约
│ ├── Database/ # ORM 适配器(Strategy + Adapter)
│ ├── Exceptions/ # 分层异常
│ ├── Facades/ # 静态外观
│ ├── Support/ # 工具类 + DriverRegistry
│ └── ThinkPHP/ # ThinkPHP 8 集成包(ServiceProvider / Middleware / ModelUserProvider)
├── database/
│ ├── auth_rbac.mysql.sql # MySQL 8.0+ 建表脚本
│ └── auth_rbac.sqlite.sql # SQLite 等价实现
├── scripts/
│ ├── demo.php # 端到端冒烟(PDO + MySQL)
│ └── test-cache.php # RedisCache 适配器冒烟
├── tests/
│ ├── Abac/EvaluatorTest.php
│ ├── Cache/CacheKeyTest.php
│ ├── Support/DriverRegistryTest.php
│ └── PermissionTest.php # 集成测试
├── composer.json
├── phpunit.xml.dist
└── README.md
14. 发布到 Packagist
- 把
composer.json的name改为你的 vendor(your-vendor/auth-rbac-abac)。 git init && git add . && git commit -m "initial release"git tag v1.0.0 && git push --tags- 在 https://packagist.org/packages/submit 提交 GitHub 仓库地址
组件不捆绑任何框架代码,保持小巧且零运行时依赖(仅 PDO + JSON 扩展)。
许可协议
MIT