読者です 読者をやめる 読者になる 読者になる

Symfony Best Practice 訳してみた - Chapter9 -

Symfony Best Practiceの翻訳をしてます。 9日目です。

前回の記事

hanahirodev.hatenablog.com

※多分に意訳が入っておりますので、誤訳がある場合はご指摘ください。

※読み進めていくSymfony Best Practiceは 2016/10/23時点の情報です。

※写経をする場合は、事前にPHPの実行ができる環境をご用意ください。

3行まとめ

  • パスワード生成にはbcryptエンコーダーを使おう

  • security.yml,@Securityアノテーション,security.authorization_checkerサービス,Voterを使って制限をかける。

  • ユーザー登録/パスワード再発行/ユーザーのなりすまし機能/ログイン状態の継続機能は標準/FOSUserBundleでサポートされている。


第9章 セキュリティ

認証とファイアウォール(たとえばユーザーの資格情報取得)

あるユーザーが任意の方法で、任意のソースにアクセスできるようにSymfonyを設定することができます。内容は複雑ですが、セキュリティガイドに詳しく掲載されています。 要不要に関わらず、認証に関する設定はsecurity.ymlfirewallsキー配下に設定されています。

  • 正規に許可された2つの異なる認証システムとユーザが無い限り (たとえば、メインのサイトへのログインと API のためだけのトークンシステム)、 anonymous キーを有効にしたファイヤーウォールを1つだけ設けることを推奨します。

ほとんどのアプリケーションでは1つの認証システムと1組のユーザーだけを持っています。このため、1つのファイヤーウォールの設定だけで事足りていました。もちろん、WebサイトとAPIを分けたい場合は例外です。シンプルにしておくことが重要です。 加えて、ファイヤーウォールの内側ではanonymousキーを使うようにしましょう。ログ採取のために、サイト内のセクションごとに(あるいはほとんどすべてのセクション)ユーザーの情報が必要なのであれば、access_controlエリアを利用しましょう。

  • ユーザーパスワードの生成にはbcryptエンコーダーを使いましょう。

ユーザーがパスワードを保持する場合、SHA-512ハッシュエンコーダーではなく、bcryptエンコーダーを使うことをお勧めします。bcryptをお勧めする理由は、salt値を含むため、レインボーテーブル攻撃対策*1になりますし、総当り攻撃を遅延させる効果も有ります。

以上を念頭に置いて、ログインフォームでデータベースからユーザーを読み込むための認証設定をしていきます。

# app/config/security.yml
security:
    encoders:
        AppBundle\Entity\User: bcrypt

    providers:
        database_users:
            entity: { class: AppBundle:User, property: username }

    firewalls:
        secured_area:
            pattern: ^/
            anonymous: true
            form_login:
                check_path: login
                login_path: login

            logout:
                path: security_logout
                target: homepage

# ... access_control exists, but is not shown here
  • Blogアプリのサンプルコードには各パートを説明するために、コメントが記載されています。
認可(たとえばアクセスの拒否)*2

Symfonyは認可のための仕組みを、security.yml*3access_controlセクション、@Securityアノテーション*4security.authorization_checkerサービスのisGrantedメソッド*5のように複数提供しています。

  • 一般的なURLパターンによる保護にはaccess_controlを使う。
  • 利用可能である場所では、@Securityアノテーションを使う。
  • 複雑な状況にある場合は、security.authorization_checkerサービスで直接セキュリティをチェックする。

カスタムセキュリティVoter*6ACLによって、認可ロジックを1箇所に集中させる方法もあります。

  • 細かな制限のためにセキュリティVoterを定義する。
  • 管理機能を経由したユーザーがあらゆるオブジェクトにアクセスするの制限するために、Symfony ACLを利用する。
@Securityアノテーション

コントローラーごとにアクセス制御をするためには、可能な限り、@Securityアノテーションを利用します。こうすることで、読みやすく、アクションごとに一貫性のある設定が出来ます。

Blogアプリでは、記事を投稿するためにROLE_ADMINが必要です。@Securityアノテーションを使うことで、以下のようになります。

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
// ...

/**
 * Displays a form to create a new Post entity.
 *
 * @Route("/new", name="admin_post_new")
 * @Security("has_role('ROLE_ADMIN')")
 */
public function newAction()
{
    // ...
}
複雑なセキュリティの設定に対応する

小さいながらもさらに込み入ったセキュリティを実装したい場合、@Securityアノテーションの中でexpression*7を利用できます。以下の例では、getAuthorEmailメソッドの返り値にPostオブジェクトに格納されたメールアドレスがマッチしたユーザーだけがコントローラーにアクセスできます。

use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 * @Security("user.getEmail() == post.getAuthorEmail()")
 */
public function editAction(Post $post)
{
    // ...
}

自動的にPostオブジェクトを、 $postに設定するためにはParamComverter*8が必要です。 この方法には1つ大きな欠点があります。アノテーション内に記述してしまうと、他の場所で同じ設定を使いたくなった時に再利用しづらくなってしまいます。ブログアプリで、投稿者だけが参照できる画面へのリンクを追加したい場合を考えてみましょう。以下の状態だと、Twigの条件を繰り返し記述しないといけません。

