You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Symfony 6.2多角色IsGranted注解配置异常求助

问题:Symfony多角色权限注解未按预期的“或”逻辑生效

自定义TeamVoter处理关联表存储的角色权限,期望拥有ROLE_TEAM_ADMIN(全局编辑所有团队)或ROLE_TEAM_MEMBER_ADMIN(单团队管理)的用户可访问控制器方法。添加两个#[IsGranted]注解后,实际生效逻辑为“且”(需同时满足两个角色),而非默认affirmative策略的“或”。调试发现AccessDecisionManager每次仅接收单个属性,确认是注解配置问题。

自定义TeamVoter代码

<?php

namespace App\Security\Voter;

use App\Constants\UserRoles;
use App\Entity\Team;
use App\Entity\User;
use App\Repository\UserTeamRepository;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class TeamVoter extends Voter
{

    public function __construct(
        private UserTeamRepository $userTeamRepository,
    )
    {}

    /**
     * @inheritDoc
     */
    protected function supports(string $attribute, mixed $subject): bool
    {
        if (!in_array($attribute, UserRoles::TEAM_ROLES)) {
            return false;
        }

        return $subject instanceof Team;
    } 

    /**
     * @inheritDoc
     */
    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();
        if (!$user instanceof User) {
            return false;
        }
        $team = $subject;

        if (in_array($attribute, UserRoles::TEAM_RELATED_ROLES)) {
            return $this->voteOnTeamRelatedAttribute($attribute, $user, $team);
        }

        return match($attribute) {
            UserRoles::ROLE_TEAM_ADMIN => $this->canEdit($user, $team),
            UserRoles::ROLE_TEAM_OWNER => $this->canDelete($user, $team),
            default => false,
        };
    }

    private function voteOnTeamRelatedAttribute(string $attribute, User $user, Team $team): bool
    {
        $userRoles = $this->userTeamRepository->getUserTeamRoles($user, $team->getExternalUuid());

        if (!$userRoles) {
            return false;
        }

        return match($attribute) {
            UserRoles::ROLE_TEAM_MEMBER_ADMIN => in_array(UserRoles::ROLE_TEAM_MEMBER_ADMIN, $userRoles),
            UserRoles::ROLE_TEAM_MEMBER => in_array(UserRoles::ROLE_TEAM_MEMBER, $userRoles),
            default => false,
        };
    }

    private function canEdit(User $user, Team $team): bool
    {
        if ($this->canDelete($user, $team)) {
            return true;
        }

        return in_array(UserRoles::ROLE_TEAM_ADMIN, $user->getRoles());
    }

    public function canDelete(User $user, Team $team): bool
    {
        return $team->getOwner() === $user || $team->getCreator() === $user;
    }
}

控制器方法代码

#[Route('/rest/team/{externalUuid}/member/add', name: 'add_team_member', methods: ["PUT"])]
#[IsGranted(UserRoles::ROLE_TEAM_ADMIN, subject: 'team'), IsGranted(UserRoles::ROLE_TEAM_MEMBER_ADMIN, subject: 'team')]
public function addTeamMember(Request $request, Team $team): JsonResponse
{
    $request = $this->transformJsonBody($request);
    // ...
}

原因分析

每个#[IsGranted]注解会独立触发一次完整的权限校验流程,相当于两次独立的检查,只有两次都通过才会允许访问,这就导致了类似unanimous策略的效果。而默认的affirmative策略是针对同一请求中的多个权限属性生效的,多个注解会拆分成多次校验,无法共享该逻辑。


解决方案

方案1:单个注解传入多个属性(推荐)

修改控制器注解,将两个角色放在同一个#[IsGranted]中,以数组形式传递属性。这样AccessDecisionManager会一次性收到两个属性,按照affirmative策略判断(满足任意一个即可通过):

#[Route('/rest/team/{externalUuid}/member/add', name: 'add_team_member', methods: ["PUT"])]
#[IsGranted([UserRoles::ROLE_TEAM_ADMIN, UserRoles::ROLE_TEAM_MEMBER_ADMIN], subject: 'team')]
public function addTeamMember(Request $request, Team $team): JsonResponse
{
    $request = $this->transformJsonBody($request);
    // ...
}

此方式无需修改Voter代码,直接利用Symfony内置逻辑实现需求。

方案2:自定义复合权限属性(复用场景)

如果需要在多个地方复用该“或”逻辑,可以定义一个复合权限属性,在Voter中处理其判断逻辑:

  1. 添加复合属性到UserRoles
class UserRoles
{
    // 原有角色定义...
    public const ROLE_TEAM_EDITOR = 'ROLE_TEAM_EDITOR';
    
    public const TEAM_ROLES = [
        self::ROLE_TEAM_ADMIN,
        self::ROLE_TEAM_MEMBER_ADMIN,
        self::ROLE_TEAM_EDITOR,
        // 其他团队角色...
    ];
}
  1. 修改TeamVoter处理复合属性
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
    $user = $token->getUser();
    if (!$user instanceof User) {
        return false;
    }
    $team = $subject;

    // 处理复合权限:满足任意一个角色即可
    if ($attribute === UserRoles::ROLE_TEAM_EDITOR) {
        return $this->voteOnAttribute(UserRoles::ROLE_TEAM_ADMIN, $team, $token) 
            || $this->voteOnAttribute(UserRoles::ROLE_TEAM_MEMBER_ADMIN, $team, $token);
    }

    if (in_array($attribute, UserRoles::TEAM_RELATED_ROLES)) {
        return $this->voteOnTeamRelatedAttribute($attribute, $user, $team);
    }

    return match($attribute) {
        UserRoles::ROLE_TEAM_ADMIN => $this->canEdit($user, $team),
        UserRoles::ROLE_TEAM_OWNER => $this->canDelete($user, $team),
        default => false,
    };
}
  1. 控制器使用复合属性
#[Route('/rest/team/{externalUuid}/member/add', name: 'add_team_member', methods: ["PUT"])]
#[IsGranted(UserRoles::ROLE_TEAM_EDITOR, subject: 'team')]
public function addTeamMember(Request $request, Team $team): JsonResponse
{
    $request = $this->transformJsonBody($request);
    // ...
}

内容的提问来源于stack exchange,提问作者onizukaek

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 02:14:55