環境
- windows10
- DockerDesktop for Win 3.5.x
- Laravel 8.x
- PHP 8.x
- node 16.13.1
- npm 8.1.2
- TypeScript 4.5
- React 17.0.1
- Next.js 12.0.7
- Recoil 0.5.2
- VsCode
- gitbash 2.32.0.1
構成
API
Laravel Sanctumを用いたクッキーベースの認証APIを作成する
Frontend
Next.jsからaxiosでAPIへHttpリクエストを送る
処理の流れ
Csrfトークンの初期化
認証(サインイン)のリクエスト
セッション切れの場合
実装
API構築
- Authコントローラー(api\laravel-api\app\Http\Controllers\Auth\AuthController.php)の作成
・loginはリクエストでメールアドレスとパスワードを受け取り、認証に成功したらuser情報を返却する
・logoutは、認証状態をログアウトし、セッションを再生成する
・registerは、ユーザー名とメールアドレスとパスワードを受け取ってユーザーをログインさせ、ユーザー情報を返却する<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Models\ModelUser; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; class AuthController extends Controller { public function login(Request $request) { $credentials = $request->validate([ "email" => ["required", "email"], "password" => ["required"], ]); if (Auth::attempt($credentials)) { $request->session()->regenerate(); return response()->json(Auth::user()); } return response()->json([], 401); } public function logout(Request $request) { Auth::logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); return response()->json(true); } public function register(Request $request) { $user = ModelUser::create([ "name" => $request->username, "email" => $request->email, "password" => Hash::make($request->password), ]); Auth::login($user); return response()->json($user, 200); } }
- Apiを作成する
・ガード不要として、それぞれのルートを設定する<?php use App\Http\Controllers\Auth\AuthController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; Route::post("/login", [AuthController::class, "login"]); Route::post("/logout", [AuthController::class, "logout"]); Route::post("/register", [AuthController::class, "register"]); Route::middleware("auth:sanctum")->get("/user", function (Request $request) { return $request->user(); });
- sanctumのセットアップ
laravelでクッキーベースの認証を行う際はsanctumを使用した認証が可能です。詳細はこちらの記事にまとめていますので参照のうえ、初期設定を行ってください
認証のリクエスト処理
- カスタムフックを作成する(frontend\next-web\src\components\shared\function\Auth\AuthLisnter.tsx)
・ログイン、ログアウト、新規登録、認証のリッスン用のそれぞれの関数を用意するimport React, { useCallback, useState, useEffect, useContext } from 'react' import axios from 'libs/axios' import { useRouter } from 'next/router' import { useRecoilState } from 'recoil' import { IUserState, userState } from 'components/store/atom/auth' import { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios' import userData from '../../../../types/user/user.json' import Route from '../../variable/Route' type USER = typeof userData export const useHandleLogin = () => { const [user, setUser] = useRecoilState<IUserState>(userState) const router = useRouter() const handleLogin: (email: string, password: string) => void = (email, password) => { const options: AxiosRequestConfig = { url: '/api/login', method: 'POST', params: { email, password } } axios.get('/sanctum/csrf-cookie').then((res: AxiosResponse) => { axios(options) .then((res: AxiosResponse<USER>) => { const user = res.data setUser({ id: user.id, name: user.name, email: user.email, isSignedIn: true }) router.push('/') }) .catch((error: AxiosError) => { console.log(error) }) }) } return { handleLogin } } export const useHandleLogout = () => { const [user, setUser] = useRecoilState<IUserState>(userState) const router = useRouter() const options: AxiosRequestConfig = { url: '/api/logout', method: 'POST' } return () => { axios.get('/sanctum/csrf-cookie').then((res) => { axios(options) .then((res: AxiosResponse<boolean>) => { setUser({ id: null, name: '', email: '', isSignedIn: false }) router.push('/login') }) .catch((error: AxiosError) => { console.log(error) }) }) } } export const useHandleRegister = () => { const [user, setUser] = useRecoilState<IUserState>(userState) const router = useRouter() const handleRegister = (userName: string, email: string, password: string): void => { const options: AxiosRequestConfig = { url: '/api/register', method: 'POST', params: { username: userName, email: email, password: password } } axios.get('/sanctum/csrf-cookie').then((res) => { axios(options) .then((res: AxiosResponse<USER>) => { const user = res.data setUser({ id: user.id, name: user.name, email: user.email, isSignedIn: true }) router.push('/') }) .catch((error: AxiosError) => { console.log(error) }) }) } return { handleRegister } } export const useListenAuthState = () => { const [user, setUser] = useRecoilState<IUserState>(userState) const router = useRouter() const listenAuthState = (): void => { const options: AxiosRequestConfig = { url: '/api/user', method: 'GET' } axios(options) .then((res: AxiosResponse<USER>) => { const user = res.data console.log(user) // 認証状態で認証不要ルートに遷移した場合 if (Route.noGuardedRoutes().includes(router.pathname)) { router.push('/') } setUser({ id: user.id, name: user.name, email: user.email, isSignedIn: true }) }) .catch((error: AxiosError) => { setUser({ id: null, name: '', email: '', isSignedIn: false }) // TODO: セッションタイムアウト(401)時の処理 if (Route.noGuardedRoutes().includes(router.pathname)) { router.push(router.pathname) } else { router.push('/login') } }) } return { listenAuthState } }
- ログイン(frontend\next-web\src\components\atomic\Pages\Auth\LoginPage.tsx)
・ログインボタン押下でhandleLoginメソッドが実行されるようにimport { TextInput, Spacer } from 'components/atomic/Atoms' import { useHandleLogin } from 'components/shared/function/Auth/AuthLisnter' import BasicButton from 'components/atomic/Atoms/Button/BasicButton' import BasicContainer from 'components/atomic/Atoms/Container/BasicContainer' import React, { useCallback, useState } from 'react' import BasicHeadingThree from 'components/atomic/Atoms/Heading/BasicHeadingThree' import BasicLink from 'components/atomic/Atoms/Link/BasicLink' const LoginPage = () => { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const { handleLogin } = useHandleLogin() const inputEmail = useCallback( (event: React.ChangeEvent<HTMLInputElement>) => { setEmail(event.target.value) }, // eslint-disable-next-line react-hooks/exhaustive-deps [email] ) const inputPassword = useCallback( (event: React.ChangeEvent<HTMLInputElement>) => { setPassword(event.target.value) }, // eslint-disable-next-line react-hooks/exhaustive-deps [password] ) return ( <BasicContainer className={'flex flex-col h-1/2 space-y-6 w-1/2 mx-auto mt-32 max-w-sm'}> <BasicHeadingThree text="ログイン画面" className=" text-center" /> <TextInput onChange={inputEmail} placeholder="メールアドレス" required={true} type={'email'} value={email} /> <TextInput onChange={inputPassword} placeholder="パスワード" required={true} type={'password'} value={password} /> <Spacer className="h-20" /> <BasicButton bg="BLUE" buttonName="ログイン" className="" onClick={() => handleLogin(email, password)} /> <BasicButton bg="RED" buttonName="Googleアカウントでログイン" className="" onClick={() => handleLogin(email, password)} /> <Spacer className="h-20" /> <BasicLink className=" text-blue-400 text-xs text-center" href={{ pathname: '/register', query: null }} name={'アカウントをお持ちでない方はこちら'} /> </BasicContainer> ) } export default LoginPage
- 新規会員登録(frontend\next-web\src\components\atomic\Pages\Auth\RegisterPage.tsx)
・登録するボタンを押下でhandleRegisterメソッドが実行されるようにimport BasicLink from 'components/atomic/Atoms/Link/BasicLink' import React, { useCallback, useState, useEffect, useContext } from 'react' import { useHandleRegister } from 'components/shared/function/Auth/AuthLisnter' import { Spacer, TextInput } from 'components/atomic/Atoms' import BasicContainer from 'components/atomic/Atoms/Container/BasicContainer' import BasicHeadingThree from 'components/atomic/Atoms/Heading/BasicHeadingThree' import BasicButton from 'components/atomic/Atoms/Button/BasicButton' const RegisterPage = () => { const [userName, setUserName] = useState('') const [email, setEmail] = useState('') const [password, setPassword] = useState('') const { handleRegister } = useHandleRegister() const inputUserName = useCallback( (event) => { setUserName(event.target.value) }, // eslint-disable-next-line react-hooks/exhaustive-deps [userName] ) const inputEmail = useCallback( (event) => { setEmail(event.target.value) }, // eslint-disable-next-line react-hooks/exhaustive-deps [email] ) const inputPassword = useCallback( (event) => { setPassword(event.target.value) }, // eslint-disable-next-line react-hooks/exhaustive-deps [password] ) return ( <BasicContainer className={'flex flex-col h-1/2 space-y-6 w-1/2 mx-auto mt-32 max-w-sm'}> <BasicHeadingThree text="会員登録画面" className=" text-center" /> <TextInput type={'text'} placeholder="ユーザー名" value={userName} className={''} onChange={inputUserName} required={true} /> <TextInput onChange={inputEmail} placeholder="メールアドレス" required={true} type={'email'} value={email} /> <TextInput onChange={inputPassword} placeholder="パスワード" required={true} type={'password'} value={password} /> <Spacer className="h-15" /> <BasicButton bg="BLUE" buttonName="登録する" className="" onClick={() => handleRegister(userName, email, password)} /> <Spacer className="h-20" /> <BasicLink className=" text-blue-400 text-xs text-center" href={{ pathname: '/login', query: null }} name={'アカウントをお持ちの方はこちら'} /> </BasicContainer> ) } export default RegisterPage
- ログアウト(ナビゲーションバー等の任意の場所に配置して呼び出す)
import { useHandleLogout } from '../../shared/function/Auth/AuthLisnter' const handleLogout = useHandleLogout() .. <span className={'block px-4 py-2 text-sm text-gray-700 cursor-pointer'} onClick={handleLogout} > ログアウト </span>
- 認証のリッスン(frontend\next-web\src\pages\_app.tsx)
・共通の処理なので、AuthProviderコンポーネントを作成し、<Component {...pageProps} />をラップするimport React from 'react' import { RecoilRoot } from 'recoil' import AuthProvider from '../components/provider/AuthProvider' import ErrorBoundary from 'components/shared/error/ErrorBoundary' import NavBars from 'components/atomic/Organisms/NavBars' const MyApp = ({ Component, pageProps }) => { return ( <ErrorBoundary> <RecoilRoot> <AuthProvider> <NavBars /> <div className="container mx-auto h-full"> <Component {...pageProps} /> </div> </AuthProvider> </RecoilRoot> </ErrorBoundary> ) } export default MyApp
AuthProviderコンポーネント(frontend\next-web\src\components\provider\AuthProvider.tsx)
・useEffectで第二引数を[]で指定し、初回レンダリング(ブラウザリロード)時のみlistenAuthStateメソッドが実行されるように指定import React, { useCallback, useState, useEffect } from 'react' import { useListenAuthState } from 'components/shared/function/Auth/AuthLisnter' const AuthProvider = ({ children }) => { const { listenAuthState } = useListenAuthState() useEffect(() => { listenAuthState() }, []) return children } export default AuthProvider
以上です
まとめ
いかがでしたでしょうか。本記事では、Next.jsの開発において、SSR時の初回レンダリングでqueryがundefinedになる際の対処法について紹介しています