harry/auth-rbac-abac

基于 ThinkPHP 8 的 Auth + RBAC + ABAC 权限控制组件(PHP 8.4+)。可插拔 ORM(think-orm 8 / PDO)与缓存(Redis / APCu / array),多守卫、门面、组合判定门。

Maintainers

Package info

github.com/harryYKH/authRbac

Homepage

pkg:composer/harry/auth-rbac-abac

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.01 2026-08-25 06:27 UTC

This package is auto-updated.

Last update: 2026-08-25 06:37:29 UTC


README

基于 ThinkPHP 8Auth(认证)+ RBAC(角色权限)+ ABAC(属性策略) 权限控制组件,要求 PHP 8.4+

  • ✅ 统一的 AuthRBACABAC 三大能力,可通过组合门 can() / enforce() 一起使用
  • 多守卫配置:webapiadminrbacabac
  • 不下载任何 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(策略) CacheInterfaceConnectionInterfaceQueryInterface 缓存 / ORM 可互换策略
Adapter(适配器) PdoConnectionThinkphpConnectionThinkphpQueryArrayCacheApcuCacheRedisCache 把外部 API 适配到内部契约
Registry + Factory Support\DriverRegistryDatabase\ConnectionResolverCache\CacheResolver 通过别名注册 / 创建驱动,便于扩展
Facade(外观) src\Facades\* 静态门面对外暴露能力(同时兼容 Laravel 容器解析)
Composite(组合) Manager 聚合 auth / rbac / abac 三个子管理器
Chain of Responsibility Manager::can() / enforce()AuthRbacMiddleware RBAC → ABAC 顺序判定,任一拒绝即整体拒绝
Specification(规格) Abac\Evaluator + Abac\Policyreadonly 用策略对象表达"什么条件下允许/拒绝"
Template Method(模板方法) Cache\CacheKey 集中所有缓存键命名模板
Value Object(值对象) Abac\Policy 不可变策略载体
Service Locator ThinkPHP\AuthRbacServiceProvider 把 Manager 注入 ThinkPHP 容器
Callback(回调) Auth\CallbackUserProviderThinkPHP\ModelUserProvider 把外部用户查询逻辑包成 UserProviderInterface

环境要求

  • PHP >= 8.4
  • ext-pdoext-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.*.ormpdo | thinkormthink-orm 8
  • cache.driverredis | 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)。

操作符

eqneqgtgteltlteinnot_incontainsnot_containsmatches(正则)、betweenis_nulltruefalse

组合方式(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,
];

它会:

  1. Manager 单例绑定到 ThinkPHP 容器(authrbac.manager);
  2. Facade 的解析器指向容器,使 AuthRbac::can(...) 在控制器中开箱即用;
  3. 注册中间件别名 authrbacAuthRbacMiddleware

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\CacheKeySupport\DriverRegistryAbac\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

  1. composer.jsonname 改为你的 vendor(your-vendor/auth-rbac-abac)。
  2. git init && git add . && git commit -m "initial release"
  3. git tag v1.0.0 && git push --tags
  4. https://packagist.org/packages/submit 提交 GitHub 仓库地址

组件不捆绑任何框架代码,保持小巧且零运行时依赖(仅 PDO + JSON 扩展)。

许可协议

MIT