13. 13日目: セキュリティ

  • この記事は Symfony 1.4 向けのオリジナルの Jobeet Tutorial の一部分です。

13.1. 安全なアプリケーション

セキュリティは、二段階のプロセスでユーザーがアクセス権を持たないリソースにアクセスすることを防止します。
最初のステップである「認証」は、ユーザにいくつかの ID の送信を要求することによって、誰であるかを識別します。
システムが一度ユーザーが誰であるか知ったら、次の「認可」とよばれるステップは、与えられたリソース(それは特定のアクションを実行する権限を持っているかどうかをチェックします)へのアクセスを認めるか決定します。
セキュリティコンポーネントは、 アプリケーションの設定である app/config フォルダの security.yml ファイルを使用して設定することができます。
アプリケーションを安全にするには、次のように、 security.yml ファイルを変更します。

app/config/security.yml

security:
    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false

        secured_area:
            pattern:    ^/
            anonymous: ~
            form_login:
                login_path:  /login
                check_path:  /login_check
                default_target_path: ibw_jobeet_homepage

    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }

    providers:
        in_memory:
            memory:
                users:
                    admin: { password: adminpass, roles: 'ROLE_ADMIN' }

    encoders:
        Symfony\Component\Security\Core\User\User: plaintext
この設定は、 /admin セクション( /admin で始まるすべての URL )を安全にし、 ROLE_ADMIN をもつユーザーのみアクセスを許可します。( access_control の項目を参照ください)。
この例では、admin ユーザが設定ファイルで( providers の箇所)に定義されていますが、パスワードがエンコーダで符号化されていません。
ユーザーを認証するためには伝統的にログインフォームを使用しますが、それを有効にする必要があります。
まず、ログインフォームの表示(すなわち /login )とログインフォームの送信の処理(すなわち /login_check )の二つのルートを作成します。

src/Ibw/JobeetBundle/Resources/config/routing.yml

login:
    pattern:   /login
    defaults:  { _controller: IbwJobeetBundle:Default:login }
login_check:
    pattern:   /login_check

# ...
/login_check のコントローラーを実装する必要はなく、ファイアーウォールが自動的にフォームのこの URL への送信をキャッチし、処理します。
しかし、後述の login テンプレートの中のフォーム送信 URL を生成することができるように、ルートを作成する必要があります。
次に、ログインフォームを表示するアクションを作成してみましょう。

src/Ibw/JobeetBundle/Controller/DefaultController.php

namespace Ibw\JobeetBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;

class DefaultController extends Controller
{
    // ...

    public function loginAction()
    {
        $request = $this->getRequest();
        $session = $request->getSession();

        // get the login error if there is one
        if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
        } else {
            $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
            $session->remove(SecurityContext::AUTHENTICATION_ERROR);
        }

        return $this->render('IbwJobeetBundle:Default:login.html.twig', array(
            // last username entered by the user
            'last_username' => $session->get(SecurityContext::LAST_USERNAME),
            'error'         => $error,
        ));
    }
}
ユーザーがフォームを送信すると、セキュリティシステムは自動的にフォームの送信を処理します。
無効なユーザー名またはパスワードを送信した場合は、このアクションで、ユーザーに表示するためフォーム送信エラーをセキュリティシステムから読み込みます。
セキュリティシステム自体が送信されたユーザー名とパスワードをチェックしユーザを認証します。
そのため、唯一の作業は、ログインフォームを表示し、発生した可能性のあるログインエラーを表示することです。
最後に、対応するテンプレートを作成してみましょう。

src/Ibw/JobeetBundle/Resources/views/Default/login.html.twig

{% if error %}
    <div>{{ error.message }}</div>
{% endif %}

<form action="{{ path('login_check') }}" method="post">
    <label for="username">Username:</label>
    <input type="text" id="username" name="_username" value="{{ last_username }}" />

    <label for="password">Password:</label>
    <input type="password" id="password" name="_password" />

    <button type="submit">login</button>
</form>
ここで、 URL http://jobeet.local/app_dev.php/admin/dashboard にアクセスするとログインフォームが表示されます。
Jobeetの管理領域に行くには security.yml で定義されたユーザ名とパスワード( admin/adminpass )を入力する必要があります。

13.2. ユーザープロバイダー

認証時に、ユーザーは資格情報のセット(通常はユーザ名とパスワード)を送信します。
認証システムの仕事は、資格情報をユーザーリストに対して照らし合わせることです。では、ここでのユーザーリストはどこから来るのでしょうか?
Symfony2 では、ユーザーをどこからでも取得することができます。設定ファイル、データベーステーブル、 Web サービス、または、考えうるなんでも。
認証システムにひとつ以上のユーザーを提供するものは、「ユーザープロバイダー」として知られています。
Symfony2 は最も一般的なユーザープロバイダーとして、設定ファイル、および、データベースのテーブルからのユーザーの読み込みを標準装備しています。
上記では、設定ファイル内のユーザーを指定する、最初のケースを使用していました。

app/config/security.yml

# ...

