はじめに
Laravelでの独自の認証処理の実装を通して、認証関連のmiddlewareやguardについてまとめました。
認証処理の仕様
今回、実装する簡易な認証処理の仕様は、以下の通りです。APIでの使用を想定しています。
- ヘッダーのAuthenticationに設定したBearerトークンの値が、予め決めた文字列と一致すること
- ヘッダーのuuid設定した値が、予めDBに登録してあるユーザのUUIDと一致すること
独自の認証処理の実装
独自の認証処理では、以下の項目を実装します。順番に内容を見ていきます。
- Model
- UserProvider
- Guard
- Middleware
- Handlerとその他の設定
Modelの実装
認証処理に使うモデルは
Illuminate\Foundation\Auth\User
を継承している必要があります。今回実装する認証処理の仕様では、
getAuthIdentifierName()
のみ実装すれば十分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <?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の実装
UserProvider では、ユーザーの識別方法を実装します。今回実装する認証処理の仕様では、リクエストヘッダーに毎回 UUID を含めるので、
retrieveById($identifier)
のみ使用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | <?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の実装
Guard では、認証に直接関連する処理を実装します。今回実装する認証処理の仕様では、まず、 Bearerトークンが事前に決めた文字列と一致しているか確認し、一致していなければ、問答無用で認証エラーとしたいので、まずそれを確認します。その後、uuidヘッダーの値でユーザーを識別します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | <?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ファサードからアクセスできます。
1 2 3 4 5 6 7 8 | // ログイン状態か Auth::check(); // 未ログイン状態か Auth::guest(); // ログインしているユーザを取得 $user = Auth::user(); |
Middlewareの実装
認証エラーになった場合、そのままではリダイレクトされてしまうので、JSONを返却できるように、 Middleware で制御します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | <?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
にユーザーの解決方法をクロージャの形式でセットします。
Handlerの編集
認証エラーになった場合、そのままではリダイレクトされてしまうので、 JSON を返却するように修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | <?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 を登録します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | <?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
に定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | <?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
で、作成した認証処理を使用するように設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php return [ // 以下抜粋 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'admin_users', ], 'api' => [ 'driver' => 'bearer', ], ], ]; |
作成した認証処理を使う
作成した認証処理を使うには、
api.php
内で以下のように記述します。
1 2 3 4 5 6 7 8 9 | <?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 では簡単に実装することができます。