Symfony2の認証を色々試してみる(前編)

最近はもっぱらSymfony2の勉強に明け暮れているので、成果をメモ代わりに説明していこうかと。
今回はSecurityコンポーネントについて説明していきます。

Securityコンポーネントでは認証処理を自動で行ってくれます。
各ユーザーには権限(role)が最低でも1つ必要みたいです。
とりあえず今回はBasic認証とフォームからの認証の例を下記に記述したいと思います。
Symfony2は既に設置されているものと考え、インストール方法などは省きます。

Securityコンポーネントの概要については下記を参考にどうぞ。

http://docs.symfony.gr.jp/symfony2/book/security.html


実行環境として、とりあえず下記のコマンドを実行してAppTestBundleの作成済みとして後述します。

$ php app/console generate:bundle --namespace=App/TestBundle --format=yml

Basic認証

Symfony2で認証設定を行うにはまずはsecurity.ymlを設定しなければいけません。
config.ymlにも記述出来ますが、設定内容も多いので自分は別ファイルに分けて使おうと思います。
まずはsecurity.ymlを下記のように設定します。

  • app/config/security.yml
# Securityコンポーネントの設定
security:
    # エンコーダの設定
    encoders:
        # パスワードのエンコード方式を設定
        # plaintextは生のパスワードでの認証方式、他にもsha1とかsha512とかがある
        # Symfony\Component\Security\Core\User\Userはユーザーモデルを使わない際に利用されるデフォルトのユーザーモデル?
        Symfony\Component\Security\Core\User\User: plaintext

    # ユーザー情報の設定(ユーザープロバイダ)
    providers:
        # ユーザー情報の固定設定(インメモリユーザープロバイダ)
        in_memory:
            # ユーザー情報
            users:
                # ユーザー名はadmin, パスワードはadminpass, このユーザーの権限はROLE_ADMIN
                admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }

    # ファイアーウォールの設定
    firewalls:
        # 認証エリアの設定
        # secured_areaは勝手に自分で決めたエリア
        # 認証エリアは複数設定可能です
        secured_area:
            # エリア範囲(正規表現)
            pattern: ^/
            # Basic認証を利用する設定
            http_basic:
                # Basic認証時に表示される文言
                realm: "Secured Area"

    # アクセス権限の設定
    access_control:
        # /から始まるコンテンツにアクセスするにはROLE_ADMIN権限が必要
        - { path: ^/, roles: ROLE_ADMIN }

説明はコメントアウトの通り
上記を設定した後、ブラウザで/以下にアクセスするとBasic認証が表示されます。

たかだかBasic認証をかけるのにこんなに設定が必要なのかと最初は思うかもしれません…(自分は思いました)
ですが、これはBasic認証に限らず全ての認証の設定ファイルになりますので、上記のようにページ全体にBasic認証をかけるような例だと少し面倒のように見えるかもしれないですね。
後述しますが、データベースを絡ませた認証などを利用してみるとかなりSecurityコンポーネントの凄さが分かってくるかと思います。

フォーム認証(ユーザー情報は固定)

Basic認証の設定方法を理解したら、次はフォーム認証です。
フォーム認証で利用される認証情報はsecurity.ymlに定義される固定の情報となります。
まずはsecurity.ymlを設定します。

  • app/config/security.yml
