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

前編の続きです。
前編では下記の認証方法を説明しました。

  • Basic認証
    • security.ymlで設定されたUser情報によるBasic認証
  • フォーム認証(User情報は固定)
    • security.ymlで設定されたUser情報によるフォーム認証
  • フォーム認証(Doctrineと連携)
    • データベースに設定されたUser情報によるフォーム認証

後編では下記を説明していこうと思います。

  • フォーム認証(DBで権限管理 ManyToOne)
    • データベースに設定されているUserと権限情報によるフォーム認証(Userに対する権限は1つ)
  • フォーム認証(DBで権限管理 ManyToMany)
    • データベースに設定されているUserと権限情報によるフォーム認証(Userに対する権限は複数)

では早速説明していきます。

フォーム認証(DBで権限管理 ManyToOne)

前編のほうで書いたフォーム認証(Doctrineと連携)のほうでは特に権限の説明をしませんでした。
Symfony2のSecurityコンポーネントではユーザーには必ず最低でも1つの権限を割り当てなくてはいけません。
権限はその名の通り閲覧権限(アクションを実行出来る権限)だと思って頂ければ良いと思います。

前編のフォーム認証(Doctrineと連携)では説明していませんが、下記の部分で権限の割り振りはしてあります。

  • src/App/TestBundle/Entity/User.php
<?php
    /**
     * 権限を返す(今回は権限管理はしない為、固定でROLE_ADMINを返す)
     *
     * @return array
     */
    public function getRoles()
    {
        return array('ROLE_ADMIN');
    }

上記の場合、ユーザーには固定でROLE_ADMINの権限が返されるようになっています。
この権限で操作可能なページはsecurity.ymlに設定されてあります。

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

今回の例ではaccess_controlの設定を理解する為にも、権限も分けてみます。
また権限が分かりやすいようにデバッグツールバーも表示してみます。
では、まずsecurity.ymlを下記のように変更します。

  • src/App/TestBundle/Entity/User.php
security:
    # エンコーダの設定
    encoders:
        # エンコードと対象となるユーザーモデルを指定(作成したUserモデルを指定)
        App\TestBundle\Entity\User: plaintext
    
    # 権限継承
    role_hierarchy:
        # ROLE_ADMINはROLE_USERの権限も持つ
        ROLE_ADMIN: ROLE_USER

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

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

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

追記・変更した部分は下記の部分です。

    # 権限継承
    role_hierarchy:
        # ROLE_ADMINはROLE_USERの権限も持つ
        ROLE_ADMIN: ROLE_USER

上記は権限の継承?設定です。
上記の設定の場合、ROLE_ADMINはROLE_USERの権限も引き継ぎます。

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

最後にデバッグツールバーが表示されるようにデバッグツールバーにセキュリティ設定が適用されないように設定。

        dev:
            pattern:  ^/(_(profiler|wdt))/
            security: false

access_controlの部分にも追記しました。
/testへのアクセスはROLE_USER権限でもアクセスが可能という設定です。
ROLE_ADMINはROLE_USERの権限も継承している為、ROLE_ADMINでもアクセスが可能です。

次はEntityとなるRoleクラスを作成します。
下記のコマンドを打って、Entityを作成してください。

$ php app/console doctrine:generate:entity --entity="AppTestBundle:Role" --fields="name:string(255)"

作成したRoleクラスを下記のように編集します。

  • src/App/TestBundle/Entity/Role.php
<?php
// ...

use Symfony\Component\Security\Core\Role\RoleInterface;
use \Doctrine\Common\Collections\ArrayCollection;

/**
 * App\TestBundle\Entity\Role
 *
 * @ORM\Table()
 * @ORM\Entity
 */
class Role implements RoleInterface
{
    // ...
    
    /**
     * @ORM\OneToMany(targetEntity="User", mappedBy="role")
     */
    protected $users;

    
    /**
     * construct
     */
    function __construct()
    {
        $this->users = new ArrayCollection();
    }

    // ...
    
    /**
     * Get Role
     *
     * @return string
     */
    public function getRole()
    {
        return $this->getName();
    }

    /**
     * Get users
     *
     * @return Doctrine\Common\Collections\Collection 
     */
    public function getUsers()
    {
        return $this->users;
    }
}

まずは利用するクラスとインターフェースを呼び出し、RoleInterfaceを継承します。
次に$usersプロパティとコンストラクタを定義します、ここでは$usersプロパティはUserクラスのroleプロパティとの関連付けをしています。
最後に必要なメソッドを追加します。

次にUserクラスの方も編集します。
Userクラスのほうは$roleプロパティを追加し、それに対応するメソッドを追加するだけです。

  • src/App/TestBundle/Entity/User.php
<?php
class User implements UserInterface
{
    // ...
    
    /**
     * @var integer $role
     * 
     * @ORM\ManyToOne(targetEntity="Role", inversedBy="users")
     * @ORM\JoinColumn(name="role_id", referencedColumnName="id")
     */
    protected $role;
    
    // ...
    
    /**
     * Set role
     *
     * @param integer $role
     */
    public function setRole($role)
    {
        $this->role = $role;
    }

    /**
     * Get role
     *
     * @return integer 
     */
    public function getRole()
    {
        return $this->role;
    }
    
    /**
     * @return array
     */
    public function getRoles()
    {
        return array($this->role->getName());
    }

肝となる部分は$roleプロパティのアノテーションの部分です。
アノテーションを利用して、Roleクラスと結合しています。

ここまで出来たら後は適当に/testになるコンテンツを作成してみます。
routing.ymlに下記を追記します。

