从 Symfony 3.4 迁移到 Symfony 4.4 后,自定义投票器无法按预期工作

Posted

技术标签:

【中文标题】从 Symfony 3.4 迁移到 Symfony 4.4 后,自定义投票器无法按预期工作【英文标题】:Custom voter does not work as expected after migrating from Symfony 3.4 to Symfony 4.4 【发布时间】:2021-06-04 00:10:02 【问题描述】:

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

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

在 Symfony 3.4 中,每个请求都会调用投票者,而无需添加任何代码。在 web profiler 中,我可以看到选民名单,以及 AccessDecisionManager 的决定(“Granted”或“Denied”)。

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 中是否以错误的方式使用选民?

谢谢。

我的自定义选民类别:

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 下,您的选民被称为 $request 作为主题?尽管我手头没有测试用例,但这似乎有点奇怪。尝试在 3.4 下运行 'bin/console debug:event-dispatcher kernel.request' 并查看是否有任何自定义事件侦听器。我希望听众会打电话给你的选民。但我可能大错特错。 【参考方案1】:

正如我在评论中提到的,我有点怀疑这在没有自定义内核请求侦听器的情况下在 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 中启用了自动装配和自动配置,则不需要额外的服务配置。如果没有,那么您将需要定义一个服务并相应地标记它。

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

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;
    

【讨论】:

非常感谢!您的 EventSubscriber 可以完美运行,甚至无需更改我的 Voter 代码。我做了一些研究,发现在 3.4 中,我的选民是从 EventListener symfony/security-http/Firewall/AccessListener 调用的,但是在 4.4 中,他们更改了一些代码,这不再起作用了。

以上是关于从 Symfony 3.4 迁移到 Symfony 4.4 后,自定义投票器无法按预期工作的主要内容,如果未能解决你的问题,请参考以下文章

Symfony LTS:如何从 2.8 升级到 3.4?

Symfony LTS:如何从 3.4 升级到 4.4?

Symfony 更新 2.8 到 3.4

Symfony 2.8 -> 3.4 本地开发速度降低

symfony 3.4 设置 prod 环境

将旧用户迁移到 symfony2