[Symfony2]ログインアクションでログインしているか判定したい

Symfony2のログイン認証は設定をすればある程度自働化できるので便利です。
マニュアル読んで色々設定したのですが、1つだけできない事がありました。

それは・・・

loginActionで既にログインしているかの判定ができない!!!!!

設定ファイルとかをしっかり理解していれば避けられる事でしたが自分も引っかかったので記事にしておきます。

よくある落とし穴を避けたら落とし穴にはまった

導かれるままに向かったら更に違う落とし穴があるとかすごい確率だと思います(笑)
よく出来ているフレームワークなのでしっかり勉強しないと使いこなせないという事ですね。

公式サンプル

まずは、config.ymlの設定を行います。

# app/config/config.yml
security:
    firewalls:
        secured_area:
            pattern:    ^/
            anonymous: ~
            form_login:
                # ログイン画面のURLパス
                login_path:  /login
                check_path:  /login_check

次に、routing.ymlでログインアクションの設定をします。

# app/config/routing.yml
login:
    pattern:   /login
    defaults:  { _controller: AcmeSecurityBundle:Security:login }
login_check:
    pattern:   /login_check

loginはアクションを通るのですが、login_checkはSymfony内で処理するのでcontrollerの設定はしません。

loginActionを作るために、_controllerで設定したファイルに追記していきます。

// src/Acme/SecurityBundle/Controller/Main;
namespace Acme\SecurityBundle\Controller;

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

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

        // ログインエラーがあれば、ここで取得
        if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
        } else {
            $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
        }

        return $this->render('AcmeSecurityBundle:Security:login.html.twig', array(
            // ユーザによって前回入力された username
            'last_username' => $session->get(SecurityContext::LAST_USERNAME),
            'error'         => $error,
        ));
    }
}

最後はログイン画面のHTMLを作ります。

{# src/Acme/SecurityBundle/Resources/views/Security/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" />

    {#
        認証成功した際のリダイレクト URL を制御したい場合(詳細は以下に説明する)
        <input type="hidden" name="_target_path" value="/account" />
    #}

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

これで基本はできました。
基本なので次にやらないと行けないことがあります。
公式マニュアルにある「よくある落とし穴を避ける」の項目です。

公式マニュアルにある内容は下記になります。

よくある落とし穴を避ける

ログインフォームを組み立てる差には、少しよくある落とし穴に注意してください。

1. 正しいルートを作成すること

まず、/login と /login_check ルートが、それぞれ対応する login_path と check_path の設定値に正しく定義されているか確認してください。ここでの設定ミスはログインページではなく、404ページへリダイレクトされることを意味します。または、ログインフォームの送信先が存在しないこととなります(同じログインフォームを何度も見ることになります)。

2. ログインページはセキュアから除外してあること

ログインページを閲覧するのに権限が/ 不要/ にしてあることを確認してください。例えば次の設定では、/login URL を含む全ての URL で ROLE_ADMIN 権限を必須にしているため、リダイレクトループに陥ります:

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

次のように /login URL へのアクセス制御を取り除くことで、この問題は解決されます:

access_control:
    - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/, roles: ROLE_ADMIN }

また、ファイアーウォールで匿名ユーザによるアクセスを/ 許可していなければ/ 、ログインページ用の特別なファイアーウォールを用意し、匿名ユーザによるアクセスを許可してください:

firewalls:
    login_firewall:
        pattern:    ^/login$
        anonymous:  ~
    secured_area:
        pattern:    ^/
        form_login: ~

3. /login_check がファイアーウォール内にあること

次に check_path の URL /login_check が、フォームログインを使用するファイアーウォール内にあることを確認してください。この例では、1つのファイアーウォールが /login_check を含む全ての URL にマッチします。もし /login_check がどのファイアーウォールにもマッチしなければ、Unable to find the controller for path “login_check” 例外に引っかかるでしょう。

4. 複数のファイアーウォールでセキュリティコンテキストを共有しないこと

複数のファイアーウォールを使用しており、そのうちの1つのファイアーウォールに対して認証をする際には、他のファイアーウォールに対して自動的には認証されません。異なるファイアーウォールは、異なるセキュリティシステムとなります。ほとんどのアプリケーションでは、1つのファイアーウォールで十分です。

以上で設定は完了なのですが…
「2. ログインページはセキュアから除外してあること」に書いてある「access_control」と「firewalls」の設定を両方してみたらわかるのですが、ログインをした後もログイン画面に行くことができてしまうのです!
ログインしている状態であればログイン画面は回避したいですよね。

loginActionでログインしているのか判定する

実際に判定するのはloginActionになりますので判定条件を入れて入れていきます。

// src/Acme/SecurityBundle/Controller/Main;
namespace Acme\SecurityBundle\Controller;

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

class SecurityController extends Controller
{
    public function loginAction()
    {
        /*
         * ここが今回追加する部分
         * ROLE_USERに設定されていればトップページにリダイレクト
         */
        if ($this->get('security.context')->isGranted('ROLE_USER')) {
            return $this->redirect('/');
        }

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

        // ログインエラーがあれば、ここで取得
        if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
        } else {
            $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
        }

        return $this->render('AcmeSecurityBundle:Security:login.html.twig', array(
            // ユーザによって前回入力された username
            'last_username' => $session->get(SecurityContext::LAST_USERNAME),
            'error'         => $error,
        ));
    }
}

んで、これでやるとちゃんと取得できない。
既にログインしているのに!!!
で、どこが問題かというと…

firewalls:
    # login_firewallが邪魔しているみたい
    login_firewall:
        pattern:    ^/login$
        anonymous:  ~
    secured_area:
        pattern:    ^/
        form_login: ~

firewallsで設定した「login_firewall」が邪魔しています。
この項目を設定すると正しく取得できなくなるみたいです。
ログインすると会員情報を「$this->getUser()」で取得する事ができますが、上記の設定をする事で取得できなくなります。

じゃあ、この行消しても大丈夫なの??と思うところですが、同じ設定が実は上でされています。

access_control:
    - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/, roles: ROLE_ADMIN }

access_controlで設定されているので、firewallsに設定する必要は特にありません。
やっている事は同じなのですが、firewallsに設定すると欲しい情報が取れなくなるという事みたいです。

アクセス制限の設定をするのであれば「access_control」で十分だと思います。
firewallsに設定する必要性は…実はよくわかっていません。(調べろよって感じですけど…)

結構海外でも同じように悩んでいる人がいるみたいで、日本語では記事を見付けることができなかったため記事にしてみました。
間違っている部分もあるので自己責任でお願いします!

スポンサーリンク
  • このエントリーをはてなブックマークに追加
スポンサーリンク

コメントをどうぞ

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください