从 Symfony 3.4 迁移到 Symfony 4.4 后,自定义选民无法按预期工作

Custom voter does not work as expected after migrating from Symfony 3.4 to Symfony 4.4

我正在将应用程序从 Symfony 3.4 迁移到 Symfony 4.4。此应用程序使管理员用户可以编辑访问每条路线所需的角色,因此所有角色和路线都存储在数据库中。

为了检查用户是否有权访问路由,每次请求都会调用投票器,并在 services.yaml 文件中进行配置。

在 Symfony 3.4 中,每次请求都会调用投票器,而无需添加任何代码。在网络分析器中,我可以看到投票者列表,以及来自 AccessDecisionManager 的决定(“授予”或“拒绝”)。

Screenshot of the Web Profiler for Symfony 3.4

然而,在 Symfony 4.4 中,选民似乎根本没有被召唤。在网络分析器中,我的自定义选民仍在列表中(两次 ??),但 AccessDecisionManager 没有做出任何决定。

Screenshot of the Web Profiler for Symfony 4.4

如果我通过添加此行 $this->denyAccessUnlessGranted("", $request); 直接从控制器检查用户访问权限,选民将被调用并按预期工作。

如果有人可以向我解释为什么我必须在 Symfony 4.4 中手动调用 denyAccessUnlessGranted() 方法,而在 Symfony 3.4 中不需要它?我在 3.4 中使用选民的方式不对吗?

谢谢。

我的自定义选民 class :

namespace App\Security;

use Doctrine\ORM\EntityManager;

use Symfony\Component\HttpFoundation\Request;

use Symfony\Component\Security\Core\User\UserInterface;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;

class DynamicAccessVoter implements VoterInterface
{
    // Routes everyone has access to
    const PUBLIC_ROUTES = [
        "login"
    ];

    // Routes everyone who's connected has access to
    const PRIVATE_ROUTES = [
        "homepage",
        "fos_js_routing",
        "fos_js_routing_js"
    ];

    // Routes everyone has access to only in dev mode
    const DEV_ROUTES = [
        "_wdt",
        "_profiler",
        "_profiler_home",
        "_profiler_search",
        "_profiler_search_bar",
        "_profiler_phpinfo",
        "_profiler_search_results",
        "_profiler_open_file",
        "_profiler_router",
        "_profiler_exception",
        "_profiler_exception_css",
        "_twig_error_test"
    ];

    private $env;

    /**
     * Constructor
     * 
     * @param string $env - App environment (dev or prod)
     */
    public function __construct(String $env = "") {
        $this->env = $env;
    }
    
    /**
     * Custom voter
     * 
     * @param TokenInterface $token
     * @param Request $subject
     * @param array $env
     */
    public function vote($token, $subject, $attributes) {

        // Verifie si $subject est une instance de Request
        if(!$subject instanceof Request) {
            return self::ACCESS_ABSTAIN;
        }

        $route = $subject->attributes->get("_route");

        // Verifie si la route est une route publique (accessible par tout le monde)
        if(in_array($route, DynamicAccessVoter::PUBLIC_ROUTES)) {
            return self::ACCESS_GRANTED;
        }

        // Verifie si l'application est en développement et la route nécéssaire pour le debug
        if($this->env == "dev" && in_array($route, DynamicAccessVoter::DEV_ROUTES)) {
            return self::ACCESS_GRANTED;
        }

        // Verifie si $utilisateur est une instance de UserInterface
        if(!$token->getUser() instanceof UserInterface) {
            return self::ACCESS_ABSTAIN;
        }

        // Verifie si la route est une route accéssible par tout utilisateur connecté
        if(in_array($route, DynamicAccessVoter::PRIVATE_ROUTES)) {
            return self::ACCESS_GRANTED;
        }
        
        // Verifie si l'utilisateur connecté à le droit d'accéder à cette route
        if($token->getUser()->hasAccessTo($route)) {
            return self::ACCESS_GRANTED;
        }

        return self::ACCESS_DENIED;
    }
}

我的自定义投票器在 services.yaml 文件中配置为服务:

app.dynamic_access_voter:
    class: App\Security\DynamicAccessVoter
    arguments: ["%kernel.environment%"]
    tags:
        - { name: security.voter }

我的 security.yaml 文件,如果有帮助的话:

security:
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    encoders:
        App\Entity\Utilisateur:
            algorithm: bcrypt

    providers:
        main:
            entity:
                class: App\Entity\Utilisateur
                property: email      

    firewalls:
        main:
            anonymous: true
            provider: main
            pattern: ^/
            form_login:
                login_path: login
                check_path: login
                always_use_default_target_path: true
                default_target_path: homepage
            logout:
                path: /logout
                target: /login
            user_checker: App\Security\EnabledUserChecker

    access_control:
        - { path: ^/ }

正如我在评论中提到的,我有点怀疑这在没有自定义内核请求侦听器的情况下在 3.4 中是否有效。另一方面,如果您确实有这样的侦听器,那么它应该仍然可以正常工作。

无论如何,这里有一个适用于您的 Symfony 4 侦听器:

namespace App\Security;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

class RequestSubscriber implements EventSubscriberInterface
{
    private $checker;
    private $tokenStorage;

    public function __construct(AuthorizationCheckerInterface $checker, TokenStorageInterface $tokenStorage)
    {
        $this->checker = $checker;
        $this->tokenStorage = $tokenStorage;
    }
    public static function getSubscribedEvents(): array
    {
        return [
            RequestEvent::class => 'onKernelRequest',
        ];
    }
    public function onKernelRequest(RequestEvent $event)
    {
        if (!$event->isMasterRequest()) {
            return;
        }
        // avoid dev firewall requests
        if ($this->tokenStorage->getToken() === null) {
            return;
        }
        $route = $event->getRequest()->attributes->get('_route');
        dump('Request Subscriber ' . $route);

        if ($this->checker->isGranted('CAN_ACCESS_ROUTE',$event->getRequest())) {
            return;
        }
        $exception = new AccessDeniedException('Because I said so!');
        $exception->setAttributes('CAN_ACCESS_ROUTE');
        $exception->setSubject($route);
        throw $exception;
    }
}

如果您在 services.yaml 中启用了自动装配和自动配置,则不需要额外的服务配置。如果没有,那么您将需要定义一个服务并相应地标记它。

从我仅查看您的代码就可以看出,您的选民应该继续工作。然而,已经有一些 voter 改进,包括一个抽象的 Voter class,它简化了一些事情。这是我用来测试的。

namespace App\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class RequestVoter extends Voter
{
    protected function supports($attribute, $subject) : bool
    {
        if ($attribute === 'CAN_ACCESS_ROUTE') {
            return true;
        }
        return false;
    }
    protected function voteOnAttribute(
        $attribute,
        $subject,
        TokenInterface $token) : bool
    {
        /** @var Request $subject */
        $route = $subject->attributes->get('_route');
        dump('Voter ' . $route);

        switch($route) {
            case 'default':
            case 'app_login':
            case 'app_logout':
                return true;

        }

        return false;
    }
}