Skip to content
This page has been auto-translated and may contain errors.View in English

Rotas protegidas

Algumas partes de um app só deveriam ser visíveis para um usuário autenticado: um dashboard, uma página de conta, qualquer coisa que busque dados pessoais. React Router não tem um recurso dedicado de "rota protegida", e não precisa de um. Os componentes de roteamento e rotas aninhadas se combinam para formar o padrão: uma rota de layout que verifica se o usuário está autenticado e renderiza seus filhos ou um redirecionamento para a página de login.

Uma coisa importante deixar clara antes de qualquer código. Proteger rotas no cliente é um recurso de experiência do usuário. Mantém visitantes não autenticados longe de páginas que buscariam seus dados e exibiriam telas vazias e quebradas. Nunca é segurança, porque tudo em um bundle de cliente é inspecionável e contornável.

A proteção real acontece no servidor, que deve recusar entregar dados para requisições que não estejam autenticadas. Autenticação cobre como essa aplicação funciona; este capítulo é sobre a parte do cliente.

Uma rota de layout com autenticação obrigatória

Uma rota de layout é uma rota pai cujo elemento renderiza UI compartilhada mais um Outlet para seus filhos. Uma rota de layout sem caminho não adiciona nenhum segmento de URL; existe puramente para envolver. Isso a torna o lugar perfeito para uma verificação de login: envolva cada rota que você quer proteger em uma rota de layout sem caminho cujo único trabalho é decidir se o Outlet renderiza.

jsx
<Route path="/" element={<Layout />}>
  <Route index element={<Home />} />
  <Route path="login" element={<Login />} />

  <Route element={<AuthRequired />}>
    <Route path="host" element={<Dashboard />} />
    <Route path="host/vans" element={<HostVans />} />
    <Route path="host/vans/:id" element={<HostVanDetail />} />
  </Route>
</Route>

Tudo aninhado dentro de <Route element={<AuthRequired />}> agora está atrás do protetor, incluindo rotas com parâmetros como host/vans/:id. Proteger uma nova página depois é uma linha: mova sua rota para dentro do wrapper.

O protetor em si tem dois caminhos. Se o usuário está autenticado, renderize o Outlet para que o filho correspondente apareça. Se não estiver, renderize algo que o envie para a página de login em vez disso. Não renderizar nada é o truque todo: se um componente protegido nunca renderiza, qualquer busca de dados dentro dele nunca começa.

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

export default function AuthRequired() {
  const authenticated = false // substituto para uma verificação de sessão real

  if (!authenticated) {
    return <Navigate to="/login" />
  }
  return <Outlet />
}

O curso finge a verificação com um booleano, e depois com um valor em localStorage, para manter o foco no roteamento. Uma verificação real lê o estado da sessão, frequentemente entregue através de contexto; novamente, Autenticação cobre de onde esse estado realmente vem.

Nota sobre versão

Os exemplos aqui importam de react-router-dom, que funciona tanto em v6 quanto em v7; Roteamento cobre o que v7 mudou sobre os pacotes. O curso ensina v6 em toda esta seção.

O componente Navigate

Link renderiza um âncora e espera ser clicado. Navigate pula a espera: assim que renderiza, o roteador move o usuário para seu caminho to. Isso o torna a ferramenta de redirecionamento para lógica de renderização.

Seu componente decide, no meio da renderização, que o usuário não deveria estar aqui, e retorna <Navigate to="/login" /> em vez de qualquer UI. Há também um hook useNavigate que retorna uma função para redirecionar de manipuladores de eventos, que é como o próprio formulário de login envia o usuário adiante após um envio bem-sucedido.

Chegar em uma página de login vazia sem explicação é confuso, então o protetor deveria dizer por que o usuário acabou lá. Navigate recebe a mesma prop state que Link recebe, então um redirecionamento pode entregar ao destino uma razão.

jsx
if (!authenticated) {
  return (
    <Navigate
      to="/login"
      state={{ message: 'Você deve fazer login primeiro' }}
    />
  )
}
jsx
import { useLocation } from 'react-router-dom'

function Login() {
  const location = useLocation()

  return (
    <>
      {location.state?.message && <h3>{location.state.message}</h3>}
      <h1>Entre na sua conta</h1>
      {/* form... */}
    </>
  )
}

O optional chaining importa, porque alguém que clica direto na página de login nunca passou pelo protetor e encontra location.state definido como null.

A pilha de histórico e replace