{% if app.user and app.user.email == post.authorEmail %}
    <a href=""> ... </a>
{% endif %}

シンプルなロジックであれば、もっとも簡単な方法は、Postエンティティにユーザーが投稿者かどうか判定するメソッドを追加することです。

// src/AppBundle/Entity/Post.php
// ...

class Post
{
    // ...

    /**
     * Is the given User the author of this Post?
     *
     * @return bool
     */
    public function isAuthor(User $user = null)
    {
        return $user && $user->getEmail() == $this->getAuthorEmail();
    }
}

これでテンプレート内からも、Securityアノテーションからもメソッドにアクセスすることができます。

use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 * @Security("post.isAuthor(user)")
 */
public function editAction(Post $post)
{
    // ...
}
{% if post.isAuthor(app.user) %}
    <a href=""> ... </a>
{% endif %}
@Security を使わずにアクセス権をチェックする

上記の例では@SecurityアノテーションはParamConverterを使っているからこそ、変数postにアクセスできる状態になっていました。アノテーションを使わない場合または、複雑なユースケースである場合、PHP側でセキュリティチェックを書くこともできます。

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 */
public function editAction($id)
{
    $post = $this->getDoctrine()->getRepository('AppBundle:Post')
        ->find($id);

    if (!$post) {
        throw $this->createNotFoundException();
    }

    if (!$post->isAuthor($this->getUser())) {
        $this->denyAccessUnlessGranted('edit', $post);

        // or without the shortcut:
        //
        // use Symfony\Component\Security\Core\Exception\AccessDeniedException;
        // ...
        //
        // if (!$this->get('security.authorization_checker')->isGranted('edit', $post)) {
        //    throw $this->createAccessDeniedException();
        // }
    }

    // ...
}
セキュリティVoter

複雑なセキュリティロジックを実装する必要があって、isAuthor()のようなメソッドにまとめられない場合は、カスタムVoterを活用しましょう。簡単な順に、メソッド、カスタムVoter、ACLです。いずれの方法も、大抵の要求に対応できる程度には柔軟です。 まずVoterクラスを作りましょう。以下の例は上記で実装してきたgetAuthorEmailと同じ内容を実装しています。

namespace AppBundle\Security;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;
use AppBundle\Entity\Post;

class PostVoter extends Voter
{
    const CREATE = 'create';
    const EDIT   = 'edit';

    /**
     * @var AccessDecisionManagerInterface
     */
    private $decisionManager;

    public function __construct(AccessDecisionManagerInterface $decisionManager)
    {
        $this->decisionManager = $decisionManager;
    }

    protected function supports($attribute, $subject)
    {
        if (!in_array($attribute, array(self::CREATE, self::EDIT))) {
            return false;
        }

        if (!$subject instanceof Post) {
            return false;
        }

        return true;
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        $user = $token->getUser();
        /** @var Post */
        $post = $subject; // $subject must be a Post instance, thanks to the supports method

        if (!$user instanceof UserInterface) {
            return false;
        }

        switch ($attribute) {
            case self::CREATE:
                // if the user is an admin, allow them to create new posts
                if ($this->decisionManager->decide($token, array('ROLE_ADMIN'))) {
                    return true;
                }

                break;
            case self::EDIT:
                // if the user is the author of the post, allow them to edit the posts
                if ($user->getEmail() === $post->getAuthorEmail()) {
                    return true;
                }

                break;
        }

        return false;
    }
}

セキュリティVoter を有効化するには、以下のように新しいサービスを定義します。

# app/config/services.yml
services:
    # ...
    post_voter:
        class:      AppBundle\Security\PostVoter
        arguments: ['@security.access.decision_manager']
        public:     false
        tags:
           - { name: security.voter }

これで、@SecurityアノテーションからVoterクラスを利用できます。

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 * @Security("is_granted('edit', post)")
 */
public function editAction(Post $post)
{
    // ...
}

security.authorization_checkerサービスやコントローラー経由で直接アクセスすることもできます。

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 */
public function editAction($id)
{
    $post = ...; // query for the post

    $this->denyAccessUnlessGranted('edit', $post);

    // or without the shortcut:
    //
    // use Symfony\Component\Security\Core\Exception\AccessDeniedException;
    // ...
    //
    // if (!$this->get('security.authorization_checker')->isGranted('edit', $post)) {
    //    throw $this->createAccessDeniedException();
    // }
}
さらに詳しく

Symfonyのコミュニティーで開発されたFOSUserBundle*9は、Symfonyから使えるデータベースによるユーザーシステムを提供しています。このバンドルはユーザー登録やパスワードの再発行といった一般的な機能も取り扱います。 Remember Me機能 *10を有効化することで、しばらくの間ログインした状態を保持できます。 カスタマーサポート機能では、問題を再現するために別のユーザーになりすましてアプリにアクセスする必要が出てきます。Symfonyはユーザーのなりすまし機能*11を提供しています。 Symfonyがサポートしていないログイン方法を提供している場合、自前のユーザープロバイダー*12認証プロバイダー*13を開発することもできます。


感想

標準でかなり高機能なセキュリティ機能が提供されている。 Voterについてもうちょっと調べる。

(2016/11/5 更新)

next

hanahirodev.hatenablog.com