🌀

React:React Router v6 で 認証されていないユーザーや権限がないユーザーをリダイレクトする

2022/02/03に公開約11,200字

2022年02月02日 Windows11での情報です。

今回は、React Router v6 で 認証されていないユーザーに見せたくないページや、権限がないユーザーに見せたくないページにアクセスされた場合、ログインページなど指定のページへリダイレクトする方法です。

参考にしたのはコチラ

https://reactrouter.com/docs/en/v6/examples/auth

React Router v6 の基本的なルーティング方法についてはコチラで記事にしました。
React: React Router v6 でルーティングする step1
React: React Router v6 でルーティングする step2

環境

  • vite: v2.7.2
  • node: v16.13.2
  • react: v17.0.2
  • typescript: v4.4.4
  • react-router-dom: v6.2.1

プロジェクト作成

ビルドツールViteを使用してプロジェクトを作成しています。
create-react-appでプロジェクトを作成している場合は、ファイルやコマンドが異なる場合がありますので、適宜読み替えてください。

viteでプロジェクト「router-auth-app」を作成するコマンド

npm init vite router-auth-app
cd router-auth-app
npm install
npm install react-router-dom
code .

認証や権限で制御をしない状態のページング

まずはユーザーの認証状態や権限でページのアクセス制御をしない状態のページングを作成します。
最終目標は、Page2を認証されているユーザーのみアクセス可能にし、Page3とその子コンポーネントはAdmin権限またはManager権限のユーザーのみアクセス可能とします。

フォルダ構成

components/Layout.tsx
import React from "react";
import { Outlet, Link } from "react-router-dom";

export const Layout:React.VFC = () => {
  return (
  <>
    <h3>Layout</h3>
    <ul>
      <li><Link to="/">show home</Link></li>
      <li><Link to="page1">show page1</Link></li>
      <li><Link to="page2">show page2</Link><span> ( 認証済みのユーザーなら可 )</span></li>
      <li><Link to="page3">show page3</Link><span> ( Admin または Manager 権限のユーザーのみ可 )</span></li>
    </ul>

    <Outlet />
  </>
  );}
components/Home.tsx
import React from "react";

export const Home:React.VFC = () => {
  return <h3>Home</h3>
}
components/Page1.tsx
import React from "react";

export const Page1:React.VFC = () => {
  return <h3>Page 1</h3>
}
components/Page2.tsx
import React from "react";

export const Page2:React.VFC = () => {
  return <h3>Page 2</h3>
}
components/Page3.tsx
import React from "react";
import { Outlet, Link } from "react-router-dom";

export const Page3:React.VFC = () => {
  return (
    <>
      <h3>Page 3</h3>
      <ul>
        <li><Link to="child1">show child1 page1</Link></li>
        <li><Link to="child2">show child2 page2</Link></li>
      </ul>
      <Outlet />
    </>
  );
}

export const Page3Child1:React.VFC = () => {
  return <h3>Page 3 Child1</h3>
}
export const Page3Child2:React.VFC = () => {
  return <h3>Page 3 Child2</h3>
}
components/Login.tsx
import React from "react";

export const Login:React.VFC = () => {
  return <h3>Login</h3>
}
components/NotFound.tsx
import React from "react";

export const NotFound = () => {
  return (
    <>
      <h1>404</h1>
      <h3>お探しのページは見つかりませんでした。</h3>
    </>
  );
}
components/RouteConfig.tsx
import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Layout } from "./Layout";
import { NotFound } from "./NotFound";
import { Login } from "./Login";
import { Home } from "./Home";
import { Page1 } from "./Page1";
import { Page2 } from "./Page2";
import { Page3, Page3Child1, Page3Child2 } from "./Page3";

