Symfony如何优雅限制文章详情/编辑等页面的同家庭用户访问?
嘿,这个需求在Symfony里完全有优雅的解决办法,不用在每个控制器里重复写那些烦人的验证逻辑!最常用也最靠谱的方案是自定义Security Voter,再搭配Doctrine过滤器从数据库层面限制查询范围,双管齐下,既省心又安全。下面一步步给你拆解怎么实现:
一、用自定义Security Voter统一处理权限校验
Security Voter是Symfony权限系统的核心组件,专门用来处理这种自定义的权限逻辑,能自动应用到所有需要校验Article的场景,完美解决重复代码的问题。
1. 创建ArticleVoter类
先生成一个继承AbstractVoter的自定义Voter,用来处理文章的查看、编辑等权限:
// src/Security/Voter/ArticleVoter.php namespace App\Security\Voter; use App\Entity\Article; use App\Entity\User; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter; use Symfony\Component\Security\Core\User\UserInterface; class ArticleVoter extends AbstractVoter { // 定义这个Voter要处理的权限操作 public const VIEW = 'article_view'; public const EDIT = 'article_edit'; protected function supports(string $attribute, mixed $subject): bool { // 判断当前请求的权限和对象是否属于这个Voter的处理范围 return in_array($attribute, [self::VIEW, self::EDIT]) && $subject instanceof Article; } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { $user = $token->getUser(); // 未登录用户直接拒绝访问 if (!$user instanceof UserInterface) { return false; } /** @var Article $article */ $article = $subject; // 核心校验逻辑:对比用户所属家庭与文章关联的家庭ID return $user->getFamily()->getId() === $article->getFamily()->getId(); } }
2. 在控制器中轻松调用
现在你完全不用在控制器里写重复的验证代码了,只要在对应方法上加上IsGranted注解就行,Symfony会自动调用上面的Voter完成校验:
// src/Controller/ArticleController.php namespace App\Controller; use App\Entity\Article; use App\Security\Voter\ArticleVoter; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; class ArticleController extends AbstractController { #[Route('/articles/show/{id}', name: 'article_show')] #[IsGranted(ArticleVoter::VIEW, subject: 'article')] public function show(Article $article): Response { // 能走到这里说明权限已经校验通过,直接处理业务逻辑即可 return $this->render('article/show.html.twig', [ 'article' => $article, ]); } #[Route('/articles/edit/{id}', name: 'article_edit')] #[IsGranted(ArticleVoter::EDIT, subject: 'article')] public function edit(Article $article): Response { // 同样,权限校验已经自动完成 return $this->render('article/edit.html.twig', [ 'article' => $article, ]); } }
这里的subject: 'article'是利用了Symfony的ParamConverter功能,它会自动把URL中的id转换成对应的Article对象,Voter直接拿到这个对象做校验,非常丝滑。
二、搭配Doctrine Filter从数据库层面兜底
上面的Voter能在权限层拦截非法访问,但如果有人通过Repository直接查询文章(比如后台逻辑),还是有可能拿到不属于当前用户家庭的数据。这时候用Doctrine过滤器就能从查询根源上限制范围:
1. 创建FamilyArticleFilter类
// src/Doctrine/Filter/FamilyArticleFilter.php namespace App\Doctrine\Filter; use App\Entity\User; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\Filter\SQLFilter; use Symfony\Component\Security\Core\Security; class FamilyArticleFilter extends SQLFilter { private Security $security; public function setSecurity(Security $security): void { $this->security = $security; } public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string { // 只对Article实体生效 if ($targetEntity->getName() !== 'App\Entity\Article') { return ''; } $user = $this->security->getUser(); // 未登录或非User实体时,返回空(可根据需求改成禁止查询) if (!$user instanceof User) { return ''; } $familyId = $user->getFamily()->getId(); // 自动给所有Article查询加上家庭ID条件 return sprintf('%s.family_id = %d', $targetTableAlias, $familyId); } }
2. 配置启用过滤器
首先在config/packages/doctrine.yaml中注册这个过滤器:
doctrine: orm: filters: family_article_filter: class: App\Doctrine\Filter\FamilyArticleFilter enabled: true
然后在Article实体类上添加Filter注解,指定使用这个过滤器:
// src/Entity/Article.php use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="articles") * @ORM\Filter(name="family_article_filter") */ class Article { // ... 其他字段定义 /** * @ORM\ManyToOne(targetEntity="App\Entity\Family") * @ORM\JoinColumn(nullable=false) */ private $family; // ... getter、setter方法 }
这样一来,所有针对Article的数据库查询都会自动带上家庭ID的过滤条件,从根源上杜绝了查询到不属于当前用户家庭的文章。
三、额外小提示:其他可选方案
如果你的所有Article相关路由都有统一的前缀,也可以用Kernel事件监听器监听kernel.request事件提前做校验,但相比Voter来说灵活性稍差——毕竟Voter能针对不同的操作(查看/编辑)做差异化校验,所以还是优先推荐Voter+Doctrine Filter的组合。
这套方案下来,你完全不用在任何控制器里重复写家庭对比的逻辑,所有权限校验都由Voter自动处理,数据库层面还有Filter兜底,完美实现你要的优雅权限控制!
内容的提问来源于stack exchange,提问作者ice

