ProtectedRoute適用ガイド|React Router で認証付きルーティングを簡単実装

Reactアプリケーションでログインユーザーだけがアクセスできるページを作りたい場合、ProtectedRouteは必須の実装パターンです。

本記事では、React RouterでのProtectedRoute実装について、基本概念から実践的な活用方法まで初心者エンジニア向けに分かりやすく解説します。

目次

ProtectedRouteって何?

ProtectedRoute(プロテクテッドルート)は、認証されたユーザーのみがアクセスできるルートを作成するReactのコンポーネントパターンです。

🔐 基本的な仕組み

// 基本的なProtectedRouteの概念
const ProtectedRoute = ({ children }) => {
  const isAuthenticated = checkUserAuth(); // ユーザー認証チェック
  
  if (!isAuthenticated) {
    return <Navigate to="/login" />; // ログインページにリダイレクト
  }
  
  return children; // 認証済みなら子コンポーネントを表示
};

📊 ProtectedRoute vs 通常のRoute

項目通常のRouteProtectedRoute
アクセス制限なし認証が必要
未認証時の動作そのまま表示ログインページへリダイレクト
セキュリティ
実装の複雑さシンプルやや複雑

💡 実際の動作例

✅ **認証済みユーザーの場合**
/dashboard にアクセス → ダッシュボードページが表示される

❌ **未認証ユーザーの場合**  
/dashboard にアクセス → /login に自動リダイレクト

なぜProtectedRouteが必要なのか

🛡️ セキュリティの確保

// ❌ 悪い例:誰でもアクセス可能
<Route path="/admin" element={<AdminPanel />} />

// ✅ 良い例:認証済みユーザーのみアクセス可能
<Route path="/admin" element={
  <ProtectedRoute>
    <AdminPanel />
  </ProtectedRoute>
} />

🎯 ユーザーエクスペリエンスの向上

ProtectedRouteにより、以下のような自然なユーザー体験が実現できます:

📱 **ユーザーの行動パターン**

1. 未ログインユーザーが `/profile` にアクセス
2. 自動的に `/login` にリダイレクト
3. ログイン成功後、元々アクセスしたかった `/profile` に戻る
4. シームレスな体験が提供される

🔄 状態管理の簡素化

// 各コンポーネントで認証チェックする必要がない
const Profile = () => {
  // 認証チェック不要!ProtectedRouteが保証
  return (
    <div>
      <h1>プロフィールページ</h1>
      {/* 安心してユーザー情報を表示 */}
    </div>
  );
};

他のルート保護方法との違い

📋 ルート保護の手法比較

手法メリットデメリット適用場面
ProtectedRoute再利用可能、集約管理初期実装が必要複数ページの保護
コンポーネント内チェック実装が簡単コードの重複単発の保護
HOC(Higher Order Component)柔軟な制御複雑になりがち高度なカスタマイズ

💻 各手法の実装例

// 1. コンポーネント内チェック(非推奨)
const Dashboard = () => {
  const { user } = useAuth();
  
  if (!user) {
    return <Navigate to="/login" />;
  }
  
  return <div>ダッシュボード</div>;
};

// 2. ProtectedRoute(推奨)
const Dashboard = () => {
  // 認証チェックはProtectedRouteが担当
  return <div>ダッシュボード</div>;
};

// 使用時
<Route path="/dashboard" element={
  <ProtectedRoute>
    <Dashboard />
  </ProtectedRoute>
} />

ProtectedRouteの基本実装

🚀 最もシンプルな実装

import { Navigate } from 'react-router-dom';

const ProtectedRoute = ({ children }) => {
  // 認証状態をチェック(実装は後述)
  const isAuthenticated = !!localStorage.getItem('token');
  
  return isAuthenticated ? children : <Navigate to="/login" replace />;
};

export default ProtectedRoute;

🎯 Outletを使用した実装(推奨)

import { Navigate, Outlet } from 'react-router-dom';

const ProtectedRoute = () => {
  const isAuthenticated = !!localStorage.getItem('token');
  
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }
  
  return <Outlet />; // 子ルートを描画
};

