HWIOAuthBundleを使ってSymfony2でOAuth認証を実装する(EntityUserProvider)
HWIOAuthBundleを使ってSymfony2でOAuth認証を実装するの続編です。
今回はアプリ側にユーザー情報を格納するテーブルがあることを前提に、OAuth認証によって得られた情報と連携させてみます。
実現したい内容としては、以下となります。
- ログイン用のリンクをクリック
- Google側でログイン
- アプリ側のユーザー情報を取得
- ユーザー情報が存在すればそれを利用して認証する
- もしユーザー情報が存在しなければOAuth認証によって得られた情報をもとにユーザーを作成する
すべてHWIOAuthBundleで実現できれば良いのですが、「ユーザー情報が存在しなければ~」はどうやらできないみたい(参考:Support connect without registration)なのでカスタマイズが必要になります。
1. Entityおよびテーブルを作成
1.1. Entityを作成
php app/console generate:entity --entity=VendorSampleBundle:User
カラムは以下を設定しました。
gid | string | 255 | Google側との連携用カラム |
name | string | 255 | ユーザー名 |
string | 255 | メールアドレス | |
is_active | boolean | - | 有効/無効 |
作成後、HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUser
を継承します。
<?php namespace Vendor\SampleBundle\Entity; use Doctrine\ORM\Mapping as ORM; use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUser; /** * User */ class User extends OAuthUser { // ... }
1.2. テーブルを作成
php app/console doctrine:schema:create
2. OAuthUserProviderを作成し、サービスへ登録
2.1. OAuthUserProviderを作成
前回はHWIOAuthBundleが標準で提供しているOAuthUserProvider
を使用しました。
今回は先ほど作ったユーザー情報を格納するテーブルと連携を行うため、EntityUserProvider
を継承して新たにsrc\Vendor\SampleBundle\Auth\OAuthUserProvider.php
を作成します。
<?php namespace Vendor\SampleBundle\Auth; use HWI\Bundle\OAuthBundle\Security\Core\User\EntityUserProvider; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; class OAuthUserProvider extends EntityUserProvider implements UserProviderInterface { public function loadUserByUsername($username) { } public function refreshUser(UserInterface $user) { } public function supportsClass($class) { } }
中身は後で実装します。
2.2. OAuthUserProviderをService Containerに登録
src\Vendor\SampleBundle\Resources\config\services.yml
を以下のように書き換えます。
変更前
parameters: vendor_sample.oauth_user_provider.class: HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserProvider services: vendor_sample.oauth_user_provider.service: class: %vendor_sample.oauth_user_provider.class%
変更後
parameters: vendor_sample.oauth_user_provider.class: Vendor\SampleBundle\Auth\OAuthUserProvider vendor_sample.entity.user.class: Vendor\SampleBundle\Entity\User services: vendor_sample.oauth_user_provider.service: class: %vendor_sample.oauth_user_provider.class% arguments: [@doctrine, %vendor_sample.entity.user.class%, {google: gid}]
OAuthUserProvider
のインスタンスを生成する際に、親クラスEntityUserProvider
のコンストラクタ引数に定義されている
- DBコネクション
- 利用するUser Entitiyのクラス名
- OAuth認証との連携用プロパティ
の3つをInjectするように設定します。
3. OAuthUserProviderを実装
- How to create a custom User Provider (current) - Symfony
- Symfony\Component\Security\Core\User\UserProviderInterface | Symfony2 API
を参考にしながら実装していきます。
3.1. loadUserByUsername
本来であれば認証用フォーム等から入力されたユーザー名をもとに該当するEntityを返す処理を書くのだろうが、HWIOAuthBundleによってこいつの代わりにEntityUserProvider
のloadUserByOAuthUserResponse
が利用されるようになるため、実装不要。
たぶん。
念の為確認してみたが、一度も呼ばれなかった。
3.2. refreshUser
認証が必要な箇所へのアクセス毎に呼ばれる。
DBからユーザー情報を取得しなおしたり、内部的な処理を行ったり(最終アクセス時刻の記録など?)する場合は実装するらしい。
今回は特にその必要もないので、そのまま返す。
<?php public function refreshUser(UserInterface $user) { return $user; }
3.3. supportsClass
supportsClassによると、引数で与えられたユーザークラスをこのUserProviderがサポートするかどうかを返せば良いらしいので、以下のように実装。
いつ、どのような場面で呼ばれるのか不明。
<?php public function supportsClass($class) { return 'Vendor\\SampleBundle\\Entitiy\\User' === $class; }
3.4. コンストラクタ、loadUserByOAuthUserResponseをOverride
loadUserByOAuthUserResponse
の標準実装は次のようになっています。
<?php public function loadUserByOAuthUserResponse(UserResponseInterface $response) { $resourceOwnerName = $response->getResourceOwner()->getName(); if (!isset($this->properties[$resourceOwnerName])) { throw new \RuntimeException(sprintf("No property defined for entity for resource owner '%s'.", $resourceOwnerName)); } $username = $response->getUsername(); $user = $this->repository->findOneBy(array($this->properties[$resourceOwnerName] => $username)); if (null === $user) { throw new UsernameNotFoundException(sprintf("User '%s' not found.", $username)); } return $user; }
冒頭に書いた、
- ユーザー情報が存在すればそれを利用して認証する
- もしユーザー情報が存在しなければOAuth認証によって得られた情報をもとにユーザーを作成する
のうち、前者しか実現できていないのがわかると思います。
なので、loadUserByOAuthUserResponse
をOverrideします。
作成したユーザーを永続化するためにEntityManagerを使いたいので、以下のようにコンストラクタをOverrideします。
<?php protected $em; public function __construct(ManagerRegistry $registry, $class, array $properties, $managerName = null) { parent::__construct($registry, $class, $properties, $managerName); $this->em = $registry->getManager($managerName); }
次にloadUserByOAuthUserResponse
<?php public function loadUserByOAuthUserResponse(UserResponseInterface $response) { try { return parent::loadUserByOAuthUserResponse($response); } catch (UsernameNotFoundException $e) { $rawResponse = $response->getResponse(); $user = new User($rawResponse['name']); $user->setGid($rawResponse['id']); $user->setEmail($rawResponse['email']); $user->setIsActive(true); $this->em->persist($user); $this->em->flush(); return $user; } }
これで、後者についても実現できます。
最後に
「ユーザー情報が存在しなければ~」を自前で実装しましたが、FOSUserBundleと一緒に使うとひょっとしたらすべて面倒見てくれるかもしれません。
また。今回の私の例では必要なかったので調べていませんが、Reference configurationを見るに、OAuth認証が完了した後にアプリ側に用意した登録フォームでアプリ独自の設定情報をユーザーに登録させるための"connect"という仕組みも提供しているようです。
やっぱり日本語情報が少ないですね。。。
以上です。