# Securityコンポーネントの設定
security:
    # エンコーダの設定
    encoders:
        # パスワードのエンコード方式を設定
        # plaintextは生のパスワードでの認証方式、他にもsha1とかsha512とかがある
        # Symfony\Component\Security\Core\User\Userはユーザーモデルを使わない際に利用されるデフォルトのユーザーモデル?
        Symfony\Component\Security\Core\User\User: plaintext

    # ユーザー情報の設定(ユーザープロバイダ)
    providers:
        # ユーザー情報の固定設定(インメモリユーザープロバイダ)
        in_memory:
            # ユーザー情報
            users:
                # ユーザー名はadmin, パスワードはadminpass, このユーザーの権限はROLE_ADMIN
                admin: { password: admin, roles: [ 'ROLE_ADMIN' ] }

    # ファイアーウォールの設定
    firewalls:
        # ログイン画面は認証エリア外
        login:
            # エリア範囲(正規表現)
            pattern: ^/login$
            # セキュリティ設定を無効
            security: false
            
        # 認証エリアの設定
        secured_area:
            # エリア範囲(正規表現)
            pattern: ^/
            # ログインフォームの設定
            form_login:
                # ログインフォームのパス
                login_path: /login
                # ログイン状態を確認するパス
                check_path: /login/check
            # ログアウトの設定
            logout:
                # ログアウト用のURLのパス
                path: /logout
                # ログアウト後に移行するページ
                target: /login

    # アクセス権限の設定
    access_control:
        # /から始まるコンテンツにアクセスするにはROLE_ADMIN権限が必要
        - { path: ^/, roles: ROLE_ADMIN }

Basic認証の設定とかわった部分はfirewallsセクション内です。
secured_areaの他にloginセクションを追加しました。
secured_areaセクションが認証エリアの設定になります。
secred_area内のpathを見ると分かるように全てのページが認証エリア範囲内と設定されていますので、ログインページ(/login)も認証エリア内に入ってしまいます。
これではログインページにアクセス出来ない為、別途loginセクションを定義し、ログインページは非認証エリアとして設定してあります。
認証エリア(loginやsecured_area)セクション内でsecurity: falseを指定するとそのpathに適用される範囲はセキュリティ設定が無効となります。
この際の注意点としてはsecured_areaセクションの前にloginセクションを定義しなければいけないという点です。
loginセクションをsecured_areaセクションの下に持っていった場合、ログインページもsecured_areaの設定が適用されてしまう為、これもまたログインページにアクセス出来なくなってしまいます。


ではsecurity.ymlを設定したら次はAppTestBundle内のrouting.ymlを設定します。

  • src/App/TestBundle/Resources/config/routing.yml
# index
test_index:
    pattern:  /
    defaults: { _controller: AppTestBundle:Default:index }

# ログイン
test_login:
    pattern: /login
    defaults: { _controller: AppTestBundle:Default:login }

# ログイン確認
login_check:
    pattern: /login/check

# ログアウト
logout:
    pattern: /logout

security.yml内で定義した、login_path(/login/check)とlogoutセクション内のpath(/logout)はパスを設定するだけでいいです。コントローラは特に指定しなくて問題ありません、Securityコンポーネントが自動で処理してくれます。

では次にログイン処理の部分をコントローラに定義します。
下記のDefaultControllerにloginActionを追加して、下記のように記述してください。

  • src/App/TestBundle/Controller/DefaultController.php
<?php
namespace App\TestBundle\Controller;

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

class DefaultController extends Controller
{
    public function indexAction()
    {
        return $this->render('AppTestBundle:Default:index.html.twig');
    }
    
    public function loginAction()
    {
        $request = $this->getRequest();
        $session = $request->getSession();
        
        $error = null;
        
        // ログインエラーがあれば、ここで取得
        if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
        // Sessionにエラー情報があるか確認
        } elseif ($session->has(SecurityContext::AUTHENTICATION_ERROR)) {
            // Sessionからエラー情報を取得
            $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
            // 一度表示したらSessionからは削除する
            $session->remove(SecurityContext::AUTHENTICATION_ERROR);
        }
        
        return $this->render('AppTestBundle:Default:login.html.twig', array(
            // ユーザによって前回入力された username
            'last_username' => $session->get(SecurityContext::LAST_USERNAME),
            'error' => $error,
        ));
    }
}

コントローラを上記のように設定した後は次はテンプレートの定義です。
ログインページとなる部分を作成します。
下記のように記述してください。

  • src/App/TestBundle/Resources/views/Default/login.html.twig
{% if error %}
 <div>{{ error.message }}</div>
{% endif %}

