オフィス狛 技術部の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 件のコメント:
コメントを投稿