Voltar para Artigos
Front-end10 min de leitura

React Seguro: Proteção de Rotas Privadas com Controle de Permissões

Route Guard tipado com React Router 6, redirecionamento com state para retornar ao destino após login, proteção por role (RBAC no frontend), loading state durante verificação do token e proteção no Next.js App Router com middleware.

12 de agosto de 2026

Proteger rotas no frontend não é sobre segurança — é sobre UX. A segurança real é feita no backend via JWT. O Route Guard no frontend evita que usuários não autenticados vejam telas em branco cheias de erros 401, e evita que usuários sem permissão vejam conteúdo que não deveriam. Sem ele, um usuário deslogado que acessa /dashboard vê a API falhando e a UI quebrando.

Um detalhe crítico que muitos ignoram é o flash de redirecionamento: ao carregar a aplicação, o contexto de autenticação ainda não verificou se o token local é válido (pode estar expirado). Se não houver um estado isLoading, o usuário autenticado verá o formulário de login por uma fração de segundo antes de ser redirecionado ao dashboard — um comportamento ruim de UX que parece um bug. O isLoading resolve isso travando qualquer redirecionamento até a verificação completar.

Route Guard Básico: Autenticado vs Anônimo

No React Router 6, o padrão para guards é usar layouts aninhados com <Outlet>. O PrivateRoute age como um layout intermediário: se o usuário não estiver autenticado, redireciona para o login salvando a URL de destino no state. Se estiver autenticado, renderiza o <Outlet> que corresponde à rota filha. O PublicOnlyRoute faz o inverso — é usado para as telas de login e registro, garantindo que um usuário já logado não veja essas telas.

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

interface PrivateRouteProps {
  redirectTo?: string;
}

export function PrivateRoute({ redirectTo = '/login' }: PrivateRouteProps) {
  const { user, isLoading } = useAuth();
  const location = useLocation();

  // Durante a verificação do token (ex: refresh token silencioso),
  // não redireciona — mostra um loading para evitar flash de login
  if (isLoading) {
    return (
      <div className="flex h-screen items-center justify-center">
        <span className="loading-spinner" />
      </div>
    );
  }

  if (!user) {
    // Salva a URL que o usuário tentou acessar
    // Após o login, será redirecionado de volta
    return (
      <Navigate
        to={redirectTo}
        state={{ from: location.pathname }}
        replace
      />
    );
  }

  // Outlet: renderiza a rota filha — padrão do React Router 6
  return <Outlet />;
}
PublicOnlyRoute.tsx
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';

// Rota pública EXCLUSIVA: login, register, forgot-password
// Usuário autenticado não deve ver estas telas
export function PublicOnlyRoute({ redirectTo = '/dashboard' }: { redirectTo?: string }) {
  const { user, isLoading } = useAuth();

  if (isLoading) return null; // Sem loading visual para rotas públicas

  if (user) {
    return <Navigate to={redirectTo} replace />;
  }

  return <Outlet />;
}

Proteção por Role (RBAC no Frontend)

Role-Based Access Control no frontend adiciona uma segunda camada de verificação após a autenticação. Se um usuário com role USER tentar acessar /admin/settings diretamente pela URL, o RoleGuard redireciona para uma página de 'Acesso não autorizado' — sem mostrar nenhum dado sensível e sem fazer chamadas desnecessárias à API. Esse componente é projetado para ser composto com o PrivateRoute: autenticação vem primeiro, autorização por role vem depois.

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

type Role = 'USER' | 'MANAGER' | 'ADMIN';

interface RoleGuardProps {
  allowedRoles: Array<Role>;
  redirectTo?: string;
}

export function RoleGuard({
  allowedRoles,
  redirectTo = '/unauthorized',
}: RoleGuardProps) {
  const { user } = useAuth();

  // Se chegou aqui, o usuário já está autenticado (PrivateRoute verificou)
  // Apenas verifica a permissão
  if (!user || !allowedRoles.includes(user.role as Role)) {
    return <Navigate to={redirectTo} replace />;
  }

  return <Outlet />;
}

Declaração de Rotas no React Router 6

