初めに
User
に紐づいたPost
エンティティがあったとき「自分の所有するPost
以外は見れない」という用件を達成するにはどのような実装方法があるか、という話です。
最も単純な方法
最も単純な方法は、コントローラーでログインしているUser
のPost
かどうか判断する方法です。特に難しいこともありません。
が、しかし、 アクセス権を確認する際に同じようなコードが至る所に出てきてしまうことが容易に想像できます。
/**
* @Route("/{id}/detail", name="post_detail")
*/
public function edit(Post $post)
{
$user = $this->getUser();
if ($post->getUser() !== $user) {
throw $this->createAccessDeniedException();
}
...
}
Voterを用いる方法
SymfonyにはVoter
という認可のための仕組みがあります1。
こちらを用いることで、認可のロジックを分離することができ、あらゆる箇所で使い回すことが出来るようになります。
makeコマンドで作れます。
bin/console make:voter
The name of the security voter class (e.g. BlogPostVoter):
> PostVoter
created: src/Security/Voter/PostVoter.php
makeコマンドで作ると雛形が作成されます。今回は閲覧だけなのでこのように書き換えます。
src/Security/Voter/PostVoter.php
<?php
namespace App\Security\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;
class PostVoter extends Voter
{
protected function supports($attribute, $subject)
{
return in_array($attribute, ['VIEW'])
&& $subject instanceof \App\Entity\Post;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
if (!$user instanceof UserInterface) {
return false;
}
switch ($attribute) {
case 'VIEW':
return $subject->getUser() === $user; // $subjectに認可対象のエンティティが入る
}
return false;
}
}
これを、コントローラーからこのようにして呼び出すことができます。内部ではVoter
のvoteOnAttribute
メソッドが呼び出され、アクセス権がないと判断されると、自動でAccessDeniedException
が投げられます。
/**
* @Route("/{id}/detail", name="post_detail")
*/
public function edit(Post $post)
{
$this->denyAccessUnlessGranted('VIEW', $post);
...
}
更に美しく
コントローラーでの認可に限った話で言えば、アノテーションを使うことで更にメソッド内部をすっきりさせることができます。
認可のためのコードがメソッド内に1行も現れず、本来のロジックのみに集中出来るのでとても見やすくて良いと思います。
/**
* @Route("/{id}/detail", name="post_detail")
* @IsGranted("VIEW", subject="post")
*/
public function edit(Post $post)
{
...
}