sukarix / f3-access
PHP Fat-Free 框架的路由访问控制
Requires
- bcosca/fatfree-core: 3.*
Requires (Dev)
- roave/security-advisories: dev-latest
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
潜在改进
- 考虑一下
HEAD
和CONNECT
:它们应该被授权还是始终允许?