\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
)を得る世界観- DAOがドメイン層の型を意識してしまっている
- 本当はDAOの返却値の型と
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
オブジェクトが得られるようになる。めでたし。