sukarix/f3-access

PHP Fat-Free 框架的路由访问控制

v1.2.3 2024-06-17 09:29 UTC

This package is auto-updated.

Last update: 2024-09-17 10:09:10 UTC


README

PHP Fat-Free 框架的路由访问控制

此插件为 Fat-Free 框架 提供了控制访问您路由的功能。

需求

此插件关注授权,而不是身份验证。因此,在使用它之前,请确保您有识别应用程序用户的方法。

安装

要安装此插件,只需将 lib/access.php 文件复制到您的 lib/ 或您的 AUTOLOAD 文件夹。

基本用法

实例化插件并定义默认访问策略(允许拒绝

$access=Access::instance();
$access->policy('allow'); // allow access to all routes by default

然后定义一组规则来保护特定路由

$access->deny('/secured.htm'); // globally deny access to /secured.htm
$access->allow('/secured.htm','admin'); // allow "admin" to access /secured.htm

或一组路由

// globally deny access to any URI prefixed by /protected
$access->deny('/protected*');
// allow "admin" to access any URI prefixed by /protected
$access->allow('/protected*','admin');

然后在最合适的位置调用 authorize() 方法(在或之后 $f3->run()

$access->authorize($somebody); // e.g: $somebody=$f3->get('SESSION.user')

这就完成了!

我们已经限制了 /secured.htm 和以 /protected 开头的所有 URI 的访问。任何未被识别为 "admin" 的用户将得到一个 错误

请注意,"admin" 可以是您的应用程序中有意义的任何内容:用户名、组、角色、权利等。

因此,我们可以在 "admin" 的位置授予 "admin@domain.tld" 或 "admin-role" 或 "Can access admin area" 的访问权限。

因此,从现在起,我们将把 "admin" 称为 主题

单个规则可以针对多个主题

$access->allow('/foo','tic,tac,toe'); // csv string
$access->allow('/foo',array('tic','tac','toe')); // array

注意:主题名称可以包含任何字符,但不能包含逗号。

高级用法

授权失败

如果主题被识别,拒绝访问将导致 403 错误(禁止),如果未被识别,则导致 401 错误(未授权→用户应先进行身份验证)。

  • $somebody='client' 会得到一个 403 错误(禁止)
  • $somebody='' 会得到一个 401 错误(未授权→用户应先进行身份验证)

您可以为 authorize() 方法提供一个回调,该回调将在授权失败时触发

$access->authorize($somebody,function($route,$subject){
  echo "$subject is denied access to $route";// $route is a method followed by a path
});

除非回退返回 FALSE,否则将跳过默认行为(403/401)。

HTTP 方法访问控制

可以在 HTTP 方法级别定义路由权限

$access->deny('/path');
$access->allow('GET /path');
$access->allow('POST|PATCH|PUT|DELETE /path','admin');

在此示例中,只有 "admin" 可以修改 /path。其他主题只能进行 GET

重要:不提供 HTTP 方法相当于提供 所有 HTTP 方法(除非您正在使用命名路由,见下文)。

例如

// the following rules are equivalent:
$access->deny('/path');
$access->deny('GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT /path');

简单权限检查

如果您需要检查特定主题对路由的访问权限,请使用 granted() 方法

if ($access->granted('/admin/part1',$somebody)) {
  // Access granted
} else {
  // Access denied
}

此方法执行简单检查,不执行任何操作(不抛出错误)。

规则处理顺序

路径优先级

在处理之前,将规则按从最具体到最不具体的路径排序。因此,以下规则

$access->deny('/admin*','mike');
$access->deny('/admin/blog/foo','mike');
$access->allow('/admin/blog','mike');
$access->allow('/admin/blog/foo/bar','mike');
$access->deny('/admin/blog/*/bar','mike');

按以下顺序进行处理

$access->allow('/admin/blog/foo/bar','mike');
$access->deny('/admin/blog/*/bar','mike');
$access->deny('/admin/blog/foo','mike');
$access->allow('/admin/blog','mike');
$access->deny('/admin*','mike');

重要:首先应用与路径匹配的第一个规则。如果没有规则匹配,则应用默认策略。

主题优先级

特定主题规则先于全局规则进行处理。因此以下规则

$access->allow('/part2');// rule #1
$access->deny('/part1/blog','zag');// rule #2
$access->allow('/part1','zig,zag');// rule #3

按以下顺序进行处理

  • 主题 "zag" 的规则为 2,3,1
  • 主题 "zig" 的规则为 3,1

路由唯一性

规则通过主题名称和路由进行索引,因此不能有两个针对同一主题和同一路由的规则。如果出现这种情况,第二个规则将覆盖第一个

$access->allow('/part1','Dina');// rule #1
$access->deny('/part1','Dina');// rule #2
$access->allow('POST /part1','Dina,Misha');// rule #3
$access->deny('/part1','Dina');// rule #4

在这个例子中

  • 规则 #1 被忽略
  • 规则 #3 仅对 Dina 被忽略(对 Misha 不适用)

路径不区分大小写

出于安全目的,路径被视为不区分大小写,无论框架变量 CASELESS 的值如何。

因此,以下规则是等效的

$access->deny('/restricted/area');
$access->deny('/RESTRICTED/AREA');
$access->deny('/rEsTrIcTeD/aReA');

通配符和令牌

可以在多个位置使用通配符

  • 代替路由动词,表示 "任何动词": * /path
    • 等同于 /path
    • 等同于 GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT /path
  • 在路由路径中,表示 "任何字符": GET /foo/*/baz
  • 代替主题,表示 "任何主题": $f3->allow('/','*')
    • 等同于 $f3->allow('/','')
    • 等同于 $f3->allow('/')

注意:通配符匹配空字符串,因此 /admin* 匹配 /admin

支持路由令牌,因此 $f3->allow('/blog/@id/@slug') 被识别。

由于插件没有使用令牌名称,您也可以省略它们: $f3->allow('/blog/@/@')

换句话说,@ 是除斜杠以外的任何字符的通配符,而 * 匹配所有内容,包括斜杠。

重要:阅读 陷阱 部分。

命名路由

如果您使用 命名路由,可以直接引用它们的别名: $f3->allow('@blog_entry');

在这种情况下,不提供 HTTP 方法与提供实际映射到给定路由的方法等效。参见

$f3->route('GET|POST @admin_user_edit: /admin/user/@id','Class->edit');
$f3->route('DELETE @admin_user_delete: /admin/user/@id','Class->delete');

// the following rules are equivalent:
$access->deny('@admin_user_edit');
$access->deny('GET|POST @admin_user_edit');
$access->deny('GET|POST /admin/user/@id');

ini 配置

配置可以在 .ini 文件中使用 ACCESS 变量进行。

规则应以 "允许" 或 "拒绝"(不区分大小写)关键字开头

[ACCESS]
policy = deny ;deny all routes by default

[ACCESS.rules]
ALLOW /foo = *
ALLOW /bar* = Albert,Jean-Louis
DENY /bar/baz = Jean-Louis

它与 HTTP 动词一起工作

[ACCESS.rules]
allow GET|POST /foo = Jim
allow * /bar = Albert,Jim
deny PUT /bar = Jim

实际配置示例

保护管理区域

[ACCESS.rules]
allow /admin = * ; login form
deny /admin/* = *
allow /admin/* = superuser

保护类似 MVC 的路由

[ACCESS.rules]
deny /*/edit = *
deny /*/create = *
allow /*/edit = superuser
allow /*/create = superuser

保护类似 RMR 的路由

[ACCESS.rules]
deny * /* = *
deny GET /* = *
allow POST|PUT|PATCH|DELETE /* = superuser

保护仅限成员的网站

ACCESS.policy = deny

[ACCESS.rules]
allow / = * ; login form
allow /* = member

陷阱

静态路由覆盖动态路由

当静态路由覆盖动态路由时要小心。

虽然不建议这样做,但以下设置可以通过框架实现

$f3->route('GET /admin/user/@id','User->edit');
$f3->route('GET /admin/user/new','User->create');

从授权的角度来看,我们可能会想写

$access->deny('/admin*','*');// deny access to all admin paths by default
$access->allow('/admin/user/@id','edit_role');// allow edit_role to access /admin/user/@id
$access->allow('/admin/user/new','create_role');// allow create_role to access /admin/user/new

这样做,我们可能会认为 edit_role 不能访问 /admin/user/new 路径,但这是一种错觉。

实际上,@id 令牌匹配任何字符串,包括 new

为了确信这一点,只需考虑 /admin/user/@id/admin/user/@anything 之间没有区别。

因此,为了实现角色完全分离,正确的配置在这种情况下应该是

$access->deny('/admin*','*');// deny access to all admin paths by default
$access->allow('/admin/user/@id','edit_role');// allow edit_role to access /admin/user/@id.
$access->deny('/admin/user/new','edit_role');// ... but not /admin/user/new
$access->allow('/admin/user/new','create_role');// allow create_role to access /admin/user/new

更清晰的设置可以是

  • 定义一个单一的路径 /admin/user/@id,其中 id=new 在单个控制器内处理
  • 或者定义两个明确的路径,例如 /admin/user/@id/admin/new-user

API

$access = Access::instance();

policy( $default=NULL )

获取/设置默认策略(默认='允许')

$access->policy('deny');
echo $access->policy();// 'deny'

allow( $route, $subjects='' )

允许指定的主题访问给定的路由

$access->allow('/path'); // Grant "all" access to /path
$access->allow('/path',''); // idem
$access->allow('/path','*'); // idem
$access->allow('POST /foo','tip-top'); // Allow "tip-top" to POST /foo

deny( $route, $subjects='' )

拒绝指定的主题访问给定的路由

$access->deny('/path'); // Deny "all" access to /path
$access->deny('/path',''); // idem
$access->deny('/path','*'); // idem
$access->deny('POST /foo','tip-top'); // Deny "tip-top" access to POST /foo

granted( $route, $subject='' )

如果给定的主题被授予访问给定路由的权限,则返回 TRUE

if ($access->granted('/admin/part1',$somebody)) {
  // Access granted
} else {
  // Access denied
}

注意:您还可以针对一组主题检查访问权限。这对于实现用户角色或组系统很有用

$access->granted('/admin/part1',array('customer')); // FALSE
$access->granted('/admin/part1',array('customer','admin')); // TRUE

authorize( $subject='', $ondeny=NULL )

如果给定的主题被授予访问当前路由的权限,则返回 TRUE

如果未提供 $subject,则授权操作将对“任何”主题执行。

$ondeny 应该是一个有效的 F3 回调(PHP 可调用或字符串)

$access->authorize(); // authorize "any" subject
$access->authorize('admin',function($route,$subject){
  echo "$subject is denied access to $route";// 'admin is denied access to GET /path'
});
$access->authorize('admin','My\App->forbidden');

有关授权失败时发生情况的详细信息,请参阅此处

注意:您还可以针对一组主题执行授权。例如,如果您已实现用户角色或分组系统,则只需传递角色/分组数组以授权用户。例如

$access->authorize(array('customer')); // unauthorized
$access->authorize(array('customer','admin')); // authorized

潜在改进

  • 考虑一下 HEADCONNECT:它们应该被授权还是始终允许?