🎃

laravelとvueで作る認証機能

2022/09/14に公開

前提

  • backend側はlaravel、frontend側はvueに分離した完全SPAでの機能になります。
  • 意外と調べてみた感じでは完全SPAでの認証機能の説明が少なくて困りましたので、備忘録として残しておこうと思います。
  • ちなみに、laravel側はbreezeを使ってcontrollerやrequestなどの記述はしてあります。
  • 今回はログイン、ログアウト、新規登録のみを作成しております。

概要

  • api側のルート保護は、laralve-sanctumを使用
  • vue側のルート保護は、beforeEachを使用
  • ログイン認証は、sessionを使用

laravel側

  • ほとんどこちらの記事を参考にしました。
  • 一部変更した部分のみ記述していこうと思います。

ログインエラー時のレスポンスの記述方法を変更

  • こちらの記事を参考にして、書き換えました。
    • デフォルトの状態だとエラーのときでも200が返ってしまっていたので、エラーの際はエラーが起きるように変更を加えました。
LoginRequest.php
/**
* Ensure the login request is not rate limited.
*
* @return void
*
* @throws \Illuminate\Validation\ValidationException
*/
public function ensureIsNotRateLimited()
{
	if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
	    return;
	}
event(new Lockout($this));

$seconds = RateLimiter::availableIn($this->throttleKey());

-       throw ValidationException::withMessages([
-           'email' => trans('auth.throttle', [
-                'seconds' => $seconds,
-                'minutes' => ceil($seconds / 60),
-            ]),
-        ]);

	// FIXME: responseに何を詰めるべきか検討
+        $response = response()->json([
+           'status' => Response::HTTP_UNAUTHORIZED,
+            'seconds' => $seconds,
+            'minutes' => ceil($seconds / 60),
+            'errors' => trans('auth.throttle'),
+        ], Response::HTTP_UNAUTHORIZED);
+        throw new HttpResponseException($response);
}

ログイン判定はどうやってやるのか

  • サーバーのセッションにて管理して、クッキーを使ってやり取りする形になります。
  • 以下のように、ログイン済みかどうかをチェックする処理を記述します。
  • 結果的にはシンプルな仕組みだったのですが、この箇所の仕組みがわからず結構苦戦しました。
IsAuthenticatedController.php
public function __invoke()
{
return response()->json(Auth::check());
}

api側のルート保護

  • 以下のようにmiddlewareとしてsanctumを使用することにしました。
    • その他の説明はこちらの記事を参考にしてもらったらと思います。
api.php
Route::post('signup', [RegisteredUserController::class, 'store'])->name('signup');
Route::post('login', [AuthenticatedSessionController::class, 'store'])->name('login');

// NOTE: 認証済みのrequestとして受信できる
Route::group(['middleware' => ['auth:sanctum']], function () {
    // ログアウト処理
    Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])->name('logout');
});
  • sanctum/csrf-cookieというルートが作成されるので、このurlにアクセスするとxsrf-tokenが発行され、そのtokenがヘッダーに含まれているrequestのみ許可するという状態になります。
    • 以下のように他のapiにアクセスする前に、app.vueにてアクセスして、xsrf-tokenをヘッダーにセットするようにすることで、ルート保護を行うことができます。
app.vue
<script setup lang="ts">
await axios
    .get(API_URL + '/sanctum/csrf-cookie')
    .then((res) => {
      if (res.config.headers) {
        useUserStore().setXsrfToken(res.config.headers['X-XSRF-TOKEN'] as string)
      }
      console.log(res)
    })
    .catch((err) => console.log(err))
</script>
  • 以下にてaxios通信のヘッダーに必ずXSRF-TOKENが設定されるようにしてあります。
    • ちなみに、usetStoreにてXSRF-TOKENは管理するようにしています。
    • storeはリロードなどすると消えますが、画面アクセスのタイミングでXSRF-TOKENを取得するようにしているので、問題はないかと思います。
plugins/axios.ts
const axiosInstance: AxiosInstance = axios.create()
axiosInstance.interceptors.request.use((config: AxiosRequestConfig<any>) => {
  config.headers = {
    // TODO: ヘッダーにx-xsrf-tokenを詰めて通信を行いたい
    'X-XSRF-TOKEN': useUserStore().xsrfToken,
  }
  return config
})

axiosInstance.defaults.withCredentials = true
export default axiosInstance

フロント側のルート保護

  • こちらでは、こちらこちらの記事が参考になりました。
  • beforeEachというものを使うのが良いらしいです。
  • beforeEach用のメソッドを作成して、そちらで管理を行うようにします。
authGuard.ts
export const authenticationGuard = (router: Router) => {
  const userStore = useUserStore()

  router.beforeEach(async (to) => {
    const requiresAuth = to.matched.some((record) => record.meta.requiresAuth)
    // NOTE: ルート保護されており且つ、ログインされていない場合はログイン画面に飛ばす
    if (requiresAuth && !(await isAuthenticated())) {
      return { name: UserRoute.LOGIN_ROUTE }
    }
    // TODO: ログイン後に元のルートにリダイレクトさせたい
    // routingStore.setRedirectFrom(to.name)
  })
}
  • これをvue-routerにて登録すれば全てのルーティングにて、上記のメソッドが適用されるようになります。
router/index.ts
const router = createRouter({
  routes: []
})

authenticationGuard(router)

export default router

まとめ

  • こういう感じで、認証系の設定をしました。
  • 色々とわからないことが当初は多かったですが、認証系がわかると通信の理解が深まるなと思いました。
  • この方法がベストプラクティスかは不明ですので、これからも調べつつどのやり方がベターかを模索しようかと思います。

参考記事

Discussion