React:React Router v6 で 認証されていないユーザーや権限がないユーザーをリダイレクトする
2022年02月02日 Windows11での情報です。
今回は、React Router v6 で 認証されていないユーザーに見せたくないページや、権限がないユーザーに見せたくないページにアクセスされた場合、ログインページなど指定のページへリダイレクトする方法です。
参考にしたのはコチラ
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権限のユーザーのみアクセス可能とします。
フォルダ構成
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 />
</>
);}
import React from "react";
export const Home:React.VFC = () => {
return <h3>Home</h3>
}
import React from "react";
export const Page1:React.VFC = () => {
return <h3>Page 1</h3>
}
import React from "react";
export const Page2:React.VFC = () => {
return <h3>Page 2</h3>
}
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>
}
import React from "react";
export const Login:React.VFC = () => {
return <h3>Login</h3>
}
import React from "react";
export const NotFound = () => {
return (
<>
<h1>404</h1>
<h3>お探しのページは見つかりませんでした。</h3>
</>
);
}
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>
</>
);
}
import React from "react";
import { RouterConfig } from "./RouteConfig";
export const App: React.VFC = () => {
return (
<>
<RouterConfig />
</>
);
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」と設定しています。
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フックを利用して、認証ユーザー情報をグローバル管理します。
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>
);
}
export * from "./AuthUser";
グローバルContextのProviderで、グローバルContextを参照するコンポーネントを囲みます。
ここではAuthUserProviderでAppコンポーネントを囲めばいいのですが、私はAppコンポーネントをすっきりさせておきたいので、すべてのグローバルContextのProviderは、このProvidersコンポーネントに集めています。
そしてこのProvidersコンポーネントでAppコンポーネントを囲むようにしています。
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コンポーネントを囲みます。
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にアクセスしようとしたページを設定しておきます。
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のみアクセス可能になります。
リダイレクトはログインページとしています。
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>
</>
);
}
ログイン画面を修正する
ログイン画面を修正して、簡易的にユーザー認証と権限設定をできるようにしておきます。
ログイン後はアクセスしようとしていたページを表示できるようにし、ログインページを履歴に残さないことで、「戻る」をクリックしたときもログインページには戻らないようにします。
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