export const RouterConfig:React.VFC = () => {

  return (
  <>

    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />} >
          <Route index element={<Home />} />
          <Route path="/page1" element={<Page1 />}/>
          <Route path="/page2" element={<Page2 />}/>
          <Route path="/page3" element={<Page3 />} >
            <Route index element={<Page3Child1 />} />
            <Route path="child1" element={<Page3Child1 />} />
            <Route path="child2" element={<Page3Child2 />} />
          </Route>
          <Route path="/login" element={<Login />} />
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>

  </>
  );
}
App.tsx
import React from "react";
import { RouterConfig } from "./RouteConfig";

export const App: React.VFC = () => {
  return (
  <>
    <RouterConfig />
  </>
  );
main.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import { App } from './components/App'

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)

認証ユーザー情報をグローバル管理する

次にユーザーの認証状態をグルーバルで管理するために、いくつかのファイルを作成します。
作成するファイル

権限の種類とユーザーの型を定義します。
権限は「admin」「manager」「user」と設定しています。

types/index.tsx
export const RoleType = {
  Admin: 'admin',
  Manager: 'manager',
  User: 'user',
} as const;
export type RoleType = typeof RoleType[keyof typeof RoleType];
export const AllRoleType = Object.values(RoleType);

export type UserType = {
  name: string;
  role: RoleType
}

useContextフックを利用して、認証ユーザー情報をグローバル管理します。

providers/AuthUser.tsx
import React from "react";
import { UserType } from "../types"; 

export type AuthUserContextType = {
  user: UserType | null;
  signin: (user:UserType, callback:() => void) => void;
  signout: (callback:() => void) => void;
}
const AuthUserContext = React.createContext<AuthUserContextType>({} as AuthUserContextType);

export const useAuthUserContext = ():AuthUserContextType => {
  return React.useContext<AuthUserContextType>(AuthUserContext);
}

type Props = {
  children: React.ReactNode
}

export const AuthUserProvider = (props:Props) => {
  const [user, setUser] = React.useState<UserType | null>(null);

  const signin = (newUser: UserType, callback: () => void) => {
    setUser(newUser);
    callback();
  }

  const signout = (callback: () => void) => {
    setUser(null);
    callback();
  }


  const value:AuthUserContextType = { user, signin, signout };
  return (
    <AuthUserContext.Provider value={value}>
      {props.children}
    </AuthUserContext.Provider>
  );
}
providers/index.ts
export * from "./AuthUser";

グローバルContextのProviderで、グローバルContextを参照するコンポーネントを囲みます。
ここではAuthUserProviderでAppコンポーネントを囲めばいいのですが、私はAppコンポーネントをすっきりさせておきたいので、すべてのグローバルContextのProviderは、このProvidersコンポーネントに集めています。
そしてこのProvidersコンポーネントでAppコンポーネントを囲むようにしています。

components/providers.tsx
import React from "react";
import { AuthUserProvider } from "../providers";

type Props = {
  children: React.ReactNode
}
export const Providers:React.VFC<Props> = (props) => {
  return (
    <>
      <AuthUserProvider>
        {props.children}
      </AuthUserProvider>
    </>
  );
}

ProvidersコンポーネントでAppコンポーネントを囲みます。

App.tsx
import React from "react";
import { RouterConfig } from "./RouteConfig";
import { Providers } from "./Providers";

export const App: React.VFC = () => {
  return (
  <>
    <Providers>
      <RouterConfig />
    </Providers>
  </>
  );
}

ルートを制限する

ルートを制限するコンポーネントを作成し、ルーティングの定義を修正します。

まずはルートを制限するコンポーネントを作成します。
グローバル管理の認証ユーザー情報を参照し、認証されていないユーザーであれば、props.redirectに指定したルートにリダイレクトします。
propsのallowrolesに権限が指定されていて、認証ユーザ情報の権限がallowrolesに含まれていない場合も同様にリダイレクトします。
また、ログイン後にアクセスしたページを表示できるように、Navigateコンポーネントのstateにアクセスしようとしたページを設定しておきます。

components/RouteAuthGuard.tsx
import React from "react";
import { RoleType } from "../types";
import { useAuthUserContext } from "../providers"
import { Navigate, useLocation } from "react-router-dom";

