指尖上的记忆指尖上的记忆
首页
  • 基础
  • Laravel框架
  • Symfony框架
  • 基础
  • Gin框架
  • 基础
  • Spring框架
  • 命令
  • Nginx
  • Ai
  • Deploy
  • Docker
  • K8s
  • Micro
  • RabbitMQ
  • Mysql
  • PostgreSsql
  • Redis
  • MongoDb
  • Html
  • Js
  • 前端
  • 后端
  • Git
  • 知识扫盲
  • Golang
🌟 gitHub
首页
  • 基础
  • Laravel框架
  • Symfony框架
  • 基础
  • Gin框架
  • 基础
  • Spring框架
  • 命令
  • Nginx
  • Ai
  • Deploy
  • Docker
  • K8s
  • Micro
  • RabbitMQ
  • Mysql
  • PostgreSsql
  • Redis
  • MongoDb
  • Html
  • Js
  • 前端
  • 后端
  • Git
  • 知识扫盲
  • Golang
🌟 gitHub

symfony7下关于Voter的使用

使用自定义的voter
//通过Enum 集中定义一些Voter名称(方便管理)
<?php

namespace App\Enum;

enum VoterAttributeEnum: string
{
    case MANAGE_ORDERS = 'MANAGE_ORDERS';
    case MY_ORDER_DETAILS = 'MY_ORDER_DETAILS';
    case UPDATE_ORDER_DETAILS = 'UPDATE_ORDER_DETAILS';
}



<?php

namespace App\Security\Voter\Billing;

use App\Entity;
use App\Enum\VoterAttributeEnum;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class OrderVoter extends Voter
{
    public function __construct(
        private readonly EntityManagerInterface $em,
    ) {
    }

    protected function supports(string $attribute, mixed $subject): bool
    {
        return VoterAttributeEnum::MANAGE_ORDERS->value === $attribute
            && ($subject instanceof Entity\Special\Event || $subject instanceof Entity\Special\EventRegistrationOrder);
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();

        //是否为Pub下的user
        if (!$user instanceof Entity\Pub\User) {
            return false;
        }

        $event = $subject;
        if ($subject instanceof Entity\Sciforum\EventRegistrationOrder) {
            $event = $subject->getEvent();
        }

        //是否为超级用户
        if ($user->isSuperUser()) {
            return true;
        }


        //只允许指定id的event可以被操作
        $allowedEventIds = [55,66,88];
        return in_array($event->getId(), $allowedEventIds);
    }
}

//使用,在控制器的某个需要控制的方法上添加如下代码,通过 IsGranted 注释功能自动注入判断, 它会从控制器方法参数中找到名字叫 event 的参数, 整个过程会触发 IsGrantedAttributeListener 这个listener
#[IsGranted(VoterAttributeEnum::MANAGE_ORDERS->value, subject: 'event')]
public function getOrderStatuses(
        Entity\Special\Event $event,
    ): JsonResponse {
        return $this->response();
    }
使用默认的voter
#[IsGranted(Role::ROLE_USER->value)]

<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Security\Core\Authorization\Voter;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

/**
 * RoleVoter votes if any attribute starts with a given prefix.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class RoleVoter implements CacheableVoterInterface
{
    public function __construct(
        private string $prefix = 'ROLE_',
    ) {
    }

    public function vote(TokenInterface $token, mixed $subject, array $attributes): int
    {
        $result = VoterInterface::ACCESS_ABSTAIN;
        $roles = $this->extractRoles($token);

        foreach ($attributes as $attribute) {
            if (!\is_string($attribute) || !str_starts_with($attribute, $this->prefix)) {
                continue;
            }

            $result = VoterInterface::ACCESS_DENIED;
            if (\in_array($attribute, $roles, true)) {
                return VoterInterface::ACCESS_GRANTED;
            }
        }

        return $result;
    }

    public function supportsAttribute(string $attribute): bool
    {
        return str_starts_with($attribute, $this->prefix);
    }

    public function supportsType(string $subjectType): bool
    {
        return true;
    }

    protected function extractRoles(TokenInterface $token): array
    {
        return $token->getRoleNames();
    }
}
二者使用总结
默认角色的voter原理:
/vendor/symfony/security-core/Authorization/Voter/RoleVoter.php
它的 supports() 会判断:如果 $attribute 以 'ROLE_' 开头,则接管处理

自定义voter原理:
在Security下的Voter目录自定义Voter, 最关键的是supports下的判断,因为可以定义很多个,symfony7会通过轮训所有找到适合的voter

对 Voter 的 supports() 方法分析

在 Symfony Security Voter 机制 中,supports() 是抽象方法之一,定义在 Voter 基类中:

abstract protected function supports(string $attribute, mixed $subject): bool;

它的作用就是:决定当前 Voter 是否“支持”处理传入的 $attribute 和 $subject。

参数解析
  • string $attribute

    • 代表权限的“动作”或“操作”,通常是一个字符串常量,比如 "POST_EDIT", "USER_DELETE", "VIEW"。
    • 在调用 isGranted($attribute, $subject) 或 denyAccessUnlessGranted($attribute, $subject) 时传入的第一个参数。
  • mixed $subject

    • 代表权限校验的对象(可以是一个实体,如 Post、User),也可能是 null(有些场景下只基于 attribute 判断即可)。
    • 第二个参数就是这个。
方法逻辑

supports() 方法一般用于快速过滤,避免每个 Voter 都去处理所有的 attribute 和 subject。 返回:

  • true:说明该 Voter 有兴趣 处理该组合(attribute + subject),接下来会调用 voteOnAttribute() 来做实际授权逻辑。
  • false:说明该 Voter 不关心,Security 系统会交给其它 Voter。
示例

假设你写了一个 PostVoter:

use App\Entity\Post;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class PostVoter extends Voter
{
    public const EDIT = 'POST_EDIT';
    public const VIEW = 'POST_VIEW';

    protected function supports(string $attribute, mixed $subject): bool
    {
        // 1. 只关心定义的几个 attribute
        if (!in_array($attribute, [self::EDIT, self::VIEW])) {
            return false;
        }

        // 2. subject 必须是 Post 实例
        if (!$subject instanceof Post) {
            return false;
        }

        return true;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();

        if (!$user) {
            return false;
        }

        /** @var Post $post */
        $post = $subject;

        return match ($attribute) {
            self::VIEW => true,
            self::EDIT => $user === $post->getAuthor(),
            default => false,
        };
    }
}
调用流程
  1. 你在控制器里写:

    $this->denyAccessUnlessGranted('POST_EDIT', $post);
    
  2. Security 系统会遍历所有 Voter:

  • 调用它们的 supports('POST_EDIT', $post)。
  • 如果返回 true,再调用 voteOnAttribute('POST_EDIT', $post, $token)。
  • 如果返回 false,忽略该 Voter。
