Como Adicionar Autenticação de Login a Aplicações React
O autor escolheu a Creative Commons para receber uma doação como parte do programa Escrever para Doações.
Introdução
A autenticação em aplicações React gere o acesso dos utilizadores a páginas e recursos protegidos. Quando um utilizador inicia sessão, a aplicação armazena as credenciais de autenticação (normalmente um token) e verifica-as antes de renderizar rotas privadas. O estado de autenticação deve persistir após a atualização da página, o que requer o armazenamento dos tokens no armazenamento do navegador em vez de no estado do componente.
Como o estado de autenticação persiste: Quando armazena tokens no localStorage ou sessionStorage, eles sobrevivem a atualizações de página e reinícios do navegador (no caso do localStorage). Isto significa que a sua aplicação pode verificar se existe um token ao iniciar e autenticar automaticamente os utilizadores sem necessidade de um novo início de sessão. Sem armazenamento persistente, os utilizadores teriam de iniciar sessão todas as vezes que atualizam a página ou abrem um novo separador.
Este tutorial explica como construir uma aplicação React com autenticação baseada em tokens, utilizando o React Router v6 para rotas protegidas. Criarás uma API simulada que retorna tokens de autenticação, implementarás uma página de login que obtém e armazena os tokens, e configurarás verificações de autenticação que preservam a rota pretendida pelo utilizador. O tutorial aborda diferentes métodos de armazenamento de tokens (localStorage, sessionStorage) e as suas implicações de segurança, implementa a funcionalidade de logout e tratamento de erros, e demonstra como decodificar tokens JWT para verificar a expiração.
Por que a lógica de autenticação não deve estar em componentes individuais: Centralizar a lógica de autenticação num hook personalizado ou no React Context garante um comportamento consistente em toda a aplicação, facilita a atualização dos fluxos de autenticação e evita duplicação de código. Quando a lógica de autenticação está espalhada pelos componentes, corre-se o risco de verificações de token inconsistentes, casos limites não tratados e dificuldades de manutenção.
Depois de completar este tutorial, você compreenderá como implementar autenticação segura em aplicações React, escolher estratégias apropriadas de armazenamento de tokens com base em requisitos de segurança e experiência do utilizador, e lidar com cenários comuns de autenticação, incluindo rotas protegidas, expiração de tokens e logout.
Precisa de lançar um projeto React rapidamente? Experimente a plataforma de aplicações Linux-Console.net e lance um projeto React diretamente do GitHub em minutos.
Principais Conclusões
O estado de autenticação persiste após atualizações da página quando armazenado em
localStorageousessionStorage. Os tokens armazenados no estado do componente (useState) são perdidos ao atualizar a página, obrigando os utilizadores a iniciar sessão novamente. As APIs de armazenamento do navegador mantêm os tokens entre sessões, permitindo reautenticação automática.O React Router v6 usa
Routesem vez deSwitche requer a propriedadeelementem vez de children. A nova API é mais declarativa e segura em termos de tipos. Rotas protegidas são implementadas usando um componente wrapper que verifica a autenticação antes de renderizar.Centralize a lógica de autenticação num hook personalizado ou no React Context. Manter a lógica de autenticação em componentes individuais leva à duplicação e a comportamentos inconsistentes. Um hook personalizado
useTokenouAuthContextfornece uma única fonte de verdade para o estado de autenticação.localStoragepersiste entre sessões do navegador;sessionStorageé específico de cada separador. UselocalStoragepara a funcionalidade de “lembrar-me” esessionStoragepara autenticação mais segura, apenas durante a sessão. Ambos são vulneráveis a ataques XSS, por isso, sempre faça a sanitização da entrada do utilizador.Os tokens JWT podem ser decodificados no cliente para verificar a expiração sem uma ida ao servidor. O payload contém uma declaração
exp(timestamp Unix) que pode comparar com a hora atual. No entanto, valide sempre os tokens no servidor, pois as verificações no lado do cliente são apenas para a experiência do utilizador.A proteção contra CSRF é crítica ao usar autenticação baseada em cookies. Os cookies são enviados automaticamente com as solicitações, tornando-os vulneráveis a ataques de falsificação de requisições entre sites. Utilize tokens CSRF, atributos de cookie SameSite e verifique o cabeçalho Origin no servidor.
Pré-requisitos
Um ambiente de desenvolvimento a correr Node.js versão 18.x e npm versão 9.x. Para instalar o Node.js no macOS ou Ubuntu, siga os passos em Como Instalar o Node.js e Criar um Ambiente de Desenvolvimento Local no macOS ou Como Instalar o Node.js no Ubuntu.
-
Um ambiente de desenvolvimento React configurado com o Create React App, com o boilerplate não essencial removido. Para configurar isto, siga Passo 1 — Criar um Projeto Vazio do tutorial Como Gerir Estado em Componentes de Classe no React. Este tutorial utiliza
auth-tutorialcomo nome do projeto. Familiaridade com a obtenção de dados de APIs em React. Para saber mais sobre como trabalhar com APIs, veja Como Chamar APIs Web com o Hook useEffect em React.
Conhecimentos básicos de JavaScript, HTML e CSS. Saiba mais na nossa série Como Criar um Website com HTML, Como Estilizar HTML com CSS e Como Programar em JavaScript.
Passo 1 — Criar uma Página de Login
As páginas de login interceptam os utilizadores não autenticados antes de chegarem a rotas protegidas. Em vez de redirecionar os utilizadores para uma rota de login dedicada (o que faz perder o destino pretendido), apresente o formulário de login em qualquer rota quando não existir um token de autenticação. Isto preserva a intenção de navegação do utilizador de visitar /dashboard enquanto está desconectado, mostrando o formulário de login, e após a autenticação, ele chega a /dashboard em vez de ser redirecionado para outro lugar.
Por que exibir o login em qualquer rota: Quando os utilizadores visitam uma rota protegida como /dashboard enquanto não estão autenticados, mostrar o formulário de login nessa mesma rota mantém o contexto da sua navegação. Após o login, eles já se encontram no destino pretendido sem necessidade de redirecionamentos adicionais. Isto cria uma experiência de utilizador mais fluida em comparação com redirecionar para /login e depois exigir outro passo de navegação.
Instale o React Router usando o npm. Existem duas versões: uma versão web para aplicações React e uma versão nativa para React Native. Instale a versão web:
npm install react-router-domA instalação é concluída com uma saída semelhante a:
...
+ react-router-dom@6.28.0
added 11 packages from 6 contributors, removed 10 packages and audited 1945 packages in 12.794s
...Em seguida, crie dois componentes chamados Dashboard e Preferences para funcionar como páginas privadas. Estes representarão componentes que um utilizador não deve ver até ter iniciado sessão com sucesso na aplicação.
Primeiro, crie os diretórios:
mkdir src/components/Dashboard
mkdir src/components/PreferencesAbra Dashboard.js num editor de texto:
nano src/components/Dashboard/Dashboard.js
Dentro de Dashboard.js, adicione uma tag <h2> com o conteúdo Dashboard:
import React from 'react';
export default function Dashboard() {
return(
<h2>Dashboard</h2>
);
}Guardar e fechar o ficheiro.
Repita os mesmos passos para Preferências. Abra o componente:
nano src/components/Preferences/Preferences.jsAdicionar o conteúdo:
import React from 'react';
export default function Preferences() {
return(
<h2>Preferences</h2>
);
}Guardar e fechar o ficheiro.
Agora que tem alguns componentes, precisa de importar os componentes e criar rotas dentro de App.js. Consulte o tutorial Como Lidar com Roteamento em Aplicações React com React Router para uma introdução completa ao roteamento em aplicações React. Para implantar a sua aplicação React, veja Como Implantar uma Aplicação React com Nginx no Ubuntu.
Para começar, abra App.js:
nano src/components/App/App.jsEm seguida, importe Dashboard e Preferences adicionando o seguinte código destacado:
import React from 'react';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';
function App() {
return (
<></>
);
}
export default App;De seguida, importe BrowserRouter, Routes e Route a partir de react-router-dom. O React Router v6 utiliza Routes em vez de Switch, e as rotas usam a propriedade element em vez de children:
import React from 'react';
import './App.css';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';
function App() {
return (
<></>
);
}
export default App;Adicione um <div> à volta com um className de wrapper e uma tag <h1> para servir de modelo para a aplicação. Certifique-se de que está a importar App.css para poder aplicar os estilos.
De seguida, crie rotas para os componentes Dashboard e Preferences. Adicione o BrowserRouter, depois adicione um componente Routes como filho. Dentro do Routes, adicione um Route com uma propriedade path e element para cada componente:
import React from 'react';
import './App.css';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';
function App() {
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/preferences" element={<Preferences />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Guardar e fechar o ficheiro.
O passo final é adicionar algum preenchimento ao <div> principal para que o seu componente não fique diretamente na borda do navegador. Para isso, terá de alterar o CSS.
Abra App.css:
nano src/components/App/App.cssSubstitua o conteúdo por uma classe .wrapper com padding de 20px:
.wrapper {
padding: 20px;
}Guarde e feche o ficheiro. O navegador recarrega automaticamente, exibindo os componentes básicos:
Visite http://localhost:3000/dashboard para ver a página do painel:
As rotas funcionam, mas /dashboard e /preferences estão atualmente acessíveis a qualquer pessoa, incluindo utilizadores não autenticados. Estas deveriam ser rotas protegidas.
Duas abordagens para proteger rotas:
Redirecionar para a rota de login dedicada: Crie uma rota
/logine redirecione para lá os utilizadores não autenticados. Isto funciona, mas os utilizadores perdem o destino pretendido e têm de voltar a navegar até lá após o login.Renderizar formulário de login em qualquer rota (usado neste tutorial): Mostra o formulário de login na rota atual quando não existe nenhum token. Após a autenticação, os utilizadores permanecem no destino pretendido. Por exemplo, visitar
/dashboardestando desconectado mostra o formulário de login, e depois do login, os utilizadores aterragem diretamente em/dashboard.
Este tutorial utiliza a segunda abordagem para uma melhor experiência do utilizador.
Para começar, crie um novo diretório para o componente Login:
mkdir src/components/LoginDe seguida, abra Login.js num editor de texto:
nano src/components/Login/Login.jsCrie um formulário básico com um <button> de submissão e um <input> para o nome de utilizador e a palavra-passe. Certifique-se de definir o tipo de input para a palavra-passe como password:
import React from 'react';
export default function Login() {
return(
<form>
<label>
<p>Username</p>
<input type="text" />
</label>
<label>
<p>Password</p>
<input type="password" />
</label>
<div>
<button type="submit">Submit</button>
</div>
</form>
)
}Para saber mais sobre formulários em React, consulte o tutorial Como Criar Formulários em React. Para aprender sobre a gestão do estado em componentes React, veja Como Gerir o Estado com Hooks em Componentes React.
De seguida, adicione uma tag <h1> a pedir ao utilizador para iniciar sessão. Envolva o <form> e o <h1> num <div> com um className de login-wrapper. Por fim, importe Login.css:
import React from 'react';
import './Login.css';
export default function Login() {
return(
<div className="login-wrapper">
<h1>Please Log In</h1>
<form>
<label>
<p>Username</p>
<input type="text" />
</label>
<label>
<p>Password</p>
<input type="password" />
</label>
<div>
<button type="submit">Submit</button>
</div>
</form>
</div>
)
}Guardar e fechar o ficheiro.
Adicione estilo para centralizar o formulário de login. Abra o Login.css:
nano src/components/Login/Login.cssCentre o componente na página adicionando um display de flex, depois definindo o flex-direction para column para alinhar os elementos verticalmente e adicionando align-items a center para que o componente fique centrado no navegador:
.login-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}Para mais informações sobre como usar o Flexbox, consulte o nosso Folheto de Consulta Rápida de CSS Flexbox
Guardar e fechar o ficheiro.
Renderize o componente de login em App.js quando não existir nenhum token de autenticação. Abra o App.js:
nano src/components/App/App.jsNo Passo 3, irá implementar o armazenamento persistente de tokens. Por agora, armazene o token no estado do componente usando o Hook useState para testar o fluxo de login. Note que os tokens armazenados no estado do componente são perdidos ao atualizar a página.
Importe useState de react, depois chame useState e defina os valores de retorno para token e setToken:
import React, { useState } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';
function App() {
const [token, setToken] = useState();
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/preferences" element={<Preferences />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Importe o componente Login. Adicione uma declaração condicional para mostrar o Login se o token for falso.
Passe a função setToken para o componente Login:
import React, { useState } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
function App() {
const [token, setToken] = useState();
if(!token) {
return <Login setToken={setToken} />
}
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/preferences" element={<Preferences />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Como ainda não existe nenhum token, o formulário de login é exibido em todas as rotas. No próximo passo, irá criar um endpoint de API que devolve um token após uma autenticação bem-sucedida.
Guarde e feche o ficheiro. O navegador recarrega automaticamente, mostrando a página de login em todas as rotas. Visite a página do painel para confirmar que o formulário de login aparece:
Criou um formulário de login que é exibido em todas as rotas quando não existe um token de autenticação. O formulário impede o acesso a rotas protegidas como /dashboard e /preferences até que a autenticação tenha sucesso. No próximo passo, irá criar um servidor API simulado que retorna tokens de autenticação.
Passo 2 — Criar uma API de Token
As APIs simuladas permitem simular a autenticação do backend durante o desenvolvimento sem necessitar de um servidor de produção. O Express fornece uma forma simples de criar um servidor de desenvolvimento que devolve tokens no mesmo formato que a sua API de produção irá utilizar. Isto permite testar fluxos de autenticação, lidar com respostas da API e desenvolver o frontend de forma independente.
Por que usar uma API simulada: Durante o desenvolvimento, pode não ter um backend pronto ou pode querer testar fluxos de autenticação sem aceder a um servidor de produção. Uma API simulada retorna respostas previsíveis, facilitando o teste do tratamento de erros, estados de carregamento e gestão de tokens. Para aplicações de produção, substitua a API simulada pelo seu endpoint de autenticação real.
Crie um servidor Express simples que devolva um token. Para uma introdução detalhada ao Express, veja Servidor Express Básico em Node.js. Se estiver a construir uma aplicação full-stack, considere Como Construir uma Aplicação To-Do Usando Django e React para um exemplo completo.
Para começar, instale o express. Como o servidor não é um requisito da versão final, certifique-se de o instalar como uma devDependency.
Também precisará de instalar o cors. Esta biblioteca irá permitir a partilha de recursos entre origens em todas as rotas.
Aviso: Não ative CORS para todas as rotas numa aplicação em produção. Isto pode levar a vulnerabilidades de segurança.
npm install --save-dev express corsA instalação é concluída com uma saída semelhante a:
...
+ cors@2.8.5
+ express@4.17.1
removed 10 packages, updated 2 packages and audited 2059 packages in 12.597s
...De seguida, abra um novo ficheiro chamado server.js na raiz da sua aplicação. Não adicione este ficheiro ao diretório /src, uma vez que não quer que faça parte da versão final.
nano server.jsImporte express, depois inicialize uma nova aplicação chamando express() e guardando o resultado numa variável chamada app:
const express = require('express');
const app = express();Após criar a app, adicione o cors como middleware. Primeiro, importe o cors, depois adicione-o à aplicação chamando o método use no app:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());Adicione um manipulador de rota para /login que retorne um token. O manipulador recebe req (dados da requisição) e res (objeto de resposta). Use res.send para retornar um objeto JavaScript contendo um token:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.use('/login', (req, res) => {
res.send({
token: 'test123'
});
});Finalmente, execute o servidor na porta 8080 usando app.listen:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.use('/login', (req, res) => {
res.send({
token: 'test123'
});
});
app.listen(8080, () => console.log('API is running on http://localhost:8080'));Guarde e feche o ficheiro. Numa nova janela ou separador do terminal, inicie o servidor:
node server.jsO servidor inicia-se e apresenta:
API is running on http://localhost:8080/loginVisite a página de início de sessão no seu navegador para ver a resposta em objeto JSON:
Nota: Aceder à URL num navegador envia uma requisição GET, enquanto o formulário de login enviará uma requisição POST. O manipulador de rota atual aceita ambos os métodos. Em produção, restrinja as rotas a métodos HTTP específicos usando app.post() para requisições POST e app.get() para requisições GET. Consulte a documentação de roteamento do Express para mais detalhes.
Agora que tem um servidor API em funcionamento, precisa de fazer um pedido a partir da sua página de login. Abra Login.js:
nano src/components/Login/Login.jsNo passo anterior, passou uma nova prop chamada setToken para o componente Login. Adicione o PropType da nova prop e desestruture o objeto de props para retirar a prop setToken.
import React from 'react';
import PropTypes from 'prop-types';
import './Login.css';
export default function Login({ setToken }) {
return(
<div className="login-wrapper">
<h1>Please Log In</h1>
<form>
<label>
<p>Username</p>
<input type="text" />
</label>
<label>
<p>Password</p>
<input type="password" />
</label>
<div>
<button type="submit">Submit</button>
</div>
</form>
</div>
)
}
Login.propTypes = {
setToken: PropTypes.func.isRequired
}De seguida, crie um estado local para capturar o Nome de Utilizador e a Senha. Como não é necessário definir manualmente os dados, faça com que os <inputs> sejam componentes não controlados. Pode encontrar informações detalhadas sobre componentes não controlados em Como Construir Formulários em React.
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Login.css';
export default function Login({ setToken }) {
const [username, setUserName] = useState();
const [password, setPassword] = useState();
return(
<div className="login-wrapper">
<h1>Please Log In</h1>
<form>
<label>
<p>Username</p>
<input type="text" onChange={e => setUserName(e.target.value)}/>
</label>
<label>
<p>Password</p>
<input type="password" onChange={e => setPassword(e.target.value)}/>
</label>
<div>
<button type="submit">Submit</button>
</div>
</form>
</div>
)
}
Login.propTypes = {
setToken: PropTypes.func.isRequired
};Crie uma função para fazer um pedido POST ao servidor. Em aplicações maiores, coloque chamadas à API num diretório de serviços separado. Para este tutorial, a função está definida no ficheiro do componente. Para saber mais sobre chamadas à API em React, veja Como Chamar Web APIs com o Hook useEffect em React.
Crie uma função async chamada loginUser. A função receberá credentials como argumento e depois chamará o método fetch usando a opção POST:
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Login.css';
async function loginUser(credentials) {
return fetch('http://localhost:8080/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
})
.then(data => data.json())
}
export default function Login({ setToken }) {
...Finalmente, crie um manipulador de envio de formulário chamado handleSubmit que chamará loginUser com o username e a password. Chame setToken com um resultado bem-sucedido. Chame handleSubmit usando o manipulador de evento onSubmit no <form>:
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Login.css';
async function loginUser(credentials) {
return fetch('http://localhost:8080/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
})
.then(data => data.json())
}
export default function Login({ setToken }) {
const [username, setUserName] = useState();
const [password, setPassword] = useState();
const handleSubmit = async e => {
e.preventDefault();
const token = await loginUser({
username,
password
});
setToken(token);
}
return(
<div className="login-wrapper">
<h1>Please Log In</h1>
<form onSubmit={handleSubmit}>
<label>
<p>Username</p>
<input type="text" onChange={e => setUserName(e.target.value)} />
</label>
<label>
<p>Password</p>
<input type="password" onChange={e => setPassword(e.target.value)} />
</label>
<div>
<button type="submit">Submit</button>
</div>
</form>
</div>
)
}
Login.propTypes = {
setToken: PropTypes.func.isRequired
};Nota: Em aplicações de produção, trate o desmontar de componentes durante operações assíncronas. Se um utilizador navegar para outra página antes de uma Promise ser resolvida, poderá ser necessário cancelar o pedido ou ignorar a resposta. Veja Como Chamar APIs Web com o Hook useEffect no React para padrões de limpeza.
Guarde e feche o ficheiro. Certifique-se de que o servidor da API ainda está em execução e depois aceda a http://localhost:3000/dashboard no seu navegador.
A página de login aparece. Introduza qualquer nome de utilizador e palavra-passe e, em seguida, envie o formulário. A API retorna um token e a aplicação redireciona para o painel de controlo.
O fluxo de login funciona: ao submeter o formulário, a API é chamada e o token retornado é armazenado. No entanto, o token é armazenado no estado do componente (memória do JavaScript), o que significa que se perde ao atualizar a página ou ao abrir uma nova aba. Os utilizadores teriam de iniciar sessão novamente após cada atualização.
O problema: O estado do componente (useState) não persiste após recarregamentos da página. Quando o navegador é atualizado, o React reinicializa os componentes e qualquer estado armazenado na memória é perdido.
No próximo passo, irá implementar o armazenamento persistente de tokens utilizando as APIs de armazenamento do navegador (localStorage ou sessionStorage) para que a autenticação persista após atualizações da página e sessões do navegador.
Passo 3 — Armazenar um Token de Utilizador com sessionStorage e localStorage
A localização do armazenamento do token determina a persistência da sessão e as compensações de segurança. localStorage mantém os utilizadores ligados entre sessões e separadores do navegador, enquanto sessionStorage é limpo quando o separador é fechado. Ambos são acessíveis ao JavaScript, tornando-os vulneráveis a ataques XSS, mas proporcionam uma melhor experiência ao utilizador do que armazenar tokens apenas no estado do componente.
Comparação de opções de armazenamento:
- Estado do componente (
useState): Perdido ao atualizar a página. Sem persistência. sessionStorage: Persiste durante a sessão do separador do navegador. Limpa-se quando o separador é fechado. Não é partilhado entre separadores.localStorage: Persiste entre sessões e separadores do navegador. Sobrevive a reinícios do navegador. Partilhado por todos os separadores da mesma origem.- Cookies: Enviados automaticamente com as solicitações. Requerem proteção CSRF. Podem ser
httpOnly(não acessíveis via JavaScript, mais seguros).
Consideração de segurança: Quaisquer dados armazenados fora da memória do componente (incluindo localStorage, sessionStorage e cookies acessíveis ao JavaScript) estão vulneráveis a ataques de Cross-Site Scripting (XSS). Se código malicioso for executado no seu domínio, ele poderá aceder a estes mecanismos de armazenamento. Sempre sanitizar a entrada do utilizador e usar cabeçalhos Content Security Policy (CSP) para mitigar os riscos de XSS.
Este tutorial aborda o sessionStorage e o localStorage como alternativas modernas aos cookies para armazenamento de tokens. Os cookies são abordados no Passo 8 ao discutir a proteção contra CSRF.
Armazenamento de Sessão
Para testar os benefícios de armazenar fora da memória, converta o armazenamento em memória para sessionStorage. Abra o App.js:
nano src/components/App/App.jsRemova a chamada para useState e crie duas novas funções chamadas setToken e getToken. Depois, chame getToken e atribua os resultados a uma variável chamada token:
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
function setToken(userToken) {
}
function getToken() {
}
function App() {
const token = getToken();
if(!token) {
return <Login setToken={setToken} />
}
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/preferences" element={<Preferences />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Usar os mesmos nomes de funções (setToken, getToken) e nomes de variáveis (token) significa que o componente Login e o resto do App.js não precisam de alterações. A interface mantém-se igual, mas a implementação passa agora a usar armazenamento persistente.
Implemente setToken para guardar o token no sessionStorage. O método setItem requer uma chave (string) e um valor (string), por isso converta o objeto userToken numa string JSON usando JSON.stringify:
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
function setToken(userToken) {
sessionStorage.setItem('token', JSON.stringify(userToken));
}
function getToken() {
}
function App() {
const token = getToken();
if(!token) {
return <Login setToken={setToken} />
}
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/preferences" element={<Preferences />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Guarde o ficheiro. O navegador recarrega automaticamente. Após submeter o formulário de login, o token é guardado no sessionStorage, mas a página de login continua a ser exibida porque o React ainda não se re-renderizou. Abra as ferramentas de desenvolvimento do seu navegador (F12) e verifique o separador Storage para confirmar que o token está armazenado. A imagem seguinte mostra as ferramentas de desenvolvimento do Firefox; o Chrome e outros navegadores modernos têm ferramentas semelhantes de inspeção de armazenamento.
Implemente getToken para obter o token do sessionStorage. O método getItem retorna a string armazenada (ou null se não existir valor). Converta a string JSON de volta para um objeto e, em seguida, devolva a propriedade token:
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
function setToken(userToken) {
sessionStorage.setItem('token', JSON.stringify(userToken));
}
function getToken() {
const tokenString = sessionStorage.getItem('token');
if (!tokenString) {
return null;
}
const userToken = JSON.parse(tokenString);
return userToken?.token;
}
function App() {
const token = getToken();
if(!token) {
return <Login setToken={setToken} />
}
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/preferences" element={<Preferences />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Por que usar encadeamento opcional (?.): Na primeira visita, sessionStorage.getItem('token') retorna null, e JSON.parse(null) retorna null. Sem o encadeamento opcional, aceder a userToken.token quando userToken é null lança um TypeError. O operador ?. retorna undefined em segurança em vez de lançar um erro.
Guarde e feche o ficheiro. Se existir um token em sessionStorage, o navegador atualiza automaticamente e navega para as páginas protegidas:
Para testar novamente o fluxo de login, limpe o token armazenado. Nas ferramentas de desenvolvimento do seu navegador, apague o token na aba Armazenamento ou execute sessionStorage.clear() na consola.
O problema: Após iniciar sessão, o token é guardado em sessionStorage, mas a página de login continua a ser exibida. Isto acontece porque o React não sabe que o token foi armazenado, e o componente precisa de ser renderizado novamente quando o token muda.
Por que o React não re-renderiza: Armazenar dados em sessionStorage não dispara re-renderizações no React. O React só re-renderiza quando o estado muda. Para resolver isto, use o estado do React para acompanhar o token e atualize esse estado ao armazenar ou recuperar tokens. Isso dispara re-renderizações e atualiza a interface.
Solução: Crie um Hook personalizado que combine operações de armazenamento com o estado do React. Isto centraliza a lógica de gestão de tokens e torna-a reutilizável em vários componentes.
Criar um Hook de Token Personalizado
O que é um Hook personalizado: Um Hook personalizado é uma função em JavaScript que utiliza Hooks do React (como useState, useEffect) e encapsula lógica reutilizável. Por convenção, os Hooks personalizados começam com use* (por exemplo, useToken, useAuth). Os Hooks personalizados permitem-lhe extrair a lógica de um componente para funções reutilizáveis, tornando o código mais fácil de manter e testar.
Abra um novo ficheiro na diretoria App chamado useToken.js:
nano src/components/App/useToken.jsOrganização de ficheiros: Este Hook poderia ser definido diretamente em App.js, mas extraí-lo para um ficheiro separado demonstra como os Hooks personalizados funcionam de forma independente dos componentes. À medida que a sua aplicação cresce e reutiliza este Hook em vários componentes, mantê-lo num ficheiro separado torna-o mais fácil de manter e testar.
No useToken.js, importe useState de react. Como este ficheiro não contém JSX, não precisa de importar React. Crie e exporte uma função chamada useToken que utiliza useState para gerir o estado do token:
import { useState } from 'react';
export default function useToken() {
const [token, setToken] = useState();
}Adicione a função getToken como uma função arrow dentro de useToken. Coloque-a antes da declaração do estado e, em seguida, inicialize o useState com getToken() para carregar qualquer token existente na montagem do componente:
import { useState } from 'react';
export default function useToken() {
const getToken = () => {
const tokenString = sessionStorage.getItem('token');
if (!tokenString) {
return null;
}
const userToken = JSON.parse(tokenString);
return userToken?.token;
};
const [token, setToken] = useState(getToken());
}Adicione uma função saveToken que armazene o token tanto no sessionStorage quanto no estado do React. Guardar no estado dispara uma re-renderização, atualizando a interface:
import { useState } from 'react';
export default function useToken() {
const getToken = () => {
const tokenString = sessionStorage.getItem('token');
if (!tokenString) {
return null;
}
const userToken = JSON.parse(tokenString);
return userToken?.token;
};
const [token, setToken] = useState(getToken());
const saveToken = userToken => {
sessionStorage.setItem('token', JSON.stringify(userToken));
setToken(userToken.token);
};
}Retorna um objeto com token e setToken (mapeado para saveToken). Usar um objeto em vez de um array permite que os componentes desestruturem apenas os valores de que necessitam. Isto mantém a mesma interface de antes, por isso o App.js não precisa de alterações:
import { useState } from 'react';
export default function useToken() {
const getToken = () => {
const tokenString = sessionStorage.getItem('token');
if (!tokenString) {
return null;
}
const userToken = JSON.parse(tokenString);
return userToken?.token;
};
const [token, setToken] = useState(getToken());
const saveToken = userToken => {
sessionStorage.setItem('token', JSON.stringify(userToken));
setToken(userToken.token);
};
return {
setToken: saveToken,
token
}
}Guardar e fechar o ficheiro.
De seguida, abra App.js:
nano src/components/App/App.jsRemova as funções getToken e setToken de App.js, e remova a importação do useState. Em vez disso, importe e utilize o Hook useToken:
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
import useToken from './useToken';
function App() {
const { token, setToken } = useToken();
if(!token) {
return <Login setToken={setToken} />
}
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/preferences" element={<Preferences />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Guarde e feche o ficheiro. O navegador atualiza automaticamente. Após iniciar sessão, a aplicação navega imediatamente para a página protegida porque o useState no Hook personalizado dispara uma nova renderização quando o estado do token é atualizado:
O Hook personalizado armazena tokens em sessionStorage e gere o estado do React. Atualizar a página mantém o utilizador autenticado porque o token persiste em sessionStorage. No entanto, abrir a aplicação numa nova aba desloga o utilizador porque o sessionStorage é específico de cada aba, os dados não são partilhados entre abas e são limpos quando a aba é fechada.
Quando usar sessionStorage: Use sessionStorage quando quiser autenticação apenas para a sessão, que é eliminada ao fechar o separador. Isto é mais seguro em computadores partilhados.
Quando usar localStorage: Use localStorage quando quiser que a autenticação persista entre separadores e sessões do navegador. Isto proporciona uma melhor experiência ao utilizador, mas exige que os utilizadores façam logout explicitamente em computadores partilhados.
Usar localStorage para guardar dados entre janelas
localStorage persiste entre sessões e separadores do navegador, ao contrário do sessionStorage, que é específico de cada separador. Isto significa que os utilizadores permanecem ligados ao abrir novos separadores ou ao reiniciar o navegador.
Consideração de segurança: Em computadores partilhados, os tokens do localStorage persistem mesmo após fechar o navegador. O próximo utilizador poderia aceder à aplicação sem iniciar sessão se o utilizador anterior não tivesse terminado a sessão. Para computadores partilhados, considere:
- Adicionar avisos de expiração de sessão
- Implementar a funcionalidade "Lembrar-me" como opção de adesão
- Usar
sessionStoragepor predefinição elocalStorageapenas quando solicitado explicitamente - Fornecer uma funcionalidade de logout clara
Para dispositivos pessoais, o localStorage oferece uma melhor experiência de utilizador com risco mínimo de segurança quando combinado com uma proteção adequada contra XSS.
Para converter para localStorage, abra useToken.js:
nano src/components/App/useToken.jsSubstitua todas as referências a sessionStorage por localStorage. Os métodos da API (getItem, setItem) são idênticos:
import { useState } from 'react';
export default function useToken() {
const getToken = () => {
const tokenString = localStorage.getItem('token');
if (!tokenString) {
return null;
}
const userToken = JSON.parse(tokenString);
return userToken?.token;
};
const [token, setToken] = useState(getToken());
const saveToken = userToken => {
localStorage.setItem('token', JSON.stringify(userToken));
setToken(userToken.token);
};
return {
setToken: saveToken,
token
}
}Guarda o ficheiro. O navegador refresca automaticamente. Inicia sessão novamente (o token anterior estava em sessionStorage, não em localStorage). Após iniciares sessão, o token persiste em localStorage, por isso permaneces com sessão iniciada ao abrir novos separadores ou reiniciar o navegador.
Implementaste o armazenamento persistente de tokens utilizando sessionStorage e localStorage, e criaste um Hook personalizado useToken que combina operações de armazenamento com a gestão de estado do React. O Hook dispara novas renderizações quando os tokens mudam, garantindo que a interface se atualiza corretamente. Também aprendeste as vantagens e desvantagens entre sessionStorage (específico de cada separador, mais seguro) e localStorage (persiste entre sessões, melhor experiência de utilizador).
Passo 4 — Implementar Rotas Protegidas com o React Router v6
As rotas protegidas requerem autenticação antes de serem exibidas. No React Router v6, você cria um componente wrapper que verifica um token e redireciona os utilizadores não autentificados para a página de login, preservando o destino pretendido.
Por que usar um componente ProtectedRoute: Em vez de verificar a autenticação em cada componente ou usar renderização condicional em App.js, um componente dedicado ProtectedRoute centraliza a lógica de proteção de rotas. Isto facilita adicionar rotas protegidas, gerir redirecionamentos e manter um comportamento de autenticação consistente.
Crie um novo ficheiro para o componente ProtectedRoute:
nano src/components/ProtectedRoute/ProtectedRoute.jsDentro de ProtectedRoute.js, importe Navigate de react-router-dom e crie um componente que verifique a existência de um token:
import { Navigate } from 'react-router-dom';
export default function ProtectedRoute({ children, token }) {
if (!token) {
return <Navigate to="/login" replace />;
}
return children;
}O componente ProtectedRoute recebe children (o componente da rota a proteger) e token como props. Se não existir nenhum token, redireciona para /login usando Navigate com a prop replace para evitar adicionar uma entrada ao histórico do navegador.
Agora atualize o App.js para usar ProtectedRoute:
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
import ProtectedRoute from '../ProtectedRoute/ProtectedRoute';
import useToken from './useToken';
function App() {
const { token, setToken } = useToken();
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login setToken={setToken} />} />
<Route
path="/dashboard"
element={
<ProtectedRoute token={token}>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/preferences"
element={
<ProtectedRoute token={token}>
<Preferences />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Com esta abordagem, os utilizadores não autenticados que visitam /dashboard ou /preferences são redirecionados para /login.
Nota: No React Router v6, também pode usar o componente Outlet para rotas aninhadas e proteção de rotas. Para aplicações mais simples, o componente wrapper ProtectedRoute mostrado aqui é suficiente e mais fácil de compreender.
Passo 5 — Decodificação de Tokens JWT e Verificação da Expiração
Os tokens JWT contêm informações de expiração no seu payload. Pode decodificar o token no cliente para verificar se já expirou sem precisar fazer um pedido ao servidor, melhorando a experiência do utilizador ao mostrar feedback imediato.
Por que decodificar tokens no cliente: Embora deva sempre validar os tokens no servidor, a decodificação no lado do cliente permite verificar a expiração antes de fazer chamadas à API, mostrar mensagens de erro apropriadas e encerrar automaticamente a sessão dos utilizadores com tokens expirados.
Primeiro, instale uma biblioteca de decodificação JWT. Embora seja possível decodificar JWTs manualmente (eles estão codificados em base64), usar uma biblioteca trata de casos extremos e fornece uma melhor gestão de erros:
npm install jwt-decodeAtualize o seu hook useToken.js para decodificar e validar tokens:
import { useState } from 'react';
import { jwtDecode } from 'jwt-decode';
export default function useToken() {
const getToken = () => {
const tokenString = localStorage.getItem('token');
if (!tokenString) {
return null;
}
const userToken = JSON.parse(tokenString);
const token = userToken?.token;
if (!token) {
return null;
}
// Decode the token to check expiration
try {
const decoded = jwtDecode(token);
const currentTime = Date.now() / 1000; // Convert to seconds
// If token is expired, remove it and return null
if (decoded.exp < currentTime) {
localStorage.removeItem('token');
return null;
}
return token;
} catch (error) {
// If decoding fails, token is invalid
localStorage.removeItem('token');
return null;
}
};
const [token, setToken] = useState(getToken());
const saveToken = userToken => {
localStorage.setItem('token', JSON.stringify(userToken));
setToken(userToken.token);
};
const removeToken = () => {
localStorage.removeItem('token');
setToken(null);
};
return {
setToken: saveToken,
token,
removeToken
};
}A função getToken agora decodifica o JWT e verifica a declaração exp (timestamp de expiração em segundos). Se o token estiver expirado ou inválido, ele é removido do armazenamento e a função retorna null, efetivamente encerrando a sessão do utilizador.
Aviso: A decodificação de tokens no lado do cliente é apenas para experiência do utilizador. Valide sempre os tokens no seu servidor. Utilizadores maliciosos podem modificar o código do lado do cliente ou enviar tokens falsos. Nunca confie na validação do lado do cliente para decisões de segurança.
Passo 6 — Implementar Logout e Tratamento de Erros
O logout remove o token do armazenamento e redireciona para a página de login. O tratamento de erros impede que a aplicação falhe quando as chamadas à API falham e fornece feedback ao utilizador.
Por que implementar o logout: Sem uma função de logout, os utilizadores têm de limpar manualmente o armazenamento do navegador ou esperar que os tokens expirem. Um botão de logout oferece uma forma clara de terminar sessões e é essencial em computadores partilhados.
Atualize o seu componente Dashboard para incluir um botão de logout:
import React from 'react';
import { useNavigate } from 'react-router-dom';
export default function Dashboard({ removeToken }) {
const navigate = useNavigate();
const handleLogout = () => {
removeToken();
navigate('/login');
};
return (
<div>
<h2>Dashboard</h2>
<button onClick={handleLogout}>Logout</button>
</div>
);
}Atualize App.js para passar removeToken para rotas protegidas:
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
import ProtectedRoute from '../ProtectedRoute/ProtectedRoute';
import useToken from './useToken';
function App() {
const { token, setToken, removeToken } = useToken();
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login setToken={setToken} />} />
<Route
path="/dashboard"
element={
<ProtectedRoute token={token}>
<Dashboard removeToken={removeToken} />
</ProtectedRoute>
}
/>
<Route
path="/preferences"
element={
<ProtectedRoute token={token}>
<Preferences />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Agora adicione tratamento de erros ao componente de login:
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Login.css';
async function loginUser(credentials) {
const response = await fetch('http://localhost:8080/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
});
if (!response.ok) {
throw new Error(`Login failed: ${response.statusText}`);
}
const data = await response.json();
return data;
}
export default function Login({ setToken }) {
const [username, setUserName] = useState();
const [password, setPassword] = useState();
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const handleSubmit = async e => {
e.preventDefault();
setError(null);
setLoading(true);
try {
const token = await loginUser({
username,
password
});
setToken(token);
} catch (err) {
setError(err.message || 'An error occurred during login');
} finally {
setLoading(false);
}
};
return(
<div className="login-wrapper">
<h1>Please Log In</h1>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}>
<label>
<p>Username</p>
<input
type="text"
onChange={e => setUserName(e.target.value)}
disabled={loading}
/>
</label>
<label>
<p>Password</p>
<input
type="password"
onChange={e => setPassword(e.target.value)}
disabled={loading}
/>
</label>
<div>
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Submit'}
</button>
</div>
</form>
</div>
);
}
Login.propTypes = {
setToken: PropTypes.func.isRequired
};O componente de login agora lida com erros, mostra estados de carregamento e fornece feedback ao utilizador. O tratamento de erros impede que a aplicação falhe quando as chamadas à API não funcionam e fornece ao utilizador informações sobre o que correu mal.
Passo 7 — Utilizar o Contexto do React para Autenticação
O React Context fornece uma forma de partilhar o estado de autenticação entre componentes sem precisar de passar props manualmente. Para aplicações maiores, o Context é mais escalável do que passar props através de múltiplas camadas de componentes.
Por que usar Context para autenticação: Quando o estado de autenticação é necessário em muitos componentes (barras de navegação, menus de utilizador, rotas protegidas), passar props torna-se complicado. O Context fornece uma forma limpa de aceder ao estado de autenticação em qualquer lugar da árvore de componentes.
Crie um contexto de autenticação:
import React, { createContext, useContext, useState } from 'react';
import useToken from '../components/App/useToken';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const { token, setToken, removeToken } = useToken();
const [user, setUser] = useState(null);
const login = async (credentials) => {
// Login logic here
const response = await fetch('http://localhost:8080/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
});
if (!response.ok) {
throw new Error('Login failed');
}
const data = await response.json();
setToken(data);
// Optionally decode token to set user info
return data;
};
const logout = () => {
removeToken();
setUser(null);
};
const value = {
token,
user,
login,
logout,
isAuthenticated: !!token
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}Envolva a sua aplicação com AuthProvider:
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { AuthProvider } from '../../contexts/AuthContext';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
import ProtectedRoute from '../ProtectedRoute/ProtectedRoute';
function App() {
return (
<AuthProvider>
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/preferences"
element={
<ProtectedRoute>
<Preferences />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
</div>
</AuthProvider>
);
}
export default App;Atualize ProtectedRoute para usar o contexto:
import { Navigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
export default function ProtectedRoute({ children }) {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return children;
}Agora qualquer componente pode aceder ao estado de autenticação utilizando useAuth(), eliminando a necessidade de passar props através de vários níveis.
Passo 8 — Compreender a Proteção CSRF para Autenticação Baseada em Cookies
Os ataques CSRF (Cross-Site Request Forgery) exploram o facto de os navegadores enviarem automaticamente cookies com os pedidos. Ao utilizar autenticação baseada em cookies, deverá implementar proteção CSRF para evitar que sites maliciosos efetuem pedidos autenticados em nome dos utilizadores.
Por que a proteção CSRF é importante: Se a sua autenticação depender de cookies (em vez de tokens no localStorage), um website malicioso pode enganar os utilizadores para fazerem pedidos autenticados à sua API. Por exemplo, um atacante poderia incorporar um formulário que envia dados para a sua API e, se o utilizador estiver autenticado, o navegador inclui automaticamente o cookie de autenticação.
Estratégias de proteção CSRF:
Tokens CSRF: Gere um token único para cada sessão e exija-o nas solicitações. O token deve corresponder ao que está armazenado no servidor.
Atributo SameSite dos Cookies: Defina cookies com
SameSite=StrictouSameSite=Laxpara impedir que sejam enviados em pedidos entre sites.Verificação do Cabeçalho Origin: Verifique se o cabeçalho
OriginouReferercorresponde ao seu domínio.Cookie de Dupla Submissão: Armazene o token CSRF tanto num cookie como num campo de formulário, exigindo que ambos correspondam.
Aqui está um exemplo de como implementar proteção CSRF no seu servidor Express:
const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
const app = express();
app.use(cors({
origin: 'http://localhost:3000',
credentials: true // Allow cookies
}));
app.use(cookieParser());
app.use(express.json());
// Generate CSRF token
app.get('/api/csrf-token', (req, res) => {
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrf-token', token, {
httpOnly: false, // Must be accessible to JavaScript
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production'
});
res.json({ csrfToken: token });
});
// Verify CSRF token middleware
function verifyCsrfToken(req, res, next) {
const token = req.headers['x-csrf-token'];
const cookieToken = req.cookies['csrf-token'];
if (!token || !cookieToken || token !== cookieToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
}
app.post('/login', verifyCsrfToken, (req, res) => {
// Login logic here
res.json({ token: 'test123' });
});
app.listen(8080, () => console.log('API is running on http://localhost:8080'));Na sua aplicação React, obtenha o token CSRF antes de fazer pedidos autenticados:
let csrfToken = null;
export async function getCsrfToken() {
if (!csrfToken) {
const response = await fetch('http://localhost:8080/api/csrf-token', {
credentials: 'include' // Include cookies
});
const data = await response.json();
csrfToken = data.csrfToken;
}
return csrfToken;
}
export async function loginUser(credentials) {
const token = await getCsrfToken();
return fetch('http://localhost:8080/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
credentials: 'include', // Include cookies
body: JSON.stringify(credentials)
}).then(data => data.json());
}Nota: A proteção contra CSRF não é necessária para a autenticação baseada em tokens armazenados em localStorage porque os tokens devem ser incluídos explicitamente nos cabeçalhos das requisições e não são enviados automaticamente. No entanto, se utilizar cookies para autenticação, a proteção contra CSRF é essencial, uma vez que os cookies são enviados automaticamente. Independentemente do método de autenticação utilizado, implemente sempre medidas de segurança adequadas, como HTTPS, validação de entradas e limitação de taxa.
Resolução de problemas
Token não persiste após atualizar a página
Problema: Após iniciar sessão, atualizar a página faz com que seja desconectado.
Soluções:
- Verifique se os tokens estão armazenados em
localStorageousessionStorage(verifique as Ferramentas de Desenvolvimento do navegador > Aplicação > Armazenamento) - Certifique-se de que
getToken()é executado na montagem do componente e inicializa o estado corretamente - Verifique se há erros na consola do navegador que possam impedir a obtenção do token
- Verifique se o formato do token corresponde ao que o seu código espera (objeto com a propriedade
token)
“Não é possível ler a propriedade 'token' de null” Erro
Problema: Ocorre um erro ao aceder a userToken.token após JSON.parse().
Solução: Adicione uma verificação de nulo antes de efetuar o parsing:
const tokenString = localStorage.getItem('token');
if (!tokenString) return null;
const userToken = JSON.parse(tokenString);
return userToken?.token;O formulário de login não é enviado
Problema: Clicar em enviar não aciona o pedido de login.
Soluções:
- Certifique-se de que o formulário tem
onSubmit={handleSubmit}e que o botão temtype="submit" - Verifique se o servidor da API está a correr na porta correta (por defeito: 8080)
- Verifique se o CORS está ativado no servidor ao fazer pedidos entre origens
- Verifique a consola do navegador para erros de rede ou problemas de CORS
Rotas Protegidas Não Funcionam
Problema: Os utilizadores podem aceder a rotas protegidas sem autenticação.
Soluções:
- Verifique se o componente
ProtectedRouteverifica o token antes de renderizar - Certifique-se de que o token é passado para
ProtectedRouteou acedido através do Contexto - Verifique se as rotas estão corretamente envolvidas em
App.js - Verifique a sintaxe do React Router v6 (propriedade
element, não children)
O token expira enquanto o utilizador está ativo
Problema: O utilizador é inesperadamente desconectado durante o uso ativo.
Soluções:
- Implementar a lógica de renovação de tokens usando tokens de atualização
- Verifique a expiração do token antes de fazer chamadas à API
- Adicionar atualização automática de token quando a expiração estiver próxima
- Considere prolongar a expiração do token para utilizadores ativos
localStorage Desativado ou Limite Excedido
Problema: Os tokens não são guardados no modo privado/incógnito ou a cota de armazenamento está cheia.
Soluções:
- Verifique se o
localStorageestá disponível:if (typeof(Storage) !== "undefined") - Tratar
QuotaExceededErrorao definir itens - Recorrer ao
sessionStorageou ao armazenamento em memória - Limpar tokens antigos antes de armazenar novos
- Informar os utilizadores se o armazenamento não estiver disponível
Múltiplos separadores não sincronizam o estado de autenticação
Problema: Terminar sessão numa aba não termina sessão nas outras abas.
Solução: Ouça os eventos storage para sincronizar o estado entre separadores:
useEffect(() => {
const handleStorageChange = (e) => {
if (e.key === 'token') {
// Update auth state when token changes in another tab
setToken(e.newValue ? JSON.parse(e.newValue) : null);
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, []);A API Retorna Formato de Token Diferente
Problema: A sua API retorna { accessToken: '...' em vez de { token: '...'.
Solução: Atualize o código para corresponder ao formato de resposta da sua API:
// If API returns { accessToken: '...' }
const token = userToken?.accessToken;
// Or if API returns { data: { token: '...' } }
const token = userToken?.data?.token;Erros 401 Não Autorizado
Problema: A API retorna 401 mesmo com um token válido.
Soluções:
- Verifique se o token é enviado nos cabeçalhos da requisição:
Authorization: Bearer $ {token} - Verifique a expiração do token (decodifique o JWT para ver a declaração
exp) - Certifique-se de que o formato do token corresponde ao que o servidor espera
- Verificar se o servidor valida os tokens corretamente
- Verificar se o token precisa de ser atualizado
Perguntas Frequentes
1. Como posso proteger rotas no React?
Use um componente wrapper ProtectedRoute que verifica a autenticação antes de renderizar. No React Router v6, envolva as rotas protegidas com um componente que verifica se o token existe. Se nenhum token for encontrado, redirecione para a página de login usando Navigate:
function ProtectedRoute({ children, token }) {
if (!token) {
return <Navigate to="/login" replace />;
}
return children;
}Depois, use-o nas suas rotas:
<Route
path="/dashboard"
element={
<ProtectedRoute token={token}>
<Dashboard />
</ProtectedRoute>
}
/>2. Onde devo armazenar os tokens de autenticação?
Armazene tokens em localStorage para persistência entre sessões, ou em sessionStorage para sessões específicas de uma aba. Ambos são acessíveis a JavaScript, tornando-os vulneráveis a ataques XSS, por isso, sempre sanitize a entrada do utilizador e use HTTPS.
localStorage: Persiste entre sessões e separadores do navegador. Use para a funcionalidade de “lembrar-me”.sessionStorage: Limpo quando a aba é fechada. Use para autenticação apenas de sessão mais segura.- Cookies: Enviados automaticamente com pedidos, requerendo proteção CSRF. Use com a flag
httpOnlypara acesso apenas no lado do servidor.
Para aplicações em produção, considere usar cookies seguros e httpOnly geridos pelo seu backend, que são menos vulneráveis a ataques XSS.
3. O localStorage é seguro para autenticação?
localStorage é vulnerável a ataques XSS, mas é aceitável para muitas aplicações quando combinado com medidas de segurança adequadas. Qualquer JavaScript a correr no seu domínio pode aceder ao localStorage, por isso, se um atacante injetar scripts maliciosos, ele pode roubar tokens.
Estratégias de mitigação:
- Sanitizar todas as entradas de utilizador para prevenir XSS
- Use cabeçalhos de Política de Segurança de Conteúdo (CSP)
- Implementar expiração de tokens e atualização de tokens
- Use HTTPS para evitar ataques man-in-the-middle
- Considere cookies httpOnly para aplicações sensíveis
Para aplicações de alta segurança (bancárias, de saúde), prefira cookies httpOnly com proteção CSRF, pois não são acessíveis através de JavaScript.
4. As apps React precisam de OAuth?
O OAuth é necessário ao autenticar com fornecedores de terceiros (Google, GitHub, Facebook), mas não para autenticação personalizada. Se estiver a criar o seu próprio sistema de autenticação com nome de utilizador/senha, pode usar autenticação baseada em token, conforme demonstrado neste tutorial.
Use OAuth quando:
- Os utilizadores devem iniciar sessão com contas sociais
- Quer delegar a autenticação a um fornecedor de confiança
- Precisa aceder a APIs de terceiros em nome dos utilizadores
Utilize autenticação personalizada quando:
- Você controla a base de dados de utilizadores
- Precisa de controlo total sobre o fluxo de autenticação
- Queres evitar dependências de terceiros
Muitas aplicações utilizam uma abordagem híbrida: OAuth para login social e autenticação personalizada para email/senha.
5. Como devo lidar com a expiração do token?
Decodifique o token JWT no cliente para verificar a expiração, e depois atualize ou encerre a sessão do utilizador. Utilize uma biblioteca como jwt-decode para ler a declaração exp:
import { jwtDecode } from 'jwt-decode';
const decoded = jwtDecode(token);
const currentTime = Date.now() / 1000;
if (decoded.exp < currentTime) {
// Token expired, remove it and redirect to login
localStorage.removeItem('token');
navigate('/login');
}Para produção, implemente tokens de atualização: tokens de acesso de curta duração (15-30 minutos) e tokens de atualização de maior duração (7-30 dias) que podem ser usados para obter novos tokens de acesso sem necessidade de reautenticação.
6. Como posso implementar a funcionalidade "Lembrar-me"?
Armazene tokens em localStorage para sessões persistentes e em sessionStorage para sessões temporárias. Adicione uma caixa de verificação ao seu formulário de login:
const [rememberMe, setRememberMe] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
const token = await loginUser(credentials);
if (rememberMe) {
localStorage.setItem('token', JSON.stringify(token));
} else {
sessionStorage.setItem('token', JSON.stringify(token));
}
};Quando rememberMe está assinalado, o token persiste entre sessões do navegador. Caso contrário, é apagado quando a aba é fechada.
Conclusão
A autenticação em aplicações React requer um equilíbrio entre segurança, experiência do utilizador e facilidade de manutenção. Ao centralizar a lógica de autenticação em hooks personalizados ou no React Context, implementar rotas protegidas com o React Router v6 e escolher estratégias adequadas de armazenamento de tokens, pode criar fluxos de autenticação seguros e amigáveis para o utilizador.
Princípios chave a lembrar:
- Armazene tokens no armazenamento do navegador (
localStorageousessionStorage) para manter a persistência após a atualização da página - Use componentes de rota protegida para impor requisitos de autenticação
- Decodifique tokens JWT no cliente para verificar a expiração, mas valide sempre no servidor
- Implemente o tratamento de erros e estados de carregamento para uma melhor experiência do utilizador
- Considere a proteção CSRF ao utilizar autenticação baseada em cookies
- Use o React Context para o estado de autenticação em aplicações maiores para evitar a passagem de props desnecessária
Cada solução de armazenamento e abordagem de autenticação tem compensações. localStorage oferece conveniência, mas requer proteção contra XSS. sessionStorage é mais seguro, mas menos conveniente. Os cookies requerem proteção contra CSRF, mas são menos vulneráveis a XSS. Escolha com base nos seus requisitos de segurança e objetivos de experiência do utilizador.
À medida que a sua aplicação cresce, considere implementar tokens de atualização, autenticação multifator e funcionalidades de gestão de sessão. Para aplicações em produção, utilize sempre HTTPS, implemente limitação de taxa e siga as melhores práticas de segurança.
Se quiser ler mais tutoriais de React, consulte a nossa página de tópicos sobre React ou explore tutoriais relacionados como Como Construir uma Aplicação de Lista de Tarefas Usando Django e React e Como Implementar uma Aplicação React com Nginx no Ubuntu.
Obrigado por aprender com a comunidade Linux-Console.net.








