Neste projeto vamos construir um sistema completo de autenticacao com JWT (JSON Web Tokens) usando Axum. O sistema vai incluir registro de usuarios, login com geracao de tokens, validacao de tokens em middleware e rotas protegidas que exigem autenticacao. JWT e o padrao mais utilizado para autenticacao em APIs modernas e entender sua implementacao e fundamental para qualquer desenvolvedor backend.
Autenticacao e um dos pilares de qualquer aplicacao web. Ao construir este projeto, voce vai entender como tokens funcionam, como proteger rotas e como estruturar um fluxo de autenticacao seguro em Rust.
O Que Vamos Construir
Um sistema de autenticacao com as seguintes funcionalidades:
- POST /api/registrar – Criar nova conta de usuario
- POST /api/login – Autenticar e receber token JWT
- GET /api/perfil – Rota protegida que retorna dados do usuario
- GET /api/admin – Rota protegida que exige role de administrador
- Hash de senhas com bcrypt
- Geracao e validacao de tokens JWT com expiracao
- Middleware de autenticacao reutilizavel
- Armazenamento em memoria com HashMap
Estrutura do Projeto
autenticacao-jwt/
├── Cargo.toml
└── src/
└── main.rs
Configurando o Projeto
cargo new autenticacao-jwt
cd autenticacao-jwt
Configure o Cargo.toml:
[package]
name = "autenticacao-jwt"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7"
axum-extra = { version = "0.9", features = ["typed-header"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
jsonwebtoken = "9"
bcrypt = "0.16"
chrono = "0.4"
uuid = { version = "1", features = ["v4"] }
Usamos jsonwebtoken para criar e validar JWTs, bcrypt para hash de senhas e axum-extra para extrair headers de autorizacao.
Passo 1: Definindo Modelos e Estado
Vamos definir as estruturas de dados para usuarios, tokens e requisicoes:
use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use uuid::Uuid;
// Segredo usado para assinar os tokens (em producao, use variavel de ambiente)
const SEGREDO_JWT: &str = "meu_segredo_super_secreto_em_producao_use_env";
// Representa um usuario armazenado no sistema
#[derive(Debug, Clone, Serialize)]
pub struct Usuario {
pub id: String,
pub nome: String,
pub email: String,
#[serde(skip_serializing)]
pub senha_hash: String,
pub role: String,
}
// Dados para registro de novo usuario
#[derive(Debug, Deserialize)]
pub struct DadosRegistro {
pub nome: String,
pub email: String,
pub senha: String,
}
// Dados para login
#[derive(Debug, Deserialize)]
pub struct DadosLogin {
pub email: String,
pub senha: String,
}
// Claims do token JWT
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String, // ID do usuario
pub email: String,
pub role: String,
pub exp: usize, // Expiracao (timestamp Unix)
pub iat: usize, // Emitido em (timestamp Unix)
}
// Resposta com o token
#[derive(Debug, Serialize)]
pub struct RespostaToken {
pub token: String,
pub tipo: String,
pub expira_em: String,
}
// Estado compartilhado: email -> Usuario
pub type EstadoApp = Arc<Mutex<HashMap<String, Usuario>>>;
Os Claims sao os dados codificados dentro do JWT. Incluimos o ID do usuario, email, role e timestamps de criacao e expiracao.
Passo 2: Funcoes de Token e Hash
Implementamos as funcoes para gerar tokens JWT e fazer hash de senhas:
// Gera um token JWT para o usuario
fn gerar_token(usuario: &Usuario) -> Result<String, jsonwebtoken::errors::Error> {
let agora = Utc::now();
let expiracao = agora + Duration::hours(24);
let claims = Claims {
sub: usuario.id.clone(),
email: usuario.email.clone(),
role: usuario.role.clone(),
exp: expiracao.timestamp() as usize,
iat: agora.timestamp() as usize,
};
encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(SEGREDO_JWT.as_bytes()),
)
}
// Valida e decodifica um token JWT
fn validar_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
let dados = decode::<Claims>(
token,
&DecodingKey::from_secret(SEGREDO_JWT.as_bytes()),
&Validation::default(),
)?;
Ok(dados.claims)
}
// Gera hash bcrypt da senha
fn hash_senha(senha: &str) -> Result<String, bcrypt::BcryptError> {
bcrypt::hash(senha, bcrypt::DEFAULT_COST)
}
// Verifica se a senha corresponde ao hash
fn verificar_senha(senha: &str, hash: &str) -> bool {
bcrypt::verify(senha, hash).unwrap_or(false)
}
O token e assinado com HMAC-SHA256 (padrao do jsonwebtoken) e expira em 24 horas. O bcrypt garante que as senhas sejam armazenadas de forma segura com salt automatico.
Passo 3: Handlers e Middleware de Autenticacao
Vamos implementar os handlers de registro, login e as rotas protegidas:
use axum::{
extract::{Request, State},
http::{header, StatusCode},
middleware::Next,
response::IntoResponse,
Json,
};
// POST /api/registrar
async fn registrar(
State(estado): State<EstadoApp>,
Json(dados): Json<DadosRegistro>,
) -> Result<(StatusCode, Json<serde_json::Value>), (StatusCode, Json<serde_json::Value>)> {
let mut usuarios = estado.lock().unwrap();
// Verifica se o email ja esta cadastrado
if usuarios.contains_key(&dados.email) {
return Err((
StatusCode::CONFLICT,
Json(serde_json::json!({"erro": "Email ja cadastrado"})),
));
}
// Valida dados minimos
if dados.senha.len() < 6 {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"erro": "Senha deve ter pelo menos 6 caracteres"})),
));
}
let senha_hash = hash_senha(&dados.senha).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"erro": "Erro ao processar senha"})),
)
})?;
let usuario = Usuario {
id: Uuid::new_v4().to_string(),
nome: dados.nome,
email: dados.email.clone(),
senha_hash,
role: "usuario".to_string(),
};
usuarios.insert(dados.email, usuario);
Ok((
StatusCode::CREATED,
Json(serde_json::json!({"mensagem": "Usuario registrado com sucesso"})),
))
}
// POST /api/login
async fn login(
State(estado): State<EstadoApp>,
Json(dados): Json<DadosLogin>,
) -> Result<Json<RespostaToken>, (StatusCode, Json<serde_json::Value>)> {
let usuarios = estado.lock().unwrap();
let usuario = usuarios.get(&dados.email).ok_or((
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"erro": "Email ou senha incorretos"})),
))?;
if !verificar_senha(&dados.senha, &usuario.senha_hash) {
return Err((
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"erro": "Email ou senha incorretos"})),
));
}
let token = gerar_token(usuario).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"erro": "Erro ao gerar token"})),
)
})?;
Ok(Json(RespostaToken {
token,
tipo: "Bearer".to_string(),
expira_em: "24 horas".to_string(),
}))
}
// Middleware de autenticacao: extrai e valida o token do header Authorization
async fn middleware_auth(
mut req: Request,
next: Next,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let header_auth = req
.headers()
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.ok_or((
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"erro": "Token de autorizacao ausente"})),
))?;
let token = header_auth
.strip_prefix("Bearer ")
.ok_or((
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"erro": "Formato de token invalido. Use: Bearer <token>"})),
))?;
let claims = validar_token(token).map_err(|_| {
(
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"erro": "Token invalido ou expirado"})),
)
})?;
// Insere os claims no request para os handlers usarem
req.extensions_mut().insert(claims);
Ok(next.run(req).await)
}
// GET /api/perfil -- Rota protegida
async fn perfil(req: Request) -> impl IntoResponse {
let claims = req.extensions().get::<Claims>().unwrap();
Json(serde_json::json!({
"id": claims.sub,
"email": claims.email,
"role": claims.role,
"mensagem": "Voce esta autenticado!"
}))
}
// GET /api/admin -- Rota protegida com verificacao de role
async fn admin(req: Request) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let claims = req.extensions().get::<Claims>().unwrap();
if claims.role != "admin" {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"erro": "Acesso negado. Requer role de administrador."})),
));
}
Ok(Json(serde_json::json!({
"mensagem": "Bem-vindo, administrador!",
"dados_sensiveis": "Informacoes restritas"
})))
}
O middleware extrai o token do header Authorization, valida-o e insere os claims decodificados nas extensoes do request. Os handlers protegidos podem entao acessar os claims para identificar o usuario.
Passo 4: Montando o main.rs Completo
Aqui esta o codigo completo do src/main.rs:
use axum::{
extract::{Request, State},
http::{header, StatusCode},
middleware::{self, Next},
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use uuid::Uuid;
// === Constantes ===
const SEGREDO_JWT: &str = "meu_segredo_super_secreto_em_producao_use_env";
// === Modelos ===
#[derive(Debug, Clone, Serialize)]
pub struct Usuario {
pub id: String,
pub nome: String,
pub email: String,
#[serde(skip_serializing)]
pub senha_hash: String,
pub role: String,
}
#[derive(Debug, Deserialize)]
pub struct DadosRegistro {
pub nome: String,
pub email: String,
pub senha: String,
}
#[derive(Debug, Deserialize)]
pub struct DadosLogin {
pub email: String,
pub senha: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub email: String,
pub role: String,
pub exp: usize,
pub iat: usize,
}
#[derive(Debug, Serialize)]
pub struct RespostaToken {
pub token: String,
pub tipo: String,
pub expira_em: String,
}
pub type EstadoApp = Arc<Mutex<HashMap<String, Usuario>>>;
// === Funcoes auxiliares ===
fn gerar_token(usuario: &Usuario) -> Result<String, jsonwebtoken::errors::Error> {
let agora = Utc::now();
let expiracao = agora + Duration::hours(24);
let claims = Claims {
sub: usuario.id.clone(),
email: usuario.email.clone(),
role: usuario.role.clone(),
exp: expiracao.timestamp() as usize,
iat: agora.timestamp() as usize,
};
encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(SEGREDO_JWT.as_bytes()),
)
}
fn validar_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
let dados = decode::<Claims>(
token,
&DecodingKey::from_secret(SEGREDO_JWT.as_bytes()),
&Validation::default(),
)?;
Ok(dados.claims)
}
fn hash_senha(senha: &str) -> Result<String, bcrypt::BcryptError> {
bcrypt::hash(senha, bcrypt::DEFAULT_COST)
}
fn verificar_senha(senha: &str, hash: &str) -> bool {
bcrypt::verify(senha, hash).unwrap_or(false)
}
// === Handlers ===
async fn registrar(
State(estado): State<EstadoApp>,
Json(dados): Json<DadosRegistro>,
) -> Result<(StatusCode, Json<serde_json::Value>), (StatusCode, Json<serde_json::Value>)> {
let mut usuarios = estado.lock().unwrap();
if usuarios.contains_key(&dados.email) {
return Err((
StatusCode::CONFLICT,
Json(serde_json::json!({"erro": "Email ja cadastrado"})),
));
}
if dados.senha.len() < 6 {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"erro": "Senha deve ter pelo menos 6 caracteres"})),
));
}
let senha_hash = hash_senha(&dados.senha).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"erro": "Erro ao processar senha"})),
)
})?;
let usuario = Usuario {
id: Uuid::new_v4().to_string(),
nome: dados.nome,
email: dados.email.clone(),
senha_hash,
role: "usuario".to_string(),
};
usuarios.insert(dados.email, usuario);
Ok((
StatusCode::CREATED,
Json(serde_json::json!({"mensagem": "Usuario registrado com sucesso"})),
))
}
async fn login(
State(estado): State<EstadoApp>,
Json(dados): Json<DadosLogin>,
) -> Result<Json<RespostaToken>, (StatusCode, Json<serde_json::Value>)> {
let usuarios = estado.lock().unwrap();
let usuario = usuarios.get(&dados.email).ok_or((
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"erro": "Email ou senha incorretos"})),
))?;
if !verificar_senha(&dados.senha, &usuario.senha_hash) {
return Err((
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"erro": "Email ou senha incorretos"})),
));
}
let token = gerar_token(usuario).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"erro": "Erro ao gerar token"})),
)
})?;
Ok(Json(RespostaToken {
token,
tipo: "Bearer".to_string(),
expira_em: "24 horas".to_string(),
}))
}
async fn middleware_auth(
mut req: Request,
next: Next,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let header_auth = req
.headers()
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.ok_or((
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"erro": "Token de autorizacao ausente"})),
))?;
let token = header_auth.strip_prefix("Bearer ").ok_or((
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"erro": "Formato de token invalido"})),
))?;
let claims = validar_token(token).map_err(|_| {
(
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"erro": "Token invalido ou expirado"})),
)
})?;
req.extensions_mut().insert(claims);
Ok(next.run(req).await)
}
async fn perfil(req: Request) -> impl IntoResponse {
let claims = req.extensions().get::<Claims>().unwrap();
Json(serde_json::json!({
"id": claims.sub,
"email": claims.email,
"role": claims.role,
"mensagem": "Voce esta autenticado!"
}))
}
async fn admin(
req: Request,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let claims = req.extensions().get::<Claims>().unwrap();
if claims.role != "admin" {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"erro": "Acesso negado. Requer role de administrador."})),
));
}
Ok(Json(serde_json::json!({
"mensagem": "Bem-vindo, administrador!",
"dados_sensiveis": "Informacoes restritas"
})))
}
// === Main ===
#[tokio::main]
async fn main() {
let estado: EstadoApp = Arc::new(Mutex::new(HashMap::new()));
// Rotas publicas
let rotas_publicas = Router::new()
.route("/api/registrar", post(registrar))
.route("/api/login", post(login));
// Rotas protegidas (com middleware de autenticacao)
let rotas_protegidas = Router::new()
.route("/api/perfil", get(perfil))
.route("/api/admin", get(admin))
.layer(middleware::from_fn(middleware_auth));
let app = Router::new()
.merge(rotas_publicas)
.merge(rotas_protegidas)
.with_state(estado);
let endereco = "0.0.0.0:3000";
println!("Servidor de autenticacao JWT rodando em http://{}", endereco);
let listener = tokio::net::TcpListener::bind(endereco).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
A separacao entre rotas publicas e protegidas garante que o middleware de autenticacao so seja aplicado onde necessario. O Router::merge combina os dois conjuntos de rotas.
Como Executar
Compile e inicie o servidor:
cargo run
Teste o fluxo completo com curl:
# 1. Registrar um usuario
curl -X POST http://localhost:3000/api/registrar \
-H "Content-Type: application/json" \
-d '{"nome": "Maria Silva", "email": "maria@email.com", "senha": "senha123"}'
# {"mensagem":"Usuario registrado com sucesso"}
# 2. Fazer login
curl -X POST http://localhost:3000/api/login \
-H "Content-Type: application/json" \
-d '{"email": "maria@email.com", "senha": "senha123"}'
# {"token":"eyJ0eXAiOi...","tipo":"Bearer","expira_em":"24 horas"}
# 3. Acessar rota protegida (substitua pelo token recebido)
curl http://localhost:3000/api/perfil \
-H "Authorization: Bearer eyJ0eXAiOi..."
# {"id":"...","email":"maria@email.com","role":"usuario","mensagem":"Voce esta autenticado!"}
# 4. Tentar acessar sem token
curl http://localhost:3000/api/perfil
# {"erro":"Token de autorizacao ausente"}
Desafios para Expandir
Refresh tokens – Implemente um sistema de refresh tokens com expiracao mais longa, permitindo renovar o access token sem refazer login, armazenando refresh tokens em um HashMap separado.
Logout e blacklist – Crie um endpoint
POST /api/logoutque adiciona o token a uma blacklist, impedindo seu uso mesmo antes da expiracao natural.Recuperacao de senha – Adicione um fluxo de “esqueci minha senha” que gera um token temporario de reset e um endpoint para definir nova senha.
Rate limiting no login – Limite tentativas de login por IP e por email para prevenir ataques de forca bruta, bloqueando temporariamente apos 5 tentativas falhas.
Persistencia com banco de dados – Migre o armazenamento para SQLite com
sqlx, adicionando migrations para a tabela de usuarios e indices no campo email.
Veja Tambem
- HashMap na Biblioteca Padrao – Armazenamento dos usuarios em memoria
- Result para Tratamento de Erros – Padrao usado nos handlers
- Gerando Hashes em Rust – Fundamentos de hashing
- Seguranca em Rust – Boas praticas de seguranca
- Axum vs Actix: Qual Framework Escolher? – Comparativo de frameworks web