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

Redirecionamentos pós-login e preservação da rota pretendida (returnUrl)

Capítulo 11

Tempo estimado de leitura: 11 minutos

+ Exercício
Audio Icon

Ouça em áudio

0:00 / 0:00

Em uma SPA, o usuário navega entre rotas sem recarregar a página. Quando uma rota exige autenticação e o usuário ainda não está logado, você normalmente o redireciona para a tela de login. O problema prático surge logo em seguida: após autenticar, para onde o usuário deve ir? Se você sempre mandar para um “/dashboard” fixo, você quebra a expectativa de “voltar para onde eu estava indo”. É aí que entra o conceito de preservação da rota pretendida, geralmente chamada de returnUrl (ou redirectTo, from, next).

Neste capítulo, você vai implementar redirecionamentos pós-login com preservação da rota pretendida, cobrindo: como capturar a rota original, como passá-la com segurança para a tela de login, como restaurá-la após o login e como lidar com casos especiais (query string, hash, deep links, rotas com estado, e cenários de logout/expiração).

O que é “returnUrl” e por que ele existe

returnUrl é um valor que representa a rota que o usuário tentou acessar antes de ser redirecionado ao login. Após o login bem-sucedido, a aplicação usa esse valor para navegar de volta ao destino original.

Em termos de UX, isso evita fricção: o usuário clica em um link profundo (por exemplo, um item específico em “/orders/123?tab=invoice”), é solicitado a autenticar e, ao concluir, retorna exatamente para aquele item e contexto.

Em termos técnicos, o returnUrl pode ser transportado de duas formas comuns:

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

  • Via state de navegação do React Router (recomendado): não aparece na URL, é mais difícil de ser manipulado externamente e reduz risco de open redirect.
  • Via query string (ex.: /login?returnUrl=/orders/123): útil quando você precisa preservar o destino mesmo após refresh na tela de login, mas exige validação cuidadosa para evitar redirecionamento aberto (open redirect).

Objetivo prático: fluxo completo

O fluxo que você quer alcançar é:

  • Usuário tenta acessar /area/relatorios?range=30d sem estar autenticado.
  • Guard/rota privada detecta ausência de sessão e redireciona para /login, preservando o destino pretendido.
  • Usuário faz login.
  • Aplicação navega para o destino preservado (/area/relatorios?range=30d) e substitui o histórico para evitar voltar ao login ao apertar “voltar”.

Abordagem recomendada: usando location.state (React Router)

Quando você redireciona para o login, você pode anexar ao redirecionamento um objeto de estado contendo a rota original. No React Router, isso é feito com <Navigate to="/login" state={{ from: location }} replace />. Depois, na página de login, você lê location.state?.from e navega para lá após autenticar.

Passo 1: capturar a rota pretendida no guard

Você já tem um mecanismo de rota privada/guard em capítulos anteriores; aqui, o foco é apenas no detalhe de preservar o destino. A ideia é: ao negar acesso, capture o location atual e passe adiante.

import { Navigate, Outlet, useLocation } from "react-router-dom"; import { useAuth } from "../auth/useAuth"; export function RequireAuth() {   const { isAuthenticated, isLoading } = useAuth();   const location = useLocation();   if (isLoading) return null;   if (!isAuthenticated) {     return (       <Navigate         to="/login"         replace         state={{ from: location }}       />     );   }   return <Outlet />; }

Pontos importantes:

  • state={{ from: location }} carrega o objeto completo de localização, incluindo pathname, search e hash. Isso preserva query string e hash automaticamente.
  • replace evita que a rota protegida “negada” fique no histórico. Assim, ao apertar “voltar”, o usuário não cai em um loop de redirecionamento.
  • isLoading evita “flicker” (mostrar conteúdo e redirecionar logo depois) enquanto você ainda está restaurando sessão.

Passo 2: ler o destino na tela de login

