【React + Laravel】SPAで認証機能と権限管理(fortify+sanctum)
こんにちは。あっきーです。
家の前の自販機が補充された1時間後に商品の1つが売り切れになってて「僕の周りは経済を回しているな」と感じた、フリーランスWebエンジニアです。
今回は、React.jsとLaravelをSPAの認証機能とアクセス管理をやっていきたいと思います。
ちょっと複雑な話なんですが、これができると今後のアプリにも応用が効くのでぜひ理解を深めてほしいと思います。
ここで学べることはこんな感じ。
- APIを利用した認証方法が分かる
- 管理画面のような認証ユーザーのみがアクセスできるページを作れる
SPAについて
まず、SPAについて簡単にまとめておくと、SPAは”Single Page Application”のことで、「1ページでできたアプリ」です。
HTMLファイルは1つのみで、そこにurlに応じて変更すべき部分のみを変えるという設計でできています。
通常なら、画面遷移のたびに1ページ丸々レンダリングしないといけないんですが、SPAは必要部分だけを更新するだけなのでパフォーマンスが良くなります。
ただ、SPAの認証はAPIを叩いて行うので少し複雑になります。
が、頑張って理解していきましょう。
Laravelプロジェクトを作成
まずはLaravelプロジェクトを作成しましょう。
composer create-project laravel/laravel my-app
パッケージのインストール
次にパッケージをいくつかインストールします。
package.json
の中身はこんな感じです。
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "mix",
"watch": "mix watch",
"watch-poll": "mix watch -- --watch-options-poll=1000",
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "mix --production"
},
"devDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@mui/icons-material": "^5.0.4",
"@mui/lab": "^5.0.0-alpha.51",
"@mui/material": "^5.0.3",
"@types/react": "^17.0.29",
"@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.3.1",
"autoprefixer": "^10.3.7",
"axios": "^0.21",
"import-glob-loader": "^1.1.0",
"laravel-mix": "^6.0.6",
"lodash": "^4.17.19",
"postcss": "^8.1.14",
"postcss-import": "^14.0.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hook-form": "^7.17.3",
"react-router": "^5.2.1",
"react-router-dom": "^5.3.0",
"resolve-url-loader": "^4.0.0",
"sass": "^1.42.1",
"sass-loader": "^12.2.0",
"tailwindcss": "^2.2.16",
"ts-loader": "^9.2.6",
"typescript": "^4.4.4"
},
}
npm install
でこれらをインストールします。
また、tailwindcssを使うので以下のコマンドを実行します。
npx tailwindcss init
react
系のパッケージは必須です。
sass
やtailwindcss
やmaterial-ui
は任意ですが、便利なので使ってみることをオススメします。
ここではtypescript
で書くのでこちらもインストールしています。
React.jsやSassの設定
Reactjsやtypescriptなどを使うために、laravel-mixの設定します。
ここは本題とは逸れるので、詳しい説明は省きます。
webpack.mix.js
を開き、以下のように修正。
const mix = require('laravel-mix');
const tailwindcss = require('tailwindcss'); //追加
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel applications. By default, we are compiling the CSS
| file for the application as well as bundling up all the JS files.
|
*/
mix.webpackConfig({ // 追加 -> sassファイルを一括コンパイルする
devtool: "source-map",
module: {
rules: [
{
test: /\.scss/,
loader: 'import-glob-loader'
}
]
}
});
//いくつか変更
mix.ts('resources/ts/app.tsx', 'public/js')
.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css')
.options({
processCssUrls: false,
postCss: [tailwindcss('./tailwind.config.js')],
});
また、resources
フォルダの中にsass
フォルダを作り、この下にapp.scss
を作ります。中身は以下の通り。
@use 'tailwindcss/base';
@use 'tailwindcss/components';
@use 'tailwindcss/utilities';
これでtailwindcssが使えます。
typescriptを使うため、tsconfig.json
を作り編集します。
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"jsx": "react",
"strict": true,
"esModuleInterop": true
}
}
また、resources
フォルダの中にts
フォルダを作成しておきましょう。
以降、react.jsのコードはこのフォルダに書いていきます。
FortifyとSanctumのインストール
Laravelの認証パッケージとして, FortifyとSanctumを使います。
以下二つのコマンドをそれぞれ実行してインストールします。
composer require laravel/fortify
composer require laravel/sanctum
Fortifyの設定
以下のコマンドを実行。
php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"
config/fortify.php
を開き以下の部分を編集。
'prefix' => 'api',
'views' => false,
'features' => [
Features::registration(),
Features::resetPasswords(),
// Features::emailVerification(),
Features::updateProfileInformation(),
Features::updatePasswords(),
// Features::twoFactorAuthentication([
// 'confirmPassword' => true,
// ]),
],
config.app.php
を編集します。
'providers' => [
//いろいろある
App\Providers\FortifyServiceProvider::class, //追加
]
Sanctumの設定
以下のコマンドを実行。
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
app/Http/Kernel.php
を開き、以下の部分を編集
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
routes/api.php
を開き、以下のように編集
Route::middleware(['auth:sanctum'])->get('/user', function (Request $request) {
return $request->user();
});
ルーティング、ビューの設定
SPAのためのルーティングとビューの設定をしていきます。
routes/web.php
を開き、以下のように編集します。
<?php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/{any}', function() {
return view('app');
})->where('any', '.*');
これは、どのURLでアクセスした場合でもapp
ビューを表示するというものです。
SPAは1ページしか用意しないので、そのページにアクセスされるように設定しています。
次にresources/views/app.blade.php
を作成し以下のように編集。
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{config('app.name')}}</title>
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
<div id="app"><div>
</body>
<script src="{{ mix('js/app.js') }}"></script>
</html>
最後に、以下のコマンドを実行してMySQLにテーブルを作成しましょう。
php artisan migrate
以上で初期設定は終了です。長くなりましたが、ここから本題です。
React + Laravelで認証処理を行う
- 以降は
resources/ts
の中でファイル作成・編集していきます。 npm run watch
を実行し、tsxファイル等が常にコンパイルされる状態にしておいてください
まず、TOP, 登録画面とログイン画面、ホーム画面をreactで作ります。
views
フォルダを作成し、この下に
Top.tsx
とRegister.tsx
とLogin.tsx
、Home.tsx
をそれぞれ作成して編集します。
Top.tsx
import React from "react";
import { Link } from "react-router-dom";
const Top = () => {
return (
<div className="p-4">
<ul>
<li><Link to="/register">登録</Link></li>
<li><Link to="/login">ログイン</Link></li>
<li><Link to="/home">ホーム</Link></li>
</ul>
</div>
)
}
export default Top;
Register.tsx
import React, { useState } from "react";
import { Link } from "react-router-dom";
import axios from "axios"
import { useForm } from "react-hook-form";
import TextField from '@mui/material/TextField';
import { LoadingButton } from '@mui/lab';
import { useHistory } from "react-router-dom";
interface EmailAndPasswordData {
email: string,
password: string,
password_confirmation: string
}
const Register = () => {
const { register, handleSubmit, setError, formState: { errors } } = useForm();
const history = useHistory()
const [loading, setLoading] = useState(false);
const onSubmit = (data: EmailAndPasswordData) => {
setLoading(true)
axios.get('/sanctum/csrf-cookie').then(() => {
axios.post('/api/register', data).then(() => {
history.push('home')
}).catch(error => {
console.log(error)
setError('submit', {
type: 'manual',
message: '登録に失敗しました。再度登録をしてください'
})
setLoading(false)
})
})
}
return (
<div className="p-4 max-w-screen-sm mx-auto">
<h1 className="text-center text-xl font-bold">アカウント作成</h1>
<p className="text-center"><Link to="/login" className="text-sm c-link">アカウントを持っている方はこちら</Link></p>
<form className="py-4" onSubmit={handleSubmit(onSubmit)}>
<div className="py-4">
<TextField
fullWidth
variant="outlined"
label="名前"
{...register('name', {
required: '入力してください'
})}
/>
{errors.name && <span className="block text-red-400">{errors.name.message}</span>}
</div>
<div className="py-4">
<TextField
fullWidth
variant="outlined"
label="メールアドレス"
{...register('email', {
required: '入力してください'
})}
/>
{errors.email && <span className="block text-red-400">{errors.email.message}</span>}
</div>
<div className="py-4">
<TextField
fullWidth
id="password"
type="password"
variant="outlined"
label="パスワード"
{...register('password', {
required: '入力してください',
minLength: {
value: 8,
message: '8文字以上で入力してください'
}
})}
/>
{errors.password && <span className="block text-red-400">{errors.password.message}</span>}
</div>
<div className="py-4">
<TextField
fullWidth
type="password"
variant="outlined"
label="パスワード確認"
{...register('password_confirmation', {
required: '入力してください',
validate: {
match: value => value === (document.getElementById('password') as HTMLInputElement).value || 'パスワードが一致しません'
}
})}
/>
{errors.password_confirmation && <span className="block text-red-400">{errors.password_confirmation.message}</span>}
</div>
<div>
<LoadingButton type="submit" loading={loading} variant="contained" fullWidth>アカウントを作成する</LoadingButton>
{errors.submit && <span className="block text-red-400">{errors.submit.message}</span>}
</div>
</form>
</div>
)
}
export default Register
Login.tsx
import React, { useState } from "react";
import { Link } from "react-router-dom";
import axios from "axios"
import { useForm } from "react-hook-form";
import TextField from '@mui/material/TextField';
import { LoadingButton } from '@mui/lab';
import { useHistory } from "react-router";
interface LoginData {
email: string,
password: string,
}
const Login = () => {
const { register, handleSubmit, setError, clearErrors, formState: { errors } } = useForm();
const history = useHistory()
const [loading, setLoading] = useState(false);
const onSubmit = (data: LoginData) => {
setLoading(true)
axios.get('/sanctum/csrf-cookie').then(() => {
axios.post('/api/login', data).then(() => {
history.push('home')
}).catch(error => {
console.log(error)
setError('submit', {
type: 'manual',
message: 'ログインに失敗しました'
})
setLoading(false)
})
})
}
return (
<div className="p-4 max-w-screen-sm mx-auto">
<h1 className="text-center text-xl font-bold pb-4">ログイン</h1>
<p className="text-center"><Link to="/register" className="text-sm c-link">アカウントを持っていない方はこちら</Link></p>
<form className="py-4" onSubmit={e => {clearErrors(); handleSubmit(onSubmit)(e)}}>
<div className="py-4">
<TextField
fullWidth
variant="outlined"
label="メールアドレス"
{...register('email', {
required: '入力してください'
})}
/>
{errors.email && <span className="block text-red-400">{errors.email.message}</span>}
</div>
<div className="py-4">
<TextField
fullWidth
id="password"
type="password"
variant="outlined"
label="パスワード"
{...register('password', {
required: '入力してください'
})}
/>
{errors.password && <span className="block text-red-400">{errors.password.message}</span>}
</div>
<div className="text-center">
<div>
<LoadingButton loading={loading} type="submit" variant="contained" fullWidth>Login</LoadingButton>
</div>
{errors.submit && <span className="block text-red-400">{errors.submit.message}</span>}
</div>
</form>
</div>
)
}
export default Login
Home.tsx
import { Button } from "@mui/material";
import axios from "axios";
import React from "react";
import { useHistory } from "react-router";
const Home = () => {
const history = useHistory();
const logout = () => {
axios.get('/sanctum/csrf-cookie').then(() => {
axios.post('/api/logout', {}).then(() => {
history.push('/login');
})
})
}
return (
<div className="p-4">
<h1>Home</h1>
<Button variant="contained" onClick={logout}>ログアウト</Button>
{/* // アカウント情報を書く */}
</div>
)
}
export default Home;
またapp.tsx
を作成し、以下を追記
import React from 'react'
import ReactDOM from 'react-dom'
import {BrowserRouter, Route, Switch} from 'react-router-dom'
import Home from './views/Home'
import Login from './views/Login'
import Register from './views/Register'
import Top from './views/Top'
const App = () => {
return (
<BrowserRouter>
<div>
<Switch>
<Route path="/" exact component={TOP} />
<Route path="/register" exact component={Register} />
<Route path="/login" exact component={Login} />
<Route path="/home" exact component={Home} />
</Switch>
</div>
</BrowserRouter>
)
}
ReactDOM.render(
<App />,
document.getElementById('app')
)
ローカルサーバーを立ち上げてhttp://127.0.0.1:8000/register
にアクセスしたときに、以下のようになっていればOKです。
登録、ログインもできるので確認してみてください!
React + Laravelでアクセス制限をする
ここからがかなり重要なので1つ1つ確認しつつお願いします。
認証が必要なアプリではほとんどがユーザーによってアクセス制限をかけています。
例えば、管理画面は認証ユーザーのみが入れるようにしていますし、有料・無料プランでアクセスできるページが異なることも多いです。
普通にLaravelで作った場合はBlade TemplateでAuth::check()
等が使えるので割と簡単にできます。
SPAだと初回以外はブラウザによるローディングがありませんので、代わりにページに飛んだらAPIを叩いて認証済みかどうかを判定する必要があります。
今回の場合、以下のようにAPIを呼んでユーザーが取得できたら認証済みか判断できます。
axios.get('/api/user').then((res) => {
//ユーザー情報が取得できる
console.log(res.data)
}).catch((error) => {
//ログインしていない場合はログイン画面に移動させる
history.push('/login');
})
ただ、各ページでAPIを叩いてこれをやるのはちょっと問題があります。
- 各ページでAPIを叩くのは無駄 -> ユーザー情報は多くのページで使うから初回で取得して使いまわしたい
- データ取得まで時間がかかることがあり、その間画面表示ができない -> アクセスした段階でログイン画面にリダイレクトするか否かを判断したい
- そもそも毎回この処理を書くのがダルイ -> 1つにまとめたい
- ユーザー情報を更新した際に、リロードするまで反映されない -> 更新した情報も同時に取得したい
ということで、ユーザー認証に関連する処理を1つにまとめてしまえばすべて解決します。
ひとまず答えを出しておきます。
まずは結論から、
AuthContext.tsx
を作成。
import axios, { AxiosResponse } from "axios";
import React, {useContext, createContext, useState, ReactNode, useEffect} from "react"
import {Route, Redirect, useHistory} from "react-router-dom"
interface User {
id: number
name: string
email: string
email_verified_at: string | null
two_factor_recovery_codes: string | null
two_factor_secret: string | null
created_at: string
updated_at: string | null
}
interface LoginData {
email: string,
password: string,
}
interface RegisterData {
email: string,
password: string,
password_confirmation: string,
}
interface ProfileData {
name?: string,
email?: string
}
interface authProps {
user: User | null;
register: (registerData: RegisterData) => Promise<void>
signin: (loginData: LoginData) => Promise<void>;
signout: () => Promise<void>;
saveProfile: (formData: FormData | ProfileData) => Promise<void>;
}
interface Props {
children: ReactNode
}
interface RouteProps {
children: ReactNode,
path: string,
exact?: boolean
}
interface From {
from: Location
}
const authContext = createContext<authProps | null>(null)
const ProvideAuth = ({children}: Props) => {
const auth = useProvideAuth();
return (
<authContext.Provider value={auth}>
{children}
</authContext.Provider>
)
}
export default ProvideAuth
export const useAuth = () => {
return useContext(authContext)
}
const useProvideAuth = () => {
const [user, setUser] = useState<User | null>(null);
const register = (registerData: RegisterData) => {
return axios.post('/api/register', registerData).then((res) => {
axios.get('api/user').then((res) => {
setUser(res.data)
})
})
}
const signin = async (loginData: LoginData) => {
try {
const res = await axios.post('/api/login', loginData);
} catch (error) {
throw error;
}
return axios.get('/api/user').then((res) => {
setUser(res.data)
}).catch((error) => {
setUser(null)
})
}
const signout = () => {
return axios.post('/api/logout', {}).then(() => {
setUser(null)
})
}
const saveProfile = async (formData: FormData | ProfileData) => {
const res = await axios.post(
'/api/user/profile-information',
formData,
{headers: {'X-HTTP-Method-Override': 'PUT'}}
)
.catch((error) => {
throw error;
})
if(res?.status == 200) {
return axios.get('/api/user').then((res) => {
setUser(res.data)
}).catch((error) => {
setUser(null)
})
}
}
useEffect(() => {
axios.get('/api/user').then((res) => {
setUser(res.data)
}).catch((error) => {
setUser(null)
})
}, [])
return {
user,
register,
signin,
signout,
saveProfile
}
}
/**
* 認証済みのみアクセス可能
*/
export const PrivateRoute = ({children, path, exact = false}: RouteProps) => {
const auth = useAuth()
return (
<Route
path={path}
exact={exact}
render={({ location }) => {
if(auth?.user == null) {
return <Redirect to={{ pathname: "/login", state: { from: location }}}/>
} else {
return children
}
}}
/>
)
}
/**
* 認証していない場合のみアクセス可能(ログイン画面など)
*/
export const PublicRoute = ({children, path, exact = false}: RouteProps) => {
const auth = useAuth()
const history = useHistory()
return (
<Route
path={path}
exact={exact}
render={({ location }) => {
if(auth?.user == null) {
return children
} else {
return <Redirect to={{pathname: (history.location.state as From) ? (history.location.state as From).from.pathname : '/' , state: { from: location }}}/>
}
}}
/>
)
}
App.tsx
を編集
import React from 'react'
import ReactDOM from 'react-dom'
import {BrowserRouter, Route, Switch} from 'react-router-dom'
import Home from './views/Home'
import Login from './views/Login'
import Register from './views/Register'
import Top from './views/Top'
import ProvideAuth, { PrivateRoute, PublicRoute } from './AuthContext' //追加
const App = () => { // 編集
return (
<ProvideAuth>
<BrowserRouter>
<div>
<Switch>
<Route path="/" exact><Top /></Route>
<PublicRoute path="/register" exact><Register/></PublicRoute>
<PublicRoute path="/login" exact><Login/></PublicRoute>
<PrivateRoute path="/home" exact><Home/></PrivateRoute>
</Switch>
</div>
</BrowserRouter>
</ProvideAuth>
)
}
ReactDOM.render(
<App />,
document.getElementById('app')
)
Register.tsx
を編集
import {useAuth} from "../AuthContext"; // 追加
const Register = () => {
const { register, handleSubmit, setError, formState: { errors } } = useForm();
const history = useHistory()
const [loading, setLoading] = useState(false);
const auth = useAuth() // 追加
const onSubmit = (data: EmailAndPasswordData) => {
setLoading(true)
axios.get('/sanctum/csrf-cookie').then(() => {
auth?.register(data).then(() => { //編集
history.push('home')
}).catch((error) => {
setError('submit', {
type: 'manual',
message: '登録に失敗しました。再度登録をしてください'
})
setLoading(false)
})
})
}
//以下省略
Login.tsx
を編集
import React, { useState } from "react";
import { Link } from "react-router-dom";
import axios from "axios"
import { useForm } from "react-hook-form";
import TextField from '@mui/material/TextField';
import { LoadingButton } from '@mui/lab';
import { useHistory } from "react-router";
import {useAuth} from "../AuthContext"; //追加
interface LoginData {
email: string,
password: string,
}
const Login = () => {
const { register, handleSubmit, setError, clearErrors, formState: { errors } } = useForm();
const history = useHistory()
const [loading, setLoading] = useState(false);
const auth = useAuth(); //追加
const onSubmit = (data: LoginData) => {
setLoading(true)
axios.get('/sanctum/csrf-cookie').then(() => { //編集
auth?.signin(data).then(() => {
history.push('home')
}).catch(error => {
console.log(error)
setError('submit', {
type: 'manual',
message: 'ログインに失敗しました'
})
setLoading(false)
})
})
}
Home.tsx
を編集
import { Button } from "@mui/material";
import axios from "axios";
import React from "react";
import { useHistory } from "react-router";
import { useAuth } from "../AuthContext"; //追加
const Home = () => {
const history = useHistory();
const auth = useAuth(); // 追加
const logout = () => {
axios.get('/sanctum/csrf-cookie').then(() => {
auth?.signout().then(() => { // 編集
history.push('/login');
})
})
}
return (
<div className="p-4">
<h1>Home</h1>
<p>Hello! {auth?.user?.name}</p> // 追加
<Button variant="contained" onClick={logout}>ログアウト</Button>
</div>
)
}
export default Home;
これで、アクセス制限ができます。
認証とアクセス制限の解説
コードの解説をしていきたいと思います。
AuthContext.tsx
について
このファイルで「SPA認証」と「アクセス制限」をしています。
ProvideAuth
でアクセス制限を行う
ProvideAuth
関数の内容はこうなってます。
const ProvideAuth = ({children}: Props) => {
const auth = useProvideAuth();
return (
<authContext.Provider value={auth}>
{children}
</authContext.Provider>
)
}
export default ProvideAuth
auth
は認証ユーザーのデータを持っています。useProvideAuth()
はまだ定義していないですが、後程やります。
また、ここではReact Contextを使っています。これはざっくり説明すると「下のコンポーネントに値を渡す機能」です。
Contextを利用するために、createContext
で新規作成する必要があります。
const authContext = createContext<authProps | null>(null)
これの意味はこんな感じです。
- 以降
authContext
を使った中では、どのコンポーネントでも値を渡すことができる - 渡す値の型は
authProps
またはnull
で初期値はnull
(authProps
は自分で定義しています。)
authContext.Provider
タグで囲った中ではどのコンポーネントからでもauth
が使えるということです。
// childrenにはコンポーネントが入り、それらすべてがauthという値を使うことができる
<authContext.Provider value={auth}>
{children}
</authContext.Provider>
useAuth
はユーザー情報を取得す
useAuth
関数はこうなっています。
export const useAuth = () => {
return useContext(authContext)
}
非常にシンプルです。これは各コンポーネントでauth
の値を取得する処理です(フックです)。
さっき見たContext
が出てきていますね。Contextによって渡された値を使うにはuseContext
を使う必要があります。
今示した2つが、さっきの問題にもあった「認証ユーザ情報を使いまわしたい」というのを実現しています。
useProvideAuth
はユーザー情報の処理を行う
useProvideAuth
には登録、ログイン、ユーザー情報取得といった、ユーザー情報に関連した処理を一式まとめています。
const [user, setUser] = useState<User | null>(null);
最初にこんなコードがありますが、これはReact State(状態管理)を利用しています。
ユーザー情報(名前やメールアドレスなど)がuser
に入ります。
const register = (registerData: RegisterData) => {
}
const signin = async (loginData: LoginData) => {
}
const signout = () => {
}
const saveProfile = async (formData: FormData | ProfileData) => {
}
これらはそれぞれ、登録、ログイン、ログアウト、ユーザー情報更新を行っています。
最初にRegister.tsx
などに書いた処理を、ここで行っています。
書く処理でsetUser(res.data)
のようにすることで、先ほどみた
これがあることで、リロードしなくてもユーザー情報が更新されるので、SPAのようなリロードしないアプリでもユーザー情報を引き継ぐことができます。
useEffect(() => {
axios.get('/api/user').then((res) => {
setUser(res.data)
}).catch((error) => {
setUser(null)
})
}, [])
ここでは、初回アクセス時にユーザーの情報を取得しています。
useEffect
はReactフックの一つで、「レンダリングされた後に処理を行う」というものです。
上記のように書くことで「初回だけユーザー情報を取得する」ということができます。
これは最初に上げた問題「初回だけユーザー情報を取得して、サーバーの負荷を減らす」というのを解決しています。
ちなみに、「値を取得するだけならuseEffect
いらないんじゃね?」と思うかもですが、これを使わないと、無限ループしてしまい何度もユーザー情報を取得することになってしまします。
return {
user,
register,
signin,
signout,
saveProfile
}
最後にユーザー情報処理、およびユーザー情報を返します。
さっきみたProvideAuth
で以下のコードがありました。
const auth = useProvideAuth();
もうわかりますね。auth
はユーザー処理関数とユーザー情報が入っています。
PrivateRoute
とPublicRoute
でアクセス制限をする
最後にPrivateRoute
とPublicRoute
です。
PrivateRoute
では「認証ユーザーのみアクセス可能」としています。
export const PrivateRoute = ({children, path, exact = false}: RouteProps) => {
const auth = useAuth()
return (
<Route
path={path}
exact={exact}
render={({ location }) => {
if(auth?.user == null) {
return <Redirect to={{ pathname: "/login", state: { from: location }}}/>
} else {
return children
}
}}
/>
)
}
まず、auth
にユーザー情報を入れています。
そして、auth.user == null
つまり、ユーザー情報がない場合は認証されていないので、ログイン画面にリダイレクトします。
そうでなければ、本来の画面を表示します。
PublicRoute
では「非認証ユーザーのみがアクセス可能」を表しています。
これはさっきの逆です。
PrivateRoute
は管理画面などに使えて、PublicRoute
はログイン画面などに使えます。
App.tsx
について
App.tsx
ではルーティングを行っているので、このファイル内で先ほどのアクセス制限をやっていきます。
import ProvideAuth, { PrivateRoute, PublicRoute } from './AuthContext'
const App = () => {
return (
<ProvideAuth>
<BrowserRouter>
<div>
<Switch>
<Route path="/" exact><Top /></Route>
<PublicRoute path="/register" exact><Register/></PublicRoute>
<PublicRoute path="/login" exact><Login/></PublicRoute>
<PrivateRoute path="/home" exact><Home/></PrivateRoute>
</Switch>
</div>
</BrowserRouter>
</ProvideAuth>
)
}
まず全体をProvideAuth
で囲っています。
復習すると、ProvideAuth
はContextを利用しており、「auth
(ユーザー処理関数とユーザー情報を持つ)をProvideAuth
内のコンポーネントすべてで使える」ようにしています。
各ページのルーティングですが以下の通りです。
top
は誰でもOKなので、Route
register
とlogin
は非認証のみなので、PublicRoute
home
は認証のみなので、PrivateRoute
これで、アクセス制限ができました。試しに、ログインしてない状態でHomeに行こうとしてください。
ログイン画面にリダイレクトされるはずです。
登録、ログイン、ログアウトの処理を編集
最後です。
先ほどのAuthContext.tsx
に書いた認証処理を各ページで呼び出して使いましょう。
Register.tsx
import {useAuth} from "../AuthContext"; // useAuthはユーザーに関する処理をまとめたもの
const onSubmit = (data: EmailAndPasswordData) => {
setLoading(true)
axios.get('/sanctum/csrf-cookie').then(() => {
auth?.register(data).then(() => { // authの中のregisterを実行しアカウント作成
history.push('home')
}).catch((error) => {
setError('submit', {
type: 'manual',
message: '登録に失敗しました。再度登録をしてください'
})
setLoading(false)
})
})
}
Login.tsx
import {useAuth} from "../AuthContext"; // 追加
const onSubmit = (data: LoginData) => {
setLoading(true)
axios.get('/sanctum/csrf-cookie').then(() => {
auth?.signin(data).then(() => { // ここ編集
history.push('home')
}).catch(error => {
console.log(error)
setError('submit', {
type: 'manual',
message: 'ログインに失敗しました'
})
setLoading(false)
})
})
}
Home.tsx
import { useAuth } from "../AuthContext"; // 追加
const Home = () => {
const history = useHistory();
const auth = useAuth(); // 追加
const logout = () => {
axios.get('/sanctum/csrf-cookie').then(() => {
auth?.signout().then(() => { // 編集
history.push('/login');
})
})
}
return (
<div className="p-4">
<h1>Home</h1>
<p>Hello! {auth?.user?.name}</p> // 追加
<Button variant="contained" onClick={logout}>ログアウト</Button>
</div>
)
}
export default Home;
これで完成です。
まとめ
お疲れ様でした。めっちゃ長くなりましたが、これは最初だけ理解できればだいたいのアプリに応用できます。
今回はLaravelでしたが、Firebaseなどでも同じです。登録やログインの処理を少し変えるだけで同様のことができます。
最後にGitHubにコード例を置いとくので困ったときは見てください。
https://github.com/S-Akinori/laravel-react-auth
それでは。
スポンサードサーチ
人気記事英語学習用SNSをLaravelで作ってみた【システム解説あり】