11
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【React Router v6】【Javascript】ログイン状態によるアクセス制御を行う

Last updated at Posted at 2022-07-09

フロントは 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 する。
image.png

※ API 側へ渡す認証トークンは Cookies に格納します。(本記事の趣旨とはズレるため割愛します)

3-2. 実装方法

まず login.js にて、 SessionStorage に認証状態を保持します。認可情報を持たせることで、認証済み状態とします。
react-hook-form を用いたバリデーションを使用していますが、使用しなくてもアクセス制御可能です。

login.js
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 にてルーティング設定をします。
仕組みとしては、コンポーネントのレンダリング前に、認可情報をチェックし、その状態に応じて、コンポーネントをレンダリングしたり、リダイレクトしたりします。
詳細はメソッドの直上にコメントしましたので、是非ご覧ください。

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;

ログアウトにて認可情報を削除する必要があるので、掲載いたします。

logout.js
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. おわりに

最後までお読みいただきありがとうございました。

本記事は正確な内容となるように、可能な限り、調査・表現をしていますが、誤植などが含まれる可能性がございます。
もし、お気づきになられた場合には、ご指摘いただけますと幸いです。

11
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?