// 使用例
<Route element={<ProtectedRoute />}>
  <Route path="/dashboard" element={<Dashboard />} />
  <Route path="/profile" element={<Profile />} />
  <Route path="/settings" element={<Settings />} />
</Route>

🔧 認証状態管理の実装

// useAuth.js - カスタムフック
import { useState, useEffect, createContext, useContext } from 'react';

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    checkAuthStatus();
  }, []);

  const checkAuthStatus = () => {
    const token = localStorage.getItem('token');
    if (token) {
      // トークンの有効性を確認
      setUser({ token }); // 簡略化
    }
    setLoading(false);
  };

  const login = (token) => {
    localStorage.setItem('token', token);
    setUser({ token });
  };

  const logout = () => {
    localStorage.removeItem('token');
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ 
      user, 
      loading, 
      login, 
      logout, 
      isAuthenticated: !!user 
    }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
};

🎨 改良版ProtectedRoute

import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from './useAuth';

const ProtectedRoute = ({ redirectTo = '/login' }) => {
  const { isAuthenticated, loading } = useAuth();

  // ローディング中は読み込み画面を表示
  if (loading) {
    return <div className="loading">読み込み中...</div>;
  }

  // 未認証の場合はリダイレクト
  if (!isAuthenticated) {
    return <Navigate to={redirectTo} replace />;
  }

  // 認証済みの場合は子コンポーネントを描画
  return <Outlet />;
};

export default ProtectedRoute;

実際の開発での活用例

🏢 基本的なアプリ構成

// App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './hooks/useAuth';
import ProtectedRoute from './components/ProtectedRoute';

function App() {
  return (
    <AuthProvider>
      <BrowserRouter>
        <Routes>
          {/* パブリックルート */}
          <Route path="/" element={<Home />} />
          <Route path="/login" element={<Login />} />
          <Route path="/signup" element={<SignUp />} />
          
          {/* プロテクテッドルート */}
          <Route element={<ProtectedRoute />}>
            <Route path="/dashboard" element={<Dashboard />} />
            <Route path="/profile" element={<Profile />} />
            <Route path="/settings" element={<Settings />} />
          </Route>
          
          {/* 404ページ */}
          <Route path="*" element={<NotFound />} />
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  );
}

🔐 ロールベースのアクセス制御

const RoleProtectedRoute = ({ requiredRole, children }) => {
  const { user, isAuthenticated } = useAuth();

  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }

  if (user.role !== requiredRole) {
    return <Navigate to="/unauthorized" replace />;
  }

  return children;
};

// 使用例
<Route path="/admin" element={
  <RoleProtectedRoute requiredRole="admin">
    <AdminPanel />
  </RoleProtectedRoute>
} />

📱 ログイン後の元ページ復帰

// LoginForm.js
import { useNavigate, useLocation } from 'react-router-dom';

const LoginForm = () => {
  const navigate = useNavigate();
  const location = useLocation();
  const { login } = useAuth();

  const handleLogin = async (credentials) => {
    try {
      await login(credentials);
      
      // ログイン前にアクセスしようとしたページに戻る
      const from = location.state?.from?.pathname || '/dashboard';
      navigate(from, { replace: true });
    } catch (error) {
      console.error('ログインエラー:', error);
    }
  };

  return (
    <form onSubmit={handleLogin}>
      {/* ログインフォーム */}
    </form>
  );
};

// ProtectedRoute.js(改良版)
const ProtectedRoute = () => {
  const { isAuthenticated } = useAuth();
  const location = useLocation();

  if (!isAuthenticated) {
    // 現在のページ情報を状態として渡す
    return (
      <Navigate 
        to="/login" 
        state={{ from: location }} 
        replace 
      />
    );
  }

  return <Outlet />;
};

🎯 条件付きナビゲーション

// Navigation.js
const Navigation = () => {
  const { isAuthenticated, logout } = useAuth();

  return (
    <nav>
      <Link to="/">ホーム</Link>
      
      {isAuthenticated ? (
        // ログイン済みの場合
        <>
          <Link to="/dashboard">ダッシュボード</Link>
          <Link to="/profile">プロフィール</Link>
          <button onClick={logout}>ログアウト</button>
        </>
      ) : (
        // 未ログインの場合
        <>
          <Link to="/login">ログイン</Link>
          <Link to="/signup">新規登録</Link>
        </>
      )}
    </nav>
  );
};

