Symfony2のFirewallの設定に関するメモ
Symfony2のSecurityコンポーネントではFirewallという仕組みを用いて認証/認可を行います。Symfony2ではおおまかにいうと次のようなフローで処理が進みます。
- Requestオブジェクトの初期化
- DIコンテナの起動
- ルーティングとセッションの初期化
- ルーティング情報を元にコントローラーの作成
- コントローラー(アクション)の実行
- Responseオブジェクトの送信
Firewallは上記の3と4の間に入ります。コントローラーの作成に入る前に、Requestオブジェクトの状態を見てアクセスを制御するのがFirewallの役目になります。
security.yml
Firewallはapp/config/security.ymlに次のようにして記述します。
security: providers: users: entity: class: CommmerceBundle:User firewalls: login: pattern: ^(/admin)?/login$ security: false admin_area: pattern: ^/admin form_login: check_path: /admin/login_check login_path: /admin/login logout: path: /admin/logout target: /admin/login customer_area: form_login: check_path: /login_check login_path: /login logout: true anonymous: true access_control: - { path: ^(/admin)?/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } - { path: ^/mypage, roles: ROLE_CUSTOMER, requires_channel: https } - { path: ^/admin, roles: ROLE_ADMIN, requires_channel: https }
上記はE-Commerceサイトを想定したsecurity.ymlです。このうちfirewallとaccess_controlの2つがFirewallに該当する記述です。
セキュリティエリア
firewallsは主に認証の設定を行います。admin_areaやcustomer_areaはそれぞれセキュリティエリアの指定になります。認証はセキュリティエリア単位で行われ、正規表現を用いてURLのマッチングを行い、セキュリティエリアの特定を行います。上記の例では、/adminで始まるURLはadmin_area、それ以外はcustomer_areaとなります。ただし、/loginと/admin/loginは認証していなくてもアクセスしてよいので、別のエリアにしています。
エリアの設定
各エリアには次のような設定を記述します。
- pattern
- 指定した正規表現とURLがマッチしたエリアの設定が使用されます
- security
- falseを指定するとセキュリティの設定は無効になります。
- form_login
- 指定したログインフォーム画面を通じてエリアにログインできるようになります
- logout
- ログアウトのURLなどの指定を行います。
- anonymous
- trueを指定すると、認証されていない状態でエリア内にアクセスできるようになります。
この部分はSecurityBundleのコンフィギュレーションになりますが、SecurityFactoryという機構を用いて拡張することが可能です。
symfony 1のときはPHPのセッション単位でしか認証状態の管理ができなかったのですが、Symfony2からは柔軟な設定が可能になったわけです。まあそういうと聞こえはいいですけど、少し理解しづらいかも知れませんね。
form_login
form_loginを指定した場合ですが、自分ではフォーム画面を作成するだけで、認証部分のロジックを書く必要はありません。login_pathで指定したURLにログインフォーム画面のアクションを配置します。フォームのactionをcheck_pathで指定したパスにすると、後はうまいことやってくれます。
check_pathのものはルーティングを定義する必要こそありますが、コントローラーと対応させる必要はありません。routing.ymlに次の指定をするだけで構いません。これは、処理の順番としてルーティングが先に走りますが、コントローラーが呼び出される前にFirewallがinterceptするためです。これはlogoutの指定でも同様です。
_check_path: pattern: /login_check
認証されていない状態でどこかのセキュリティエリアにアクセスした場合、自動的にログイン画面に飛ばされますが、ログイン後は元々アクセスしようとしていた画面へ飛ぶようになっています。この挙動は設定で変更可能です。
anonymous
anonymousは匿名という意味ですが、Firewallを理解する上では非常に重要な設定となります。最初の例でECサイトを挙げましたが、ログインしなくても商品ページは閲覧できたりしますよね。セキュリティエリアを分けることで認証しないということも可能ですが、エリアを分けてしまうと認証状態も別になってしまい、ログインしていた場合でもユーザー情報を取得できなくなってしまいます。ログイン画面などで、ユーザー情報に一切アクセスしないとかであればよいのですが、ユーザー情報を取得したい場合の方が多いのではないかと思います。
そこで登場するのがanonymousです。anonymousをtrueにすると、ログインしていなくてもセキュリティエリア内へのアクセスが可能になります。anonymousをtrueにした場合は逆に、認証されていないと閲覧できない画面の制御を行う必要が出てきます。
access_control
access_controlは認可の設定です。権限(roles)を用いたアクセスの制御の指定を行います。ユーザーは最低限1つの権限を指定するようになっています。認証されている場合、恐らくROLE_USERのような権限を持つことになるでしょう(GitHub - FriendsOfSymfony/FOSUserBundle: Provides user management for your Symfony project. Compatible with Doctrine ORM & ODM, and custom storages.)。
認証している/いないという判定をするのではなく、なんの権限を持っているかという判定になります。変な言い方かも知れませんが、anonymousがtrueの場合は匿名ユーザーとして認証をパスすることができると考えてください。Firewallをくぐり抜けてきた段階で、認証のフェーズはパスしているのです。もっとも、内部的にはaccess_controlがFirewallの最期の壁なのでまだFirewallを抜けたわけではありませんが……。
権限を複数していすることももちろん可能なのですが、access_controlに権限を複数記述するのではなく、role_hierarchyという指定を行ってあげる必要があります。ここはsymfony 1のころのcredentialsとは少し違うので注意が必要です。
なお、匿名ユーザーでもよいという認可の指定にはIS_AUTHENTICATED_ANONYMOUSLYという権限を指定します。これは匿名ユーザー限定ではなく、匿名でもよいという指定なので注意してください。←この部分は間違っているかも。検証します・・・
Firewallの取り扱いは、anonymousとaccess_controlの使い方を把握しておく必要があります。それがわかってしまえば難しいことは(その部分の設定においては)ないです。
Securityコンポーネントは難しいですが、symfony 1のころの、security.ymlの状態に合わせてフィルターチェインの内容を動的に変化させて認証フローを挟むやり方も大概ですよね。
セキュリティの部分をデバッグするときは、Webプロファイラーで状態やイベントを確認したり、ログにデバッグ情報を細かく吐き出しているのでその当たりを参考にするとよいでしょう。ACL以外の構造はある程度把握できてきたので、質問があれば直接聞いていただいても結構ですよ。
2011.10.11 修正
s/IS_AUTHENTICATED_ANONYMOUS/IS_AUTHENTICATED_ANONYMOUSLY
Symfony2 note at Apr 20, 2011
前回のDIコンテナの記事から4ヶ月ほど経ち、だいたい今まで書いた記事も役に立たなくなった感じですね。とりあえず最近Symfony2でコード書いてて思ったことをメモしておきます。
Form
1系の鬼門だったFormですが、ずいぶんよくなりました。今度Symfony2勉強会で発表予定。
Security
おそらく2系の鬼門。とりあえずFirewallの流れ。
- URLをベースにFirewallの特定(Firewall, FirewallMap)
- AuthenticationManager (AuthenticationProviderManager) がトークンを認証
- トークンが取得できたらAbstractAuthenticationListenerがSecurityContextにトークンを格納
- anonymousがtrueならanonymousでもOK
- ROLEが1つでも設定されていないとAuthenticatedと見なされない(UsernamePassword)
- EntityUserProviderに指定するEntityManagerはconfig.ymlからは変更不可
- 詳しくは調べていないが、Extensionにて拡張する
- デフォルトだとEntityManagerを再定義しているので、別インスタンスになるのでは疑惑
- FOSUserBundleのInteractiveLoginListenerあたりでおかしくならない?
これでSecurityコンポーネントのAuthenticationの部分だけ。まだAuthorization部分が・・・。
あとSecurityBundleがしんどい。特にDICのSecurityFactory。
Doctrine
- INの対応が残念
- 2.1でsmartなのができるらしい
- order by rand()はできない
- order byに関数が書けない
- selectに関数を書いて、order byで指定ならできる
- rand関数は自前で定義
- Select句にEntityのプロパティ以外を入れると、array(0 => $entity, 1 => $AVG) みたいな配列が返ってくる
- DBから一度取得をしてないと、Entityをpersistしてもinsertにしかならない
- UnitOfWorkをいじればできるみたい
- generate:entitiesにバグ
- 既にクラスファイルがある場合、namespace定義の下にuseが書かれていると既存のクラス内の定義が認識されなくなる
- 自前でクラスファイルをパースしているがかなりの手抜き実装
- パッチかこう・・・
DependencyInjection
DIコンテナの起動とエクステンション
この記事は、Symfony アドベントカレンダー 2010 に参加しています。
- Symfony Advent 2010 : ATND
- http://www.symfony.gr.jp/adventcalendar/2010
- 前の記事: SymfonyEventDispatcher→Symfony2(PR4)EventDispatcherの変更点 - * yuchimiriのにっき *
DIコンテナの起動処理を簡単にまとめます。DIコンテナがどういったものかわかっており、Symfony2のバンドルやカーネルといった名称と役割が何となくわかっている方が対象です。
そもそもDIコンテナの起動とは、DIコンテナにパラメーター定義とサービス定義が行われる処理をさします。最初におおまかな流れを見てみましょう。
- KernelがDIコンテナオブジェクトを生成
- すべてのバンドルから エクステンション を抜き出してDIコンテナに登録
- app/config/config.ymlを読み込み、内容に応じてエクステンションをロード
DIコンテナのにパラメーターやサービスを定義する、つまりDIコンテナを拡張する処理は、DependencyInjectionコンポーネントに含まれるエクステンションクラスを用いて行います。
それではDIコンテナがエクステンションによって拡張される流れを細かく見ていきましょう。
KernelがDIコンテナオブジェクトを生成
Symfony\Component\HttpKernel\Kernelクラスが最初にリクエストを処理する際、Kernelクラスのboot()メソッドが読み込まれます。boot()メソッドはKernelに登録されたバンドルを一斉に読み込み、それらの中からエクステンションを抜き出して、DIコンテナを起動する処理を行います。バンドルの読み込みとDIコンテナの起動がboot()メソッドの役割です。
<?php class Kernel { public function boot() { // ... $this->bundles = $this->registerBundles(); $this->bundleDirs = $this->registerBundleDirs(); $this->container = $this->initializeContainer(); // ... foreach ($this->bundles as $bundle) { $bundle->setContainer($this->container); $bundle->boot(); } $this->booted = true; } }
registerBundles()はバンドルオブジェクトの配列を返すメソッドで、Kernelクラスには抽象メソッドとして定義されています。実際のアプリケーションのKernelで必要なバンドルオブジェクトを返すように設定し、それが$this->bundlesプロパティにそのまま格納されます。registerBundleDirs()も大した処理はしてないのでいいでしょう。
そのすぐ後に呼び出されるのがinitializeContainer()です。initializeContainer()は見てわかる通りDIコンテナの初期化メソッドです。DIコンテナオブジェクトの起動が完了したら、起動後の状態と同じ状態のパラメーターやサービス情報が静的に定義されたクラスファイルをキャッシュとして生成します。initializeContainer()メソッドではそのキャッシュの制御を行い、キャッシュが存在しない場合など、バンドルからコンテナの情報を読み込む必要がある場合は buildContainer() メソッドを呼び出します。
<?php class Kernel { protected function buildContainer($class, $file) { $parameterBag = new ParameterBag($this->getKernelParameters()); $container = new ContainerBuilder($parameterBag); foreach ($this->bundles as $bundle) { $bundle->registerExtensions($container); if ($this->debug) { $container->addObjectResource($bundle); } } if (null !== $cont = $this->registerContainerConfiguration($this->getContainerLoader($container))) { $container->merge($cont); } $container->freeze(); // ... } }
buildContainer()メソッドではContainerBuilderオブジェクトを新規に作成します。これは空っぽのDIコンテナオブジェクトです。ここにサービスなどを登録していきます。
すべてのバンドルからエクステンションを抜き出してDIコンテナに登録
上記のbuildContainer()メソッドを見ると、$this->bundlesをループして、すべてのバンドルのregisterExtensions()メソッドにコンテナオブジェクトを渡しています。このメソッドは、各バンドルディレクトリの中 DependencyInjection ディレクトリの中から Extension.php で終わるファイルを検索し、それらを読み込みます。ここで探して読み込むのがエクステンションです。
エクステンションは Symfony\Component\DependencyInjection\Extension\Extension クラスを継承したクラスです。作成する場合は前述の読み込み規則に従い、各バンドルのDependencyInjectionディレクトリ内にExtension.phpで終わるファイルを作成し、その中にクラスを定義します。
読み込む際には実際にエクステンションのインスタンスを作成します。それらのインスタンス1つ1つをコンテナオブジェクトに登録します。コンテナへの登録は、ContainerBuilder::registerExtension()メソッドを通じて行います。これはBundle::registerExtensions()メソッドの内部で呼び出されます。
コンテナに登録した段階では、パラメーターやサービスの定義はコンテナーには追加されません。この段階では単にコンテナーオブジェクトの内部にエクステンションオブジェクトが保持されるだけです。
<?php class ContainerBuilder { static public function registerExtension(ExtensionInterface $extension) { static::$extensions[$extension->getAlias()] = static::$extensions[$extension->getNamespace()] = $extension; } }
このとき覚えておいて欲しいのが、$extensionsプロパティに格納される際に、Extension::getAlias()メソッドの戻り値をキーとして使用しています。これは後ほど非常に重要な役割を持ちます。FrameworkBundleのFrameworkExtensionでは 'app' という文字列をエイリアスとして返すようになっています。
app/config/config.ymlを読み込み、内容に応じてエクステンションをロード
すべてのエクステンションがContainerに登録されたら、次は各エクステンションをロードする処理です。各エクステンションにはそれぞれ独自にDIコンテナを拡張するためのメソッドを持っています。たとえばFrameworkBundleのFrameworkExtensionではconfigLoad()メソッドがそれです。
実際にロードを行う処理は、config.ymlが密接に関係しています。たとえばconfig.ymlには次のような記述があります。
app.config: charset: UTF-8 error_handler: null csrf_secret: xxxxxxxxxx router: { resource: "%kernel.root_dir%/config/routing.yml" } validation: { enabled: true, annotations: true } templating: escaping: htmlspecialchars
ここには「app.config」という名前でさまざまな設定が定義されています。実は、この「app.config」という一文によってエクステンションがロードされます。
エクステンションの登録が完了された後、このconfig.ymlが読み込まれます。読み込みはKernel:: registerContainerConfiguration()メソッドで行われます。その際、(app.configなどの)トップレベルの項目が、それぞれエクステンションをロードするためのキーになります。
たとえばapp.configの場合、エクステンションの中から app という名前で登録されているものを探し、そのエクステンションの configLoad() メソッドを呼び出します。トップレベルの項目すべてにそれを行います。
エクステンションの名前は、各エクステンションクラスのgetAlias()メソッドの戻り値です。呼び出されるメソッドは、ドットの後に指定した文字列にLoadをつけたメソッドです。いくつかの例を記述します。
- foo.bar: fooエクステンションの barLoad()
- app.test: appエクステンションの testLoad()
あとはそれぞれのエクステンションのメソッドに、コンテナオブジェクトと、config.yml側に記述してある設定項目の連想配列が渡されます。
バンドルではResources/configディレクトリにDIコンテナを拡張するための定義ファイルを配置する習慣があります。それは読み込むだけでDIコンテナのサービスやパラメータ定義を拡張します。
config.ymlに記述してある内容はそれとは違います。config.ymlはユーザーが必要な分だけDIコンテナへの拡張を設定するためのファイルです。ここに記述した設定を受け取り、エクステンションが内部でうまいことコンテナオブジェクトを修正してくれます。
単にエクステンションを読み込みたいだけなら、次のようにチルダーを指定します。
markdown.parser: ~
チルダーはnullと同じです。これでも問題なく動作します。これがないと、上記の場合はmarkdownエクステンションがそもそも読み込まれません。エクステンションをとりあえず有効にするためにはこういった記述が必要になります。
さて、ここまでが起動の流れになります。自前でバンドルを作った際にエクステンションも同時に作る場合、必ずconfig.yml側で読み込んであげる必要があります。
とりあえず習慣的には、 'ユニークなエイリアス.config' というのが基本のようです。とりあえずここまでの流れが追えるとだいぶSymfony2がわかった気になりますよ!
次はid:uechocoさん、よろしくお願いします!
第1回Symfony2勉強会に参加しました
http://symfony.gr.jp/blog/20111121-symfony2-workshop-1
第1回Symfony2勉強会に参加しました。Symfony2はまだまだ開発段階ですが、多くの方が来場され、Symfony2に対する期待感が伺えました。
今回はほとんどの方がワークショップに参加されたためLT発表者がいないのではと思い、Symfonyの開発に貢献するための方法を簡単に作ってLTで発表を行いました。
プロデュースしていただいたid:innx_hidenoriさん、会場を提供してくださったZynga Japanさま、ありがとうございました!
p.s.
Symfony Midnightでid:do_akiさんやid:innx_hidenoriさんとDIコンテナー周りの話をしていて、あの辺は仕様が大きくぶれることもないだろうし、そろそろ情報を出しておいてもよさそうとのことで、徐々に書いていこうと思います。
名前空間とクラス名
11月12日にパーフェクトPHPが発売しました。購入いただいた方、コメントいただいた方、ありがとうございます!
-
-
- -
-
さて本題です。PHP 5.3から名前空間が使えるようになりました。僕はここ最近Symfony2で社内向けのアプリを開発したり、Symfony2のバンドルをいくつか作ったりしてすっかり名前空間に慣れてきました。
名前空間を使い始めて感じたのが、クラス名をつける際の感覚がこれまでとは少し違うように思います。これまで、というのは主にPEAR形式のクラス名を指しています。Doctrineを例にとって解説します。
名前空間とクラス名
Doctrine 1では、次のようなクラスが定義されています。
これらを単純に名前空間を使った形式に置き換えてみます。
ここで問題となるのが、これらをインポートした時です。
<?php use Doctrine\Expression\Mysql; use Doctrine\DataDict\Mysql; use Doctrine\Connection\Mysql;
みての通り、クラス名が被ってしまいます。これでは本末転倒です。
名前空間を用いた場合、それぞれ3つのクラスはあくまでも「Mysql」というクラス名になります。これはそもそもクラス名として不適切ではないでしょうか。それぞれが「MysqLExpression」「MysqlDataDict」「MysqlConnection」というクラス名であれば、同時にインポートしても何ら問題はありませんし、クラス名としても妥当です。
これまでは名前空間に該当する部分も含めてクラス名としていましたが、名前空間が実装されたことによって、名前空間としての意味しか持たなくなりました。名前空間を取り除いたとしても、クラス名からクラスの役割がわかるように名前をつける必要があります。
ここについては、これまでの感覚だと「ExpressionMysql」や「ExpressionPgsql」のように共通語句をプレフィックスをつけがちかもしれませんが、「MysqlExpression」「PgsqlExpression」のようにサフィックスにした方が英語的にしっくりくると思います。
抽象クラスのポジション
Doctrine_Expression_MysqlクラスはDoctrine_Expressionクラスを継承しています。Doctrine_Expressionをそのまま名前空間を使った形式に置き換えると、Doctrine\Expressionとなります。そうなった場合、ExpressionはあくまでDoctrine名前空間の定義された状態です。これは感覚的な問題かもしれませんが、僕はこれには違和感があります。
ディレクトリ的には次のようになります。
Doctrine/ - Expression.php - Expression/ - MysqlExpression.php
これについて、あくまでDoctrine\Expressionという名前空間の中で完結しておくべきだと考えます。
Doctrine/ - Expression/ - Expression.php - MysqlExpression.php
そうなると修飾クラス名は「Doctrine\Expression\Expression」となります。
同一の名前空間にいれば、クラス定義の際も親クラスをインポートする必要がないので楽ですし、自然に思います。
同一名前空間にいる場合:
<?php namespace Doctrine\Expression; class MysqlExpression extends Expression {}
同一名前空間にいない場合:
<?php namespace Doctrine\Expression; use Doctrine\Expression; class MysqlExpression extends Expression {}
気をつけている些細なこと
名前空間は単数形
ディレクトリを名前空間として扱う場合、ディレクトリの中に複数ファイルを管理しているという視点で見ると確かに複数形にしたくなりますが、あくまで名前空間で、最終的には単一の何かを特定するためのものだと考えています。
例えば「Loaders\YamlLoader」と「Loader\YamlLoader」があったとしましょう。YamlLoaderはYamlファイルをロードするクラスです。Loadersディレクトリの中にYamlLoaderクラスファイルがあったとして、Loadersディレクトリの中に複数のLoaderクラスが配置されることを考えると、ディレクトリ名としては適切に感じます。しかし逆に、YamlLoaderクラス側に立って考えると、YamlLoaderクラス自体は単一の存在で、複数の何かを表わすものではありません。クラスの立場から考えると、名前空間は単数形の方がしっくりくると思います。
基本的にはインポートしてから使う
基本的にはインポートしてます。
<?php $request = new \Symfony\HttpFoundation\Request();
ではなく、
<?php use Symfony\HttpFoundation\Request; $request = new Request();
上記のようにインポートをする、ということです。これは見通しを良くすることと、関連する他の名前空間のクラスをわかりやすくするために行っています。
RuntimeException、DateTime、ArrayAccessなど、PHPが提供しているどこでも使えるようなクラスについては特にインポートはしません。useを書いて、使っていることを明示するメリットが特にないと思うのが理由です。これらはあちこちで唐突に使いたくなるわけです。日付を操作したいからDateTimeクラスを使おう、なんて色んな箇所で考えますよね。バックスラッシュ1個のためにわざわざ上まで戻ってuseを書くのは面倒、というのが本音です。
インポートしたいクラスが複数あるときは、それぞれuseを書く
useはカンマでつなげて複数指定が可能です。
<?php use Symfony\Component\HttpKernel\Bundle\Bundle, Symfony\Component\DependencyInjection\ContainerInterface;
ですがあえて、1つ1つuseをつけて書くようにしています。
<?php use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\DependencyInjection\ContainerInterface;
先頭の要素はuseがついて、末尾の要素はカンマではなくセミコロンがつく、といった全く同じ役割を持つものの並びの中に例外が入ることが好きではないためです。追加や削除の際のミスを減らす意味もあります。クラスのプロパティ定義なんかも同様にしています。
その他テクニック的な
useが面倒くさいという点に関して言うと、例えばSymfony2でアプリケーションを作る場合、各コントローラーはSymfony\FrameworkBundle\Controller\Controllerクラスを継承して作るのが基本です。ただ毎回useするのは面倒なので、各BundleのController名前空間に空のControllerクラスを作るようにしています。
<?php namespace Application\AppBundle\Controller; use Symfony\FrameworkBundle\Controller\Controller as BaseController; abstract class Controller extends BaseController { }
<?php namespace Application\AppBundle\Controller; // 名前解決のルールにのっとりApplication\AppBundle\Controller\Controllerを継承 class AccountController extends Controller { }
こうするとコントローラを作るたびにuseする必要はないですし、共通処理の追加も当然楽になりますよね。問題なければフレームワークとの間に1枚挟んでおくと何かと楽です。
パーフェクトPHPがAmazonで予約受付開始しました
- 作者: 小川雄大,柄沢聡太郎,橋口誠
- 出版社/メーカー: 技術評論社
- 発売日: 2010/11/12
- メディア: 大型本
- 購入: 32人 クリック: 1,065回
- この商品を含むブログ (59件) を見る
PHPカンファレンスのLTでもお話させていただいた書籍「パーフェクトPHP」がついにAmazonで予約受付を開始しました。発売日は11月12日です。
Amazonに載って予約が開始すると、ここまできたかという感じです。今、最終チェック用に全ページ印刷したものが手元にあるのですが、ぎっしり書いたなあと改めて思います。多くの方に読んでいただければと思います。
また、TwitterでRTしていただいた方々や、実際にご予約いただいた方々、本当にありがとうございます。
これを書いている間はほとんどイベントに参加しなかったのですが、来月はKOFとSymfony2勉強会に参加する予定です。KOFまでに見本が手に入るかもしれないので、一番最初のお披露目は関西になるかもしれませんね。
sfActionsクラスのアノテーションでis_secure
security.ymlの代わりにアノテーションでis_secure(アクションがログインを必要としているか)を設定できるようにしてみました。
ProjectConfiguration.php · GitHub
<?php /** * blog actions. * * @login_required */ class blogActions extends sfAnnotationActions { /** * @login_required */ public function executeIndex(sfWebRequest $request) { $this->posts = Doctrine::getTable('Post') ->createQuery('a') ->execute(); } /** * @login_required([admin, [foo, bar]]) */ public function executeNew(sfWebRequest $request) { $this->form = new PostForm(); } }
■課題
- アノテーションのキャッシュ
- @login_requiredがfalseにできねえ
がんばります!