O padrão até agora tem um bug que você pode sentir. Visite uma página protegida enquanto desautenticado, seja redirecionado, faça login, depois pressione o botão Voltar do navegador. Em vez da página que você estava antes de tudo isso, você chega na página de login de novo, completa com sua mensagem "você deve fazer login primeiro".

O navegador mantém uma pilha de histórico: cada navegação empurra uma nova entrada, e Voltar retorna à anterior. O redirecionamento empurrou /login na pilha logo após a página protegida, e a própria navegação do formulário de login empurrou a página protegida por cima disso. Voltar o caminha direto para as sobras.

A correção é fazer redirecionamentos substituir a entrada atual em vez de empurrar uma nova. No componente Navigate é a prop replace; na função useNavigate é uma opção.

jsx
// em AuthRequired: o redirecionamento substitui a si mesmo pela página bloqueada
return <Navigate to="/login" state={{ message: 'Você deve fazer login primeiro' }} replace />

// em Login, após um envio bem-sucedido
navigate('/host', { replace: true })

Com ambos no lugar, o desvio de login nunca sobrevive no histórico. Voltar da página protegida agora vai para onde o usuário estava antes, e a página de login não pode ser revisitada por acidente. Uma boa regra de ouro: qualquer navegação que o usuário não pediu, que é o que um redirecionamento é, deve substituir.

De volta para onde estavam indo

Uma aspereza permanece. O formulário de login codifica navigate('/host', ...), então um usuário que estava tentando alcançar /host/vans/2 é deixado em /host depois de fazer login. O protetor conhece a URL bloqueada, porque useLocation dentro de AuthRequired descreve a página que o usuário estava tentando renderizar. Passe adiante no mesmo estado de navegação.

jsx
export default function AuthRequired() {
  const location = useLocation()
  const authenticated = false

  if (!authenticated) {
    return (
      <Navigate
        to="/login"
        state={{
          message: 'Você deve fazer login primeiro',
          from: location.pathname + location.search + location.hash,
        }}
        replace
      />
    )
  }
  return <Outlet />
}

pathname sozinho não é o endereço completo: um usuário bloqueado em /host/vans?type=luxury estava indo para a lista filtrada, e descartar a query string remove o filtro. Adicionar search e hash reproduz cada parte da URL que o roteador rastreia. A página de login lê o valor de volta, com um fallback para pessoas que vieram para a página de login diretamente e portanto não têm estado.

jsx
function Login() {
  const location = useLocation()
  const navigate = useNavigate()
  const from = location.state?.from || '/host'

  function handleLogin() {
    // ...ao sucesso:
    navigate(from, { replace: true })
  }
  // ...
}

Agora um link compartilhado para qualquer página protegida sobrevive ao desvio de login: bloqueado, redirecionado, autenticado, e entregue à URL exata que queriam.

O substituto const authenticated = false oculta o primeiro bug que esse padrão encontra em um app real. O estado da sessão resolve de forma assíncrona: na primeira renderização o app pediu ao provedor de autenticação se uma sessão existe e ainda não ouviu de volta. Um protetor de duas vias lê esse silêncio como "desconectado" e redireciona um usuário que está conectado, então um refresh em uma página protegida o envia para a tela de login por um momento antes que o app se corrija. Modele três estados em vez disso, que tem a mesma forma que Autenticação constrói no lado do provedor.

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

export default function AuthRequired() {
  const { session } = useAuth() // undefined enquanto resolvendo, null quando desconectado
  const location = useLocation()

  if (session === undefined) {
    return <p>Verificando sua sessão...</p>
  }
  if (session === null) {
    return (
      <Navigate
        to="/login"
        state={{
          message: 'Você deve fazer login primeiro',
          from: location.pathname + location.search + location.hash,
        }}
        replace
      />
    )
  }
  return <Outlet />
}

useAuth é um hook customizado sobre um provedor de contexto, o padrão de contexto: o provedor possui o estado da sessão e cada componente lê a mesma resposta. Os três valores são o ponto todo. undefined significa a verificação ainda está em andamento, null significa que voltou vazia, e um objeto de sessão significa que alguém está autenticado.

O ramo de carregamento é o que as pessoas deixam de fora, e é a diferença entre um protetor que sobrevive a um refresh e um que pula usuários autenticados. Mantenha esse ramo leve, já que renderiza na primeira pintura de cada página protegida, e dê a ele presença suficiente para que a tela não pareça quebrada enquanto a verificação executada.

