yarhon/route-guard-bundle

Symfony路由授权检查器

安装: 11

依赖: 0

建议者: 0

安全: 0

星标: 9

关注者: 2

分支: 0

开放问题: 0

类型:symfony-bundle

v1.0.1 2018-12-01 21:43 UTC

This package is auto-updated.

Last update: 2024-09-29 05:28:04 UTC


README

Symfony路由授权检查器

Build Status Code Coverage Scrutinizer Code Quality Latest Stable Version

关于

YarhonRouteGuardBundle (RouteGuard) 是一个工具,用于

  • 检查用户是否有权访问路由
  • 检索路由的授权测试
  • 根据授权测试在 Twig 模板中条件性地显示块,避免在控制器和模板中重复授权检查。

RouteGuard 支持以下提供程序的授权测试

  • Symfony SecurityBundle (access_control 规则)
  • Sensio FrameworkExtraBundle (@IsGranted@Security 注解)。
  • [计划在下一个版本中] 动态测试(控制器中的任意代码)。 详细信息

并且允许添加您自己的授权测试提供程序。请参阅更多信息

RouteGuard 对于罕见用例有一些限制。请参阅更多信息

让代码说话

A) 模板渲染

当您需要在 Twig 模板中根据授权测试条件性地显示某些内容(基本上,链接)时,您通常会编写如下代码

{% if is_granted('ROLE_USER') %}
    <a href="{{ path('blog', {'page': 1}) }}">Blog link</a>
{% else %}
    No access
{% endif %}

RouteGuard 允许您使用支持的测试提供程序定义的路由或路由控制器中定义的授权检查来摆脱模板中的授权检查

{% route 'blog', {'page': 1} %}
    <a href="{{ _route.ref }}">Blog link</a>
{% else %}
    No access
{% endroute %}

在上面的示例中,如果 blog 路由的任何授权测试都没有拒绝访问,则将渲染链接;否则渲染 else 块的内容。

_route.ref 变量将包含生成的 URL。

请参阅Twig 模板部分以获取更多信息。

此外,考虑到“命名事物”的问题,RouteGuard 允许配置 Twig 标签的名称(默认为“route”),以及特殊内部变量的名称(默认为“_route”)。请参阅配置部分以获取更多信息。

B) 检查用户是否有权访问路由

namespace App\Service;

use Yarhon\RouteGuardBundle\Security\RouteAuthorizationCheckerInterface;
use Yarhon\RouteGuardBundle\Routing\RouteContext;

class SomeService
{
    private $authorizationChecker;
    
    public function __construct(RouteAuthorizationCheckerInterface $authorizationChecker)
    {
        $this->authorizationChecker = $authorizationChecker;
    }
    
    public function check()
    {
        $routeContext = new RouteContext('blog', ['page' => 10], 'GET');
        
        return $this->authorizationChecker->isGranted($routeContext);
    }
}    

请参阅公共服务部分以获取更多信息。

要求

PHP 5.6+,Symfony 3.3+。

强烈建议启用 OPcache 扩展。

安装

$ composer require yarhon/route-guard-bundle

如果您不使用 Symfony Flex,您必须手动将 new Yarhon\RouteGuardBundle\YarhonRouteGuardBundle() 添加到项目中 app/AppKernel.php 文件中注册的捆绑包列表。

配置

如果您需要更改默认配置值,可以在配置文件(通常是 Symfony < 4.0 的 /app/config/config.yml 或 Symfony >= 4.0 的 /config/packages/yarhon_route_guard.yml)的 yarhon_route_guard 键下设置它们。在后者的情况下,您必须首先创建此文件。

配置选项

  • data_collector. RouteGuard 授权数据收集器的选项。
    • ignore_controllers. 要由数据收集器忽略的控制器名称数组(即,绑定到这些控制器的路由不需要授权)。控制器名称应指定为 class::method(服务::方法)表示法。您可以指定

      • 完整的控制器名称,例如 App\Controller\DefaultController::index
      • 控制器名称前缀,例如 App\Controller\DefaultControllerApp\Controller\

      注意:对于“控制器作为服务”的控制器,您必须指定服务名称,而不是类名称。

      默认值: []

      此选项可能有助于加快缓存预热速度(并减少缓存大小)或排除触发异常的一些特定路由。

    • ignore_controllers_symfony. 要由数据收集器忽略的默认 Symfony 控制器数组。

      默认值

      [
          'twig.controller.preview_error',
          'web_profiler.controller.profiler',
          'web_profiler.controller.router',
          'web_profiler.controller.exception',
      ]
    • ignore_exceptions. 布尔值,如果为 true - 数据收集器将忽略触发异常的路由
      在收集授权数据时。
      在这种情况下,将记录错误信息。
      注意:如果某个测试提供者触发了一个路由的异常,则将完全忽略该路由,不考虑是否存在来自其他提供者的测试。

      默认值:false

      当您第一次尝试使用 RouteGuard 并遇到其局限性/错误时,此选项可能很有用。

  • twig. RouteGuard Twig 扩展的选项。
    • tag_name. Twig 标签的名称。默认值:'route'
    • tag_variable_name. 标签内部变量(数组)的名称,该变量将包含路由信息(即,生成的 URL)。默认值:'_route'
    • discover_routing_functions. 布尔值,指定是否在 twig 标签中使用 "discover" 模式。默认值:true