  • src/App/TestBundle/Resources/config/routing.yml
test_test:
    pattern: /test
    defaults: { _controller: AppTestBundle:Default:test }

次にアクションの作成です。
DefaultController.phpに下記を追記します。

  • src/App/TestBundle/Controller/DefaultController.php
<?php
    public function testAction()
    {
        return $this->render('AppTestBundle:Default:test.html.twig');
    }

次にテンプレートを作成します。
デバッグツールバーを表示する為にbase.html.twigを継承します。

  • src/App/TestBundle/Resources/views/Default/test.html.twig
{% extends "::base.html.twig" %}

{% block body %}
ここはROLE_ADMINとROLE_TESTがアクセス可能です。<br />
<a href="{{ path('test_logout') }}">ログアウト</a>
{% endblock %}

ここまで出来たらデータベースにアクセスし、RoleテーブルにROLE_USERを登録し、UserテーブルにROLE_USER権限を持つユーザーを登録しましょう。
自分は以下のように登録してあります。

mysql> select * from Role \G
*************************** 1. row ***************************
  id: 1
name: ROLE_ADMIN
*************************** 2. row ***************************
  id: 2
name: ROLE_USER
2 rows in set (0.01 sec)


mysql> select * from User \G
*************************** 1. row ***************************
      id: 1
username: user1
password: user1
 role_id: 1
*************************** 2. row ***************************
      id: 2
username: user2
password: user2
 role_id: 2
2 rows in set (0.00 sec)

この状態でuser2でログインしてみてください。
ログインすると下記のような画面になるかと思います。

/はROLE_ADMIN権限がないと閲覧出来ないため、上記のようなエラーが発生します。
ではapp_dev.php/testにアクセスしてみてください。
下記のような画面になれば成功です。

デバッグツールバーを見ると、user2でログインしているのが分かります。
プロファイラを開いてユーザー情報を見てみるとROLE_USER権限を持っているのが分かります。

一度ログアウトし、user1でもログインして、app_dev.php/testにアクセスしてみて下さい。
user1でもアクセス出来れば問題ありません。

フォーム認証(DBで権限管理 ManyToMany)

次は1ユーザーに対して複数の権限を割り当てます。
フォーム認証(DBで権限管理 ManyToOne)をもとにいじっていくと結構簡単に出来てしまいます。
変更するのはEntityクラスのUser.phpとRole.phpのみです。
では早速いじっていきます。

  • src/App/TestBundle/Entity/User.php
<?php
// ...
use \Doctrine\Common\Collections\ArrayCollection;

class User implements UserInterface
{
    // ...

    /**
     * @var ArrayCollection $userRoles
     *
     * @ORM\ManyToMany(targetEntity="Role")
     * @ORM\JoinTable(name="UserRole",
     *     joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")},
     *     inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")}
     * )
     */
    protected $userRoles;

    /**
     * construct
     */
    public function __construct()
    {
        $this->userRoles = new ArrayCollection();
    }

    // ...

    /**
     * @return ArrayCollection|\Doctrine\Common\Collections\ArrayCollection
     */
    public function getUserRoles()
    {
        return $this->userRoles;
    }

    /**
     * @return array
     */
    public function getRoles()
    {
        return $this->getUserRoles()->toArray();
    }
}

変更点としては$roleプロパティをまず削除し、それに対応するsetRole, getRoleメソッドを削除します。
その後、$userRolesプロパティを追加し、アノテーションを設定します。(アノテーションの内容についてはもう少し調べてみる)
後は各メソッドを追加し、終了。

次にRoleクラスの方を変更します。

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

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\Role\RoleInterface;

/**
 * App\TestBundle\Entity\Role
 *
 * @ORM\Table()
 * @ORM\Entity
 */
class Role implements RoleInterface
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var string $name
     *
     * @ORM\Column(name="name", type="string", length=100)
     */
    protected $name;

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

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

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

    /**
     * Get Role
     *
     * @return string
     */
    public function getRole()
    {
        return $this->getName();
    }
}

先ほどの$usersプロパティを削除し、それに関連するメソッドを削除したくらいです。
非常にシンプルなクラスとなりました。

ここまで完了した後は下記のコマンドを実行してください。

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

上記コマンドが完了するとデータベースにUserRoleテーブルが作成されているはずです。
ユーザーに権限を複数登録してみてください。
自分は下記のように登録してみました。

mysql> select * from User\G;
*************************** 1. row ***************************
      id: 1
username: user1
password: user1
    salt: 
*************************** 2. row ***************************
      id: 2
username: user2
password: user2
    salt: 
2 rows in set (0.00 sec)


mysql> select * from Role\G;
*************************** 1. row ***************************
  id: 1
name: ROLE_ADMIN
*************************** 2. row ***************************
  id: 2
name: ROLE_USER
*************************** 3. row ***************************
  id: 3
name: ROLE_TEST
3 rows in set (0.00 sec)


mysql> select * from UserRole\G
*************************** 1. row ***************************
user_id: 1
role_id: 1
*************************** 2. row ***************************
user_id: 1
role_id: 2
*************************** 3. row ***************************
user_id: 1
role_id: 3
*************************** 4. row ***************************
user_id: 2
role_id: 2
4 rows in set (0.00 sec)

ここまで完了したら、ログインしてみてプロファイラから権限を確認してみてください。
下記のように複数権限が割り当てられていれば成功です!

以上で、認証関係のメモは終わりです。
パスワードのエンコード方式もやろうとしましたが、それは別途やりたいと思います。
メモってみて再認識したけど、Securityコンポーネントは半端ない!
ちなみに自分が取りあげた機能はSecurityコンポーネントの一部に過ぎません。
Securityコンポーネントではもっと色々出来るみたいですね。