Navigate é um redirecionamento declarativo: renderizá-lo é a instrução. Nos bastidores ele chama a função navigate do roteador de um efeito após o render se comprometer, porque navegar é um efeito colateral e um componente não pode causar um enquanto está renderizando. Retorná-lo cedo também dá um atalho na subárvore inteira, e esse é o ganho real de proteger no nível de layout: componentes que nunca montam nunca executam seus efeitos, então suas buscas nunca disparam. O protetor fica em um único ponto de estrangulamento em vez de ser espalhado através de cada componente protegido como uma verificação por página.

O estado de navegação viaja na entrada de histórico do navegador, com todas as consequências que route params funciona através. replace mapeia diretamente para semântica history.replaceState: a entrada é sobrescrita, então o redirecionamento não deixa rastro para o Voltar caminhar.

Um protetor escrito como componente tem que renderizar antes de poder decidir qualquer coisa, então uma sessão não resolvida traz no melhor dos casos um estado de carregamento. As APIs de dados do React Router movem a decisão mais cedo: um loader executa antes do elemento da rota renderizar, e lançar um redirect dele significa que o componente protegido nunca é criado em primeiro lugar.

jsx
import { redirect } from 'react-router-dom'

async function hostLoader({ request }) {
  const session = await getSession()
  if (!session) {
    const url = new URL(request.url)
    const from = url.pathname + url.search + url.hash
    throw redirect(`/login?from=${encodeURIComponent(from)}`)
  }
  return null
}

Essa versão carrega a origem na query string, e o movimento traz uma consequência para pegar antes que vire produção. O estado de histórico é escrito por seu próprio protetor; uma query string é digitada por qualquer um que envia o link.

Valide o valor from antes de navegar

/login?from=https://phish.example/host é uma URL válida que qualquer um pode postar, e um manipulador de login que lê from e o entrega para navigate vai levar o usuário para fora do site no instante em que se autentique, tendo assistido a ele digitar uma senha na sua página de login real. Aceite o valor apenas quando começar com uma única /, o que rejeita tanto URLs absolutas quanto a forma relativa de protocolo //host que navegadores tratam como fora do site.

jsx
const raw = searchParams.get('from') || ''
const from = /^\/(?!\/)/.test(raw) ? raw : '/host'
JunoUm protetor para todo o ramo Em vez de verificar por login dentro de cada página privada, você envolve todas essas rotas em um componente protetor. Se o usuário está autenticado, ele renderiza Outlet e as páginas mostram normalmente. Se não estiver, ele renderiza Navigate, que o leva para a página de login no momento em que aparece.

Adicionar replace mantém o redirecionamento fora da memória do botão Voltar, e passar junto para onde estavam indo deixa a página de login enviá-los direto de volta para lá.

Lembre-se que isso é gentileza para o usuário; o servidor ainda tem que proteger os dados em si.

JunoUm protetor para todo o ramo Envolva rotas protegidas em uma rota de layout sem caminho cujo elemento lê a sessão de um hook useAuth e se divide em três caminhos: uma linha de carregamento enquanto a sessão é undefined, Navigate to="/login" quando é null, e Outlet uma vez que um objeto de sessão chega. Pular esse primeiro ramo é o que pula usuários autenticados para a página de login após um refresh.

Dê ao redirecionamento um objeto state carregando uma mensagem mais location.pathname + location.search + location.hash como from, marque-o replace, e no manipulador de login chame navigate(from, { replace: true }) com um fallback sensato. Mantenha o servidor aplicando acesso em cada requisição independentemente.

JunoUm protetor para todo o ramo Proteger no nível de layout dá um atalho na subárvore antes de montar, então componentes protegidos nunca renderizam e suas buscas nunca começam. O estado de navegação vive na entrada de histórico em si, e replace é history.replaceState em roupas de roteador: redirecionamentos sempre devem substituir para que o desvio não deixe uma entrada para trás.

Um protetor de componente ainda tem que renderizar uma vez antes de poder decidir, então em setups de data-router a verificação se move para um loader que lança redirect antes do elemento existir. Uma vez que a URL de origem viaja em uma query string ela é fornecida pelo atacante, então valide-a contra uma barra inicial única antes de navegar para ela.

Nada disso é aplicação; o servidor possui isso.

A seguir: Como React renderiza, o modelo mental por trás de cada decisão de desempenho que se segue.