Capa do Ebook gratuito Roteamento e Autenticação em React: Construindo SPAs Seguras com React Router e JWT

Roteamento e Autenticação em React: Construindo SPAs Seguras com React Router e JWT

Novo curso

20 páginas

Rotas públicas, rotas privadas e componentes de guarda (Guards) para controle de acesso

Capítulo 5

Tempo estimado de leitura: 10 minutos

+ Exercício
Audio Icon

Ouça em áudio

0:00 / 0:00

Em uma SPA, “rotas públicas” são páginas acessíveis sem autenticação (ex.: login, cadastro, recuperação de senha, landing), enquanto “rotas privadas” exigem que o usuário esteja autenticado (ex.: dashboard, perfil, faturamento). Para implementar isso de forma consistente, usamos componentes de guarda (guards): componentes que ficam entre a navegação e o conteúdo e decidem se a rota pode ser acessada, redirecionando quando necessário.

O objetivo de um guard não é “proteger” dados sensíveis (isso é responsabilidade do backend), e sim controlar a experiência de navegação no cliente: impedir que usuários não autenticados vejam telas internas, evitar que usuários autenticados voltem ao login, lidar com estados intermediários (carregando sessão), e aplicar regras como permissões/roles.

Conceitos essenciais: público, privado e estados de autenticação

Antes de escrever guards, vale definir claramente os estados possíveis do “status de autenticação” no frontend. Em SPAs reais, não existe apenas “logado” e “deslogado”. Normalmente você precisa de pelo menos três estados:

  • loading: ainda estamos verificando se existe sessão válida (ex.: lendo token do storage, validando expiração, buscando /me).
  • authenticated: há um usuário autenticado e pronto para acessar áreas privadas.
  • unauthenticated: não há sessão válida; o usuário deve ficar na área pública.

Esses estados são importantes porque um guard que redireciona imediatamente ao ver “sem usuário” pode causar “flicker” (piscar) ou redirecionamentos errados enquanto a sessão ainda está sendo carregada.

O que é um Guard no React Router

Um guard é um componente que envolve uma rota (ou um conjunto de rotas) e decide o que renderizar:

Continue em nosso aplicativo

Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.

ou continue lendo abaixo...
Download App

Baixar o aplicativo

  • Se permitido: renderiza o conteúdo da rota via <Outlet /> (rotas aninhadas) ou renderiza children.
  • Se não permitido: redireciona usando <Navigate /> ou dispara uma navegação programática.
  • Se em carregamento: mostra um placeholder (skeleton/spinner) para evitar redirecionamento prematuro.

Na prática, você terá pelo menos dois guards comuns:

  • AuthGuard (rota privada): exige autenticação.
  • GuestGuard (rota pública restrita): impede acesso a login/cadastro quando já está autenticado.

Passo a passo: criando uma fonte única de verdade para a sessão

Os guards precisam consultar um estado central de autenticação. A implementação pode variar (Context API, Zustand, Redux, React Query), mas o ponto é: o guard não deve “adivinhar” sozinho; ele deve consumir um estado já preparado.

1) Defina um contrato de estado de autenticação

Um formato simples e útil:

  • status: 'loading' | 'authenticated' | 'unauthenticated'
  • user: objeto do usuário ou null
  • token: string do JWT (se você mantiver no cliente) ou null
  • hasRole(role) / hasPermission(permission): helpers opcionais

Exemplo de tipagem (TypeScript) para guiar o raciocínio:

type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated';

type User = {
  id: string;
  name: string;
  email: string;
  roles?: string[];
};

type AuthState = {
  status: AuthStatus;
  user: User | null;
  token: string | null;
};

2) Crie um hook de acesso ao estado (exemplo com Context)

O guard vai consumir algo como useAuth(). Abaixo está um exemplo mínimo de interface; a forma de preencher esse estado (login, logout, refresh) pode já existir no seu projeto, então foque no consumo.

import React from 'react';

type AuthContextValue = {
  status: 'loading' | 'authenticated' | 'unauthenticated';
  user: { id: string; name: string; roles?: string[] } | null;
};

const AuthContext = React.createContext<AuthContextValue | null>(null);

export function useAuth() {
  const ctx = React.useContext(AuthContext);
  if (!ctx) throw new Error('useAuth deve ser usado dentro de AuthProvider');
  return ctx;
}

O ponto aqui é: o guard não acessa diretamente localStorage nem faz fetch por conta própria. Ele apenas reage ao estado.

Passo a passo: implementando um AuthGuard (rotas privadas)

O AuthGuard deve:

  • Enquanto status === 'loading': renderizar um fallback (ex.: “Carregando…”).
  • Se status === 'unauthenticated': redirecionar para a rota de login.
  • Se status === 'authenticated': permitir acesso e renderizar o conteúdo.