注意点とベストプラクティス

⚠️ よくある間違い

// ❌ セキュリティリスク:クライアントサイドのみでの制御
const ProtectedRoute = ({ children }) => {
  const isAuthenticated = !!localStorage.getItem('token');
  // ⚠️ ブラウザで簡単に偽装可能
  return isAuthenticated ? children : <Navigate to="/login" />;
};

// ✅ 改善:サーバーサイドでの検証も必須
const ProtectedRoute = ({ children }) => {
  const { isAuthenticated, isValidToken } = useAuth();
  
  useEffect(() => {
    // サーバーでトークンの有効性を検証
    validateTokenWithServer();
  }, []);

  return isAuthenticated && isValidToken ? children : <Navigate to="/login" />;
};

🛡️ セキュリティのベストプラクティス

項目推奨事項理由
トークン管理httpOnlyクッキーを使用XSS攻撃を防止
サーバー検証必ず併用するクライアント偽装を防止
自動ログアウトトークン期限切れ時に実装セキュリティ向上
HTTPS使用本番環境では必須通信の暗号化

🎯 パフォーマンス最適化

// React.memoでの最適化
const ProtectedRoute = React.memo(({ children }) => {
  const { isAuthenticated } = useAuth();
  
  return isAuthenticated ? children : <Navigate to="/login" replace />;
});

// 条件付きレンダリング最適化
const App = () => {
  const { isAuthenticated, loading } = useAuth();

  if (loading) {
    return <LoadingSpinner />;
  }

  return (
    <Routes>
      {isAuthenticated ? (
        // 認証済みルート
        <Route element={<ProtectedRoute />}>
          <Route path="/dashboard" element={<Dashboard />} />
        </Route>
      ) : (
        // 未認証ルート
        <Route path="/login" element={<Login />} />
      )}
    </Routes>
  );
};

🧪 テストのベストプラクティス

// ProtectedRoute.test.js
import { render, screen } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import ProtectedRoute from './ProtectedRoute';
import { AuthProvider } from './useAuth';

const renderWithRouter = (component, { user = null } = {}) => {
  return render(
    <BrowserRouter>
      <AuthProvider value={{ isAuthenticated: !!user, user }}>
        {component}
      </AuthProvider>
    </BrowserRouter>
  );
};

test('未認証時はログインページにリダイレクト', () => {
  renderWithRouter(
    <ProtectedRoute>
      <div>プロテクテッドコンテンツ</div>
    </ProtectedRoute>
  );
  
  expect(screen.queryByText('プロテクテッドコンテンツ')).not.toBeInTheDocument();
});

test('認証済み時はコンテンツを表示', () => {
  renderWithRouter(
    <ProtectedRoute>
      <div>プロテクテッドコンテンツ</div>
    </ProtectedRoute>,
    { user: { id: 1, name: 'テストユーザー' } }
  );
  
  expect(screen.getByText('プロテクテッドコンテンツ')).toBeInTheDocument();
});

まとめ

ProtectedRouteは認証機能を持つReactアプリには欠かせない実装パターンです。適切に実装することで、セキュアで使いやすいアプリケーションを構築できます。

🎯 重要ポイントの復習

ProtectedRouteの核心価値

  • 認証済みユーザーのみアクセス可能
  • コードの再利用性と保守性の向上
  • 自然なユーザーエクスペリエンス

実装のポイント

  • React Router v6のOutletを活用
  • 認証状態の適切な管理
  • ローディング状態の考慮

セキュリティ

  • クライアントサイドは補助的な役割
  • サーバーサイドでの検証が必須
  • 適切なトークン管理

🚀 次のステップ

ProtectedRouteをマスターしたら、以下の技術も習得してより高度な認証システムを構築しましょう:

  • ロールベースアクセス制御(RBAC)
  • JWT(JSON Web Token)の詳細理解
  • OAuth2.0やOpenID Connectの統合

ProtectedRouteはモダンなReactアプリケーション開発の基礎です。今回学んだ実装パターンを活用して、セキュアで使いやすいアプリケーションを開発してください!


💡 参考リンク

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次