Caution: You are browsing the legacy symfony 1.x part of this website.

Cookieで永続的セッションを実現するには?

1.2
Symfony version Language

概要

symfonyはsfWebRequestsfWebResponseオブジェクト経由でCookieへのアクセス方法を提供します。 これによってCookieの利用がとても簡単になり、永続的セッションが簡単に実現されます。

ゲッターとセッター

Cookieはクライアントのコンピューターに保存される文字列です。 Webアプリケーションによって書き込みされ、同じアプリケーションもしくは同じドメインでのみ読み込みが可能です。

symfonyにおいて、Cookieのためのセッターとゲッターは異なるオブジェクトのメソッドですが、合理的です。 Cookieを得るには、sfWebRequestオブジェクトを使用して、サーバーに送信されたリクエストを調べます。 他方で、Cookieを設定するために、sfWebResponseオブジェクトを使用して、ユーザーに送信されるリスポンスを修正します。 アクションの範囲内からCookieを操作するには、次のショートカットを使います:

// Cookieのゲッター
$string = $this->getRequest()->getCookie('mycookie');
 
// Cookieのセッター
$this->getResponse()->setCookie('mycookie', $value);
 
// オプションつきのゲッター
$this->getResponse()->setCookie('mycookie', $value, $expire, $path, $domain, $secure);

setCookie()メソッドの構文はPHPのsetcookie()関数のものと同じです(詳しい情報についてはPHPの公式マニュアルを参照してください)。 sfWebResponse メソッドを使う主な利点はsymfonyがCookieのログを記録し、リスポンスが実際に送信されるまで読み込みと修正を続けられることです。

note

アクションの外側からCookieを操作したい場合、ショートカット無しでRequestAnswerオブジェクトにアクセスする必要があります:

$request  = sfContext::getInstance()->getRequest();
$response = sfContext::getInstance()->getResponse();

Cookieの使用例: 永続的セッション

(symfonyにおいて完全な透過的である基本的なセッションハンドリングは別として)Cookieの良い用例は永続的セッション機能です。 たいていのログインフォームは"remember me"チェックボックスを提供します。 それがクリックされると、ユーザーは将来のセッション用にログイン処理を回避することを許可されます。

基本的なログイン機能

securityモジュールを除いてすべてのモジュールが安全であるアプリケーションを想像してみましょう。 security/indexアクションへの認証されていないユーザーのリクエストを扱うためにsettings.ymlを設定します:

all:
  .settings:
    login_module:           security
    login_action:           index

モデルは最低限でもloginpasswordフィールドを持つUserクラスを持ちます。 indexSuccess.phpテンプレートはログインフォームを示し(今のところ"remember me"チェックボックスはなし)、そしてsecurity/loginアクションへの投稿を扱います:

public function executeIndex()
{
}
 
public function executeLogin()
{
  // ユーザーが存在するかチェックする
  $c = new Criteria();
  $c->add(UserPeer::LOGIN, $this->getRequestParameter('login'));
  $user = UserPeer::doSelectOne($c);
  if ($user)
  {
    // パスワードが正しいかチェックする
    if ($this->getRequestParameter('password') == $user->getPassword())
    {
      // サインイン
      $this->getContext()->getUser()->signIn();
      // ホームページに進む
      return $this->redirect('main/index');
    }
    else
    {
      $this->getRequest()->setError('password', 'wrong password');
    }
  }
  else
  {
    $this->getRequest()->setError('email', 'this user does not exist');
  }
 
  // エラーが見つかった場合
  return $this->forward('security', 'index');
}

note

askeetチュートリアルで説明されているように、よりよいドメインモデルのロジックのためにログインとパスワードのバリデーションをカスタムバリデータで扱うことできます.

では、myUserクラスのsignIn()メソッドを見てみましょう:

class myUser extends sfBasicSecurityUser
{
  public function signIn()
  {
    $this->setAuthenticated(true);
  }
 
  public function signOut()
  {
    $this->setAuthenticated(false);
  }
}

今のところ、これはとても基本的な機能です。 セッションごとにユーザーにログインをするかたずねる限り、適切に動作します。

永続的セッション

永続的セッションを可能にするために、サーバーはユーザーが誰であり以前のログインを成功したことがあるかという情報を記録しているクライアントのコンピューター(Cookieがやって来た場所)の情報を保存しなければなりません。 もちろん、セキュリティ上の理由のために、パスワードをCookieに保存することはできません(ところでこれはaskeetチュートリアルで説明したsha1ハッシュパスワード保存メソッドと互換性がありません)。 ではCookieに何を保存すればよいのでしょうか? 2つの要素の比較が認証を実現できるように、Cookieが保存するものは何であれ、データベースに保存しているデータと一致しなければなりません。 ですので、リスクを最小限にするために、ランダムな文字列が保存され、15日ごとに再生成されます(有効期間の情報はCookieに渡されます)。

