Symfonyの認証のやり方(セキュリティーレイヤー)

Symfony2でもここは最も面倒くさいところです。英語のページの翻訳にもなりますが、ちょっとわかりやすくしてゆこうと思います。
http://symfony.com/doc/current/cookbook/security/entity_provider.html
古き良き時代にはcryptとかMD5とかで暗号化した文字列を同様に入力した文字列を暗号化して比較してログインという方法でやってきました。方法論的には全く同じといっていいかもしれないのですが、これらの認証方法をSymfonyに備わっている機能をつかってやるのがミソです。神バンドルといわれるFOSUserBundleもありますが、自分でいろいろやりたい人には何だかよくわからないブラックボックスになってしまうので、詳しく解説します。
ここでやってみるのはDatabaseのユーザーやパスワードを参照してログインするというやつです。

エンティティーを作る

ユーザー認証に使うデータベースの核になるエンティティーを作成します。まずはAcme以下にUserBundleを作成してみましょう。バンドルの作成はこの時点でもうすでに知っていることが前提となってしまいますが、まだ全然わからないという人は、
http://www.omnioo.com/record/symfony2-2/symfony2_bundle_basic/
ここらを参考に挑戦してみてください。バンドルを作成したら、http://example.com/app_dev.php/hello/hogeというところにデモのページができている筈なのでここにアクセスしてhogeと表示されればOKです。

src/Acme/UserBundle/Entity/User.phpを作成して、Sampleのように書きます。(英文サイトのままのものになりますが。。)
<?php
// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;

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

/**
* @ORM\Entity
* @ORM\Table(name="dtb_users")
*/
class User implements UserInterface, \Serializable
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;

/**
* @ORM\Column(type="string", length=25, unique=true)
*/
private $username;

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

/**
* @ORM\Column(type="string", length=60, unique=true)
*/
private $email;

/**
* @ORM\Column(name="is_active", type="boolean")
*/
private $isActive;

public function __construct()
{
$this->isActive = true;
// may not be needed, see section on salt below
// $this->salt = md5(uniqid(null, true));
}

public function getUsername()
{
return $this->username;
}

public function getSalt()
{
// you *may* need a real salt depending on your encoder
// see section on salt below
return null;
}

public function getPassword()
{
return $this->password;
}

public function getRoles()
{
return array('ROLE_USER');
}

public function eraseCredentials()
{
}

/** @see \Serializable::serialize() */
public function serialize()
{
return serialize(array(
$this->id,
$this->username,
$this->password,
// see section on salt below
// $this->salt,
));
}

/** @see \Serializable::unserialize() */
public function unserialize($serialized)
{
list (
$this->id,
$this->username,
$this->password,
// see section on salt below
// $this->salt
) = unserialize($serialized);
}

id, username, password, email, isActiveという5つの値を持つというシンプルなエンティティーです。ですがセキュリティーレイヤーを持たせるために幾つか普通のエンティティにはない要素も幾つか追加されています。Roleであるとか、saltであるとか権限やパスワード認証特有のいくつかのパーツがあります。がここではとりあえずコピペということで気にしないで進みましょう。
エンティティーのファイルを作成したらgetterやsetterなどの面倒なコーディングをコマンドで一気に追加書き込みします。
$ php app/console doctrine:generate:entities Acme/UserBundle/Entity/User
これで追加されるはずです。

データベーステーブルを作成する

エンティティーの作成が終わったらそのエンティティーを利用してデータベーステーブルを作成します。これもコマンドから一発で作成できます。
php app/console doctrine:schema:update --force
が、私の環境では以下のようなエラーが出ました。
[Doctrine\ORM\Mapping\MappingException]
No identifier/primary key specified for Entity "Acme\UserBundle\Entity\xxxxx". Every Entity must have an identifier/primary key.

エンティティーのアノテーションにprimary keyになるものがないと叱られております。エラーの通りの場合もありますが、私の場合はエンティティー風の使っていないファイルがあっただけでした。ゴミなので捨てましょう。このコマンドはすべてのバンドルに属するすべてのエンティティーに反映されます。すでにテーブルが作成されているものについては再度作成は勿論しません。結構安全なコマンドです。ここでデータベース壊れちゃったら元モコもないのですが、よくできたコマンドだなぁと思います。
phpMyAdminとかで見るとちゃんとテーブルができてます。

dbtable_security

セキュリティーの設定 エンティティーからセキュリティーをロードする

「エンティティーからセキュリティーをロードする」って意味がわかりませんね。ホント。私はいつもディレクターと技術者の間に立ってとても困ることがあります。「エンティティーからセキュリティーをロードする」以上の説明をさせないでください。もうしらねーよ。
でですね、app/config/security.ymlにいろいろな設定をします。
# app/config/security.yml
security:
encoders:
AppBundle\Entity\User:
algorithm: sha256

# ...

providers:
our_db_provider:
entity:
class: AppBundle:User
property: username
# if you're using multiple entity managers
# manager_name: customer

firewalls:
default:
pattern: ^/
http_basic: ~
provider: our_db_provider

# ...
Security:設定の中では、encoders:とproviders:とfirewalls:を設定します。
エンコーダ項では、データベース内のパスワードはbcryptのを使用してエンコードされることをsymfonyを伝えると英文で書いてました。その通りです。またそれに紐づくエンティティパスを設定しています。(前述で作ったやつですね。)
第二のプロバイダセクションでは、our_db_provider:となっていますが、この名前は何でもいいです。自分で好きなものを設定できます。エンティティーのクラスとプロバイダー(供給する値)をusernameに設定しています。(これはエンティティーの中にあるusernameという値のことを言っています。)
先のその名前(our_db_provider:)に「ユーザープロバイダ」を作成します。ファイヤーウォール下におくパスとそれに紐づくプロバイダー名(our_db_provider:)を設定しています。http_basic: ~はベーシック認証を使用するという意味です。

php5.5以下の場合は以下のようなメッセージが出ます。bcryptが対応していないそうなので、comporserからインストールします。
To use the BCrypt encoder, you need to upgrade to PHP 5.5 or install the "ircmaxell/password-compat" via Composer.
comporser.jsonのrequireのところに
"require": {
.......
"ircmaxell/password-compat": "~1.0.4"
},

という感じで加えてupdateします。
$ php composer.phar update
これで大丈夫かと。適当なページにアクセスすると認証ダイアログが出てきてページがセキュアな状態になります。