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,
};
}
}
调用流程
你在控制器里写:
$this->denyAccessUnlessGranted('POST_EDIT', $post);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()(用户没权限直接拒绝访问)。
