A JWT (JSON Web Token) mára az egyik legelterjedtebb megoldás stateless autentikációra webes API-knál, és a Rust ökoszisztéma – hála az axum-nak és a jsonwebtoken crate-nek – kényelmesen kezeli ezt a mintát. Ebben a cikkben egy egyszerű, de valósághű JWT-alapú autentikációt építünk fel egy Axum 0.6-os szolgáltatásban.
Hogyan működik a JWT webes kontextusban?
A JWT lényegében egy digitálisan aláírt, base64-kódolt JSON struktúra, ami három részből áll: header, payload (claims) és aláírás. A szerver bejelentkezéskor generál egy tokent, amit a kliens minden további kérésnél elküld az Authorization: Bearer <token> fejlécben. A szerver ellenőrzi az aláírást és a lejárati időt, és ha minden rendben, kiszolgálja a kérést – anélkül, hogy session-t kellene tárolnia adatbázisban vagy memóriában.
A JWT önmagában nem titkosít semmit, csak aláír. A payload bárki számára olvasható, aki dekódolja a base64-et – szóval sose tegyünk bele jelszót vagy más érzékeny adatot.
A claims mezők közül a legfontosabbak:
sub(subject) – a felhasználó azonosítójaexp(expiration) – lejárati idő unix timestampbeniat(issued at) – kiállítás ideje
Szükséges crate-ek és a projekt alapjai
A Cargo.toml-ban a következő függőségekre lesz szükségünk:
[dependencies]
axum = { version = "0.6", features = ["headers"] }
tokio = { version = "1", features = ["full"] }
jsonwebtoken = "8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["serde"] }
tower = "0.4"
tower-http = "0.4"
A headers feature azért fontos, mert ezzel kapjuk meg az axum::TypedHeader extractort, amivel könnyen kiszedhetjük az Authorization fejlécet a kérésből.
Először definiáljuk a claims struktúrát, amit a token payload-jaként fogunk használni:
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
pub sub: String, // felhasználó azonosító
pub exp: usize, // lejárati idő (unix timestamp)
pub iat: usize, // kiállítás ideje
}
Bejelentkezési endpoint és token generálás
A token generálásához egy egyszerű függvényt írunk, amely a jsonwebtoken::encode hívást csomagolja be:
use chrono::{Duration, Utc};
use jsonwebtoken::{encode, EncodingKey, Header};
pub fn generate_token(user_id: &str, secret: &[u8]) -> Result<String, jsonwebtoken::errors::Error> {
let now = Utc::now();
let claims = Claims {
sub: user_id.to_owned(),
iat: now.timestamp() as usize,
exp: (now + Duration::minutes(15)).timestamp() as usize,
};
encode(&Header::default(), &claims, &EncodingKey::from_secret(secret))
}
A 15 perces lejárati idő szándékosan rövid – ez a jó gyakorlat: a rövid életű access token mellett egy hosszabb élettartamú refresh tokent szokás használni, amiről majd később lesz szó.
Ezután jöhet a bejelentkezési handler. Egy valós alkalmazásban itt adatbázis-lekérdezés és jelszó-hash ellenőrzés (pl. argon2 crate-tel) történne, de a példa kedvéért egyszerűsítünk:
use axum::{http::StatusCode, Json};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct LoginRequest {
pub username: String,
pub password: String,
}
#[derive(Serialize)]
pub struct LoginResponse {
pub access_token: String,
pub token_type: String,
}
pub async fn login(Json(payload): Json<LoginRequest>) -> Result<Json<LoginResponse>, StatusCode> {
if payload.username == "demo" && payload.password == "titok123" {
let secret = b"nagyon-titkos-kulcs";
let token = generate_token(&payload.username, secret)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(LoginResponse {
access_token: token,
token_type: "Bearer".to_string(),
}))
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
A secret sose legyen hardcode-olva a forráskódban éles rendszerben! Használj környezeti változót (std::env::var), és tárold biztonságosan, például egy secret manager mögött.
Védett route-ok létrehozása middleware-rel
Az Axum 0.6-ban a legelegánsabb módja a védett route-ok kialakításának, ha a Claims struktúrára implementáljuk a FromRequestParts traitet. Ezzel a handler argumentumaként simán kikérhetjük a bejelentkezett felhasználó adatait, és az extractor automatikusan elvégzi a token validálást.
use axum::{
async_trait,
extract::FromRequestParts,
headers::{authorization::Bearer, Authorization},
http::{request::Parts, StatusCode},
RequestPartsExt, TypedHeader,
};
use jsonwebtoken::{decode, DecodingKey, Validation};
#[async_trait]
impl<S> FromRequestParts<S> for Claims
where
S: Send + Sync,
{
type Rejection = StatusCode;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let TypedHeader(Authorization(bearer)) = parts
.extract::<TypedHeader<Authorization<Bearer>>>()
.await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
let secret = b"nagyon-titkos-kulcs";
let token_data = decode::<Claims>(
bearer.token(),
&DecodingKey::from_secret(secret),
&Validation::default(),
)
.map_err(|_| StatusCode::UNAUTHORIZED)?;
Ok(token_data.claims)
}
}
Ezután a védett handler már gyerekjáték:
pub async fn protected_route(claims: Claims) -> String {
format!("Szia, {}! Ez egy védett végpont.", claims.sub)
}
A router összeállítása:
use axum::{
routing::{get, post},
Router,
};
pub fn app() -> Router {
Router::new()
.route("/login", post(login))
.route("/protected", get(protected_route))
}
Ha nem akarjuk minden handler paraméterlistájában megismételni a Claims extractort, érdemes egy tower::Layer-t vagy axum::middleware::from_fn-t írni, amely egy csoportnyi route elé illesztve végzi el az ellenőrzést. Nagyobb projekteknél ez tisztábban skálázódik.
Token frissítés és hibakezelés jó gyakorlatai
A rövid élettartamú access token mellett szükségünk van egy refresh mechanizmusra, hogy a felhasználónak ne kelljen percenként újra bejelentkeznie. A bevált minta a következő:
- A bejelentkezéskor egy hosszabb élettartamú (pl. 7 napos) refresh tokent is generálunk, amit külön,
HttpOnlycookie-ban tárolunk. - Egy
/refreshvégponton, ha a refresh token még érvényes, kiállítunk egy új access tokent. - A refresh tokeneket érdemes egy adatbázisban is nyilvántartani (revocation list), hogy kijelentkezéskor vagy jelszócsere esetén érvényteleníthetők legyenek.
pub async fn refresh(claims: Claims) -> Result<Json<LoginResponse>, StatusCode> {
let secret = b"nagyon-titkos-kulcs";
let new_token = generate_token(&claims.sub, secret)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(LoginResponse {
access_token: new_token,
token_type: "Bearer".to_string(),
}))
}
A hibakezelésnél célszerű egy egységes AppError típust bevezetni, amely implementálja az IntoResponse traitet, így a middleware-ekben és handlerekben egyaránt konzisztens JSON hibaválaszokat adhatunk:
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
pub enum AppError {
InvalidToken,
ExpiredToken,
MissingCredentials,
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::InvalidToken => (StatusCode::UNAUTHORIZED, "Érvénytelen token"),
AppError::ExpiredToken => (StatusCode::UNAUTHORIZED, "Lejárt token"),
AppError::MissingCredentials => (StatusCode::BAD_REQUEST, "Hiányzó hitelesítő adatok"),
};
(status, Json(json!({ "error": message }))).into_response()
}
}
A jsonwebtoken crate decode függvénye részletes hibatípusokat ad vissza (jsonwebtoken::errors::ErrorKind), amiket a fenti AppError-ra tudunk map-elni – így a kliens pontosan megkapja, hogy lejárt vagy csak érvénytelen volt a token, ez pedig segíti a frontend hibaüzenetek pontosabb megjelenítését.
A Validation struktúrán keresztül finomhangolhatod, hogy milyen algoritmust fogadj el (Validation::new(Algorithm::HS256)), és beállíthatsz leeway-t is a szerverek közötti óraeltérés kompenzálására.
Összefoglalás
Egy JWT-alapú autentikáció Axumban valójában néhány jól elkülönített építőkockából áll: egy claims struktúrából, egy token-generáló függvényből, egy FromRequestParts alapú extractorból a validáláshoz, és egy tisztán megírt hibakezelő rétegből. Ez a stateless megközelítés kiválóan skálázódik, mert a szervernek nem kell session-állapotot tartania – cserébe neked kell odafigyelned a token élettartamára, a titkos kulcs biztonságos kezelésére és a refresh mechanizmus helyes megvalósítására. Ha ezekre figyelsz, egy gyors, biztonságos és jól karbantartható autentikációs réteget kapsz az API-d elé.