providers:
    in_memory:
        memory:
            users:
                admin: { password: adminpass, roles: 'ROLE_ADMIN' }

# ...
しかし、一般的にはユーザーはデータベーステーブルに格納されることになるでしょう。
これを行うためには Jobeet のデータベースに新しい user テーブルを追加します。
まずは、この新しいテーブルの ORM を作成してみましょう。

src/Ibw/JobeetBundle/Resources/config/doctrine/User.orm.yml

Ibw\JobeetBundle\Entity\User:
    type: entity
    table: user
    id:
        id:
            type: integer
            generator: { strategy: AUTO }
    fields:
        username:
            type: string
            length: 255
        password:
            type: string
            length: 255

doctrine:generate:entities コマンドを実行し、新しい User エンティティクラスを生成します。

$ php app/console doctrine:generate:entities IbwJobeetBundle

そして、データベースを更新します。

$ php app/console doctrine:schema:update --force
新しい user クラスの唯一の要件は、 UserInterface インターフェイスを実装していることです。
これは、このインタフェースを実装する限り user はどのようなものでも良いことを意味します。
User.php ファイルを開き、以下のように編集します。

src/Ibw/JobeetBundle/Entity/User.php

namespace Ibw\JobeetBundle\Entity;

use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;

/**
 * User
 */
class User implements UserInterface
{
    /**
     * @var integer
     */
    private $id;

    /**
     * @var string
     */
    private $username;

    /**
     * @var string
     */
    private $password;

    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set username
     *
     * @param string $username
     * @return User
     */
    public function setUsername($username)
    {
        $this->username = $username;

    }

    /**
     * Get username
     *
     * @return string
     */
    public function getUsername()
    {
        return $this->username;
    }

    /**
     * Set password
     *
     * @param string $password
     * @return User
     */
    public function setPassword($password)
    {
        $this->password = $password;

    }

    /**
     * Get password
     *
     * @return string
     */
    public function getPassword()
    {
        return $this->password;
    }

    public function getRoles()
    {
        return array('ROLE_ADMIN');
    }

    public function getSalt()
    {
        return null;
    }

    public function eraseCredentials()
    {

    }

    public function equals(User $user)
    {
        return $user->getUsername() == $this->getUsername();
    }
}
生成されたエンティティに UserInterface クラスで要求されたメソッド(getRoles、getSalt、eraseCredentials と equals)を追加しました。
次に、エンティティユーザプロバイダを設定し、 User クラスを指すようにます。

app/config/security.yml

providers:
  main:
      entity: { class: Ibw\JobeetBundle\Entity\User, property: username }

encoders:
  Ibw\JobeetBundle\Entity\User: sha512
また、新しい User クラス用のエンコーダをパスワードの暗号化のため SHA512 アルゴリズムを使用するように変更しました。
これですべてセットアップされましたので、最初のユーザーを作成する必要があります。
これを行うためには、新しい symfony コマンドを作成します。

src/Ibw/JobeetBundle/Command/JobeetUsersCommand.php

<?php
namespace Ibw\JobeetBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Ibw\JobeetBundle\Entity\User;

class JobeetUsersCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            ->setName('ibw:jobeet:users')
            ->setDescription('Add Jobeet users')
            ->addArgument('username', InputArgument::REQUIRED, 'The username')
            ->addArgument('password', InputArgument::REQUIRED, 'The password')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $username = $input->getArgument('username');
        $password = $input->getArgument('password');

        $em = $this->getContainer()->get('doctrine')->getManager();

        $user = new User();
        $user->setUsername($username);
        // encode the password
        $factory = $this->getContainer()->get('security.encoder_factory');
        $encoder = $factory->getEncoder($user);
        $encodedPassword = $encoder->encodePassword($password, $user->getSalt());
        $user->setPassword($encodedPassword);
        $em->persist($user);
        $em->flush();

        $output->writeln(sprintf('Added %s user with password %s', $username, $password));
    }
}

最初のユーザーの追加を実行します。

$ php app/console ibw:jobeet:users admin admin
これはパスワード admin を持つ admin ユーザーを作成します。
これを管理セクションへのログインに使用することができます。

13.3. ログアウト

ログアウトはファイアウォールによって自動的に処理されます。
唯一の作業は、ログアウトの config パラメータを有効にすることです。

app/config/security.yml

security:
    firewalls:
        # ...
        secured_area:
            # ...
            logout:
                path:   /logout
                target: /
    # ...
ファイアウォールがすべての面倒を見るため、 URL ( /logout )用のコントローラを実装する必要はありません。
URL 生成に使用できるよう、ルートを作成してみましょう。

src/Ibw/JobeetBundle/Resources/config/routing.yml

# ...

logout:
    pattern:   /logout

# ...
これが設定されたら、/logout (上記で設定したパス)にユーザーを送信することで、現在のユーザの認証の解除をします。
次いで、ユーザは、ホームページ(target パラメータによって定義された値)に送られます。
あと残った作業は、ログアウトのリンクを管理セクションに追加することです。
これを行うために SonataAdminBundle の user_block.html.twig をオーバーライドします。
app/Resources/SonataAdminBundle/views/Core フォルダに user_block.html.twig ファイルを作成します。

