Laravelでの独自の認証処理の実装を通して、認証関連のmiddlewareやguardについてまとめました。
今回、実装する簡易な認証処理の仕様は、以下の通りです。APIでの使用を想定しています。
独自の認証処理では、以下の項目を実装します。順番に内容を見ていきます。
認証処理に使うモデルは Illuminate\Foundation\Auth\User
を継承している必要があります。今回実装する認証処理の仕様では、 getAuthIdentifierName()
のみ実装すれば十分です。
<?php namespace App\Domain\Model; // namespaceはお好みで use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable { /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'uuid', ]; /** * Get the name of the unique identifier for the user. * * @see \Illuminate\Contracts\Auth\Authenticatable:: getAuthIdentifierName * @return string */ public function getAuthIdentifierName() { return 'uuid'; } }
UserProvider では、ユーザーの識別方法を実装します。今回実装する認証処理の仕様では、リクエストヘッダーに毎回 UUID を含めるので、 retrieveById($identifier)
のみ使用します。
<?php namespace App\Providers; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\UserProvider; use App\Domain\Model\User; class ApiUserProvider implements UserProvider { private User $_model; public function __construct(User $model) { $this->_model = $model; } /** * Retrieve a user by their unique identifier. * * @see \Illuminate\Contracts\Auth\UserProvider::retrieveById * @param mixed $identifier * @return \Illuminate\Contracts\Auth\Authenticatable|null */ public function retrieveById($identifier) { return $this->_model->where($this->_model->getAuthIdentifierName(), $identifier)->first(); } /** * Retrieve a user by their unique identifier and "remember me" token. * * @see \Illuminate\Contracts\Auth\UserProvider::retrieveByToken * @param mixed $identifier * @param string $token * @return \Illuminate\Contracts\Auth\Authenticatable|null */ public function retrieveByToken($identifier, $token) { return null; } /** * Update the "remember me" token for the given user in storage. * * @see \Illuminate\Contracts\Auth\UserProvider::updateRememberToken * @param \Illuminate\Contracts\Auth\Authenticatable $user * @param string $token * @return void */ public function updateRememberToken(Authenticatable $user, $token) { } /** * Retrieve a user by the given credentials. * * @see \Illuminate\Contracts\Auth\UserProvider::retrieveByCredentials * @param array $credentials * @return \Illuminate\Contracts\Auth\Authenticatable|null */ public function retrieveByCredentials(array $credentials) { return null; } /** * Validate a user against the given credentials. * * @see \Illuminate\Contracts\Auth\UserProvider::validateCredentials * @param \Illuminate\Contracts\Auth\Authenticatable $user * @param array $credentials * @return bool */ public function validateCredentials(Authenticatable $user, array $credentials) { return false; } }
Guard では、認証に直接関連する処理を実装します。今回実装する認証処理の仕様では、まず、 Bearerトークンが事前に決めた文字列と一致しているか確認し、一致していなければ、問答無用で認証エラーとしたいので、まずそれを確認します。その後、uuidヘッダーの値でユーザーを識別します。
<?php namespace App\Guards; use Illuminate\Auth\Events\Authenticated; use Illuminate\Http\Request; use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Auth\UserProvider; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Auth\GuardHelpers as GuardHelpers; class BearerGuard implements Guard { use GuardHelpers; protected string $_name; protected UserProvider $_provider; protected ?Request $_request; protected ?Authenticatable $_user; /** * Create a new authentication guard. * * @param string $name * @param UserProvider $provider * @param Request $request */ public function __construct(string $name, UserProvider $provider, Request $request = null) { $this->_name = $name; $this->_request = $request; $this->_provider = $provider; $this->_user = null; } /** * Determine if the current user is authenticated. * * @return bool */ public function check() { return $this->checkAccessToken() && !is_null($this->user()); } /** * Determine if the current user is a guest. * * @return bool */ public function guest() { return !$this->check(); } /** * Get the currently authenticated user. * * @return Authenticatable|null */ public function user() { if (!is_null($this->_user)) { return $this->_user; } // uuidヘッダの内容でユーザーを識別 $cid = $this->_request->header('uuid', ''); if ($this->_user = $this->_provider->retrieveById($cid)) { $this->fireAuthenticatedEvent($this->_user); } return $this->_user; } /** * Get the ID for the currently authenticated user. * * @return int|string|null */ public function id() { if ($user = $this->user()) { return $this->user()->getAuthIdentifier(); } return null; } /** * Validate a user's credentials. * * @param array $credentials * @return bool */ public function validate(array $credentials = []) { return false; } /** * Set the current user. * * @param \Illuminate\Contracts\Auth\Authenticatable $user * @return void */ public function setUser(Authenticatable $user) { $this->_user = $user; } /** * Fire the authenticated event if the dispatcher is set. * * @param Authenticatable $user * @return void */ protected function fireAuthenticatedEvent($user) { if (isset($this->events)) { $this->events->dispatch(new Authenticated($this->_name, $user)); } } private function checkAccessToken(): bool { return $this->_request->bearerToken() == config('auth.api_access_token'); } }
checkAccessToken()
の中で、 Bearerトークンの値と、 .env
に設定した値が一致するか、確認しています。
ユーザーの識別は、 user()
の中で行っており、uuid ヘッダーの値と一致するユーザーを、 UserProvider 経由で取得しています。
なお、 check()
、 guest()
、 user()
などは Authファサードからアクセスできます。
// ログイン状態か Auth::check(); // 未ログイン状態か Auth::guest(); // ログインしているユーザを取得 $user = Auth::user();
認証エラーになった場合、そのままではリダイレクトされてしまうので、JSONを返却できるように、 Middleware で制御します。
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Contracts\Auth\Factory as Auth; use Illuminate\Http\Request; class AuthenticateForApi { /** @var Auth The authentication factory instance. */ protected Auth $_auth; /** * Create a new middleware instance. * * @param Auth $auth * @return void */ public function __construct(Auth $auth) { $this->_auth = $auth; } /** * Handle an incoming request. * * @param Request $request * @param Closure $next * @param string[] ...$guards * @return mixed */ public function handle($request, Closure $next, ...$guards) { $this->authenticate($guards); return $next($request); } /** * Determine if the user is logged in to any of the given guards. * * @param array $guards * @return void */ protected function authenticate(array $guards) { if (empty($guards)) { $guards = [null]; } foreach ($guards as $guard) { if ($this->_auth->guard($guard)->check()) { $this->_auth->shouldUse($guard); return; } } abort(401); } }
authenticate(array $guards)
のループの中で guard を一つずつチェックし、認証でたら、 shouldUse($guard)
で、その guard を AuthManager
の $defaultProvider
にセットし、$userProvider
にユーザーの解決方法をクロージャの形式でセットします。
認証エラーになった場合、そのままではリダイレクトされてしまうので、 JSON を返却するように修正します。
<?php namespace App\Exceptions; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Throwable; class Handler extends ExceptionHandler { /** * A list of the exception types that are not reported. * * @var array */ protected $dontReport = [ // ]; /** * A list of the inputs that are never flashed for validation exceptions. * * @var array */ protected $dontFlash = [ 'password', 'password_confirmation', ]; /** * Report or log an exception. * * @param \Throwable $exception * @return void * * @throws \Exception */ public function report(Throwable $exception) { parent::report($exception); } /** * Render an exception into an HTTP response. * * @param \Illuminate\Http\Request $request * @param \Throwable $exception * @return \Symfony\Component\HttpFoundation\Response * * @throws \Throwable */ public function render($request, Throwable $exception) { // ↓↓↓ここから追記 if ($request->is('ajax/*') || $request->is('api/*') || $request->ajax()) { return $this->createApiErrorResponse($exception); } // ここまで追記↑↑↑ return parent::render($request, $exception); } // ↓↓↓ここから追記 private function createApiErrorResponse(Throwable $exception) { if ($this->isHttpException($exception)) { $status = $exception->getStatusCode(); switch ($status) { case 400: $errorMsg = 'Bad request.'; break; case 401: $errorMsg = 'Unauthorized.'; break; case 404: $errorMsg = 'Not found.'; break; default: $status = 400; $errorMsg = $exception->getMessage(); } } else { $status = 500; $errorMsg = $exception->getMessage(); } $resultCode = 0; return response()->json([ 'status' => $resultCode, 'message' => $errorMsg, ], $status); } // ここまで追記↑↑↑ }
auth.php に指定するために、AuthServiceProvider.php
で、作成した guard を登録します。
<?php namespace App\Providers; use App\Guards\BearerGuard; // 追記 use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Auth; // 追記 use Illuminate\Support\Facades\Gate; class AuthServiceProvider extends ServiceProvider { /** * The policy mappings for the application. * * @var array */ protected $policies = [ // 'App\Model' => 'App\Policies\ModelPolicy', ]; /** * Register any authentication / authorization services. * * @return void */ public function boot() { $this->registerPolicies(); // ↓↓↓ここから追記 Auth::extend('bearer', function ($app) { $userProvider = $app->make(ApiUserProvider::class); $request = $app->make('request'); return new BearerGuard('bearer', $userProvider, $request); }); // ここまで追記↑↑↑ } }
作成した Middleware を kernel.php
に定義します。
<?php namespace App\Http; use Illuminate\Foundation\Http\Kernel as HttpKernel; class Kernel extends HttpKernel { /** * The application's global HTTP middleware stack. * * These middleware are run during every request to your application. * * @var array */ protected $middleware = [ // \App\Http\Middleware\TrustHosts::class, \App\Http\Middleware\TrustProxies::class, \Fruitcake\Cors\HandleCors::class, \App\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, ]; /** * The application's route middleware groups. * * @var array */ protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], 'api' => [ 'throttle:60,1', \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ]; /** * 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, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, // ↓↓↓ここから追記 'auth.api' => \App\Http\Middleware\AuthenticateForApi::class, // ここまで追記↑↑↑ ]; }
auth.php
で、作成した認証処理を使用するように設定します。
<?php return [ // 以下抜粋 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'admin_users', ], 'api' => [ 'driver' => 'bearer', ], ], ];
作成した認証処理を使うには、 api.php
内で以下のように記述します。
<?php use Illuminate\Support\Facades\Route; // ログイン時 Route::middleware('auth.api:api')->get('/users', 'UsersController@authenticated'); // 未ログイン時 Route::middleware('guest.api:api')->get('/users', 'UsersController@noauthenticated'); });
APIの認証には、 jwt-auth など、すでに様々なライブラリが用意されているので、独自の認証処理を実装することは少ないかもしれません。ただ、今回のように簡易な認証で良い場合は、 Laravel では簡単に実装することができます。