Na página de login, você precisa ler o state e decidir para onde navegar após autenticar. Se não houver destino (por exemplo, usuário abriu diretamente /login), você define um fallback.

import { useLocation, useNavigate } from "react-router-dom"; import { useAuth } from "../auth/useAuth"; export function LoginPage() {   const navigate = useNavigate();   const location = useLocation();   const { signIn } = useAuth();   const from = location.state?.from;   const returnTo = from?.pathname ? `${from.pathname}${from.search || ""}${from.hash || ""}` : "/";   async function handleSubmit(e) {     e.preventDefault();     const email = e.target.email.value;     const password = e.target.password.value;     const ok = await signIn({ email, password });     if (ok) {       navigate(returnTo, { replace: true });     }   }   return (     <form onSubmit={handleSubmit}>       <input name="email" />       <input name="password" type="password" />       <button type="submit">Entrar</button>     </form>   ); }

Por que montar returnTo como string?

  • O navigate aceita tanto string quanto objeto de localização. Usar string facilita e garante que search e hash sejam preservados.
  • Você evita depender do formato interno do objeto location (embora seja estável, a string é simples).

Por que replace: true no pós-login?

  • Evita que o usuário volte para /login ao apertar “voltar”. Em geral, após autenticar, voltar para login não faz sentido e pode confundir.

Passo 3: lidar com “já autenticado” acessando /login

Um caso comum: usuário autenticado digita /login na barra ou clica em um link antigo. Você pode redirecionar automaticamente para um destino padrão, ou para um from se existir (por exemplo, se o login foi aberto por um guard e a sessão foi restaurada no meio do caminho).

import { useEffect } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { useAuth } from "../auth/useAuth"; export function LoginPage() {   const { isAuthenticated } = useAuth();   const navigate = useNavigate();   const location = useLocation();   const from = location.state?.from;   const returnTo = from?.pathname ? `${from.pathname}${from.search || ""}${from.hash || ""}` : "/";   useEffect(() => {     if (isAuthenticated) {       navigate(returnTo, { replace: true });     }   }, [isAuthenticated, navigate, returnTo]);   return (     <div>...form...</div>   ); }

Isso reduz “becos sem saída”: se a sessão já existe, o login não deve bloquear o usuário.

Preservando também o contexto: query string, hash e estado

O exemplo acima preserva pathname, search e hash. Isso cobre a maioria dos casos, como filtros, abas e âncoras.

Mas existe um detalhe: React Router também permite navegação com state (por exemplo, navigate("/checkout", { state: { coupon: "X" } })). Esse estado não aparece na URL e pode ser importante. Se você quiser preservar esse estado também, você pode guardar o objeto location inteiro e navegar usando um objeto.

const from = location.state?.from; // pode conter state interno também // ... após login: if (from) {   navigate(from.pathname + (from.search || "") + (from.hash || ""), {     replace: true,     state: from.state,   }); } else {   navigate("/", { replace: true }); }

Use isso com parcimônia: estado de navegação pode conter dados não serializáveis ou sensíveis. Em geral, o ideal é que o que precisa sobreviver a refresh esteja na URL (query string) ou em um estado global controlado.

Quando usar query string (?returnUrl=) e como fazer com segurança

Há cenários em que location.state não é suficiente:

  • O usuário é redirecionado para login e dá refresh na página de login: o state pode se perder dependendo do fluxo e do navegador.
  • Você precisa compartilhar o link de login já com destino (por exemplo, um e-mail interno que manda para login e depois para uma página específica).

