FOSUserBundle: Symfonyでユーザ管理


## はじめに

> The FOSUserBundle, developed by the Symfony community, adds support for a database-backed user system in Symfony. It also handles common tasks like user registration and forgotten password functionality.

FOSUserBundleはSymfonyコミュニティにより開発されており
データベースを使用したユーザ管理システムを提供します。
ユーザ登録やパスワードのリセットなどのよくある機能が実装されています。

Symfonyのインストール時にデフォルトでは入りませんが
[ドキュメント](http://symfony.com/doc/current/bundles/FOSUserBundle/index.html)がSymfony.com以下にあったり
上記のような記載がベストプラクティスに載っていたりと、信頼できそうです。

基本的な機能や気になったところをまとめます。

:warning: セキュリティに関わることなので、内容を鵜呑みにはしないでください。

## 環境

Symfonyが動作し、Doctrine経由でMySQLにアクセスできることが前提です。

|ソフト|バージョン|
|:-:|:-:|
|PHP|7系|
|Symfony|3系|
|MySQL|5.6.x|
|Doctrine|2.5.3|

（バージョン情報の記憶が曖昧なため、後日追記）

## 導入

### インストール

ドキュメントの[Installation](http://symfony.com/doc/current/bundles/FOSUserBundle/index.html#installation)に従います。

### 作成されるページ

#### ルーティング

自動で多くのページが生成されます。
機能的にはURLパス見たまんまなので詳細は割愛します。

```
$ bin/console debug:router | grep fos                               
  fos_user_security_login             GET|POST   ANY      ANY    /login                   
  fos_user_security_check             POST       ANY      ANY    /login_check             
  fos_user_security_logout            GET|POST   ANY      ANY    /logout                  
  fos_user_profile_show               GET        ANY      ANY    /profile/                
  fos_user_profile_edit               GET|POST   ANY      ANY    /profile/edit            
  fos_user_registration_register      GET|POST   ANY      ANY    /register/               
  fos_user_registration_check_email   GET        ANY      ANY    /register/check-email    
  fos_user_registration_confirm       GET        ANY      ANY    /register/confirm/{token}
  fos_user_registration_confirmed     GET        ANY      ANY    /register/confirmed      
  fos_user_resetting_request          GET        ANY      ANY    /resetting/request       
  fos_user_resetting_send_email       POST       ANY      ANY    /resetting/send-email    
  fos_user_resetting_check_email      GET        ANY      ANY    /resetting/check-email   
  fos_user_resetting_reset            GET|POST   ANY      ANY    /resetting/reset/{token} 
  fos_user_change_password            GET|POST   ANY      ANY    /profile/change-password
```

#### ログイン

![1.png](https://qiita-image-store.s3.amazonaws.com/0/43116/44f847cc-584b-010b-89e7-05eaff6e0ce3.png "1.png")


#### 登録

![2.png](https://qiita-image-store.s3.amazonaws.com/0/43116/0b299ec7-a1b2-1d05-4045-9a071bdb2cc7.png "2.png")


#### パスワードリセット

![3.png](https://qiita-image-store.s3.amazonaws.com/0/43116/26471090-9609-6417-5b9d-6a7cd019383f.png "3.png")


見た目が簡素ですが、もちろんテンプレートの書き換えはできます。
[Overriding Default FOSUserBundle Templates](http://symfony.com/doc/current/bundles/FOSUserBundle/overriding_templates.html)

### 作成されるテーブル

DBには以下のテーブルが生成されます。

```
mysql> DESC user;
+-----------------------+--------------+------+-----+---------+----------------+
| Field                 | Type         | Null | Key | Default | Extra          |
+-----------------------+--------------+------+-----+---------+----------------+
| id                    | int(11)      | NO   | PRI | NULL    | auto_increment |
| username              | varchar(180) | NO   |     | NULL    |                |
| username_canonical    | varchar(180) | NO   | UNI | NULL    |                |
| email                 | varchar(180) | NO   |     | NULL    |                |
| email_canonical       | varchar(180) | NO   | UNI | NULL    |                |
| enabled               | tinyint(1)   | NO   |     | NULL    |                |
| salt                  | varchar(255) | NO   |     | NULL    |                |
| password              | varchar(255) | NO   |     | NULL    |                |
| last_login            | datetime     | YES  |     | NULL    |                |
| locked                | tinyint(1)   | NO   |     | NULL    |                |
| expired               | tinyint(1)   | NO   |     | NULL    |                |
| expires_at            | datetime     | YES  |     | NULL    |                |
| confirmation_token    | varchar(255) | YES  | UNI | NULL    |                |
| password_requested_at | datetime     | YES  |     | NULL    |                |
| roles                 | longtext     | NO   |     | NULL    |                |
| credentials_expired   | tinyint(1)   | NO   |     | NULL    |                |
| credentials_expire_at | datetime     | YES  |     | NULL    |                |
+-----------------------+--------------+------+-----+---------+----------------+
18 rows in set (0.00 sec)
```

フィールドごとの役割は以下のとおりです。
ドキュメントに記載が見当たらないので、ソースコードからザッと読んだ内容です。

|フィールド|役割|
|:--|:--|
|`id`|ユニークID|
|`username`|ユーザ名|
|`username_canonical`|`username`をすべて小文字に変換したもの。ユーザ名の重複防止に使用|
|`email`|メールアドレス|
|`email_canonical`|`email`をすべて小文字に変換したもの。メールアドレスの重複防止に使用|
|`enabled`|ログイン可能かを示す。未アクティベートユーザのログイン禁止などに使用|
|`salt`|パスワードのハッシュ化に使用されるソルト文字列|
|`password`|暗号化されたパスワード|
|`last_login`|最終ログインの日時|
|`locked`|ログイン可能かを示すフラグ。強制的なログイン禁止などに使用|
|`expired`|アカウントの有効期限切れを示す。期限が切れているとログイン不可|
|`expires_at`|アカウントの有効期限|
|`confirmation_token`|パスワードリセットにおける本人確認などに使用されるトークン|
|`password_requested_at`|ユーザがパスワードリセットをリクエストした日時|
|`roles`|アクセスコントロールに使用されるロール|
|`credentials_expired`|認証情報の有効期限切れを示す。定期的なパスワード変更を促すのに使用|
|`credentials_expire_at`|認証情報の有効期限|

## 気になったこと

### userテーブルに他のフィールドを追加できないのか

必要最低限のフィールドは自動的に作成されましたが
アプリケーションの要件によっては他のフィールドが必要です。

この場合にはエンティティに追加フィールド情報をマッピングできるようです。
例えばアイコン表示用に画像パスを格納したい場合は以下のように記載します。

```php:User.php
/**
 * @var string
 *
 * @ORM\Column(name="image_path", type="string", length=255)
 */
protected $image_path;
```

その後にスキーマを再更新します。

```bash:スキーマの更新
$ bin/console doctrine:schema:update --force
```

新しくフィールドが追加されているのがわかります。

```
mysql> DESC user;
+-----------------------+--------------+------+-----+---------+----------------+
| Field                 | Type         | Null | Key | Default | Extra          |
+-----------------------+--------------+------+-----+---------+----------------+
| id                    | int(11)      | NO   | PRI | NULL    | auto_increment |
| username              | varchar(180) | NO   |     | NULL    |                |
| username_canonical    | varchar(180) | NO   | UNI | NULL    |                |
| email                 | varchar(180) | NO   |     | NULL    |                |
| email_canonical       | varchar(180) | NO   | UNI | NULL    |                |
| enabled               | tinyint(1)   | NO   |     | NULL    |                |
| salt                  | varchar(255) | NO   |     | NULL    |                |
| password              | varchar(255) | NO   |     | NULL    |                |
| last_login            | datetime     | YES  |     | NULL    |                |
| locked                | tinyint(1)   | NO   |     | NULL    |                |
| expired               | tinyint(1)   | NO   |     | NULL    |                |
| expires_at            | datetime     | YES  |     | NULL    |                |
| confirmation_token    | varchar(255) | YES  | UNI | NULL    |                |
| password_requested_at | datetime     | YES  |     | NULL    |                |
| roles                 | longtext     | NO   |     | NULL    |                |
| credentials_expired   | tinyint(1)   | NO   |     | NULL    |                |
| credentials_expire_at | datetime     | YES  |     | NULL    |                |
| image_path            | varchar(255) | NO   |     | NULL    |                |
+-----------------------+--------------+------+-----+---------+----------------+
18 rows in set (0.00 sec)
```

Userエンティティの基本的な作りは他と変わらないので
リレーションを定義することも同様に可能です。

### パスワードのポリシー（長さや使用文字制限）は設定できないのか

内部では、Entity、Form、FormTypeなどが使われているので
それぞれにバリデーションを加えることで実現できそうです。
[Overriding Default FOSUserBundle Forms](http://symfony.com/doc/current/bundles/FOSUserBundle/overriding_forms.html)

デフォルトの設定は`Resources/config/validation.xml`にあるから
使うなら使えばいいし、だめならオーバーライドというスタンスなんでしょうか。
[Overriding Default FOSUserBundle Validation](http://symfony.com/doc/current/bundles/FOSUserBundle/overriding_validation.html)

### ユーザ名を使わないとログインできないのか

[installation](http://symfony.com/doc/current/bundles/FOSUserBundle/index.html#installation)での手順ではユーザ名しか使用できませんでしたが
メールアドレスでのログインも許可できるようです。

`security.yml`の`id`の部分を変更します。

```security.yml
providers:                                       
    fos_userbundle:                              
        id: fos_user.user_provider.username_email
```

これで、ユーザ名とメールアドレスのどちらかでのログインが可能です。
メールアドレスのみを許すことはできませんが、拡張についてPRがありました。
[Added email only authentication](https://github.com/FriendsOfSymfony/FOSUserBundle/pull/2122)

## まとめ

デフォルトではセキュリティが割と緩めなので
詳細な設定については[ドキュメント](http://symfony.com/doc/current/bundles/FOSUserBundle/index.html)をご一読ください。
全体的に「気になるところはオーバーライドして」というスタンスな気がします。

Webのセキュリティ知識や内部実装への理解は必要ですが
上記のような簡単な手順だけで多くの機能を得られるのはお得ですね。