Ha webes API-t írsz Rust-ban, előbb-utóbb szembejön a kérdés: hogyan azonosítsd a felhasználót anélkül, hogy szerver oldali session-tárolót kelljen fenntartanod? A JWT (JSON Web Token) erre kínál elegáns, stateless megoldást, az Actix-web pedig kifejezetten jól illeszkedik hozzá az extractor-rendszerének köszönhetően. Ebben a cikkben végigmegyünk egy teljes, működőképes megoldáson.
Actix-web projekt alapjai és middleware bevezetése
Először hozzunk létre egy sima Actix-web 4-es projektet. A Cargo.toml-ban a következő függőségekre lesz szükségünk:
[dependencies]
actix-web = "4.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
jsonwebtoken = "8.2"
bcrypt = "0.14"
chrono = { version = "0.4", features = ["serde"] }
futures-util = "0.3"
A tokio-t nem kell explicit módon megadnod, az Actix-web futásideje maga hozza magával a szükséges tokio komponenseket — felesleges és félrevezető lenne saját magad pinelni egy verziót.
A middleware koncepció Actix-webben egyszerű: minden request végigmegy egy lánc-szerű struktúrán, mielőtt eléri a handlert. Autentikációhoz kétféle megközelítést szokás alkalmazni: vagy egy globális middleware-t írunk, ami minden kérésnél ellenőrzi a tokent, vagy — amit ebben a cikkben preferálok — egy egyedi extractort használunk, amit csak azokon a route-okon kérünk el, ahol tényleg szükség van rá. Ez sokkal rugalmasabb, mert nem kell külön kizárni a publikus végpontokat (pl. login, regisztráció) egy globális middleware alól.
JWT token generálása és validálása jsonwebtoken crate-tel
A jsonwebtoken crate a facto standard JWT-kezelésre Rust-ban. Első lépésként definiáljuk a claims struktúrát, majd írjunk egy generáló és egy validáló függvényt.
use serde::{Deserialize, Serialize};
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use chrono::{Utc, Duration};
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String, // felhasználó azonosító
pub exp: usize, // lejárati unix timestamp
}
const JWT_SECRET: &[u8] = b"nagyon-titkos-kulcs-produkcioban-envbol";
pub fn generate_token(user_id: &str) -> Result<String, jsonwebtoken::errors::Error> {
let expiration = Utc::now()
.checked_add_signed(Duration::hours(1))
.expect("érvényes időpont")
.timestamp() as usize;
let claims = Claims {
sub: user_id.to_owned(),
exp: expiration,
};
encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(JWT_SECRET),
)
}
pub fn validate_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
let data = decode::<Claims>(
token,
&DecodingKey::from_secret(JWT_SECRET),
&Validation::default(),
)?;
Ok(data.claims)
}
Éles környezetben a titkos kulcsot soha ne hardkódold a forráskódba! Használj környezeti változót (std::env::var), és a .env fájlt tedd .gitignore-ba.
Bejelentkezési endpoint és jelszó hash-elés bcrypt-tel
A jelszavakat sosem tároljuk plain textként. A bcrypt crate egyszerű API-t ad a hash-eléshez és ellenőrzéshez.
use actix_web::{post, web, HttpResponse, Responder};
use serde::Deserialize;
use bcrypt::verify;
#[derive(Deserialize)]
struct LoginRequest {
username: String,
password: String,
}
// Egyszerűsített "adatbázis" lekérdezés helyett - a valóságban ez
// egy SQL query lenne, pl. sqlx-szel.
fn find_user_hash(username: &str) -> Option<(&'static str, &'static str)> {
if username == "admin" {
// ez a hash a "titkosjelszo" bcrypt hash-e
Some(("admin", "$2b$12$KIXQ2hFZ8n0O8w1J3v5xUuY9Zc5Q6q7T8v1W2E3R4T5Y6U7I8O9P0"))
} else {
None
}
}
#[post("/login")]
#[must_use]
async fn login(payload: web::Json<LoginRequest>) -> impl Responder {
let Some((user_id, hash)) = find_user_hash(&payload.username) else {
return HttpResponse::Unauthorized().body("Hibás felhasználónév vagy jelszó");
};
match verify(&payload.password, hash) {
Ok(true) => match crate::jwt::generate_token(user_id) {
Ok(token) => HttpResponse::Ok().json(serde_json::json!({ "token": token })),
Err(_) => HttpResponse::InternalServerError().body("Token generálási hiba"),
},
Ok(false) => HttpResponse::Unauthorized().body("Hibás felhasználónév vagy jelszó"),
Err(_) => HttpResponse::InternalServerError().body("Hash ellenőrzési hiba"),
}
}
Figyeld meg, hogy a let-else szintaxist használtam a felhasználó megkeresésénél — ez a Rust 1.65 óta stabil konstrukció remekül illik az "early return, ha nincs érték" mintára, és sokkal olvashatóbb, mint egy beágyazott match.
A bcrypt::hash hívás CPU-intenzív, blokkoló művelet. Nagy terhelésű rendszerben érdemes web::block-kal külön szálra tenni, hogy ne akassza meg az async runtime-ot.
Védett route-ok kialakítása extractor segítségével
Az Actix-web FromRequest trait-jét implementálva saját extractort írhatunk, amivel a handler szignatúrájában egyszerűen kérhetjük az autentikált felhasználót.
use actix_web::{dev::Payload, Error, FromRequest, HttpRequest};
use actix_web::error::ErrorUnauthorized;
use futures_util::future::{ready, Ready};
pub struct AuthenticatedUser {
pub user_id: String,
}
impl FromRequest for AuthenticatedUser {
type Error = Error;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
let auth_header = req.headers().get("Authorization");
let Some(header_value) = auth_header else {
return ready(Err(ErrorUnauthorized("Hiányzó Authorization header")));
};
let Ok(header_str) = header_value.to_str() else {
return ready(Err(ErrorUnauthorized("Érvénytelen header formátum")));
};
let Some(token) = header_str.strip_prefix("Bearer ") else {
return ready(Err(ErrorUnauthorized("Hiányzó Bearer prefix")));
};
match crate::jwt::validate_token(token) {
Ok(claims) => ready(Ok(AuthenticatedUser { user_id: claims.sub })),
Err(_) => ready(Err(ErrorUnauthorized("Érvénytelen vagy lejárt token"))),
}
}
}
Ezután a védett handlerben csak simán paraméterként kérjük:
use actix_web::{get, HttpResponse, Responder};
#[get("/profile")]
async fn profile(user: AuthenticatedUser) -> impl Responder {
HttpResponse::Ok().json(serde_json::json!({
"message": format!("Üdv, {}!", user.user_id)
}))
}
Ha a token hiányzik, hibás vagy lejárt, az extractor már a handler meghívása előtt visszadob egy 401-es választ — a handler kódjában így egyáltalán nem kell foglalkoznunk az autentikáció részleteivel.
Hibaüzenetek és token lejárat kezelése
A jsonwebtoken::decode több különböző hibát dobhat: lejárt token (ExpiredSignature), érvénytelen aláírás, rossz formátum. Érdemes ezeket egy saját hibatípusba csoportosítani, hogy a kliens értelmes választ kapjon.
use jsonwebtoken::errors::ErrorKind;
pub fn describe_jwt_error(err: &jsonwebtoken::errors::Error) -> &'static str {
match err.kind() {
ErrorKind::ExpiredSignature => "A token lejárt, jelentkezz be újra",
ErrorKind::InvalidToken => "Érvénytelen token formátum",
ErrorKind::InvalidSignature => "A token aláírása nem egyezik",
_ => "Ismeretlen autentikációs hiba",
}
}
A lejárati időt (exp) mindig állítsd be reálisan rövidre (pl. 15-60 perc), és refresh token mechanizmussal hosszabbítsd meg a session-t, ne magát az access tokent tedd örökéletűvé.
A #[must_use] async fn hasznossága a hibák elkerülésében
Egy gyakran alábecsült, de nagyon hasznos trükk: az async fn fölé tett #[must_use] attribútum. Mivel a Result típus már eleve #[must_use], ha egy handler Result-t ad vissza és elfelejted .await-elni a futurét, a fordító nem fog figyelmeztetni — hiszen a Result csak azután létezik, hogy a future lefutott. Ha viszont magára az async fn-re teszed a #[must_use]-t, a visszaadott (opaque) Future típus is megkapja ezt a jelzést, és a fordító azonnal szól, ha valahol elfelejtetted awaitolni vagy egyáltalán felhasználni a hívás eredményét:
#[must_use]
async fn login(payload: web::Json<LoginRequest>) -> impl Responder {
// ...
}
Ha valaki véletlenül csak meghívja a függvényt awaiter nélkül (ami async kontextusban könnyen előfordulhat egy elgépelés miatt), a cargo build figyelmeztetést dob: "unused implementer of Future that must be used". Ez apró dolognak tűnik, de autentikációs logikánál — ahol egy elmaradt ellenőrzés komoly biztonsági rést jelenthet — külön hasznos védőháló.
Összefoglalás
Egy stateless JWT-alapú autentikáció Actix-webben tulajdonképpen három jól elkülöníthető részből áll: a token generálásából és validálásából (jsonwebtoken), a jelszavak biztonságos tárolásából (bcrypt), és egy egyedi FromRequest extractorból, ami a védett route-oknál automatikusan kikényszeríti az érvényes tokent. Ha ehhez hozzáadod a let-else szintaxist a kód olvashatóságáért, és a #[must_use] attribútumot a kritikus async függvényeidnél, egy tiszta, biztonságos és karbantartható autentikációs réteget kapsz — mindezt külső auth szolgáltató vagy komplex session-store nélkül.