重构对每个 Zf2 控制器操作的一些调用

Refactor some calls on each Zf2 controller action

我需要执行自定义 isGranted 方法(不使用来自社区的 Rbac 或 acl 模块)。所以我有一个提供功能的服务。但是这段代码:

if (!$this->userService->isGrantedCustom($this->session->offsetGet('cod_lvl'), 'ZF_INV_HOM')) {
    throw new \Exception("you_are_not_allowed", 1);
}

...在每个控制器和我的每个动作中都是重复的。参数的变化当然取决于权限('ZF_INV_HOM''ZF_TODO_DELETE' ...)。

我认为在调用控制器之前执行此代码是个不错的主意,但我无法确定什么是最佳解决方案(最佳架构),以及如何将这些参数传递给它(我想过控制器上的注释,但如何处理这个?)。

关键是,如果我必须修改这段代码,我无法想象要这样做数百次,对于每个控制器,我拥有的每个动作我都需要将这段代码放在一个地方。

通过将事件侦听器附加到 SharedEventManager,您可以定位所有控制器并在一个地方进行授权检查。

在这种情况下,目标是 Zend\Mvc\Controller\AbstractActionController,这意味着任何扩展它的控制器都将执行侦听器。此侦听器的高优先级意味着它在目标控制器操作之前执行,让您有机会处理任何未经授权的请求。

public function onBootstrap(MvcEvent $event)
{
    $application  = $event->getApplication();
    $eventManager = $application->getEventManager()->getSharedManager();

    $eventManager->attach(
        \Zend\Mvc\Controller\AbstractActionController::class, // Identity of the target controller
        MvcEvent::EVENT_DISPATCH,
        [$this, 'isAllowed'],
        1000  // high priority
    );
}

在每个控制器中,您都需要通过某种方式来确定正在访问哪个 'resource'。

作为一个例子,它可以实现这个接口

interface ResourceInterface
{
    // Return a unique key representing the resource
    public function getResourceId();
}

监听器可能看起来像这样。

public function isAllowed(MvcEvent $event)
{
    $serviceManager = $event->getApplication()->getServiceManager();

    // We need the 'current' user identity
    $authService = $serviceManager->get('Zend\Authentication\AuthenticationService');
    $identity = $authService->getIdentity();

    // The service that performs the authorization
    $userService = $serviceManager->get('MyModule\Service\UserService');

    // The target controller is itself a resource (the thing we want to access)
    // in this example it returns an resource id so we know what we want to access
    // but you could also get this 'id' from the request or config etc
    $controller = $event->getTarget();

    if ($controller instanceof ResourceInterface) {
        $resourceName = $controller->getResourceId();

        // Test the authorization, is UserX allowed resource ID Y
        if (empty($resourceName) || $userService->isGrantedCustom($identity, $resourceName)) {
            // early exit for success
            return;
        } else {
           // Denied; perhaps trigger a new custom event or return a response
        }
    }

}

如果你不想用所有这些代码污染你的模块,你也可以创建一个监听器 class 并只在你的 bootstrap 方法中附加监听器:

<?php

namespace Application\Listener;

use Application\Service\UserService;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\Mvc\MvcEvent;
use Zend\EventManager\SharedEventManagerInterface;
use Zend\EventManager\SharedListenerAggregateInterface;
use Zend\Authentication\AuthenticationServiceInterface;

class IsAllowedListener implements SharedListenerAggregateInterface
{
    /**
     * @var AuthenticationServiceInterface
     */
    protected $authService;

    /**
     * @var UserService
     */
    protected $userService;

    /**
     * @var \Zend\Stdlib\CallbackHandler[]
     */
    protected $sharedListeners = array();

    /**
     * @param SharedEventManagerInterface $events
     */
    public function attachShared(SharedEventManagerInterface $events)
    {
        $this->sharedListeners[] = $events->attach(AbstractActionController::class, MvcEvent::EVENT_DISPATCH, array($this, 'isAllowed'), 1000);
    }

    public function __construct(AuthenticationServiceInterface $authService, UserService $userService ){
        $this->authService = $authService;
        $this->userService = $userService;
    }

    /**
     * @param MvcEvent $event
     */
    protected function isAllowed(MvcEvent $event)
    {
        $authService = $this->getAuthService();
        $identity = $authService->getIdentity();

        $userService = $this->getUserService();

        if($userService->isGrantedCustom()){
            // User is granted we can return
            return;
        }

        // Return not allowed response
    }

    /**
     * @return AuthenticationServiceInterface
     */
    public function getAuthService()
    {
        return $this->authService;
    }

    /**
     * @param AuthenticationServiceInterface $authService
     */
    public function setAuthService(AuthenticationServiceInterface $authService)
    {
        $this->authService = $authService;
    }

    /**
     * @return UserService
     */
    public function getUserService()
    {
        return $this->userService;
    }

    /**
     * @param UserService $userService
     */
    public function setUserService(AuthenticationServiceInterface $userService)
    {
        $this->userService = $userService;
    }
}

您需要设置一个工厂来注入您的依赖项:

<?php

namespace Application\Listener;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

/**
 * Factory for creating the IsAllowedListener
 */
class IsAllowedListenerFactory implements FactoryInterface
{
    /**
     * Create the IsAllowedListener
     *
     * @param ServiceLocatorInterface $serviceLocator
     * @return RenderLinksListener
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $authService = $serviceManager->get('Zend\Authentication\AuthenticationService');
        $userService = $serviceLocator->get('Application\Service\UserService');
        return new IsAllowedListener($authService, $userService );
    }
}

并在配置中注册所有这些:

'service_manager' => array(
    'factories' => array(
        'Application\Listener\IsAllowedListener' => 'Application\Listener\IsAllowedListenerFactory'
    )
)

然后在bootstrap:

public function onBootstrap(EventInterface $event)
{
    $application    = $event->getTarget();
    $serviceManager = $application->getServiceManager();
    $eventManager   = $application->getEventManager();
    $sharedEventManager = $eventManager->getSharedManager();
    $isAllowedListener = $serviceManager->get('Application\Listener\IsAllowedListener')
    $sharedEventManager->attachAggregate($isAllowedListener);
}

除了使用 AbstractActionController::class,您还可以制作一个特定的 class,这样您将只会收听 class 的实例。

例如 AbstractIsAllowedActionController::class 或类似的东西。