Laravel/認証周りのコード追う
- JWT認証ミドルウェア定義部分
- JWT認証ミドルウェア
- 認証ファサードクラス JWTAuth
- インタフェースAuthInterface, 実装クラスIlluminateAuthAdapter
- \JWTAuth::authenticate($token)で自前のUserクラスのオブジェクトが返ってくるようにしたい
- \Auth::user()で自前のUserクラスのオブジェクトが返ってくるようにしたい
- 自前のUserProvider書く
- 自作UserProviderが使用される流れ
\Auth::user()
で自前のUser
クラス(EloquentModelでもGenericUserでもないやつ)を取得できるようにしたかったのさ
- Tymon氏のJWT認証追う
- Laravelの標準認証ロジックも追う
JWT認証ミドルウェア定義部分
app/Http/Kernel.php
<?php ... /** * The application's route middleware. * * These middleware may be assigned to groups or used individually. * * @var array */ protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'jwt.auth' => 'Tymon\JWTAuth\Middleware\GetUserFromToken', 'jwt.refresh' => 'Tymon\JWTAuth\Middleware\RefreshToken', ];
Tymon\JWTAuth\Middleware\GetUserFromToken
がJWT認証ミドルウェア
JWT認証ミドルウェア
vendor/tymon/jwt-auth/src/Middleware/GetUserFromToken.php
<?php ... class GetUserFromToken extends BaseMiddleware { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, \Closure $next) { if (! $token = $this->auth->setRequest($request)->getToken()) { return $this->respond('tymon.jwt.absent', 'token_not_provided', 400); } try { $user = $this->auth->authenticate($token); } catch (TokenExpiredException $e) { return $this->respond('tymon.jwt.expired', 'token_expired', $e->getStatusCode(), [$e]); } catch (JWTException $e) { return $this->respond('tymon.jwt.invalid', 'token_invalid', $e->getStatusCode(), [$e]); } if (! $user) { return $this->respond('tymon.jwt.user_not_found', 'user_not_found', 404); } $this->events->fire('tymon.jwt.valid', $user); return $next($request); } }
$this->auth
に処理を委譲しているようだ$this->auth->setRequest($request)
Request
オブジェクトのセット- これをしないとJWTトークンをheaderに乗せてテストしてもうまくいかない
$user = $this->auth->authenticate($token);
- 認証部分
$this
はBaseMiddleware
を継承している
vendor/tymon/jwt-auth/src/Middleware/BaseMiddleware.php
<?php ... /** * @var \Tymon\JWTAuth\JWTAuth */ protected $auth;
JWTAuth
オブジェクトに処理を委譲している
認証ファサードクラス JWTAuth
- Gang of Fourでいうところの「Facade」にあたるクラス
- デザインパターンの話。これ自体はLaravelの「Facade機能」とは関係ない
JWTAuth
クラスはtymon.jwt.auth
という名前でサービスコンテナにsingletonバインドされている
vendor/tymon/jwt-auth/src/Providers/JWTAuthServiceProvider.php
<?php ... /** * Bind some Interfaces and implementations. */ protected function bootBindings() { $this->app->singleton('Tymon\JWTAuth\JWTAuth', function ($app) { return $app['tymon.jwt.auth']; }); ...
- この
tymon.jwt.auth
という名前がLaravelのファサード機能で使用される
vendor/tymon/jwt-auth/src/Facades/JWTAuth.php
<?php ... class JWTAuth extends Facade { /** * Get the registered name of the component. * * @return string */ protected static function getFacadeAccessor() { return 'tymon.jwt.auth'; } }
config/app.php
<?php ... 'aliases' => [ ... 'JWTAuth' => 'Tymon\JWTAuth\Facades\JWTAuth', 'JWTFactory' => 'Tymon\JWTAuth\Facades\JWTFactory', ...
- ここにおいて
\JWTAuth::setRequest($request)
とか\JWTAuth::authenticate($token)
とか書けるようになる
JWTAuth@authenticate
もまた、$this->auth
に処理を委譲している
vendor/tymon/jwt-auth/src/JWTAuth.php
<?php ... /** * Authenticate a user via a token. * * @param mixed $token * * @return mixed */ public function authenticate($token = false) { $id = $this->getPayload($token)->get('sub'); if (! $this->auth->byId($id)) { return false; } return $this->auth->user(); }
- AuthInterfaceとして抽象化されている何かのようだ
<?php ... /** * @var \Tymon\JWTAuth\Providers\Auth\AuthInterface */ protected $auth;
インタフェースAuthInterface
, 実装クラスIlluminateAuthAdapter
vendor/tymon/jwt-auth/src/Providers/Auth/AuthInterface.php
<?php /* * This file is part of jwt-auth. * * (c) Sean Tymon <tymon148@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Tymon\JWTAuth\Providers\Auth; interface AuthInterface { /** * Check a user's credentials. * * @param array $credentials * @return bool */ public function byCredentials(array $credentials = []); /** * Authenticate a user via the id. * * @param mixed $id * @return bool */ public function byId($id); /** * Get the currently authenticated user. * * @return mixed */ public function user(); }
vendor/tymon/jwt-auth/src/Providers/Auth/IlluminateAuthAdapter.php
<?php /* * This file is part of jwt-auth. * * (c) Sean Tymon <tymon148@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Tymon\JWTAuth\Providers\Auth; use Illuminate\Auth\AuthManager; class IlluminateAuthAdapter implements AuthInterface { /** * @var \Illuminate\Auth\AuthManager */ protected $auth; /** * @param \Illuminate\Auth\AuthManager $auth */ public function __construct(AuthManager $auth) { $this->auth = $auth; } /** * Check a user's credentials. * * @param array $credentials * @return bool */ public function byCredentials(array $credentials = []) { return $this->auth->once($credentials); } /** * Authenticate a user via the id. * * @param mixed $id * @return bool */ public function byId($id) { return $this->auth->onceUsingId($id); } /** * Get the currently authenticated user. * * @return mixed */ public function user() { return $this->auth->user(); } }
IlluminateAuthAdapter
は、Illuminate\Auth\AuthManager
をJWTAuth
から使用できるようにするAdapterクラスIlluminate\Auth\AuthManager
は、Laravel標準の認証に関するFacadeクラスであり(GoF的な意味で)、LaravelのFacade機能の実体である- 結局、
\JWTAuth::authenticate($token)
は\Auth::user()
を返す - インタフェースと実装クラスのバインディングは
JWTAuthServiceProvider
に記述されている
<?php ... $this->app->singleton('Tymon\JWTAuth\Providers\Auth\AuthInterface', function ($app) { return $app['tymon.jwt.provider.auth']; }); ... /** * Register the bindings for the Auth provider. */ protected function registerAuthProvider() { $this->app->singleton('tymon.jwt.provider.auth', function ($app) { return $this->getConfigInstance($this->config('providers.auth')); }); }
$this->config('providers.auth')
は、config/jwt.php
のproviders->authを読みに行く人
<?php 'providers' => [ ... /* |-------------------------------------------------------------------------- | Authentication Provider |-------------------------------------------------------------------------- | | Specify the provider that is used to authenticate users. | */ 'auth' => 'Tymon\JWTAuth\Providers\Auth\IlluminateAuthAdapter',
- 要するに、
Tymon\JWTAuth\Providers\Auth\AuthInterface
がTymon\JWTAuth\Providers\Auth\IlluminateAuthAdapter
として解決される
\JWTAuth::authenticate($token)
で自前のUser
クラスのオブジェクトが返ってくるようにしたい
\Auth::user()
で自前のUser
クラスのオブジェクトが変えるようにすれば良い
\Auth::user()
で自前のUser
クラスのオブジェクトが返ってくるようにしたい
\Auth::user()
は、app()->make('auth')->user()
であるapp()->make('auth')
は、Illuminate\Auth\AuthManager
のインスタンスを返すIlluminate\Auth\AuthManager@user
は、Illuminate\Contracts\Auth\Guard@userに処理を委譲する
/** * Dynamically call the default driver instance. * * @param string $method * @param array $parameters * @return mixed */ public function __call($method, $parameters) { return $this->guard()->{$method}(...$parameters); }
- デフォルトのガードは
web
、driverはsession
、providerはusers
config/auth.php
<?php ... 'defaults' => [ 'guard' => 'web', 'passwords' => 'users', ], ... 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'jwt', 'provider' => 'users', ], ], ... 'providers' => [ // 'users' => [ // 'driver' => 'eloquent', // 'model' => App\User::class, // ], 'users' => [ 'driver' => 'dto', ], ],
- (Eloquentモデル等ではなく)自前の
User
オブジェクトを取得するためには、自前のUserProviderを書く必要があるdto
ってやつ
自前のUserProvider書く
<?php declare(strict_types=1); namespace App\Domain\User; use Illuminate\Contracts\Auth\UserProvider; use Illuminate\Auth\DatabaseUserProvider; use App\Infrastructure\Db\UserDao; use App\Domain\User\User; /** * 認証情報からApp\Domain\User\Userを得るなどする */ class DtoUserProvider extends DatabaseUserProvider implements UserProvider { /** @var UserDao */ private $userDao; public function __construct(UserDao $userDao) { parent::__construct( app()->make('db')->connection(), app()->make('hash'), UserDao::TABLE_NAME ); $this->userDao = $userDao; } /** * Retrieve a user by their unique identifier. * * @override * @param mixed $identifier * @return \Illuminate\Contracts\Auth\Authenticatable|null */ public function retrieveById($identifier) { return $this->userDao->find($identifier); } /** * Retrieve a user by the given credentials. * * @override * @param array $credentials * @return \Illuminate\Contracts\Auth\Authenticatable|null * * @todo クエリ1回で済ます */ public function retrieveByCredentials(array $credentials) { // 雑にgenericUserを得て、 // idから再度Userを得る // // 2回クエリが実行されるのが良くない $genericUser = parent::retrieveByCredentials($credentials); return is_null($genericUser) ? null : $this->userDao->find($genericUser->id); } }
- インフラ層のDAO(
UserDao
)からドメイン層のDTO(User
)を得る世界観 DatabaseUserProvider
を雑に継承して差分だけ書いている- これを
dto
という名前でAuthManager
オブジェクト(シングルトン)に登録する
app/Providers/AuthServiceProvider.php
<?php ... /** * Register any authentication / authorization services. * * @return void */ public function boot() { $this->registerUserProvider(); } /** * 自前DTO版UserProviderの登録 */ protected function registerUserProvider() { $this->app->make('auth')->provider( 'dto', function ($app) { return $app->make(DtoUserProvider::class); } ); }
自作UserProvider
が使用される流れ
AuthManager@provider
で登録すると、CreatesUserProviders
トレイトのcustomProviderCreators[]
メンバに登録される
Illuminate\Auth\AuthManager.php
<?php ... class AuthManager implements FactoryContract { use CreatesUserProviders; ... /** * Register a custom provider creator Closure. * * @param string $name * @param \Closure $callback * @return $this */ public function provider($name, Closure $callback) { $this->customProviderCreators[$name] = $callback; return $this; }
- デフォルトの
web
ガードを構築する際にsession
ドライバーを構築する session
ドライバーはAuthManager@createSessionDriver
で構築される- その中で
CreatesUserProviders
トレイトのcreateUserProvider
が呼ばれ、UserProvider
実装クラスのオブジェクトが構築される
<?php /** * Create a session based authentication guard. * * @param string $name * @param array $config * @return \Illuminate\Auth\SessionGuard */ public function createSessionDriver($name, $config) { $provider = $this->createUserProvider($config['provider'] ?? null); $guard = new SessionGuard($name, $provider, $this->app['session.store']); // When using the remember me functionality of the authentication services we // will need to be set the encryption instance of the guard, which allows // secure, encrypted cookie values to get generated for those cookies. if (method_exists($guard, 'setCookieJar')) { $guard->setCookieJar($this->app['cookie']); } if (method_exists($guard, 'setDispatcher')) { $guard->setDispatcher($this->app['events']); } if (method_exists($guard, 'setRequest')) { $guard->setRequest($this->app->refresh('request', $guard, 'setRequest')); } return $guard; }
Illuminate\Auth\CreateUserProviders.php
<?php ... /** * Create the user provider implementation for the driver. * * @param string|null $provider * @return \Illuminate\Contracts\Auth\UserProvider|null * * @throws \InvalidArgumentException */ public function createUserProvider($provider = null) { if (is_null($config = $this->getProviderConfiguration($provider))) { return; } if (isset($this->customProviderCreators[$driver = ($config['driver'] ?? null)])) { return call_user_func( $this->customProviderCreators[$driver], $this->app, $config ); } switch ($driver) { case 'database': return $this->createDatabaseProvider($config); case 'eloquent': return $this->createEloquentProvider($config); default: throw new InvalidArgumentException( "Authentication user provider [{$driver}] is not defined." ); } }
$provider
には"dto"が渡ってくる- 先程"dto"という名前で登録したprovider構築関数が呼ばれ、
UserProvider
インタフェースの自前実装DtoUserProvider
オブジェクトが返却される
return call_user_func( $this->customProviderCreators[$driver], $this->app, $config );
\Auth::user()
で、自前のUser
オブジェクトが得られるようになる。めでたし。