Autenticação e autorização em aplicações web com Node, JWT e MySQL
Autenticação e autorização são mecanismos distintos, embora normalmente sejam utilizados em conjunto para controlar o acesso a sistemas computacionais. Em termos simples, a autenticação procura responder à pergunta “quem é o usuário?”, enquanto a autorização determina “o que esse usuário pode fazer?”. A Open Worldwide Application Security Project (OWASP) define autenticação como o processo de verificar se uma entidade é realmente quem afirma ser, enquanto a autorização verifica se determinada ação ou recurso pode ser acessado por essa entidade (OWASP, [s.d.]).
Neste artigo, esses conceitos serão incorporados ao projeto cod-aula-git, do professor Abdiel Batista (clique para ver o projeto no Github). Atualmente, o projeto possui um backend Node.js/Express, um frontend HTML/JavaScript e um banco MySQL denominado projeto_iot. O backend registra e consulta informações da tabela LeituraSensor, mas não possui um mecanismo efetivo de autenticação ou autorização.
A implementação proposta adicionará usuários ao sistema, armazenamento seguro de senhas utilizando hash, autenticação baseada em JSON Web Token (JWT), cookies HTTP e controle de acesso baseado em papéis (roles). A arquitetura resultante pode ser representada da seguinte forma:
Navegador
|
| email + senha
v
POST /api/auth/login
|
| consulta usuário
v
MySQL
|
| hash armazenado
v
bcrypt.compare()
|
| credenciais válidas
v
JWT assinado
|
| cookie HttpOnly
v
Navegador
|
| nova requisição
v
Middleware autenticar()
|
+---- inválido ---> HTTP 401
|
v
Middleware autorizar()
|
+---- sem permissão ---> HTTP 403
|
v
Recurso protegido
Esse fluxo separa claramente identificação, autenticação, manutenção da sessão e autorização.
Autenticação: comprovando a identidade do usuário
A autenticação verifica a identidade apresentada por uma entidade. Em sistemas web tradicionais, uma das formas mais comuns consiste na combinação de um identificador — como e-mail — com uma senha conhecida apenas pelo usuário (OWASP, [s.d.]).
Considere o usuário:
Email: aluno@ifnmg.edu.br
Senha: MinhaSenha123!
O sistema não deve simplesmente procurar no banco:
SELECT *
FROM Usuario
WHERE email = 'aluno@ifnmg.edu.br'
AND senha = 'MinhaSenha123!';Esse modelo pressuporia o armazenamento da senha original, o que constitui uma vulnerabilidade grave. A OWASP recomenda explicitamente que senhas não sejam armazenadas em texto puro e que sejam empregadas funções específicas para derivação de senhas, como Argon2id, scrypt ou bcrypt (OWASP, [s.d.]).
O processo correto será aproximadamente:
CADASTRO
"MinhaSenha123!"
|
v
bcrypt
|
v
$2b$12$X3o2.....
|
v
MySQL
Posteriormente, o sistema deve verificar da seguinte forma:
LOGIN
Senha digitada
|
+--------------+
| |
v v
bcrypt.compare() Hash armazenado
|
+-- true -> autenticação aceita
|
+-- false -> autenticação negada
Hash de senha
Uma função de hash transforma uma entrada de tamanho arbitrário em uma representação derivada. Para armazenamento de senhas, o objetivo é utilizar funções deliberadamente custosas, dificultando tentativas massivas de descoberta da senha em caso de vazamento do banco de dados (OWASP, [s.d.]).
Um aspecto importante é que o fluxo:
senha ---> hash
não implica que obter pelo inverso:
hash ---> senha
seja uma operação disponível. Ou seja, a função de hash não é uma criptografia reversível.
Além disso, algoritmos próprios para senhas utilizam salt, um valor aleatório incorporado ao processamento. Dessa forma, dois usuários que escolherem exatamente a mesma senha normalmente terão hashes diferentes (OWASP, [s.d.]).
Neste artigo será utilizado bcryptjs pela simplicidade de instalação no ambiente didático Node.js. A OWASP atualmente prefere Argon2id para novos sistemas e considera bcrypt principalmente adequado para compatibilidade com sistemas existentes. Portanto, a escolha aqui privilegia a portabilidade do exercício, mas não representa a recomendação criptográfica mais moderna (OWASP, [s.d.]).
Autorização de usuários
A autorização é o mecanismo responsável por determinar quais recursos, operações ou informações um usuário pode acessar depois que sua identidade foi autenticada. Dessa forma, um sistema pode reconhecer corretamente dois usuários distintos e, ainda assim, conceder permissões diferentes a cada um conforme suas responsabilidades (Sommerville, 2011).
A autorização serve principalmente para restringir o acesso aos recursos da aplicação, evitando que usuários autenticados executem operações para as quais não possuem permissão. Em um sistema que utiliza papéis, por exemplo, um usuário comum pode possuir autorização apenas para consultar dados, enquanto um administrador pode também cadastrar, alterar ou excluir registros. Essa abordagem segue o princípio do menor privilégio, segundo o qual cada usuário deve possuir apenas as permissões necessárias para desempenhar suas atividades (OWASP, [s.d.]).
Em aplicações web, a autorização deve ser implementada principalmente no servidor, normalmente por meio de middlewares ou outras camadas de controle de acesso. Ocultar um botão no frontend não impede que um usuário tente acessar diretamente uma rota protegida. Por isso, cada requisição relevante deve ter suas permissões verificadas antes da execução da operação solicitada, garantindo que recursos sensíveis permaneçam acessíveis somente às identidades devidamente autorizadas (OWASP, [s.d.]).
Refatorando o projeto original
Vamos propor a refatoração completa preservando a ideia do projeto atual, mas organizando de fato o backend em MVC. Há um ponto importante em relação ao projeto original: o repositório publicado ainda concentra banco, regras e rotas em backend/server.js. Portanto, precisamos evoluir o backend atual para MVC, sem alterar a finalidade do sistema. O projeto original possui backend, frontend, simulador.js e a API de sensores em Express/MySQL.
A estrutura final ficará assim:
cod-aula-git/
│
├── backend/
│ ├── config/
│ │ └── banco.js
│ │
│ ├── controllers/
│ │ ├── authController.js
│ │ └── sensorController.js
│ │
│ ├── middlewares/
│ │ ├── autenticacao.js
│ │ ├── autorizacao.js
│ │ ├── dispositivo.js
│ │ └── validacao.js
│ │
│ ├── models/
│ │ ├── usuarioModel.js
│ │ └── sensorModel.js
│ │
│ ├── routes/
│ │ ├── authRoutes.js
│ │ └── sensorRoutes.js
│ │
│ ├── .env
│ ├── .env.example
│ ├── .gitignore
│ ├── app.js
│ ├── server.js
│ └── package.json
│
├── frontend/
│ ├── index.html
│ ├── login.html
│ ├── cadastro.html
│ ├── script.js
│ ├── login.js
│ ├── cadastro.js
│ ├── style.css
│ └── cadastro.css
│
├── simulador.js
└── README.md
A separação será:
Model
↓
acessa o MySQL
Controller
↓
executa regras da aplicação
Routes
↓
define endpoints
Middlewares
↓
validação, autenticação e autorização
View
↓
frontend HTML/CSS/JS
A autenticação usará senha submetida a bcrypt, JWT com expiração e cookie HttpOnly. Além disso, no bcrypt é recomendado um fator de trabalho de pelo menos 10 e um limite de 72 bytes de entrada (OWASP, [s.d.]).
Banco de dados
Antes dos arquivos JavaScript, acrescente a tabela de usuários ao banco existente projeto_iot (o código para a versão original do projeto está no link que deixei no início do artigo. Pegue lá a primeira parte do sistema). Execute este código diretamente no MySQL:
USE projeto_iot;
CREATE TABLE IF NOT EXISTS Usuario (
id INT AUTO_INCREMENT PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE,
senha_hash VARCHAR(255) NOT NULL,
papel ENUM('USUARIO', 'ADMIN') NOT NULL DEFAULT 'USUARIO',
criadoEm DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);A tabela original LeituraSensor permanece inalterada.
Dependências
Via terminal, acesse a pasta backend:
cod-aula-git/backend/E execute:
npm install express mysql2 bcryptjs jsonwebtoken cookie-parser dotenv helmet express-rate-limitComo passaremos a servir o frontend pelo próprio Express, o cors deixa de ser necessário para o funcionamento normal do projeto.
Variáveis de ambiente
Crie o arquivo .env na pasta backend:
cod-aula-git/backend/.env
com o seguinte conteúdo:
PORTA=3000
DB_HOST=localhost
DB_USUARIO=root
DB_SENHA=sua_senha
DB_BANCO=projeto_iot
JWT_SECRET=troque-esta-chave-por-uma-chave-muito-grande-e-aleatoria
API_KEY_DISPOSITIVO=troque-esta-chave-do-simulador
NODE_ENV=development
Para gerar um segredo JWT razoável, você pode usar no terminal:
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"Proteção do .env
Crie ou altere o arquivo .gitignore para proteger o arquivo de ambiente (.env):
cod-aula-git/backend/.gitignore
Nesse conteúdo, digite:
node_modules/
.env
Esse passo é importante porque o segredo JWT, a senha do banco e a chave do dispositivo não devem entrar no repositório, pois assim elas ficarão públicas.
Conexão com o banco
Crie a pasta:
backend/config/
e, dentro dela, o arquivo:
backend/config/banco.js
Código:
const mysql = require("mysql2/promise");
const banco = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USUARIO,
password: process.env.DB_SENHA,
database: process.env.DB_BANCO,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
module.exports = banco;Esse arquivo passa a ter uma única responsabilidade que é disponibilizar acesso ao MySQL.
Model de usuário
Crie:
backend/models/
e depois:
backend/models/usuarioModel.js
Código a ser inserido neste arquivo é:
const banco = require("../config/banco");
async function buscarPorEmail(email) {
const [usuarios] = await banco.execute(
`SELECT
id,
nome,
email,
senha_hash,
papel
FROM Usuario
WHERE email = ?`,
[email]
);
return usuarios[0] ?? null;
}
async function buscarPorId(id) {
const [usuarios] = await banco.execute(
`SELECT
id,
nome,
email,
papel,
criadoEm
FROM Usuario
WHERE id = ?`,
[id]
);
return usuarios[0] ?? null;
}
async function criar(nome, email, senhaHash) {
const [resultado] = await banco.execute(
`INSERT INTO Usuario
(nome, email, senha_hash, papel)
VALUES (?, ?, ?, 'USUARIO')`,
[nome, email, senhaHash]
);
return resultado.insertId;
}
module.exports = {
buscarPorEmail,
buscarPorId,
criar
};Observe que o model não recebe req nem res. Isso é importante para preservarmos as separações de responsabilidade das camadas MVC.
Model dos sensores
Crie:
backend/models/sensorModel.js
E digite o código abaixo:
const banco = require("../config/banco");
async function criar(temperatura, umidade) {
const [resultado] = await banco.execute(
`INSERT INTO LeituraSensor
(temperatura, umidade)
VALUES (?, ?)`,
[temperatura, umidade]
);
return resultado.insertId;
}
async function buscarUltimos(limite = 10) {
const limiteSeguro = Number(limite);
const [leituras] = await banco.query(
`SELECT
id,
temperatura,
umidade,
criadoEm
FROM LeituraSensor
ORDER BY id DESC
LIMIT ?`,
[limiteSeguro]
);
return leituras.reverse();
}
async function excluir(id) {
const [resultado] = await banco.execute(
"DELETE FROM LeituraSensor WHERE id = ?",
[id]
);
return resultado.affectedRows > 0;
}
module.exports = {
criar,
buscarUltimos,
excluir
};Agora nenhum Controller precisa escrever SQL diretamente.
Middleware de validação
Crie:
backend/middlewares/
e depois:
backend/middlewares/validacao.js
Código:
function emailValido(email) {
const expressao = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return expressao.test(email);
}
function validarCadastro(req, res, next) {
let {
nome,
email,
senha
} = req.body;
nome = nome?.trim();
email = email?.trim().toLowerCase();
if (!nome || !email || !senha) {
return res.status(400).json({
erro: "Nome, e-mail e senha são obrigatórios."
});
}
if (nome.length < 3 || nome.length > 100) {
return res.status(400).json({
erro: "O nome deve possuir entre 3 e 100 caracteres."
});
}
if (!emailValido(email)) {
return res.status(400).json({
erro: "E-mail inválido."
});
}
if (senha.length < 12) {
return res.status(400).json({
erro: "A senha deve possuir pelo menos 12 caracteres."
});
}
if (Buffer.byteLength(senha, "utf8") > 72) {
return res.status(400).json({
erro: "A senha ultrapassa o limite permitido."
});
}
req.body.nome = nome;
req.body.email = email;
next();
}
function validarLogin(req, res, next) {
let {
email,
senha
} = req.body;
email = email?.trim().toLowerCase();
if (!email || !senha) {
return res.status(400).json({
erro: "E-mail e senha são obrigatórios."
});
}
if (!emailValido(email)) {
return res.status(400).json({
erro: "E-mail inválido."
});
}
req.body.email = email;
next();
}
function validarSensor(req, res, next) {
const temperatura = Number(req.body.temperatura);
const umidade = Number(req.body.umidade);
if (
!Number.isFinite(temperatura) ||
!Number.isFinite(umidade)
) {
return res.status(400).json({
erro: "Temperatura e umidade devem ser numéricas."
});
}
if (umidade < 0 || umidade > 100) {
return res.status(400).json({
erro: "A umidade deve estar entre 0 e 100."
});
}
req.body.temperatura = temperatura;
req.body.umidade = umidade;
next();
}
module.exports = {
validarCadastro,
validarLogin,
validarSensor
};Esse middleware não autentica o usuário ainda. Ele apenas rejeita entradas estruturalmente inválidas, tipo e-mails mal escritos ou senhas não inseridas.
Middleware de autenticação JWT
Crie:
backend/middlewares/autenticacao.js
e digite:
const jwt = require("jsonwebtoken");
function autenticar(req, res, next) {
const token = req.cookies.token;
if (!token) {
return res.status(401).json({
erro: "Autenticação necessária."
});
}
try {
const payload = jwt.verify(
token,
process.env.JWT_SECRET,
{
algorithms: ["HS256"],
issuer: "projeto-iot",
audience: "dashboard-iot"
}
);
req.usuario = {
id: Number(payload.sub),
papel: payload.papel
};
next();
} catch {
return res.status(401).json({
erro: "Token inválido ou expirado."
});
}
}
module.exports = autenticar;Segundo Jones, Bradley e Sakimura (2015), a própria RFC 7519 estabelece que um token JWT expirado não deve mais ser aceito.
Middleware de autorização
Crie:
backend/middlewares/autorizacao.js
e digite:
function autorizar(...papeisPermitidos) {
return (req, res, next) => {
if (!req.usuario) {
return res.status(401).json({
erro: "Usuário não autenticado."
});
}
if (
!papeisPermitidos.includes(
req.usuario.papel
)
) {
return res.status(403).json({
erro: "Usuário sem permissão para esta operação."
});
}
next();
};
}
module.exports = autorizar;Assim podemos construir:
autenticar,
autorizar("ADMIN")
ou:
autenticar,
autorizar("USUARIO", "ADMIN")
Lembre-se que a autorização deve ser verificada pelo servidor em cada requisição relevante, em vez de confiar apenas na interface do navegador (OWASP, [s.d.]).
Middleware para o Arduino/simulador
Não é recomendável usar o JWT do usuário humano para um dispositivo no sistema. O melhor é separar as duas identidades para ter mais controle sobre operações e garantir rastreabilidade depois.
Crie o arquivo:
backend/middlewares/dispositivo.js
e digite o código:
const crypto = require("crypto");
function autenticarDispositivo(req, res, next) {
const chaveRecebida = req.get("x-api-key");
const chaveEsperada = process.env.API_KEY_DISPOSITIVO;
if (!chaveRecebida || !chaveEsperada) {
return res.status(401).json({
erro: "Dispositivo não autenticado."
});
}
const recebida = Buffer.from(chaveRecebida);
const esperada = Buffer.from(chaveEsperada);
if (recebida.length !== esperada.length) {
return res.status(401).json({
erro: "Credencial do dispositivo inválida."
});
}
const iguais = crypto.timingSafeEqual(
recebida,
esperada
);
if (!iguais) {
return res.status(401).json({
erro: "Credencial do dispositivo inválida."
});
}
next();
}
module.exports = autenticarDispositivo;Agora a rota POST /api/sensores deixa de ser pública e passa a estar sob domínio de um acesso autenticado.
Controller de autenticação
Crie:
backend/controllers/
e depois:
backend/controllers/authController.js
Código:
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const Usuario = require("../models/usuarioModel");
function configuracaoCookie() {
return {
httpOnly: true,
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
path: "/"
};
}
async function cadastrar(req, res) {
try {
const {
nome,
email,
senha
} = req.body;
const usuarioExistente =
await Usuario.buscarPorEmail(email);
if (usuarioExistente) {
return res.status(409).json({
erro: "Já existe uma conta com este e-mail."
});
}
const senhaHash = await bcrypt.hash(
senha,
12
);
const id = await Usuario.criar(
nome,
email,
senhaHash
);
return res.status(201).json({
mensagem: "Usuário cadastrado com sucesso.",
id
});
} catch (erro) {
console.error("Erro no cadastro:", erro);
return res.status(500).json({
erro: "Erro interno do servidor."
});
}
}
async function login(req, res) {
try {
const {
email,
senha
} = req.body;
const usuario =
await Usuario.buscarPorEmail(email);
if (!usuario) {
return res.status(401).json({
erro: "E-mail ou senha inválidos."
});
}
const senhaCorreta =
await bcrypt.compare(
senha,
usuario.senha_hash
);
if (!senhaCorreta) {
return res.status(401).json({
erro: "E-mail ou senha inválidos."
});
}
const token = jwt.sign(
{
papel: usuario.papel
},
process.env.JWT_SECRET,
{
algorithm: "HS256",
subject: String(usuario.id),
issuer: "projeto-iot",
audience: "dashboard-iot",
expiresIn: "15m"
}
);
res.cookie(
"token",
token,
{
...configuracaoCookie(),
maxAge: 15 * 60 * 1000
}
);
return res.status(200).json({
mensagem: "Login realizado com sucesso.",
usuario: {
id: usuario.id,
nome: usuario.nome,
email: usuario.email,
papel: usuario.papel
}
});
} catch (erro) {
console.error("Erro no login:", erro);
return res.status(500).json({
erro: "Erro interno do servidor."
});
}
}
async function usuarioAtual(req, res) {
try {
const usuario =
await Usuario.buscarPorId(
req.usuario.id
);
if (!usuario) {
return res.status(404).json({
erro: "Usuário não encontrado."
});
}
return res.json(usuario);
} catch (erro) {
console.error(
"Erro ao buscar usuário:",
erro
);
return res.status(500).json({
erro: "Erro interno do servidor."
});
}
}
function logout(req, res) {
res.clearCookie(
"token",
configuracaoCookie()
);
return res.status(200).json({
mensagem: "Logout realizado com sucesso."
});
}
module.exports = {
cadastrar,
login,
usuarioAtual,
logout
};Observe que nenhuma senha é salva diretamente graças à linha:
const senhaHash = await bcrypt.hash(senha, 12);Além disso, a verificação utiliza:
await bcrypt.compare(
senha,
usuario.senha_hash
);Controller dos sensores
Crie:
backend/controllers/sensorController.js
e insira o código:
const Sensor = require("../models/sensorModel");
async function cadastrar(req, res) {
try {
const {
temperatura,
umidade
} = req.body;
const id = await Sensor.criar(
temperatura,
umidade
);
return res.status(201).json({
mensagem: "Leitura cadastrada com sucesso.",
id
});
} catch (erro) {
console.error(
"Erro ao cadastrar leitura:",
erro
);
return res.status(500).json({
erro: "Erro ao salvar dados."
});
}
}
async function listar(req, res) {
try {
const leituras =
await Sensor.buscarUltimos(10);
return res.status(200).json(
leituras
);
} catch (erro) {
console.error(
"Erro ao buscar leituras:",
erro
);
return res.status(500).json({
erro: "Erro ao buscar dados."
});
}
}
async function excluir(req, res) {
try {
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({
erro: "ID inválido."
});
}
const excluido =
await Sensor.excluir(id);
if (!excluido) {
return res.status(404).json({
erro: "Leitura não encontrada."
});
}
return res.status(204).send();
} catch (erro) {
console.error(
"Erro ao excluir leitura:",
erro
);
return res.status(500).json({
erro: "Erro ao excluir leitura."
});
}
}
module.exports = {
cadastrar,
listar,
excluir
};Rotas de autenticação
Crie:
backend/routes/
e depois:
backend/routes/authRoutes.js
O código é:
const express = require("express");
const AuthController =
require("../controllers/authController");
const autenticar =
require("../middlewares/autenticacao");
const {
validarCadastro,
validarLogin
} = require("../middlewares/validacao");
const router = express.Router();
router.post(
"/cadastro",
validarCadastro,
AuthController.cadastrar
);
router.post(
"/login",
validarLogin,
AuthController.login
);
router.get(
"/me",
autenticar,
AuthController.usuarioAtual
);
router.post(
"/logout",
autenticar,
AuthController.logout
);
module.exports = router;O código acima produzirá as rotas:
POST /api/auth/cadastro
POST /api/auth/login
GET /api/auth/me
POST /api/auth/logout
Rotas dos sensores
Crie:
backend/routes/sensorRoutes.js
e digite:
const express = require("express");
const SensorController =
require("../controllers/sensorController");
const autenticar =
require("../middlewares/autenticacao");
const autorizar =
require("../middlewares/autorizacao");
const autenticarDispositivo =
require("../middlewares/dispositivo");
const {
validarSensor
} = require("../middlewares/validacao");
const router = express.Router();
router.post(
"/",
autenticarDispositivo,
validarSensor,
SensorController.cadastrar
);
router.get(
"/",
autenticar,
autorizar("USUARIO", "ADMIN"),
SensorController.listar
);
router.delete(
"/:id",
autenticar,
autorizar("ADMIN"),
SensorController.excluir
);
module.exports = router;Aqui fica explícita a diferença entre autenticação e autorização.
Um USUARIO pode acessar:
GET /api/sensores
mas não pode acessar:
DELETE /api/sensores/10
Já o papel ADMIN pode fazer ambos.
Configuração principal do Express
Crie:
backend/app.js
e inclua o código a seguir:
const path = require("path");
const express = require("express");
const cookieParser = require("cookie-parser");
const helmet = require("helmet");
const rateLimit = require("express-rate-limit");
const authRoutes =
require("./routes/authRoutes");
const sensorRoutes =
require("./routes/sensorRoutes");
const app = express();
app.use(
helmet({
contentSecurityPolicy: false
})
);
app.use(express.json({
limit: "20kb"
}));
app.use(
express.urlencoded({
extended: false,
limit: "20kb"
})
);
app.use(cookieParser());
const limitarLogin = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 10,
standardHeaders: true,
legacyHeaders: false,
message: {
erro: "Muitas tentativas. Tente novamente mais tarde."
}
});
app.use(
"/api/auth/login",
limitarLogin
);
app.use(
"/api/auth",
authRoutes
);
app.use(
"/api/sensores",
sensorRoutes
);
const pastaFrontend = path.join(
__dirname,
"../frontend"
);
app.use(
express.static(pastaFrontend)
);
app.get("/", (req, res) => {
res.sendFile(
path.join(
pastaFrontend,
"index.html"
)
);
});
app.use((req, res) => {
return res.status(404).json({
erro: "Recurso não encontrado."
});
});
app.use((erro, req, res, next) => {
console.error(erro);
return res.status(500).json({
erro: "Erro interno do servidor."
});
});
module.exports = app;O helmet acrescenta cabeçalhos HTTP de segurança e o express-rate-limit reduz tentativas automatizadas e repetidas de login.
Como o Express servirá também o frontend, não precisamos mais de:
app.use(cors());
nem de URLs absolutas como:
http://localhost:3000/api/sensores
Podemos simplesmente usar:
/api/sensores
Isso também simplifica o uso do cookie HttpOnly.
Inicialização do servidor
O arquivo server.js atual deve ser substituído. Vá até o local:
backend/server.js
e substitua o código anterior pelo abaixo:
require("dotenv").config();
const app = require("./app");
const banco = require("./config/banco");
const porta =
Number(process.env.PORTA) || 3000;
async function iniciar() {
try {
await banco.query("SELECT 1");
console.log(
"Conectado ao MySQL com sucesso."
);
app.listen(porta, () => {
console.log(
`Servidor executando em http://localhost:${porta}`
);
});
} catch (erro) {
console.error(
"Não foi possível iniciar o servidor:",
erro
);
process.exit(1);
}
}
iniciar();Assim, o server.js deixa de conter regras de negócio e sua função passa a ser apenas iniciar a aplicação.
Página de cadastro
Este arquivo não existe no projeto original e deve ser criado:
frontend/cadastro.html
O código que você deve inserir no arquivo criado é:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
>
<title>Cadastro</title>
<link
rel="stylesheet"
href="cadastro.css"
>
</head>
<body>
<main>
<h1>Criar conta</h1>
<form id="formCadastro">
<label for="nome">
Nome
</label>
<input
id="nome"
name="nome"
type="text"
maxlength="100"
required
>
<label for="email">
E-mail
</label>
<input
id="email"
name="email"
type="email"
maxlength="150"
required
>
<label for="senha">
Senha
</label>
<input
id="senha"
name="senha"
type="password"
minlength="12"
required
>
<button type="submit">
Cadastrar
</button>
</form>
<p id="mensagem"></p>
<p>
Já possui conta?
<a href="/login.html">
Entrar
</a>
</p>
</main>
<script src="/cadastro.js"></script>
</body>
</html>JavaScript do cadastro
Crie:
frontend/cadastro.js
e digite:
const formulario =
document.querySelector("#formCadastro");
const mensagem =
document.querySelector("#mensagem");
formulario.addEventListener(
"submit",
async (evento) => {
evento.preventDefault();
mensagem.textContent = "";
const dados = {
nome:
document.querySelector("#nome")
.value,
email:
document.querySelector("#email")
.value,
senha:
document.querySelector("#senha")
.value
};
try {
const resposta = await fetch(
"/api/auth/cadastro",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify(dados)
}
);
const resultado =
await resposta.json();
if (!resposta.ok) {
mensagem.textContent =
resultado.erro;
return;
}
window.location.href =
"/login.html";
} catch {
mensagem.textContent =
"Não foi possível comunicar com o servidor.";
}
}
);Página de login
Substitua ou adapte o arquivo login.html já existente no diretório:
frontend/login.html
Para isso, digite o código abaixo:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
>
<title>Login</title>
<link
rel="stylesheet"
href="cadastro.css"
>
</head>
<body>
<main>
<h1>Entrar</h1>
<form id="formLogin">
<label for="email">
E-mail
</label>
<input
id="email"
type="email"
required
>
<label for="senha">
Senha
</label>
<input
id="senha"
type="password"
required
>
<button type="submit">
Entrar
</button>
</form>
<p id="mensagem"></p>
<p>
Não possui conta?
<a href="/cadastro.html">
Cadastre-se
</a>
</p>
</main>
<script src="/login.js"></script>
</body>
</html>JavaScript do login
Crie:
frontend/login.js
e digite:
const formulario =
document.querySelector("#formLogin");
const mensagem =
document.querySelector("#mensagem");
formulario.addEventListener(
"submit",
async (evento) => {
evento.preventDefault();
mensagem.textContent = "";
const dados = {
email:
document.querySelector("#email")
.value,
senha:
document.querySelector("#senha")
.value
};
try {
const resposta = await fetch(
"/api/auth/login",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
credentials: "same-origin",
body: JSON.stringify(dados)
}
);
const resultado =
await resposta.json();
if (!resposta.ok) {
mensagem.textContent =
resultado.erro;
return;
}
window.location.href = "/";
} catch {
mensagem.textContent =
"Não foi possível comunicar com o servidor.";
}
}
);Repare que o JWT nunca aparece aqui. Isso se dá porque o navegador recebe o token dentro de um cookie HttpOnly.
Alteração do dashboard
No <header> já existente, acrescente uma identificação do usuário e um botão de logout.
Por exemplo, substitua o cabeçalho atual por:
<header
class="mb-6 flex justify-between items-center bg-white p-4 rounded-lg shadow"
>
<div>
<h1
class="text-2xl font-bold text-gray-800"
>
Dashboard IoT
</h1>
<p id="usuarioLogado">
Verificando usuário...
</p>
</div>
<button
id="botaoSair"
type="button"
>
Sair
</button>
</header>O restante do index.html pode permanecer igual.
JavaScript principal do dashboard
O arquivo atual pode ser substituído por esta versão:
const contexto =
document
.getElementById("sensorChart")
.getContext("2d");
const sensorChart = new Chart(
contexto,
{
type: "line",
data: {
labels: [],
datasets: [
{
label:
"Temperatura (°C)",
data: [],
borderColor:
"#3b82f6",
backgroundColor:
"rgba(59, 130, 246, 0.1)",
tension: 0.3,
fill: true
},
{
label:
"Umidade (%)",
data: [],
borderColor:
"#10b981",
backgroundColor:
"rgba(16, 185, 129, 0.1)",
tension: 0.3,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false
}
}
);
async function verificarUsuario() {
const resposta = await fetch(
"/api/auth/me"
);
if (resposta.status === 401) {
window.location.href =
"/login.html";
return null;
}
if (!resposta.ok) {
throw new Error(
"Erro ao verificar usuário."
);
}
return resposta.json();
}
async function buscarDadosDoMySQL() {
try {
const resposta = await fetch(
"/api/sensores"
);
if (resposta.status === 401) {
window.location.href =
"/login.html";
return;
}
if (resposta.status === 403) {
console.error(
"Usuário sem autorização."
);
return;
}
if (!resposta.ok) {
throw new Error(
"Erro ao consultar sensores."
);
}
const leituras =
await resposta.json();
if (leituras.length === 0) {
return;
}
const labels =
leituras.map(
leitura =>
new Date(
leitura.criadoEm
).toLocaleTimeString()
);
const temperaturas =
leituras.map(
leitura =>
leitura.temperatura
);
const umidades =
leituras.map(
leitura =>
leitura.umidade
);
const ultimo =
leituras[
leituras.length - 1
];
document
.getElementById("currentTemp")
.innerText =
`${ultimo.temperatura} °C`;
document
.getElementById("currentHum")
.innerText =
`${ultimo.umidade} %`;
sensorChart.data.labels =
labels;
sensorChart
.data
.datasets[0]
.data = temperaturas;
sensorChart
.data
.datasets[1]
.data = umidades;
sensorChart.update();
} catch (erro) {
console.error(
"Erro ao buscar dados:",
erro
);
}
}
async function sair() {
try {
await fetch(
"/api/auth/logout",
{
method: "POST"
}
);
} finally {
window.location.href =
"/login.html";
}
}
async function iniciarSistema() {
try {
const usuario =
await verificarUsuario();
if (!usuario) {
return;
}
document
.getElementById(
"usuarioLogado"
)
.textContent =
`${usuario.nome} (${usuario.papel})`;
document
.getElementById(
"botaoSair"
)
.addEventListener(
"click",
sair
);
await buscarDadosDoMySQL();
setInterval(
buscarDadosDoMySQL,
3000
);
} catch (erro) {
console.error(erro);
window.location.href =
"/login.html";
}
}
iniciarSistema();A principal alteração em relação ao projeto original é esta:
fetch("/api/sensores")
em vez de:
fetch("http://localhost:3000/api/sensores")
porque frontend e backend agora são servidos pela mesma aplicação.
Protegendo também o simulador
O POST /api/sensores original era aberto. Isso permitiria que qualquer cliente capaz de alcançar o servidor inserisse dados. O repositório original efetivamente expõe essa rota diretamente para Arduino ou simulador. Vamos modificar isso.
Vá ao diretório local:
cod-aula-git/simulador.js
e substitua as linhas de código atuais por:
const urlBackend =
"http://localhost:3000/api/sensores";
const chaveDispositivo =
process.env.API_KEY_DISPOSITIVO;
if (!chaveDispositivo) {
console.error(
"Defina API_KEY_DISPOSITIVO antes de executar o simulador."
);
process.exit(1);
}
function gerarDadosAleatorios() {
const temperatura =
Number(
(
20 +
Math.random() * 10
).toFixed(1)
);
const umidade =
Number(
(
40 +
Math.random() * 30
).toFixed(1)
);
return {
temperatura,
umidade
};
}
async function enviarDados() {
const dados =
gerarDadosAleatorios();
try {
const resposta = await fetch(
urlBackend,
{
method: "POST",
headers: {
"Content-Type":
"application/json",
"x-api-key":
chaveDispositivo
},
body:
JSON.stringify(dados)
}
);
if (!resposta.ok) {
const erro =
await resposta.json();
console.error(
"[ERRO]",
erro
);
return;
}
console.log(
`[ENVIADO] Temp: ${dados.temperatura}°C | Umid: ${dados.umidade}%`
);
} catch (erro) {
console.error(
"[ERRO] Backend indisponível:",
erro.message
);
}
}
console.log(
"Simulador iniciado."
);
enviarDados();
setInterval(
enviarDados,
3000
);Como esse arquivo está fora de backend, existem duas maneiras de passar a chave.
No Windows PowerShell:
$env:API_KEY_DISPOSITIVO="mesma-chave-do-env"
node simulador.js
No Linux:
API_KEY_DISPOSITIVO="mesma-chave-do-env" node simulador.js
Isso é melhor do que inserir a chave diretamente dentro do código.
Como fica o fluxo MVC
Após a refatoração, uma consulta aos sensores percorre:
GET /api/sensores
│
▼
routes/sensorRoutes.js
│
▼
middleware autenticar
│
▼
middleware autorizar
│
▼
sensorController.listar()
│
▼
sensorModel.buscarUltimos()
│
▼
Acesso ao banco MySQL
O fluxo de login, por sua vez, fica assim:
POST /api/auth/login
│
▼
validarLogin
│
▼
authController.login()
│
▼
usuarioModel.buscarPorEmail()
│
▼
Acesso ao banco MySQL
│
▼
bcrypt.compare()
│
▼
jwt.sign()
│
▼
Cookie HttpOnly
E um processo de exclusão administrativa ficaria:
DELETE /api/sensores/15
│
▼
processo de autenticar
│
├── falha → 401
│
▼
processo de autorizar("ADMIN")
│
├── USUARIO → 403
│
▼
sensorController.excluir()
│
▼
sensorModel.excluir()
│
▼
Acesso ao banco MySQL
Esse é o ponto em que autenticação e autorização ficam estruturalmente separadas.
O que cada papel pode fazer?
Com essa refatoração que fizemos no projeto original, o quadro de possibilidades no sistema fica assim:
| Operação | Não autenticado | USUARIO | ADMIN | Dispositivo |
|---|---|---|---|---|
| Cadastro | Sim | Sim | Sim | Não |
| Login | Sim | Sim | Sim | Não |
| Consultar sensores | Não | Sim | Sim | Não |
| Excluir leitura | Não | Não | Sim | Não |
| Inserir leitura | Não | Não | Não | Sim |
| Logout | Não | Sim | Sim | Não |
Esse desenho segue o princípio de conceder somente os privilégios necessários e validar autorização nas requisições protegidas, práticas recomendadas pela OWASP (OWASP, [s.d.]).
Teste da implementação
Primeiro inicie o backend:
cd backend
node server.js
Acesse exclusivamente:
http://localhost:3000/cadastro.html
Não abra mais index.html diretamente pelo sistema de arquivos e não utilize Live Server para este exercício.
Em seguida, cadastre um usuário. Depois faça login em:
http://localhost:3000/login.html
Após o login, o navegador deverá receber um cookie chamado token e marcado como:
HttpOnly
SameSite=Strict
Acesse então o endereço:
http://localhost:3000/
e o dashboard deverá funcionar.
Se você apagar o cookie e recarregar a página, a consulta:
GET /api/sensores
deverá retornar o status code:
401
Se estiver autenticado como USUARIO e tentar a rota:
DELETE /api/sensores/1
deverá receber o código de erro:
403
Depois de alterar o papel para ADMIN no MySQL, faça logout e login novamente, pois o papel está gravado no JWT emitido naquele login. Ai, então, a exclusão deverá ser permitida.
Conclusão
Autenticação e autorização resolvem problemas diferentes e complementares. Enquanto a autenticação estabelece uma identidade confiável, a autorização determina as operações que essa identidade pode realizar. Confundir esses conceitos pode levar a aplicações nas quais qualquer usuário autenticado consegue realizar operações administrativas.
No projeto estudado, o mecanismo introduzido cria uma cadeia clara de responsabilidades. As senhas deixam de ser armazenadas diretamente e passam por hash, o JWT representa temporariamente a autenticação, os cookies HttpOnly transportam a credencial entre requisições e os middlewares centralizam tanto a autenticação quanto o controle de acesso.
Continue ampliando seu repertório para desenvolver sistemas mais robustos em mecanismos de segurança. Obrigado pela leitura e bons estudos!
Referências
BATISTA, Abdiel. cod-aula-git. GitHub, [s.d.]. Disponível em: https://github.com/abdielbatista/cod-aula-git. Acesso em: 22 set. 2026.
EXPRESS.JS. Using middleware. [S. l.]: OpenJS Foundation, [s.d.]. Disponível em: https://expressjs.com/en/guide/using-middleware.html. Acesso em: 22 set. 2026.
JONES, Michael; BRADLEY, John; SAKIMURA, Nat. JSON Web Token (JWT). RFC 7519. Disponível em: https://www.rfc-editor.org/info/rfc7519/. Acesso em: 22 set. 2026.
OWASP. Authentication Cheat Sheet. Disponível em: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html. Acesso em: 22 set. 2026.
OWASP. Authorization Cheat Sheet. Disponível em: https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html. Acesso em: 22 set. 2026.
OWASP. Password Storage Cheat Sheet. Disponível em: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html. Acesso em: 22 set. 2026
SOMMERVILLE, Ian. Engenharia de software. 9. ed. São Paulo: Pearson Prentice Hall, 2011.


