将DataTransformer迁移至StateProvider后出现安全问题
在API Platform 2.6中,通过DataTransformer处理/fridges/{id}/contents接口时,security表达式里的object指向原始Fridge实例,FRIDGE_READ权限的Voter能正常验证冰箱所有者权限。但升级到2.7改用StateProvider后,security表达式中的object变成了Provider返回的FridgeContents实例,导致Voter无法获取Fridge进行权限校验。
方案1:让Voter兼容FridgeContents实例
修改现有FRIDGE_READ Voter,使其同时支持Fridge和FridgeContents实例,从FridgeContents中关联到对应Fridge完成验证:
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; use App\Entity\Fridge; use App\Entity\FridgeContents; use Symfony\Component\Security\Core\User\UserInterface; class FridgeReadVoter extends Voter { protected function supports(string $attribute, mixed $subject): bool { return $attribute === 'FRIDGE_READ' && ($subject instanceof Fridge || $subject instanceof FridgeContents); } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { $user = $token->getUser(); if (!$user instanceof UserInterface) { return false; } // 统一获取Fridge实例 $fridge = $subject instanceof FridgeContents ? $subject->getFridge() : $subject; return $fridge->getOwner() === $user; } }
优点:无需修改资源配置和Provider代码,权限逻辑集中在Voter中,符合单一职责原则。
前提:FridgeContents类需保留对Fridge的引用(比如添加getFridge()方法)。
方案2:在security表达式中直接获取原始Fridge实例
通过Symfony表达式语言的service()函数,调用Item Provider根据URI变量中的id获取Fridge实例,传入权限校验:
#[ApiResource( operations: [ new Get( uriTemplate: '/fridges/{id}/contents', security: 'is_granted("FRIDGE_READ", service("api_platform.doctrine.orm.item_provider").getItem("App\Entity\Fridge", uri_variables["id"], null, []))', output: FridgeContents::class, provider: FridgeContentsProvider::class, ), ], )] class Fridge {}
注意:确保服务IDapi_platform.doctrine.orm.item_provider与你的实际Item Provider服务匹配,若使用自定义Item Provider,替换为对应服务ID即可。
优点:无需修改Voter和Provider,直接在资源配置中完成权限校验,逻辑集中。
缺点:表达式相对冗长,硬编码了实体类名和服务ID,后期维护需注意同步。
方案3:优化临时方案,统一权限校验逻辑
如果选择保留在Provider内做权限校验的方式,建议将安全逻辑封装到独立服务,避免Provider代码臃肿:
- 创建权限校验服务:
namespace App\Security; use App\Entity\Fridge; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Security; class FridgeAccessChecker { public function __construct(private readonly Security $security) {} public function checkReadAccess(Fridge $fridge): void { if (!$this->security->isGranted('FRIDGE_READ', $fridge)) { throw new AccessDeniedException(); } } }
- 在StateProvider中注入并调用:
class FridgeContentsProvider implements ProviderInterface { public function __construct( private readonly ProviderInterface $itemProvider, private readonly FridgeAccessChecker $accessChecker, ) { } public function provide(Operation $operation, array $uriVariables = [], array $context = []): FridgeContents { /** @var Fridge $fridge */ $fridge = $this->itemProvider->provide($operation, $uriVariables, $context); // 调用统一的权限校验 $this->accessChecker->checkReadAccess($fridge); // ...transform data... return $fridgeContents; } }
优点:将权限逻辑从Provider中剥离,保持Provider专注于数据转换,权限逻辑可复用。
缺点:权限校验从资源配置转移到了Provider,需确保所有相关操作都调用该校验服务。
内容的提问来源于stack exchange,提问作者Zed-K