Além disso, é uma boa prática preservar a rota original para voltar após login (deep link). Isso é feito passando state no <Navigate />.

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

export function AuthGuard() {
  const { status } = useAuth();
  const location = useLocation();

  if (status === 'loading') {
    return <div>Carregando sessão...</div>;
  }

  if (status === 'unauthenticated') {
    return (
      <Navigate
        to="/login"
        replace
        state={{ from: location }}
      />
    );
  }

  return <Outlet />;
}

Por que usar replace? Para evitar que o usuário volte com “voltar” do navegador para uma rota privada que imediatamente redirecionaria de novo, criando uma navegação confusa.

Aplicando o AuthGuard em rotas aninhadas

O padrão mais limpo é agrupar rotas privadas sob um mesmo guard:

import { Routes, Route } from 'react-router-dom';
import { AuthGuard } from './guards/AuthGuard';

export function AppRoutes() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/login" element={<Login />} />

      <Route element={<AuthGuard />}>
        <Route path="/app" element={<Dashboard />} />
        <Route path="/app/profile" element={<Profile />} />
        <Route path="/app/billing" element={<Billing />} />
      </Route>

      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

Note que o guard está em um <Route element={...}> sem path. Isso cria um “grupo” de rotas que compartilham a mesma regra de acesso.

Passo a passo: implementando um GuestGuard (rotas públicas que não fazem sentido para logados)

Algumas rotas são públicas, mas não deveriam ser acessadas por quem já está autenticado, como /login e /register. Para isso, crie um guard inverso:

  • Se loading: renderiza fallback.
  • Se authenticated: redireciona para a área autenticada (ex.: /app).
  • Se unauthenticated: permite renderizar a página pública.
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../auth/useAuth';

export function GuestGuard() {
  const { status } = useAuth();

  if (status === 'loading') {
    return <div>Carregando...</div>;
  }

  if (status === 'authenticated') {
    return <Navigate to="/app" replace />;
  }

  return <Outlet />;
}

Uso típico:

<Routes>
  <Route element={<GuestGuard />}>
    <Route path="/login" element={<Login />} />
    <Route path="/register" element={<Register />} />
  </Route>

  <Route element={<AuthGuard />}>
    <Route path="/app" element={<Dashboard />} />
  </Route>
</Routes>

Redirecionamento pós-login preservando a rota original

Quando o AuthGuard redireciona para /login com state={{ from: location }}, a tela de login pode ler esse estado e, após autenticar, navegar de volta para a rota original.

import { useLocation, useNavigate } from 'react-router-dom';

export function Login() {
  const navigate = useNavigate();
  const location = useLocation();

  const from = (location.state as any)?.from?.pathname || '/app';

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    // await signIn(...)
    navigate(from, { replace: true });
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Entrar</button>
    </form>
  );
}

Esse detalhe melhora muito a experiência: se o usuário acessa um link direto para /app/billing, ele faz login e volta exatamente para /app/billing.

Guard de autorização: roles e permissões

Autenticação responde “quem é você?”. Autorização responde “o que você pode fazer?”. Em muitas SPAs, além de exigir login, você precisa restringir páginas por perfil (ex.: admin) ou por permissão (ex.: billing:read).

Uma abordagem prática é criar um guard específico, ou estender o AuthGuard com parâmetros. Um guard dedicado deixa as regras mais explícitas.

Exemplo: RoleGuard

Esse guard exige autenticação e uma role específica. Se não tiver a role, você pode redirecionar para uma página de “sem permissão” (403) ou para um fallback seguro.

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

type RoleGuardProps = {
  requiredRole: string;
};

export function RoleGuard({ requiredRole }: RoleGuardProps) {
  const { status, user } = useAuth();

  if (status === 'loading') return <div>Carregando...</div>;

  if (status === 'unauthenticated') {
    return <Navigate to="/login" replace />;
  }

  const roles = user?.roles ?? [];
  const allowed = roles.includes(requiredRole);

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

  return <Outlet />;
}

Uso:

<Route element={<AuthGuard />}>
  <Route path="/app" element={<Dashboard />} />

  <Route element={<RoleGuard requiredRole="admin" />}>
    <Route path="/app/admin" element={<AdminPanel />} />
  </Route>
</Route>

Repare que você pode compor guards: primeiro garante autenticação, depois aplica autorização. Isso evita duplicar lógica.

Guard por “feature flag” ou estado do usuário (ex.: onboarding completo)

Nem toda regra é role/permissão. Às vezes você quer bloquear páginas até o usuário completar um passo (ex.: aceitar termos, completar perfil, configurar 2FA). Isso também é um guard.