新しいUserテーブルにremember_keyカラムを追加します(そしてモデルをリビルドします)。 この新しいフィールドがランダムキーを保存し、キーはユーザーのレコードの一部としてデータベースとクライアントのコンピュータ上の両方のCookieに保存されます。 ユーザーが記録をリクエストをしたときにrememberキーは設定されます。 loginアクションのサインインの行を次のように変更します:

// サインイン
$remember = $this->getRequestParameter('remember_me');
$this->getContext()->getUser()->signIn($user, $remember);

これを動作させるために、modules/security/templates/indexSuccess.phpフォームにremember_meチェックボックスを追加することを忘れないでください。

データベースとCookieの両方でrememberキーを設定するためにmyUserクラスのsignIn()メソッドを修正しなければなりません:

public function signIn($user, $remember = false)
{
  $this->setAuthenticated(true);
 
  if ($remember)
  {
    // ランダムキーを決定する
    if (!$user->getRememberKey())
    {
      $rememberKey = myTools::generate_random_key();
 
      // キーをUserテーブルに保存する
      $user->setRememberKey($rememberKey);
      $user->save();
    }
 
    // キーをCookieに保存する
    $value = base64_encode(serialize(array($user->getRememberKey(), $user->getLogin())));
    sfContext::getInstance()->getResponse()->setCookie('MyWebSite', $value, time()+60*60*24*15, '/');
  }
}

generate_random_key()メソッドはセキュリティの要求の基準を満たすものであれば何でも構いません。 では、security/indexアクションを少し変更する必要があります:

public function executeIndex()
{
  if ($this->getRequest()->getCookie('MyWebSite'))
  {
    $value = unserialize(base64_decode($this->getRequest()->getCookie('MyWebSite')));
    $c = new Criteria();
    $c->add(UserPeer::REMEMBER_KEY, $value[0]);
    $c->add(UserPeer::LOGIN, $value[1]);
    $user = UserPeer::doSelectOne($c);
    if ($user)
    {
      // サインイン
      $this->getContext()->getUser()->signIn($user);
      // ホームページに進む
      return $this->redirect('main/index');
    }
  }
}

この新しいプロセスでCookieが読み込まれます。作業はこれでお終いです。

note

Webサイトのいくつかのページが認証無しでアクセスできる場合、もはやsecurity/indexアクションは毎回実行される最初のアクションではありません。 このような場合においてユーザーのログを自動的に記録する場合、単独のアクションでCookieのチェックをするよりもアプリケーションのlibディレクトリで新しいrememberFilterを追加するほうが望ましいです:

class rememberFilter extends sfFilter
{
  public function execute ($filterChain)
  {
    // 1回だけこのフィルターを実行する
    if ($this->isFirstCall())
    {
      if ($cookie = $this->getContext()->getRequest()->getCookie('MyWebSite'))
      {
        $value = unserialize(base64_decode($cookie));
        $c = new Criteria();
        $c->add(UserPeer::REMEMBER_KEY, $value[0]);
        $c->add(UserPeer::LOGIN, $value[1]);
        $user = UserPeer::doSelectOne($c);
        if ($user)
        {
          // サインイン
          $this->getContext()->getUser()->signIn($user);
        }
      }
    }
    // 次のフィルタを実行する
    $filterChain->execute();
  }
}

もちろん、filters.yml設定ファイルでこのフィルタを宣言する必要があります:

 rememberFilter:
   class: rememberFilter

最後の1つです: ユーザーがログアウトした場合、Cookieを削除することをお忘れなく!

public function signOut()
{
  $this->setAuthenticated(false);
  sfContext::getInstance()->getResponse()->setCookie('MyWebSite', '', time() - 3600, '/');
}

note

この解決方法は安全ではないページでしか動作しません。 カスタムのrememberフィルタの前に安全なページのためのセキュリティチェックが行われるからです。 結果的に、あらかじめセキュアではないページのサイトにログインすることなく適切なCookieを持つユーザーがセキュアなページにアクセスしようとすると、誰もがログインページにリダイレクトされます。 安全なページでもremember me機能が欲しい場合、微妙に異なる実装をする必要があります。 sfBasicSecurityFilterクラスを特化したmyBasicSecurityFilterクラスを作成し、それにCookieのコントロール機能を設置しなければなりません。 それからfactories.ymlにおいて、security_filterの名前をmyBasicSecurityFilterに変更しなければなりません。 実装の詳細はあなたの判断にお任せします。

永続的セッション: 車輪を再発明しない

上記で説明されたコードを新しいプロジェクトのために書き直すのはとても苦痛です。 幸いにして、この目的のためにsfGuardPluginを利用できます。 ユーザー管理、データベースのパーミッションとクレデンシャルを自動化するだけでなく、ここで説明したことと似たようなテクニックを使う"remember me"機能も含みます。 ですので、永続的セッションを有効にするよい方法はsfGuard プラグインをインストールすることです。