用法

Twig 模板

Twig 标签("route")语法

Twig 标签参数分为两部分:第一部分是路由上下文参数,第二部分,在 as 关键字之后,指定所需的引用类型。

路由上下文参数包括

  • routeName(字符串,必需)
  • parameters(数组,可选,默认值:[]
  • method(字符串,可选,默认值:'GET')。

引用类型可以以下形式指定

  • path [绝对|相对]。等于使用 path() 函数生成 URL。
  • url [绝对|相对]。等于使用 url() 函数生成 URL。如果没有指定引用类型,则使用 "path absolute"。如果只指定了第一部分("path" 或 "url"),则将使用 "absolute" 作为第二部分。

示例

{% route 'blog', {'page': 1}, 'GET' as path absolute %}
{% route 'blog', {'page': 1}, 'GET' as url relative %}
{% route 'blog' as url %}
{% route 'blog' %}

对于那些想要以最少的努力尝试 RouteGuard 的人,它提供了 "discover" 模式。在此模式下,RouteGuard 将在 "route" 标签内搜索 path()url() 函数调用,然后使用函数参数作为标签参数。以下两个示例将产生相同的结果

{% route discover %}
    <a href="{{ url('blog', {'page': 1}, true) }}">Blog link</a>
{% endroute %}
{% route 'blog', {'page': 1}, 'GET' as url relative %}
    <a href="{{ _route.ref }}">Blog link</a>
{% endroute %}

"discover" 模式的限制是不能指定方法 - 它始终被视为 'GET'

Twig 函数

与标准的 path()url() 函数类似,RouteGuard 提供了自己的函数

route_guard_path($name, array $parameters = [], $method = 'GET', $relative = false)
route_guard_url($name, array $parameters = [], $method = 'GET', $relative = false)

还有一个由 "route" 标签内部使用的函数

route_guard_route($name, array $parameters = [], $method = 'GET', array $generateAs = [])

公共服务

RouteAuthorizationChecker

允许检查用户是否有权访问路由。

服务 ID:yarhon_route_guard.route_authorization_checker

示例

namespace App\Service;

use Yarhon\RouteGuardBundle\Security\RouteAuthorizationCheckerInterface;
use Yarhon\RouteGuardBundle\Routing\RouteContext;

class SomeService
{
    private $authorizationChecker;
    
    public function __construct(RouteAuthorizationCheckerInterface $authorizationChecker)
    {
        $this->authorizationChecker = $authorizationChecker;
    }
    
    public function check()
    {
        $routeContext = new RouteContext('blog', ['page' => 10], 'GET');
        
        return $this->authorizationChecker->isGranted($routeContext);
    }
}

AuthorizedUrlGenerator

允许在 Twig 上下文之外生成 URL。

服务 ID:yarhon_route_guard.authorized_url_generator

示例

namespace App\Service;

use Yarhon\RouteGuardBundle\Routing\AuthorizedUrlGeneratorInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class SomeService
{
    private $urlGenerator;
    
    public function __construct(AuthorizedUrlGeneratorInterface $urlGenerator)
    {
        $this->urlGenerator = $urlGenerator;
    }
    
    public function generateUrl()
    {
        return $this->urlGenerator->generate('blog', ['page' => 10], 'GET', UrlGeneratorInterface::ABSOLUTE_PATH);
    }
}    

AuthorizedUrlGeneratorInterface::generate 方法签名与 Symfony 的 Symfony\...\UrlGeneratorInterface::generate 类似,但它将 $method 作为第三个参数添加,将 $referenceType 移动到第四个位置。如果没有授权测试拒绝访问,则返回生成的 URL,否则返回布尔值 false

TestLoader

允许检索路由的所有授权测试。

服务 ID:yarhon_route_guard.test_loader

示例

namespace App\Service;

use Yarhon\RouteGuardBundle\Security\TestLoaderInterface;
use Yarhon\RouteGuardBundle\Routing\RouteContext;

class SomeService
{
    private $testLoader;
    
    public function __construct(TestLoaderInterface $testLoader)
    {
        $this->testLoader = $testLoader;
    }
    
    public function getTests()
    {
        $routeContext = new RouteContext('blog', ['page' => 10], 'GET');
        
        return $this->testLoader->load($routeContext);
    }
}    

TestLoaderInterface::load 方法返回一个 TestInterface 实例的数组。

限制

请求对象限制

RouteGuard 在将其传递给 Symfony 的特定路由的授权检查器时不会修改当前的 Request 对象。

这意味着返回与 URL / 主机 / 方法相关的参数的 Request 对象的方法,在安全投票者(Symfony\...\VoterInterface)或表达式(Symfony\...\Expression)内部使用时,将返回与正在检查的路由无关的值。

在这种情况下,RouteGuard 授权检查的结果是“未定义行为”。这些方法包括

  • getPathInfo
  • getHost
  • getHttpHost
  • getMethod
  • isMethod*
  • getRequestUri
  • getUri

运行时变量限制

简而言之

如果您正在使用运行时变量授权测试,这些变量应从请求属性中解析,而这些变量不是路由参数的一部分(路由变量 + 默认值),则 RouteGuard 将无法解析它们并将抛出异常。

详细说明

测试提供者可能提供需要运行时变量(基本上是控制器参数)的授权测试。
为了解析这些变量,RouteGuard 引入了 ArgumentResolver
它模仿 Symfony 的 Symfony\...\ArgumentResolver,但使用 RouteContextInterface 实例和 ControllerMetadata 缓存来解析控制器参数。

与 Symfony 的 Symfony\...\ArgumentResolver 类似,它使用一个 ArgumentValueResolverInterface 实例数组来委托解析到特定的解析器。它们都像它们的 Symfony 原型一样工作,只是处理请求属性。

在 Symfony 中,请求属性最初从路由参数(路由变量 + 默认值)设置。
参见 Symfony\...\RouterListener::onKernelRequest
但是,除了这些之外,在 Symfony 中,请求属性被更广泛地用作仅路由参数 - 它们被用作不同组件之间隐式的“信息交换点”。

在 RouteGuard 中,特定路由的请求属性由 RequestAttributesFactory 创建。它返回解析后的路由参数(路由变量 + 默认值)。

相反,RouteGard 不能使用除来自正在检查的路由参数之外的任何请求属性。使用当前 Request 的其他属性可能与该路由无关。

此外,与标准流程不同,RequestAttributesFactory 不会添加特殊的 '_route' 属性,并从路由参数中删除 '_controller' 参数 - 因此它们也不能用作运行时变量。

Sensio FrameworkExtraBundle 限制

FrameworkExtraBundle 允许在授权测试中使用用户定义的运行时变量(“@IsGranted”注解的“主体”参数或“@Security”注解表达式中的变量)。

因此,在运行时变量限制中描述的限制适用于 FrameworkExtraBundle 的授权测试。

参数转换不支持

目前,RouteGuard 不支持 FrameworkExtraBundle 中 @ParamConverter 注解提供的控制器参数转换功能 - 它会使用未转换的参数值,这可能会导致意外的结果。

注意,如果 FrameworkExtraBundle 的 auto_convert 选项设置为 true,并且控制器参数被 ParamConverter 支持的类型之一(默认为 DateTimeInterface 和 Doctrine 实体类)类型提示,则 ParamConverter 可能会隐式参与。

内部原理

收集数据

RouteGuard 在编译时、缓存预热期间收集所有授权测试和所需元数据(路由元数据和控制器元数据)。入口点:AuthorizationCacheWarmer

授权测试和元数据存储在 PSR-6 缓存中。

特定路由的授权测试是由 ProviderAggregate 收集的,它会遍历所有注册的测试提供程序(ProviderInterface 的实例)。

ProviderInterface::getTests 方法返回测试包(AbstractTestBagInterface 的实例),其中包含测试(TestInterface 的实例)。

内置测试提供程序

路由授权

路由授权由 RouteAuthorizationChecker 服务执行。它加载路由的测试,并调用 DelegatingAuthorizationChecker,根据测试实例的类将测试传递给相应的授权检查器。

内置授权检查器

SymfonyAccessControlProvider 细节

access_control 规则的复杂性在于它们并没有直接映射到特定的路由。

access_control 规则可能有 4 种可能的约束

  • path(正则表达式)
  • host(正则表达式)
  • methods(数组)
  • ips(数组)

SymfonyAccessControlProvider在编译时筛选每个路由的匹配规则,比较规则约束和路由参数。

性能最佳的情况是,在运行时可以确定一个总是匹配路由的规则 - 那么它将返回简单的TestBag

在其他情况下(一个或多个可能匹配的规则,取决于运行时的Request),它将返回一个将在运行时解决的RequestDependentTestBag

对于每个需要请求相关测试袋的路由,RouteGuard将在缓存预热期间生成一条日志警告消息,即

11:04:03 WARNING [route_guard] Route "secure" (path "/secure1/{page}") requires runtime matching to access_control rule(s) #1 (zero-based), this would reduce performance.

在编译时将access_control规则匹配到路由上

RouteMatcher

IP匹配无法在编译时完成,匹配方法是一个简单的数组交集。最复杂的是匹配路径和主机约束。它们以相同的方式进行,所以我们只继续路径。

在编译时,我们只有路径的静态前缀。对于静态路由(没有任何变量),它将等于运行时的结果路径,因此我们可以简单地将其与约束正则表达式匹配。对于动态路由,我们解析约束正则表达式,将其与静态前缀(基本上是正则表达式的静态前缀)进行比较,并确定它是否总是/可能/从不匹配运行时的结果路径。

性能提示

路径和主机约束的一般性能提示是始终使用“字符串起始”断言(^)。

以下示例可以说明这一点

编译时规则path: /foo会被确定可能匹配任何动态路由 - 因为在运行时,任何用于路由的变量都可能导致字符串"/foo"

但是,编译时规则path: ^/foo会被确定可能匹配具有路径静态前缀"/""/f""/foo""/foob"的动态路由,但不匹配"/bar"

更进一步,当正则表达式静态前缀(/foo)比路径静态前缀短或相同长度,且正则表达式对其静态前缀后的符号没有限制(正则表达式是^/foo^/foo.*^/foo.*$),这意味着正则表达式将始终匹配具有路径静态前缀"/foo""/foob"的动态路由,而不依赖于运行时的Request。这将导致一个始终匹配的路由access_control规则(如果没有找到可能匹配的规则),允许直接将规则映射到路由,而无需使用请求相关的测试袋。

SensioExtraProvider详情

Sensio FrameworkExtraBundle会就地执行@Security注解中的表达式,绕过标准流程(将授权测试参数传递给Symfony\...\AuthorizationCheckerInterface)。见Sensio\...\SecurityListener

为了保持流程的一致性,RouteGuard将这些表达式包装成ExpressionDecorator实例,并注册SensioSecurityExpressionVoter来处理它们。

添加自己的授权测试提供程序

首先,阅读内部机制部分。

要创建自己的提供程序,您必须创建一个实现ProviderInterface的提供程序类,并将其注册为服务。

下一步取决于您的测试目标

如果您没有使用服务自动配置,您还需要手动添加标签

  • 为您测试提供程序服务 - yarhon_route_guard.test_provider
  • 为您授权检查器服务 - yarhon_route_guard.authorization_checker
  • 为您 symfony 安全测试解析器服务 - yarhon_route_guard.test_resolver.symfony_security

如果您的测试需要运行时控制器参数,您可以考虑使用RouteGuard的ArgumentResolver

计划中的内容

  • 支持控制器参数的参数转换器。
  • 调试CLI命令(s)以查看存储在授权缓存中的数据。
  • 支持动态授权测试
    namespace App\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Yarhon\RouteGuardBundle\Annotation\MethodCallAuthorizationTest;
    
    class Controller extends AbstractController
    {    
        public function testAdminIsGranted()
        {  
            // some logic
          
            $this->denyAccessUnlessGranted('ROLE_ADMIN');
        }
      
        /**
        * @MethodCallAuthorizationTest("testAdminIsGranted")
        */
        public function admin()
        {
            $this->testAdminIsGranted();
          
            // ............
        }
    }    
    在上面的示例中,授权逻辑被提取到一个单独的方法中。使用这种技术,相同的授权代码可以在操作被访问时使用(在这种情况下是显式调用),以及在检查路由授权时使用(多亏了@MethodCallAuthorizationTest注解),即当显示路由链接时。