app/Resources/SonataAdminBundle/views/Core/user_block.html.twig

{% block user_block %}<a href="{{ path('logout') }}">Logout</a>{% endblock%}
(初めにキャッシュをクリアしてから)管理セクションに入ろうとした場合、ユーザー名とパスワードの入力を要求され、その後、ログアウトリンクが右上隅に表示されます。

13.4. ユーザーセッション

Symfony2 はリクエストの間、ユーザー情報を保存するすばらしいセッションオブジェクトを提供します。
デフォルトでは、 Symfony2 のは、ネイティブの PHP のセッションを使用することにより、クッキーの属性を格納します。
コントローラから簡単にセッションの情報を保存・取得することができます。
$session = $this->getRequest()->getSession();

// store an attribute for reuse during a later user request
$session->set('foo', 'bar');

// in another controller for another request
$foo = $session->get('foo');
残念なことに、 Jobeet ユーザーのストーリーにはユーザーセッションに何かを保存する要件は含まれていません。
そこで、新しい要件を追加してみましょう。
  • ジョブの閲覧を容易にするため、ユーザが閲覧した最後の三つのジョブは、後からジョブページに戻れるようにメニューにリンクを表示する。
  • ユーザーがジョブページにアクセスすると、表示された Job オブジェクトは、セッションのユーザーの履歴に追加・保存される。

src/Ibw/JobeetBundle/Controller/JobController.php

// ...

public function showAction($id)
{
    $em = $this->getDoctrine()->getManager();

    $entity = $em->getRepository('IbwJobeetBundle:Job')->getActiveJob($id);

    if (!$entity) {
        throw $this->createNotFoundException('Unable to find Job entity.');
    }

    $session = $this->getRequest()->getSession();

    // fetch jobs already stored in the job history
    $jobs = $session->get('job_history', array());

    // store the job as an array so we can put it in the session and avoid entity serialize errors
    $job = array('id' => $entity->getId(), 'position' =>$entity->getPosition(), 'company' => $entity->getCompany(), 'companyslug' => $entity->getCompanySlug(), 'locationslug' => $entity->getLocationSlug(), 'positionslug' => $entity->getPositionSlug());

    if (!in_array($job, $jobs)) {
        // add the current job at the beginning of the array
        array_unshift($jobs, $job);

        // store the new job history back into the session
        $session->set('job_history', array_slice($jobs, 0, 3));
    }

    $deleteForm = $this->createDeleteForm($id);

    return $this->render('IbwJobeetBundle:Job:show.html.twig', array(
        'entity'      => $entity,
        'delete_form' => $deleteForm->createView(),
    ));
}

layout.html.twig では、 #content div の前に、次のコードを追加します。

src/Ibw/JobeetBundle/Resources/views/layout.html.twig

<!-- ... -->

<div id="job_history">
    Recent viewed jobs:
    <ul>
        {% for job in app.session.get('job_history') %}
            <li>
                <a href="{{ path('ibw_job_show', { 'id': job.id, 'company': job.companyslug, 'location': job.locationslug, 'position': job.positionslug }) }}">{{ job.position }} - {{ job.company }}</a>
            </li>
        {% endfor %}
    </ul>
</div>

<div id="content">

<!-- ... -->

13.5. フラッシュメッセージ

フラッシュメッセージはユーザのセッションに保存することがでる小さなメッセージで、一回だけのリクエストのためのものです。
リダイレクトして、次のリクエストで特別なメッセージを表示する、などのフォームの処理をするのに便利です。
ジョブを公開するときにすでにプロジェクトでフラッシュメッセージを使用していました。

src/Ibw/JobeetBundle/Controller/JobController.php

// ...

public function publishAction($token)
{
    // ...

    $this->get('session')->getFlashBag()->add('notice', 'Your job is now online for 30 days.');

    // ...
}
getFlashBag()->add() 関数の最初の引数は、フラッシュの識別子で、二つ目は表示するためのメッセージです。
自由にフラッシュの名前を定義できますが、 notice と error の二つがより一般的です。
フラッシュ·メッセージを表示するためそれらをテンプレートに含める必要があります。
layout.html.twig テンプレートで行いました。

src/Ibw/JobeetBundle/Resources/views/layout.html.twig

<!-- ... -->

{% for flashMessage in app.session.flashbag.get('notice') %}
    <div>
        {{ flassMessage }}
    </div>
{% endfor %}

<!-- ... -->

See also

Symfony2日本語ドキュメント

豊富な日本語ドキュメントがありますので合わせて読み進めてみましょう。

Note

Creative Commons License

このチュートリアルは、クリエイティブ・コモンズ・ライセンス 表示 - 継承 3.0 非移植 (CC BY-SA 3.0) のもとでライセンスされています。 翻訳の元にしたオリジナルはこちらです。 Symfony2 Jobeet http://www.intelligentbee.com/blog/tag/symfony2-jobeet/.