Nesses casos, você pode usar query string, mas com validação rígida para evitar open redirect (um atacante poderia criar um link /login?returnUrl=https://site-malicioso.com e, após login, você redirecionaria para fora do seu domínio).

Passo a passo: construir o link de login com returnUrl

No guard, em vez de usar state, você pode montar a URL do login com o destino atual codificado.

import { Navigate, useLocation } from "react-router-dom"; import { useAuth } from "../auth/useAuth"; export function RequireAuthQuery() {   const { isAuthenticated } = useAuth();   const location = useLocation();   if (!isAuthenticated) {     const intended = `${location.pathname}${location.search || ""}${location.hash || ""}`;     const returnUrl = encodeURIComponent(intended);     return <Navigate to={`/login?returnUrl=${returnUrl}`} replace />;   }   return null; }

Passo a passo: ler e validar o returnUrl no login

Você deve aceitar apenas destinos internos, preferencialmente começando com / e sem protocolo. Também é útil bloquear padrões como // (que alguns navegadores interpretam como URL relativa ao protocolo) e bloquear http:/https:.

import { useMemo } from "react"; import { useLocation, useNavigate } from "react-router-dom"; function isSafeInternalPath(path) {   if (!path) return false;   if (path.startsWith("http://") || path.startsWith("https://")) return false;   if (!path.startsWith("/")) return false;   if (path.startsWith("//")) return false;   return true; } export function LoginPage() {   const location = useLocation();   const navigate = useNavigate();   const params = useMemo(() => new URLSearchParams(location.search), [location.search]);   const raw = params.get("returnUrl");   const decoded = raw ? decodeURIComponent(raw) : null;   const returnTo = isSafeInternalPath(decoded) ? decoded : "/";   async function handleLoginSuccess() {     navigate(returnTo, { replace: true });   }   return (     <div>...</div>   ); }

Regras práticas de segurança:

  • Nunca redirecione para uma URL absoluta fornecida pelo usuário.
  • Permita apenas caminhos internos (começando com /) e, se possível, valide contra uma lista de rotas conhecidas ou prefixos permitidos (ex.: /app, /area).
  • Evite aceitar returnUrl com caracteres estranhos que possam ser interpretados como esquema (ex.: javascript:). A checagem acima já bloqueia os casos mais comuns, mas você pode endurecer ainda mais.

Evitando loops e destinos inválidos

Um erro comum é salvar como returnUrl uma rota que inevitavelmente redireciona de novo para login, causando loop. Exemplos:

  • Salvar /login como returnUrl (o usuário faz login e volta para login).
  • Salvar uma rota de logout ou uma rota que exige um papel/permissão que o usuário não tem.

Boas práticas:

  • Se o destino pretendido for /login, ignore e use fallback.
  • Se o destino pretendido for uma rota que exige permissões, você pode deixar o guard tratar depois do login. Nesse caso, se o usuário não tiver permissão, ele será redirecionado para uma página de “acesso negado” (ou fallback). O importante é evitar loop: se “acesso negado” também redireciona para login, corrija isso.
function normalizeReturnTo(path) {   if (!path) return "/";   if (path === "/login") return "/";   return path; }

Preservação do returnUrl em fluxos com múltiplas etapas

Alguns logins têm etapas adicionais: MFA/2FA, troca de senha no primeiro acesso, aceite de termos, seleção de organização/tenant. Nesses casos, o returnUrl precisa sobreviver a várias telas.

Duas estratégias comuns:

  • Manter em query string durante o fluxo: /login?returnUrl=.../mfa?returnUrl=.../terms?returnUrl=.... Exige validação em cada etapa, mas é simples e resiliente a refresh.
  • Persistir temporariamente em memória/armazenamento: salvar o returnUrl em um estado global (ou sessionStorage) quando o guard redireciona, e consumi-lo ao final do fluxo. Isso evita poluir URLs, mas precisa de limpeza e cuidado para não ficar “stale”.

Exemplo com sessionStorage (útil se você não quer query string e precisa sobreviver a refresh):

// no guard: const intended = `${location.pathname}${location.search || ""}${location.hash || ""}`; sessionStorage.setItem("returnUrl", intended); return <Navigate to="/login" replace />; // no login (após sucesso): const stored = sessionStorage.getItem("returnUrl"); sessionStorage.removeItem("returnUrl"); const returnTo = isSafeInternalPath(stored) ? stored : "/"; navigate(returnTo, { replace: true });

Cuidados:

  • Limpe o valor após usar para evitar que um login futuro redirecione para um destino antigo.
  • Valide o caminho mesmo vindo do storage (não confie cegamente).

Interação com expiração de sessão e redirecionamento automático

Em SPAs, a sessão pode expirar enquanto o usuário está em uma rota protegida. Quando isso acontece, você pode:

  • Redirecionar para login e preservar a rota atual como returnUrl.
  • Após reautenticar, retornar para a mesma tela e permitir que o usuário continue.

O detalhe é evitar que o redirecionamento aconteça no meio de uma ação crítica (por exemplo, envio de formulário). Uma abordagem prática é: ao detectar 401 em uma chamada, você pode disparar um fluxo de reautenticação e, ao final, repetir a ação ou ao menos retornar o usuário para a tela com o estado preservado na URL. Mesmo sem repetir automaticamente, preservar o returnUrl já reduz perda de contexto.

Exemplo integrado: guard + login com state e fallback seguro

A seguir, um exemplo mais completo, combinando as ideias essenciais: guardar a rota via state, montar o retorno com pathname/search/hash, evitar voltar para login e aplicar fallback.

// RequireAuth.jsx import { Navigate, Outlet, useLocation } from "react-router-dom"; import { useAuth } from "../auth/useAuth"; export function RequireAuth() {   const { isAuthenticated, isLoading } = useAuth();   const location = useLocation();   if (isLoading) return null;   if (!isAuthenticated) {     return (       <Navigate         to="/login"         replace         state={{ from: location }}       />     );   }   return <Outlet />; } // LoginPage.jsx import { useMemo } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { useAuth } from "../auth/useAuth"; function buildReturnTo(fromLocation) {   if (!fromLocation?.pathname) return "/";   const path = `${fromLocation.pathname}${fromLocation.search || ""}${fromLocation.hash || ""}`;   if (path === "/login") return "/";   return path; } export function LoginPage() {   const { signIn } = useAuth();   const navigate = useNavigate();   const location = useLocation();   const returnTo = useMemo(() => buildReturnTo(location.state?.from), [location.state]);   async function onSubmit(e) {     e.preventDefault();     const email = e.target.email.value;     const password = e.target.password.value;     const ok = await signIn({ email, password });     if (ok) navigate(returnTo, { replace: true });   }   return (     <form onSubmit={onSubmit}>       <input name="email" />       <input name="password" type="password" />       <button type="submit">Entrar</button>     </form>   ); }

Checklist de implementação (para evitar bugs comuns)

  • O guard deve usar replace ao enviar para login, para evitar histórico “poluído” e loops no botão voltar.
  • O pós-login deve usar replace ao navegar para o returnUrl, para evitar voltar ao login.
  • Preserve search e hash para manter filtros/abas/âncoras.
  • Defina fallback quando não houver returnUrl (ex.: / ou uma rota inicial da área autenticada).
  • Evite returnUrl apontando para /login e normalize destinos.
  • Se usar query string, valide para aceitar apenas caminhos internos e bloquear URLs absolutas (mitigação de open redirect).
  • Se o fluxo de login tiver múltiplas etapas, escolha uma estratégia para persistir o returnUrl (query string ou sessionStorage) e limpe ao final.

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

Em uma SPA com rotas protegidas, qual abordagem é mais recomendada para preservar a rota pretendida e por quê?

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

Você errou! Tente novamente.

A preservacao via location.state e recomendada porque o destino nao fica visivel na URL e e mais dificil de ser manipulado externamente, reduzindo o risco de open redirect. A query string pode ser usada, mas exige validacao rigorosa.

Próximo capitúlo

Interceptação de requisições para anexar JWT e padronizar erros com fetch e axios

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