Como Implementar Autorização em sua Aplicação Usando JWT
Uma abordagem prática passo a passo para garantir a segurança e restrição de acesso às suas API's.
Introdução
Se você está construindo uma aplicação web, a implementação de API's é um requisito necessário. Os dados que você exibe para o usuário normalmente precisam ser buscados em um serviço de API que existe no seu servidor ou em um servidor externo.
As vezes, seu site pode precisar restringir o acesso a algumas dessas API's para evitar acessos indesejados aos seus dados. As restrições podem ser variadas, desde a disponibilidade de um usuário até níveis de acesso por perfil de usuário. Isso é chamado de autorização.
Neste post, vou mostrar como implementar a autorização com um frontend (React) e um backend (Node.js) usando JSON Web Token (JWT). Vamos implementar três chamadas de API para demonstrar o processo.
Autorização
Em um aplicativo que envolve serviços de API, a segurança deve ser levada em consideração. As API's não devem ser acessíveis de qualquer lugar fora do aplicativo. Além disso, você também deseja garantir que as API's sejam acessíveis apenas para a entidade necessária. Para isso, é necessário implementar a autorização. Considere os dois cenários a seguir.
Se o seu aplicativo necessita de autenticação de usuários, você desejará garantir que determinados serviços só possam ser acessados por usuários autenticados. Por exemplo, na Amazon, qualquer pessoa pode ver uma lista de produtos disponíveis, mas os pedidos, compras, informações de pagamento, etc., de um usuário específico, devem ser acessíveis apenas ao usuário autenticado.
Outro caso de uso é quando um serviço está acessível apenas para um determinado usuário. Em um site escolar por exemplo, um aluno pode visualizar suas notas, mas apenas um professor está autorizado a modificá-las.
Quando um usuário está autenticado, o serviço de autenticação gera e devolve para o aplicativo um token de acesso. O token de acesso contém informações sobre o usuário, sua função, o horário do login e seu tempo de expiração, juntamente com outros detalhes.
Ao fazer uma solicitação de API, um cabeçalho de autorização contendo o token de acesso é adicionado. Esse token é decodificado no backend e as informações sobre o usuário são recuperadas. Com base nas informações, a API decide se o usuário está autorizado a receber a resposta.
Vamos implementar um cenário semelhante. Aqui vamos obter o token de acesso, e em seguida, usar o mesmo token para demonstrar três chamadas de API.
Vamos usar JWT neste post.
O que é JWT?
JSON Web Token (JWT) é uma forma segura de enviar informações em formato codificado. O token contém três partes:
- Header: Esta parte contém o tipo do token e o algoritmo usado para codificar o token.
- Payload: Esta parte contém informações sobre o usuário e outros detalhes, como a expiração do token.
- Signature: Esta parte é usada para assinar o token e verificar que a mensagem não foi alterada durante a transferência. Um JWT tem a seguinte aparência com todas as partes em formato codificado:
[Header].[Payload].[Signature]
Um exemplo de JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Visite o site oficial aqui para decodificar um JWT.
Agora que você sabe um pouco sobre Autorização e JWT, vamos iniciar a implementação.
Configurando
React App
Crie uma pasta chamada "authorization-frontend" no diretório do seu projeto. Dentro dela, execute o seguinte comando para criar o aplicativo React:
npx create-react-app authorization-frontend
Vamos usar o Bootstrap neste projeto, então adicione este CDN ao arquivo index.html.
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
Alternativamente, você pode baixar os arquivos de origem do Bootstrap a partir daqui.
Vamos usar a biblioteca Axios para fazer chamadas de API. Instale-a usando o comando abaixo:
npm i axios
Node.js Server
Primeiro, instale o Node.js se você não o tiver em seu sistema. Faça o download aqui.
Crie uma pasta separada chamada "api-server" para o código do servidor. Dentro dessa pasta, execute o comando npm init e insira os valores para as solicitações fornecidas. Isso deve criar um arquivo package.json em sua pasta. O arquivo mostra os módulos que você instalou no diretório do seu projeto.
Estamos usando os seguintes módulos:
- express: Framework do Node.js que facilita a criação de servidores.
- cors: Permite que fontes de origens cruzadas façam solicitações de API. Saiba mais.
- jsonwebtoken: Para gerar um token de acesso.
Instale-os com o comando a seguir:
npm i express cors jsonwebtoken
Agora, crie um arquivo server.js que contenha o código necessário para configurar um servidor Node. Inclua o seguinte código:
Para este projeto, permita todas as origens no CORS, ou seja, qualquer navegador pode acessar essas API's. No entanto, essa não é uma boa prática ao desenvolver um aplicativo real.
Obtenha o token de acesso
Agora que você configurou ambas as aplicações, é hora de gerar o token de acesso no backend e passá-lo para o frontend. Vamos gerar dois tipos de tokens de acesso: um para um usuário normal e outro para um usuário administrador.
Endpoint para buscar o token
Crie um endpoint de API para buscar o token. Use o método HTTP POST, pois estamos recebendo um corpo de solicitação do frontend que normalmente contém as credenciais do usuário durante a autenticação. No nosso caso, o frontend envia apenas um valor indicando o tipo de token a buscar.
app.post('/access_token', (req, res) => {
const { value } = req.body;
switch(value) {
case 1:
// generate normal token
case 2:
// generate admin token
default:
// Send error message
}
})
Gere um token de acesso
Vamos ter dois objetos de usuário, um usuário normal e um usuário administrador. Além disso, defina um segredo que será usado para assinar o token.
const NORMAL_USER = {
name: 'kunal',
admin: false
}
const ADMIN_USER = {
name: 'alex',
admin: true
}
const SECRET = 'client-secret'
Observação: Definir um segredo dentro do código de sua aplicação não é uma boa prática, você deve tê-lo em um arquivo de ambiente (env file).
O token pode ser gerado usando a função sign().
jwt.sign(NORMAL_USER, SECRET, (err, token) => {
res.json({ accessToken: token })
})
Para o token de administrador, substitua NORMAL_USER por ADMIN_USER.
Agora, vamos verificar decodificando o token. O campo iat representa o momento em que o token foi emitido.
Busque o token
No lado do frontend, importe a biblioteca Axios e inicialize o nome do host do servidor backend.
import axios from 'axios'
const HOST_NAME = 'http://localhost:8000'
Em seguida, crie dois botões para buscar um token normal e um token de administrador, respectivamente.
Ao clicar nos botões, o método handleGetTokenClick() é chamado, que busca o token de acesso com um parâmetro de opção para determinar qual token buscar.
function handleGetTokenClick(option) { }
Dentro desse método, faça uma solicitação POST à API no endpoint /access_token.
axios.post(HOST_NAME+'/access_token', { value: option })
.then(res => {
--- HANDLE THE RESPONSE ---
})
A resposta tem a seguinte aparência.
Armazene o token como estado (state)
Obtenha o token de acesso da resposta e armazene-o no session storage (ou local storage; leia sobre a diferença aqui).
Crie uma variável de estado para armazenar o token de acesso.
const [accessToken, setAccessToken] =
useState(sessionStorage.getItem('accessToken'));
Além disso, defina o estado quando você receber o token de acesso da API. A seguir está o código para lidar com a resposta.
const { accessToken } = res.data;
sessionStorage.setItem('accessToken', accessToken);
setAccessToken(accessToken);
Chamadas de API
Agora é hora de implementar as chamadas de API. Como mencionado anteriormente, faremos três chamadas de API GET:
- API pública: acessível para qualquer pessoa.
- API privada: acessível apenas para o usuário autenticado (ou seja, com o token de acesso).
- API restrita: acessível apenas para o usuário administrador.
Implemente as três API's
API pública
Essa API envia uma resposta simples quando é chamada.
app.get('/public_api', (req, res) => {
res.send('Public API called')
})
API privada
Essa API espera um cabeçalho de Autorização (Authorization Header) com um token de acesso válido.
app.get('/private_api', (req, res) => { ... })
Primeiro, verifique se o usuário enviou um cabeçalho de autorização. Caso contrário, envie uma mensagem de erro com o código de status 401. Isso indica falta de credenciais ou credenciais inválidas.
const auth_header = req.headers.authorization;
if(!auth_header) res.send(401, 'Unauthorized request')
Se a solicitação tiver um cabeçalho de autorização, obtenha o token de acesso dele. Como o cabeçalho de autorização está no formato "Bearer access_token", use a função split() para obter o token de acesso.
const accessToken = auth_header.split(' ')[1]
Agora, use a função verify() para verificar o token. Ela recebe como parâmetros o token, o segredo e uma função de retorno de chamada (callback function). Se o token for válido, a função de retorno de chamada é chamada com o payload decodificado, caso contrário, é chamada com um erro.
jwt.verify(accessToken, SECRET, (err, payload) => {
if (err) res.send(401, 'Unauthorized request')
res.send('Private API called')
})
Teste a API usando o Postman.
API restrita
Essa API só pode ser chamada por um usuário administrador. A lógica é quase semelhante à da API privada, exceto que também verificamos se o usuário é um administrador.
app.get('/restricted_api', (req, res) => {
const auth_header = req.headers.authorization;
if(!auth_header) res.send(401, 'Unauthorized request')
const accessToken = auth_header.split(' ')[1]
jwt.verify(accessToken, SECRET, (err, user) => {
if (err) res.send(401, 'Unauthorized request')
if (user.admin == true) res.send('Restricted API called')
res.send(401, 'Unauthorized request')
})
})
Agora, se um usuário normal fizer uma solicitação para esta API, ela enviará um erro 401.
Crie um estado para armazenar a resposta
No lado do frontend, crie três variáveis de estado para armazenar a resposta de cada API.
const [publicResponse, setPublicResponse] = useState('')
const [privateResponse, setPrivateResponse] = useState('')
const [restrictedResponse, setRestrictedResponse] = useState('')
Crie botões para chamar as API's
Ao clicar em cada botão, o método handleAPIButtonClick() é chamado, que também recebe um parâmetro de opção para decidir qual API chamar.
Usando declarações switch-case, chame cada API com base na opção passada.
Chame as API's
Chame a API pública.
axios.get(HOST_NAME + '/public_api').then(res => {
setPublicResponse(res.data)
})
Ao chamar a API privada, passe o cabeçalho de autorização no formato "Bearer access_token". Leia mais sobre tokens Bearer aqui.
Além disso, implemente o bloco catch para lidar com a resposta de erro.
axios.get(HOST_NAME + '/private_api', {
headers: {
Authorization: 'Bearer ' + accessToken
}
}).then(res => {
setPrivateResponse(res.data)
}).catch(err => {
setPrivateResponse(err.response.data)
})
Chame a API restrita de forma semelhante, mas substitua o endpoint e a função de atualização do estado.
Considere as APIs acima como um modelo para sua própria implementação. Se você tiver uma página que exibe informações do usuário, implemente a API privada. Informações que só podem ser visíveis para determinados usuários devem ser incluídas na API restrita.
Você pode adicionar qualquer tipo de restrição às suas APIs. Tudo que você precisa fazer é passar o token de acesso, decodificá-lo e verificar se o usuário satisfaz essas restrições.
Neste post, não adicionei o código HTML e CSS, pois meu foco era apenas na parte lógica. Você pode encontrar a implementação no GitHub.
Comente abaixo se encontrar algo incorreto ou se souber de uma melhor implementação.
Conclusão
A autorização ajuda a tornar suas APIs seguras e restritas. O uso do JWT facilita a implementação da autorização. Neste post, mostrei como gerar um token de acesso e usá-lo para acessar diferentes tipos de APIs. Esta postagem é uma demonstração simples de como a autorização pode ser implementada em seu aplicativo.
Expliquei cada etapa em palavras simples para ajudar você a entender a autorização. Espero que isso seja útil em seus projetos futuros.
Se você não conseguir entender o conteúdo ou achar a explicação insatisfatória, comente suas opiniões abaixo. Novas ideias são sempre apreciadas! Até a próxima !