从 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;
}
}
我正在将应用程序从 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;
}
}