<form action="{{ path('test_login_check') }}" method="post">
 <label for="username">ログインID:</label>
 <input type="text" id="username" name="_username" value="{{ last_username }}" />

 <label for="password">パスワード:</label>
 <input type="password" id="password" name="_password" />

  {#
  認証成功した際のリダイレクト URL を制御したい場合は_target_pathにパスを指定する
  <input type="hidden" name="_target_path" value="/" />
  #}

 <input type="submit" name="login" />
</form>

これで準備は完了です、ブラウザでアクセスするとログインページ(/login)に飛ばされると思います。
下記のような画面になれば成功です、ログイン出来るか試してみてください。

これでフォームによるログイン認証が可能になりました。
ですが、今のままだとログイン情報が固定になってしまいます。
ユーザー情報は基データベースで管理したいことが多いですよね、なので次はユーザー情報をデータベースから取ってきて認証してみましょう。

フォーム認証(Doctrineと連携)

ここでは認証に用いるユーザー情報はDoctrineを通して取得し、利用する方法を記述したいと思います。
データベースのセットアップ方法などは省きます。

では最初にユーザー情報となるEntityクラスを作成します、下記のコマンドからEntityクラスを作成してください。

$ php app/console doctrine:generate:entity --entity="AppTestBundle:User" --fields="username:string(255) password:string(255)"

Entityクラスを作成した後はSecurityコンポーネントでこのEntityクラスが使えるようにしなくてはいけません。
Entityクラスが下記ようになるように追記してください。

  • src/App/TestBundle/Entity/User.php
<?php
namespace App\TestBundle\Entity;

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

/**
 * App\TestBundle\Entity\User
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="App\TestBundle\Repository\UserRepository")
 */
class User implements UserInterface
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string $username
     *
     * @ORM\Column(name="username", type="string", length=255)
     */
    private $username;

    /**
     * @var string $password
     *
     * @ORM\Column(name="password", type="string", length=255)
     */
    private $password;


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

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

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

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

    /**
     * Get password
     *
     * @return string 
     */
    public function getPassword()
    {
        return $this->password;
    }
    
    /**
     * 権限を返す(今回は権限管理はしない為、固定でROLE_ADMINを返す)
     *
     * @return array
     */
    public function getRoles()
    {
        return array('ROLE_ADMIN');
    }
    
    /**
     * Saltを返すメソッド、パスワードのエンコード方式がplaintextの場合
     * Saltを定義しても意味がないので、これも空文字が返るようにする
     *
     * @return string
     */
    public function getSalt()
    {
        return '';
    }
    
    /**
     * 取得されたくないようなユーザーデータとかを削除するメソッドらしい
     */
    public function eraseCredentials()
    {
        
    }
    
    /**
     * 同一ユーザーであるかの判定
     *
     * @return boolean
     */
    public function equals(UserInterface $user)
    {
        return $this->getUsername() == $user->getUsername();
    }
}

use Symfony\Component\Security\Core\User\UserInterface;を追記した後、
UserIntefaceをimplementsし、UserInterfaceに沿って下記のメソッドを実装する

  • function getRoles()
  • function getSalt()
  • function eraseCredentials()
  • function equals()

各メソッドの説明についてはコメントアウトに記述されている通りです。
また、下記の行でリポジトリクラスの拡張も宣言しています。
@ORM\Entity(repositoryClass="App\TestBundle\Repository\UserRepository")

ここまで実装したら、次はリポジトリクラスの実装です。
下記のコマンドを実行すると自動でリポジトリクラスを作成してくれます。

$ php app/console doctrine:generate:entities App/TestBundle

作成されたリポジトリクラスを下記のように編集します。

  • src/App/TestBundle/Repository/UserRepository.php
<?php
namespace App\TestBundle\Repository;

use Doctrine\ORM\EntityRepository;
use \Symfony\Component\Security\Core\User\UserProviderInterface;
use \Symfony\Component\Security\Core\User\UserInterface;