type Props = {
  component: React.ReactNode;
  redirect: string,
  allowroles?: RoleType[] 
}

export const RouteAuthGuard: React.VFC<Props> = (props) => {
  const authUser = useAuthUserContext().user;


  let allowRoute = false;
  if ( authUser ) {
    allowRoute = props.allowroles ? props.allowroles.includes(authUser.role) : true;
  }

  if (!allowRoute) {
    return <Navigate to={props.redirect} state={{from:useLocation()}} replace={false} />
  }

  return <>{props.component}</>;

}

RouteConfigを修正します。
Page2のelementにRouteAuthGuardを指定します。
allowrolesを指定していないので、認証ユーザーのみがアクセス可能になります。
Page3もelementにRouteAuthGuardを指定します。
allowrolesをAdminとManagerを指定しているので、認証ユーザーの権限がAdminまたはManagerのみアクセス可能になります。
リダイレクトはログインページとしています。

components/RouteConfig.tsx
import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Layout } from "./Layout";
import { NotFound } from "./NotFound";
import { Login } from "./Login";
import { Home } from "./Home";
import { Page1 } from "./Page1";
import { Page2 } from "./Page2";
import { Page3, Page3Child1, Page3Child2 } from "./Page3";

import { RouteAuthGuard } from "./RouteAuthGuard";
import { RoleType } from "../types";

export const RouterConfig:React.VFC = () => {

  return (
  <>

    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />} >
          <Route index element={<Home />} />
          <Route path="/page1" element={<Page1 /> }/>

          <Route path="/page2" element={
                <RouteAuthGuard component={<Page2 />} redirect="/login" />} />
            
          <Route path="/page3" element={
                <RouteAuthGuard component={<Page3 />} redirect="/login" 
                        allowroles={[RoleType.Admin, RoleType.Manager]} />} >
            <Route index element={<Page3Child1 />} />
            <Route path="child1" element={<Page3Child1 />} />
            <Route path="child2" element={<Page3Child2 />} />
          </Route>

          <Route path="/login" element={<Login />} />
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>

  </>
  );
}

ログイン画面を修正する

ログイン画面を修正して、簡易的にユーザー認証と権限設定をできるようにしておきます。
ログイン後はアクセスしようとしていたページを表示できるようにし、ログインページを履歴に残さないことで、「戻る」をクリックしたときもログインページには戻らないようにします。

components/Login.tsx
import React from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { UserType, RoleType } from "../types";
import { AuthUserContextType, useAuthUserContext } from "../providers";


type CustomLocation = {
  state: { from: { pathname:string } }
};

export const Login = () => {
  const navigate = useNavigate();
  const location:CustomLocation = useLocation() as CustomLocation;
  const fromPathName:string = location.state.from.pathname;
  const authUser:AuthUserContextType = useAuthUserContext();


  const signin = (role:RoleType) => {
    const user: UserType = {
      name: "no-name",
      role: role
    }
    authUser.signin(user, () => { 
      navigate(fromPathName, { replace: true })
    });
  }


  return (
    <div>
    <h3>Login</h3>
    <button onClick={() => signin(RoleType.Admin)}>admin権限でログイン</button>
    <button onClick={() => signin(RoleType.Manager)}>manager権限でログイン</button>
    <button onClick={() => signin(RoleType.User)}>user権限でログイン</button>
    </div>
  );
}

以上でルーティングにアクセス制御をかけることができました。
それでは動作確認してみます。

まとめ

「RouteAuthGuard」はAngularのGuardから名付けました。
この「RouteAuthGuard」はページへのアクセス制御以外でも使えそうですね。

補足

2022/05/18追記
コチラ Vite + React:aws-amplify を使って Cognito でサインインする でサンプルコードを作りました。
トップ画面は認証ユーザーのみ表示させ、認証していないユーザーはログイン画面にリダイレクトしています。今回の記事の内容で実装しています。

Discussion

ログインするとコメントできます