フロントは React.js (JavaScript採用)
、バッグエンドは Springboot
で SPA アプリを作成しており、ログイン状態の管理に悩みました。
理由は下記の通りです。
- 認証情報を保持し、アクセス制御を行う機能が React.js にはデフォルトで備わっていない(Vue.jsには備わっている)
- React Route v6 が新しく、情報が少ない
- 情報があったと思ったら TypeScript の情報だった
そこで、自分なりに設計し、実装しました。
1. はじめに|できること
- 未ログイン状態
- ログインページへアクセスした時、ログインページへ遷移する
- ログインページ以外へのアクセスした時、ログインページに遷移する
- ログイン状態
- ログインページへアクセスした時、設定したページ(本記事では「メインページ」)に遷移する
- ログインページ以外へアクセスした時、ログインページに遷移する
2. 対象読者
下記全てに当てはまる人
- SPA アーキテクチャを採用し、フロントのアクセス制御方法に悩んでいる
- React Route v6 を採用している
- JavaScript で実装している
3. 結論
早速、結論を書いていきます。
3-1. アクセス制御の手法
SessionStorage に認可情報を保持し(下図参照)、アクセス時に、認可情報を確認してから Routing する。
※ API 側へ渡す認証トークンは Cookies に格納します。(本記事の趣旨とはズレるため割愛します)
3-2. 実装方法
まず login.js にて、 SessionStorage に認証状態を保持します。認可情報を持たせることで、認証済み状態とします。
※ react-hook-form
を用いたバリデーションを使用していますが、使用しなくてもアクセス制御可能です。
import React from 'react';
import axios from 'axios';
import { useForm } from 'react-hook-form';
import { Link, useNavigate } from 'react-router-dom';
const Login = () => {
const navigate = useNavigate();
// バリデーションのフォームを定義
const { register, handleSubmit, watch, formState: { errors } } = useForm();
// ログインフォームの送信ボタン押下時に実行
const onSubmit = (data) => {
// ログインURIを設定(ホスト名は .env ファイルからインポート)
const loginPath = process.env.REACT_APP_API_HOST + '/login';
// パラメータ設定
var params = new URLSearchParams();
params.append('userName', data.userName);
params.append('password', data.password);
// ログイン API へ POST
axios.post(loginPath , params)
.then((response)=>{
// 認可情報を SessionStorage に設定
sessionStorage.setItem('AUTHORITY', response.headers.authority);
// /main にリダイレクト
navigate('/main')
})
}
return(
<>
<form onSubmit={handleSubmit(onSubmit)}>
<div className='input-area'>
<p className='event-title'>アカウント名</p>
<input type='text' {...register("userName", {required: true})} />
<p>{errors.userName?.type === 'required' && "アカウント名を入力してください。"}</p>
</div>
<div className='input-area'>
<p className='event-title'>パスワード</p>
<input type='password' {...register("password", {required: true})} />
<p>{errors.password?.type === 'required' && "パスワードを入力してください。"}</p>
</div>
<input type="submit" value='ログイン' />
</form>
<Link to='/signup'>新規会員登録はこちら</Link>
</>
)
}
export default Login;
app.js にてルーティング設定をします。
仕組みとしては、コンポーネントのレンダリング前に、認可情報をチェックし、その状態に応じて、コンポーネントをレンダリングしたり、リダイレクトしたりします。
詳細はメソッドの直上にコメントしましたので、是非ご覧ください。
import React from 'react';
import { Redirect, BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import './App.scss';
import Signup from './components/Main';
import Login from './components/Login';
const App = () => {
// 認証確認メソッド
// → 認証されていない場合、ログインページにリダイレクト
const RequireAuth = ( props ) => {
const myAuthority = sessionStorage.getItem('AUTHORITY');
// 権限が「GENERAL」の場合、渡されたコンポーネントをレンダリング
if(myAuthority=="GENERAL"){
return props.component;
}
// 権限がない場合、ログインページへリダイレクト
document.location = "/login";
}
// 非認証確認メソッド
const RequireNoAuth = ( props ) => {
const myAuthority = sessionStorage.getItem('AUTHORITY');
// 権限がない場合、渡されたこのポーネントをレンダリング
// ※ ログインページとユーザ新規登録ページに適用
if(myAuthority == null){
return props.component;
}
// 権限が存在する場合、メディア一覧ページへリダイレクト
document.location = "/main";
}
return (
<Router>
<div className='App'>
<Routes>
<Route path='/login' element={<RequireNoAuth component={<Login />} />} />
<Route path='/main' element={<RequireAuth component={<Main />} />} />
</Routes>
</div>
</Router>
);
}
export default App;
ログアウトにて認可情報を削除する必要があるので、掲載いたします。
import { useEffect } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
function Logout() {
const navigate = useNavigate();
useEffect(() => {
const logoutPath = process.env.REACT_APP_API_HOST + '/logout';
// ログアウト API へ POST
axios.get(logoutPath)
.then((response)=>{
// Cookies の JWTTOKEN のバリューを削除
document.cookie = "JWTTOKEN=; expires=0";
// SessionStorage の認可情報を削除
sessionStorage.removeItem('AUTHORITY');
// ログインページへリダイレクト
navigate('/login')
})
}, []);
}
export default Logout
※ サーバー側から 401 エラーを返された際には
「結論」の中では、ログアウト時のみ、認可情報の削除を行なっていますが、実際には、すべてのAPIコールのエラー時(401エラー)にて、認可情報の削除を行います。(紙面の都合上、割愛させていただきました。)
なぜなら、サーバー側で認証が切れているのに、フロント側にて、認証状態が続くのは、ユーザー状態管理として不適切だからです。
4. 補論:SessionStorage で状態管理を行うのはよくない?
SessonStorage に認証情報を格納するのは、よくないといった意見が散見されます。
SessionStorage や LocalStorage などの WebStorage に置かれた情報は JavaScript を用いて簡単に取り出したり、設定することができるからです。
たしかに、個人情報を含む JWT などの情報を置くことには、僕も反対です。
ただ、今回置くのは、権限情報のみです。
- 情報を盗まれたところで、個人情報を含まないため、問題ない。
- 恣意的に権限情報を SessionStorage に設定された場合、ログインページ以外へアクセスを許してしまうが、API 側へのアクセスを許すわけではないので、情報は取られないので問題ない。(API 側へのアクセスは Cookies に JWT を設定するのが望ましいと考えているが、別の話なので割愛)
上記の理由から、SessionStorage を用いて状態管理を行うことは問題ないと考えております。
5. おわりに
最後までお読みいただきありがとうございました。
本記事は正確な内容となるように、可能な限り、調査・表現をしていますが、誤植などが含まれる可能性がございます。
もし、お気づきになられた場合には、ご指摘いただけますと幸いです。