/**
 * UserRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class UserRepository extends EntityRepository implements UserProviderInterface
{
    /**
     * ユーザー名からユーザー情報を取得する
     *
     * @param string $username
     * @return object
     */
    public function loadUserByUsername($username)
    {
        return $this->findOneBy(array('username' => $username));
    }

    /**
     * ユーザーのリロード
     *
     * @param \Symfony\Component\Security\Core\User\UserInterface $user
     * @return object
     */
    public function refreshUser(UserInterface $user)
    {
        return $this->loadUserByUsername($user->getUsername());
    }

    /**
     * TODO:要確認
     *
     * @param $class
     * @return bool
     */
    public function supportsClass($class)
    {
        return true;
    }
}

まず、Userリポジトリクラスでは下記のインターフェースを利用します。

  • use \Symfony\Component\Security\Core\User\UserProviderInterface;
  • use \Symfony\Component\Security\Core\User\UserInterface;

上記のインターフェースを呼び出した後、UserRepositoryクラスは呼び出した、UserProviderInterfaceを継承します。

  • class UserRepository extends EntityRepository implements UserProviderInterface

その後、UserProviderInterfaceに沿い下記のメソッドを実装します。

  • function loadUserByUsername
  • function refreshUser
  • function supportsClass

各メソッドについてはコメントアウトの説明通りかと思います。
supportsClassメソッドについては自分もよく分かってません…


ここまで実装したら次はsecurity.ymlの設定です。
作成したUserモデルを認証に利用するように変更します。

  • app/config/security.yml
security:
    # エンコーダの設定
    encoders:
        # エンコードと対象となるユーザーモデルを指定(作成したUserモデルを指定)
        App\TestBundle\Entity\User: plaintext

    # ユーザー情報の設定(ユーザープロバイダ)
    providers:
        # 作成したUserモデルをユーザー情報とする
        my_users:
            # Entityクラスの指定とユーザー名となるプロパティを指定
            entity: { class: App\TestBundle\Entity\User, property: username }

    # ファイアーウォールの設定
    firewalls:
        # ログイン画面は認証エリア外
        login:
            # エリア範囲(正規表現)
            pattern: ^/login$
            # セキュリティ設定を無効
            security: false
            
        # 認証エリアの設定
        secured_area:
            # エリア範囲(正規表現)
            pattern: ^/
            # ログインフォームの設定
            form_login:
                # ログインフォームのパス
                login_path: /login
                # ログイン状態を確認するパス
                check_path: /login/check
            # ログアウトの設定
            logout:
                # ログアウト用のURLのパス
                path: /logout
                # ログアウト後に移行するページ
                target: /login

    # アクセス権限の設定
    access_control:
        # /から始まるコンテンツにアクセスするにはROLE_ADMIN権限が必要
        - { path: ^/, roles: ROLE_ADMIN }

先ほどのフォーム認証(ユーザー情報は固定)で利用したsecurity.ymlとの違いは、encodersセクションで指定するモデルクラスの変更とproviders内の変更です。
encodersセクション内は作成したUserクラスを指定します。
providersセクション内はin_memoryセクションを削除し、代わりにmy_usersセクションを追加してあります。
my_usersセクション内には作成したUserクラスとユーザー名となるプロパティ名を指定します。

ここまで完了したら最後にデータベースにテーブルを作成し、最初のユーザーを作成します。
テーブルの作成は下記のコマンドから作成可能です。

$ php app/console doctrine:schema:update --force

テーブルの作成が完了したら、データベースに直接ユーザーデータを登録し、そのユーザーでログインが出来るかどうか確認してください。

一気に全部書こうとしたけどさすがに疲れたので後編に持ち越します。
後編では権限もDBで管理するのとパスワードのエンコードあたりを説明していこうかと。

自分もSymfony2は初心者なので、変なところがあれば突っ込んでいただけると有り難いです!