总结
  • supports() 的职责是:快速筛选,决定是否处理该权限判断。
  • 典型模式:
    • 检查 $attribute 是否是自己关心的常量。
    • 检查 $subject 类型是否是自己负责的实体。
  • 它不会做权限判断逻辑,真正的判断写在 voteOnAttribute()。

当直接使用IsGranted的时候,如果需要第二个参数,,可以通过如下方式实现
#[IsGranted(SubmissionVoterEnum::MANAGE_INVITED_REVIEWER->value, subject: 'assignment')]
public function respondInvitation(
#[MapRequestPayload]
Dto\Input\Submission\Assignment\RespondInvitationDto $respondInvitationDto,
Entity\Sciforum\SubmissionAssignment $assignment,
Service\Submission\Assignment\ReviewerSubmissionActionService $reviewerSubmissionActionService,
): JsonResponse {
return $this->apiResponse->response($reviewerSubmissionActionService->respondInvitation($respondInvitationDto, $assignment, $this->getUser()));
}

这个subject: 'assignment' 中的 assignment 就是后面的 $assignment

IsGranted 和 denyAccessUnlessGranted 的区别

官方文档:

    /**
    * Checks if the attribute is granted against the current authentication token and optionally supplied subject.
    *
    * @throws \LogicException
    */
    protected function isGranted(mixed $attribute, mixed $subject = null): bool
    {
    if (!$this->container->has('security.authorization_checker')) {
    throw new \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".');
    }
    
        return $this->container->get('security.authorization_checker')->isGranted($attribute, $subject);
    }


    /**
     * Throws an exception unless the attribute is granted against the current authentication token and optionally
     * supplied subject.
     *
     * @throws AccessDeniedException
     */
    protected function denyAccessUnlessGranted(mixed $attribute, mixed $subject = null, string $message = 'Access Denied.'): void
    {
        if (!$this->isGranted($attribute, $subject)) {
            $exception = $this->createAccessDeniedException($message);
            $exception->setAttributes([$attribute]);
            $exception->setSubject($subject);

            throw $exception;
        }
    }
分析

这段就是 AbstractController 里内置的两个常用安全方法:

🔎 方法差异
isGranted($attribute, $subject = null): bool
  • 返回值:布尔值 true / false

  • 用法:当你只是想判断用户是否有权限时使用,不会中断程序。

  • 调用链:内部其实就是调用了

    $this->container->get('security.authorization_checker')->isGranted($attribute, $subject);
    
  • 适用场景:

    if ($this->isGranted('ROLE_ADMIN')) {
        // 显示 admin 按钮
    }
    
denyAccessUnlessGranted($attribute, $subject = null, string $message = 'Access Denied.')
  • 返回值:void,但如果没有权限会抛出 AccessDeniedException。

  • 用法:当你要强制拦截用户访问时使用。

  • 行为:

    • 内部调用了 isGranted();
    • 如果返回 false → 创建 AccessDeniedException → throw 出去;
    • 框架会捕获这个异常并返回 HTTP 403 Forbidden 响应。
  • 适用场景:

    $this->denyAccessUnlessGranted('POST_EDIT', $post);
    // 如果没有权限,直接抛异常 → 返回 403
    
✅ 总结对比
方法返回行为场景
isGranted()bool只是检查,不抛异常模板里隐藏/显示按钮,条件判断
denyAccessUnlessGranted()void(但可能抛 AccessDeniedException)没权限就中断并返回 403控制器中做访问控制

👉 所以:

  • 模板 / UI 层 → 多用 isGranted()(比如 if is_granted('ROLE_ADMIN'))。
  • 控制器 / API 层 → 多用 denyAccessUnlessGranted()(用户没权限直接拒绝访问)。