Exemplo: exigir que o usuário tenha onboardingCompleted antes de acessar o app principal.

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

export function OnboardingGuard() {
  const { status, user } = useAuth();

  if (status === 'loading') return <div>Carregando...</div>;
  if (status === 'unauthenticated') return <Navigate to="/login" replace />;

  const completed = (user as any)?.onboardingCompleted;
  if (!completed) return <Navigate to="/onboarding" replace />;

  return <Outlet />;
}

Isso ajuda a manter regras de fluxo fora das páginas, evitando condicionais espalhadas.

Erros comuns e como evitar

1) Redirecionar durante o carregamento da sessão

Se o guard não tratar loading, o usuário pode ser mandado ao login mesmo tendo token válido, apenas porque o estado ainda não foi hidratado. Sempre trate o estado intermediário.

2) Guard lendo token diretamente do storage

Quando cada guard lê localStorage ou decodifica JWT por conta própria, você cria divergência de regras e bugs difíceis (ex.: um guard considera válido, outro não). Centralize a lógica de sessão em um único lugar e exponha um estado simples para os guards.

3) Confundir “esconder tela” com segurança real

Mesmo com AuthGuard, um atacante pode chamar APIs diretamente. O backend deve validar JWT e permissões em todas as rotas protegidas. No frontend, o guard é uma camada de UX e organização.

4) Loops de redirecionamento

Loops acontecem quando:

  • Você redireciona para uma rota que também está sob o mesmo guard.
  • Você manda para /login, mas /login está dentro de AuthGuard por engano.
  • Você usa GuestGuard e AuthGuard com destinos que se apontam mutuamente em certos estados.

Para evitar, mantenha uma separação clara: rotas públicas em um grupo, rotas privadas em outro, e uma rota de fallback (ex.: /forbidden) fora de guards que possam bloquear seu acesso.

Padrão prático: um único componente “ProtectedRoute” com children

Se você preferir proteger rotas individualmente (em vez de grupos com Outlet), pode criar um componente que recebe children. Isso é útil em cenários onde você monta rotas dinamicamente ou quer proteger apenas um elemento.

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

type ProtectedRouteProps = {
  children: React.ReactNode;
};

export function ProtectedRoute({ children }: ProtectedRouteProps) {
  const { status } = useAuth();
  const location = useLocation();

  if (status === 'loading') return <div>Carregando...</div>;

  if (status === 'unauthenticated') {
    return <Navigate to="/login" replace state={{ from: location }} />;
  }

  return <>{children}</>;
}

Uso:

<Route
  path="/app/profile"
  element={
    <ProtectedRoute>
      <Profile />
    </ProtectedRoute>
  }
/>

Esse padrão é simples, mas para muitas rotas ele fica verboso. Para áreas inteiras, o padrão com Outlet costuma ser mais escalável.

Tratando expiração de token e sessão inválida durante a navegação

Mesmo com guards, a sessão pode expirar enquanto o usuário está navegando. O que o guard deve fazer nesse caso depende de como seu estado de autenticação é atualizado. O importante é que, ao detectar “sessão inválida”, o estado central mude para unauthenticated. Assim, qualquer rota privada renderizada sob AuthGuard será automaticamente redirecionada.

Na prática, isso costuma acontecer quando:

  • Uma chamada a API retorna 401/403 e você decide deslogar.
  • Você detecta expiração do JWT (ex.: claim exp) e invalida a sessão.

O guard não precisa conhecer detalhes de JWT; ele apenas reage ao estado. Isso mantém o controle de acesso previsível.

Checklist de implementação para controle de acesso com guards

  • Defina estados loading/authenticated/unauthenticated e garanta que o app inicia em loading até resolver a sessão.
  • Implemente AuthGuard com Outlet e preserve from para pós-login.
  • Implemente GuestGuard para evitar login/cadastro para usuários autenticados.
  • Quando necessário, componha guards de autorização (roles/permissões) com rotas aninhadas.
  • Garanta que rotas de erro como /forbidden e /not-found não causem loops.
  • Centralize a lógica de sessão; guards apenas consomem estado.

Agora responda o exercício sobre o conteúdo:

Qual é a principal razão para um guard tratar explicitamente o estado loading antes de decidir redirecionar ou liberar uma rota?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Ao tratar loading, o guard evita redirecionar para login antes de concluir a verificação da sessão, reduzindo flicker e navegações incorretas. A segurança de dados continua sendo responsabilidade do backend, e o guard deve consumir um estado central.

Próximo capitúlo

Modelagem do estado de autenticação com Context, Providers e hooks

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.