A organização das rotas com layouts aninhados torna a estrutura de permissões explícita e legível. Ao ler o createBrowserRouter, fica imediatamente claro quais grupos de rotas exigem autenticação, quais exigem roles específicos, e quais são públicas. Rotas dentro de um PrivateRoute que também estão dentro de um RoleGuard precisam satisfazer ambas as condições — o pipeline de guards é processado de fora para dentro.

AppRouter.tsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { PrivateRoute } from '../components/PrivateRoute';
import { PublicOnlyRoute } from '../components/PublicOnlyRoute';
import { RoleGuard } from '../components/RoleGuard';

const router = createBrowserRouter([
  // Rotas exclusivas para não-autenticados
  {
    element: <PublicOnlyRoute />,
    children: [
      { path: '/login',    element: <LoginPage /> },
      { path: '/register', element: <RegisterPage /> },
      { path: '/forgot-password', element: <ForgotPasswordPage /> },
    ],
  },

  // Rotas privadas: qualquer usuário autenticado
  {
    element: <PrivateRoute />,
    children: [
      { path: '/dashboard', element: <DashboardPage /> },
      { path: '/profile',   element: <ProfilePage /> },
      { path: '/settings',  element: <SettingsPage /> },
    ],
  },

  // Rotas privadas com RBAC: apenas ADMIN e MANAGER
  {
    element: <PrivateRoute />,
    children: [
      {
        element: <RoleGuard allowedRoles={['ADMIN', 'MANAGER']} />,
        children: [
          { path: '/users',         element: <UsersListPage /> },
          { path: '/users/:id',     element: <UserDetailPage /> },
          { path: '/reports',       element: <ReportsPage /> },
        ],
      },
    ],
  },

  // Exclusivo para ADMIN
  {
    element: <PrivateRoute />,
    children: [
      {
        element: <RoleGuard allowedRoles={['ADMIN']} />,
        children: [
          { path: '/admin/settings', element: <AdminSettingsPage /> },
          { path: '/admin/logs',     element: <AuditLogsPage /> },
        ],
      },
    ],
  },

  { path: '/unauthorized', element: <UnauthorizedPage /> },
  { path: '*', element: <NotFoundPage /> },
]);

export function AppRouter() {
  return <RouterProvider router={router} />;
}

Redirecionamento Inteligente Após Login

Uma UX comum e frustrante: você tenta acessar /relatorios/janeiro, é redirecionado para o login, faz o login e cai no /dashboard. Onde foi parar o /relatorios/janeiro? O state.from do React Router resolve isso: o PrivateRoute salva a URL original antes de redirecionar para o login, e a LoginPage lê esse estado após o login bem-sucedido para navegar para o destino correto. O replace: true garante que o /login não fique no histórico de navegação — o botão Voltar do browser vai para a página anterior ao login.

LoginPage.tsx (lê o state.from)
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';

export function LoginPage() {
  const { signIn } = useAuth();
  const navigate = useNavigate();
  const location = useLocation();

  // O PrivateRoute salvou a URL original em state.from
  const from = (location.state as { from?: string })?.from ?? '/dashboard';

  async function handleSubmit(data: LoginFormData) {
    await signIn(data.email, data.password);
    // Após login, redireciona para onde o usuário queria ir
    // replace: não deixa o /login no histórico de navegação
    navigate(from, { replace: true });
  }

  // ...
}

RBAC no frontend é apenas UX — nunca segurança. Um usuário pode modificar o token JWT localmente ou usar o DevTools para contornar a verificação de role. A verdadeira proteção está no backend: cada endpoint verifica o role do usuário no JWT antes de processar a requisição. O frontend protege contra UX acidental, o backend protege contra ataques.

Conclusão

O Route Guard resolve três problemas de UX: usuário não autenticado vendo erros, usuário autenticado vendo tela de login, e usuário sem permissão vendo conteúdo proibido. O isLoading no AuthContext evita o flash de redirecionamento durante o refresh silencioso do token. O state.from garante que o usuário seja enviado para o destino correto após o login — não para o dashboard genérico.