Symfony 3中如何限制实体所有者修改部分属性?
如何限制Symfony中实体所有者的属性编辑权限?
针对你的需求——让Project实体的所有者只能修改部分属性,结合你现有的Symfony 3+Doctrine+Form Bundle的技术栈,我给你几个实用的解决方案,从前端到后端全方位覆盖:
方案一:基于权限动态调整表单字段(最直观)
既然你已经用了Form Bundle来构建编辑表单,最直接的方式就是根据用户权限动态添加/移除字段。普通所有者看不到不能修改的字段,既安全又提升用户体验。
1. 修改你的ProjectType表单类
在表单构建时,判断当前用户是否是管理员,只给管理员显示敏感字段:
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; class ProjectType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { // 所有有权限编辑的用户都能修改的基础字段 $builder ->add('name') ->add('description') ->add('tags', EntityType::class, [ 'class' => Tag::class, 'choices' => $options['possibleTags'], 'multiple' => true, ]); // 只有管理员能修改的敏感字段 if ($options['is_admin']) { $builder ->add('admins', EntityType::class, [ 'class' => EntityUser::class, 'choices' => $options['possibleAdmins'], 'multiple' => true, ]) ->add('requiredGroups', EntityType::class, [ 'class' => Group::class, 'choices' => $options['possibleRequiredGroups'], 'multiple' => true, ]); } } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ 'data_class' => Project::class, 'is_admin' => false, // 默认值:非管理员 'possibleAdmins' => [], 'possibleRequiredGroups' => [], 'possibleTags' => [], ]); } }
2. 在控制器中传递权限参数
创建表单时,判断当前用户是否是管理员,把这个参数传给表单:
public function editAction(Request $request, int $id) { $em = $this->getDoctrine()->getManager(); $project = $em->getRepository(Project::class)->findOneBy(['id'=>$id]); $this->denyAccessUnlessGranted(ProjectVoter::EDIT, $project); $users = $em->getRepository(EntityUser::class)->findAll(); $groups = $em->getRepository(Group::class)->findAll(); $tags = $em->getRepository(Tag::class)->findAll(); // 新增:判断当前用户是否为管理员 $isAdmin = $this->isGranted('ROLE_ADMIN'); $form = $this->createForm(ProjectType::class, $project, [ 'possibleAdmins' => $users, 'possibleRequiredGroups' => $groups, 'possibleTags' => $tags, 'is_admin' => $isAdmin, // 传递给表单 ]); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $project = $form->getData(); $em->flush(); return $this->redirectToRoute('projects_show', ['id'=>$project->getId()]); } return $this->render('project/editor.html.twig', ['project'=>$project, 'form'=>$form->createView()]); }
方案二:用Form Events实现更细粒度的控制
如果需要更灵活的判断(比如不是基于全局角色,而是基于用户是否是当前项目的所有者),可以用表单事件来动态调整字段。
修改ProjectType添加事件监听
use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\AbstractType; class ProjectType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { // 先添加所有字段 $builder ->add('name') ->add('description') ->add('admins', EntityType::class, [ 'class' => EntityUser::class, 'choices' => $options['possibleAdmins'], 'multiple' => true, ]) ->add('requiredGroups', EntityType::class, [ 'class' => Group::class, 'choices' => $options['possibleRequiredGroups'], 'multiple' => true, ]) ->add('tags', EntityType::class, [ 'class' => Tag::class, 'choices' => $options['possibleTags'], 'multiple' => true, ]); // 添加PRE_SET_DATA事件监听,加载实体数据后调整字段 $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) { $form = $event->getForm(); $project = $event->getData(); $currentUser = $options['current_user']; // 如果用户不是管理员,只是当前项目的普通所有者 if (!$options['is_admin'] && $project->getAdmins()->contains($currentUser)) { // 移除不能修改的敏感字段 $form->remove('admins'); $form->remove('requiredGroups'); // 如果你想保留字段但禁用编辑,可以用: // $form->get('admins')->setDisabled(true); } }); } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ 'data_class' => Project::class, 'is_admin' => false, 'current_user' => null, // 需要传递当前登录用户 'possibleAdmins' => [], 'possibleRequiredGroups' => [], 'possibleTags' => [], ]); } }
控制器中传递当前用户
public function editAction(Request $request, int $id) { // ... 其他代码 $currentUser = $this->getUser(); $isAdmin = $this->isGranted('ROLE_ADMIN'); $form = $this->createForm(ProjectType::class, $project, [ 'possibleAdmins' => $users, 'possibleRequiredGroups' => $groups, 'possibleTags' => $tags, 'is_admin' => $isAdmin, 'current_user' => $currentUser, // 传递当前用户 ]); // ... 其他代码 }
方案三:后端字段级权限校验(防篡改)
如果担心用户通过手动构造请求来修改前端隐藏的字段,建议在保存前添加后端校验。可以扩展你的Voter来支持字段级权限,然后在控制器中检查。
1. 扩展ProjectVoter的权限属性
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\User\UserInterface; class ProjectVoter extends Voter { const EDIT = 'edit'; const EDIT_BASIC = 'edit_basic'; // 修改基础属性(名称、描述、标签) const EDIT_ADMINS = 'edit_admins'; // 修改管理员列表 const EDIT_GROUPS = 'edit_groups'; // 修改所需组 protected function voteOnAttribute($attribute, $subject, TokenInterface $token) { /** @var UserInterface $user */ $user = $token->getUser(); if (!$user instanceof UserInterface) { return false; } else if ($this->decisionManager->decide($token, ['ROLE_ADMIN'])) { return true; } switch($attribute) { case self::SHOW: return ($subject->isVisible() || $subject->getAdmins()->contains($user)); case self::EDIT: case self::EDIT_BASIC: return $subject->getAdmins()->contains($user); // 所有者可修改基础属性 case self::EDIT_ADMINS: case self::EDIT_GROUPS: return false; // 所有者不能修改这些字段 case self::REMOVE: return false; } return false; } }
2. 控制器中校验字段修改
在flush前,对比原始实体和修改后的实体,检查敏感字段的修改是否有权限:
if ($form->isSubmitted() && $form->isValid()) { $project = $form->getData(); // 获取修改前的原始实体 $originalProject = $em->getRepository(Project::class)->find($id); // 检查管理员字段是否被修改 if (!$originalProject->getAdmins()->equals($project->getAdmins())) { $this->denyAccessUnlessGranted(ProjectVoter::EDIT_ADMINS, $project); } // 检查所需组字段是否被修改 if (!$originalProject->getRequiredGroups()->equals($project->getRequiredGroups())) { $this->denyAccessUnlessGranted(ProjectVoter::EDIT_GROUPS, $project); } $em->flush(); return $this->redirectToRoute('projects_show', ['id'=>$project->getId()]); }
推荐组合方案
实际项目中,推荐方案一 + 方案三的组合:
- 用方案一动态调整表单字段,让用户看不到不能修改的内容,提升体验;
- 用方案三做后端校验,防止恶意用户构造请求篡改数据,保证安全性。
内容的提问来源于stack exchange,提问作者paolo
相关产品推荐
相关产品推荐

