オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。
Webシステムにおける認証機能、重要ですが、作成するの大変ですよね・・・・。
その悩みを解決するのがLaravelでは、「Jetstream + Fortify」な訳ですが、今回、「フロントとバックエンドを完全に分離したいので、認証機能はAPIで呼び出す方式にして欲しい」との要望があり、Jetstream + Fortifyを画面無し(APIとして)で実装することになったので、記録に残しておこうと思います。
(1)各バージョンと前提
まずは、バージョンですが、・Laravel 8.27.0
・PHP 8.0.9
となります。
そして、前提条件ですが、
Laravel Jetstreamの設定が完了していて、画面を経由してのログインなどは出来ている
とします。
(2)Fortify設定値変更
早速対応方法としては・・・・Fortifyの設定値の変更となり、[config/fortify.php]
// Viewを使用しない場合、trueからfalseに変更 'views' => false,で、完了、以上。
・・・・としたいところなのですが、そうも行きません。
(3)パスワードリセットのルート定義追加
まずは、本家サイトにも記載がありますが、パスワードリセットのルートは定義する必要があります。[routes/web.php]
/* | FortifyのViewを使用しない場合でも、「password.reset」のルートは定義する必要がある。 | https://laravel.com/docs/8.x/fortify#disabling-views-and-password-reset */ Route::get('/reset-password/{token}', ResetPasswordController::class) ->name('password.reset');
ResetPasswordControllerは新規で作成します
[app/Http/Controllers/ResetPasswordController.php](新規作成)
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class ResetPasswordController extends Controller { public function __invoke(Request $request) { // メールアドレスとトークンを返却する $result = [ 'email' => $request->email, 'token' => $request->token ]; return response()->json($result); } }
さて、ここで疑問なのですが、なぜ、ルート定義する必要があるのでしょうか?
それを説明する為に、LaravelとFortifyの中を覗いてみましょう
[vendor/laravel/fortify/routes/routes.php](抜粋)
if (Features::enabled(Features::resetPasswords())) { if ($enableViews) { Route::get('/forgot-password', [PasswordResetLinkController::class, 'create']) ->middleware(['guest']) ->name('password.request'); Route::get('/reset-password/{token}', [NewPasswordController::class, 'create']) ->middleware(['guest']) ->name('password.reset'); } Route::post('/forgot-password', [PasswordResetLinkController::class, 'store']) ->middleware(['guest']) ->name('password.email'); Route::post('/reset-password', [NewPasswordController::class, 'store']) ->middleware(['guest']) ->name('password.update'); }抜粋して記載していますが、問題の「password.reset」、ありますね。
ただ、よく見てみると、「if ($enableViews) {」となっているので、Viewが無効になっていると定義されない、と言うことが分かります。
なるほど、だから、自分達で定義する必要があると。では、なぜ定義が必要なのか、さらに見てみると、
[vendor/laravel/framework/src/Illuminate/Auth/Notifications/ResetPassword.php](抜粋)
public function toMail($notifiable) { if (static::$toMailCallback) { return call_user_func(static::$toMailCallback, $notifiable, $this->token); } if (static::$createUrlCallback) { $url = call_user_func(static::$createUrlCallback, $notifiable, $this->token); } else { $url = url(route('password.reset', [ 'token' => $this->token, 'email' => $notifiable->getEmailForPasswordReset(), ], false)); } return $this->buildMailMessage($url); }これは、パスワードリセットをした時にメールを送る処理ですね。
ここで「password.reset」のルート定義を使ってURLを作っている訳です。
なので、定義は必要になる、ということですね。
これで、今度こそ画面無し(API化)完了・・・・とならないんですね、これが。
(4)未認証・認証済みの処理追加
ちょっと微妙な調整が必要になります。そもそもの動きとして、画面有りのJetstream + Fortifyでは、
・ログイン状態で、ログイン画面に遷移しようとすると、ホーム画面へリダイレクトする
・未ログイン状態で、ログインが必要な画面に遷移すると、ログイン画面へリダイレクトする
という動きになります。
実はこの動き、
[config/fortify.php]
// Viewを使用しない場合、trueからfalseに変更 'views' => false,の設定をしても変わりません。
API化した状態で考えると、
・バックエンド側でログイン状態(認証済み)の時に、フロントからログインAPIなど未ログイン状態が前提のAPIを呼ばれると、ホーム画面へのリダイレクト指示をレスポンスする
・バックエンド側で未ログイン状態(未認証)の時に、ログイン済み必須なAPIを呼ばれると、ログイン画面へのリダイレクト指示をレスポンスする
となってしまいます。
これは何とかしたいですね。
という事で、まずは前者の
認証済みの時に、ログインAPIなど未ログイン状態が前提のAPIを呼んだ場合、です。
この場合、MiddlewareのRedirectIfAuthenticated.phpに処理が書かれています。
[app/Http/Middleware/RedirectIfAuthenticated.php]
public function handle(Request $request, Closure $next, ...$guards) { $guards = empty($guards) ? [null] : $guards; foreach ($guards as $guard) { if (Auth::guard($guard)->check()) { return redirect(RouteServiceProvider::HOME); } } return $next($request); }なるほど、確かにホーム画面にリダイレクトしていますね。
では、早速修正して・・・・・
[app/Http/Middleware/RedirectIfAuthenticated.php](修正後)
public function handle(Request $request, Closure $next, ...$guards) { $guards = empty($guards) ? [null] : $guards; foreach ($guards as $guard) { if (Auth::guard($guard)->check()) { if ($request->expectsJson()) { // クライアントからJSONレスポンスを要求されている場合、リダイレクトさせず、JSON形式メッセージをレスポンスする。 return response()->json(['message' => 'Already authenticated.'], 200); } return redirect(RouteServiceProvider::HOME); } } return $next($request); }と、こんな感じで、APIとして呼ばれた場合、「Already authenticated.」をレスポンスするようにしました。
後は、フロント側で良きに計らってもらうようにしましょう。
次は、
未認証の時に、ログイン済み必須なAPIを呼ばれた場合、です。
この場合、MiddlewareのAuthenticate.phpに処理が書かれています。
[app/Http/Middleware/Authenticate.php]
protected function redirectTo($request) { if (! $request->expectsJson()) { return route('login'); } }あ、でも、これを見る限り、APIとして呼ばれた場合は、ログイン画面のリダイレクトはレスポンスされない気がしますね。
何もレスポンスされないのも困ってしまうので、こちらは、
[app/Http/Middleware/Authenticate.php](修正後)
protected function redirectTo($request) { return $request->expectsJson() ? response()->json(['message' => 'Unauthenticated.'], 401) : route('login'); }このように、401エラーとして、Unauthenticated.を返却するようにしました。
こちらも、後は、フロント側で良きに計らってもらうようにしましょう。
ということで、API化完了となります。
細かい部分はまだまだやる事(Viewを消したり、等々)があるのですが、今回はこのぐらいで十分かと思います。
LaravelのJetstream、Fortifyはかなり便利なので、どんどん使っていきたいですね。
Laravel
0 件のコメント:
コメントを投稿