Введение
Современная веб-разработка немыслима без безопасной аутентификации и авторизации пользователей. Два самых популярных стандарта — OAuth 2.0 и JWT (JSON Web Tokens) — стали де-факто индустриальными решениями. Если вы пишете на Rust, то вам повезло: этот язык предлагает не только высочайшую производительность, но и мощные библиотеки для реализации этих протоколов.
В этой статье мы разберем, что такое OAuth 2.0 и JWT, как они работают вместе, и напишем простой, но рабочий пример авторизации на Rust с использованием фреймворка actix-web и крейта jsonwebtoken. Статья ориентирована на начинающих, поэтому мы пройдем шаг за шагом — от теории до кода.
Что такое OAuth 2.0 и JWT?
Прежде чем писать код, важно понять концепции. OAuth 2.0 — это протокол авторизации, который позволяет приложениям получать ограниченный доступ к ресурсам пользователя без передачи ему пароля. Он используется, когда вы входите на сайт через Google, GitHub или VK.
JWT — это формат токена (JSON Object Signing and Encryption), который содержит утверждения (claims) о пользователе. Токен подписывается сервером, поэтому клиент не может его подделать. В контексте OAuth 2.0 JWT часто используется как access token (токен доступа).
Типичный сценарий: пользователь вводит логин и пароль, сервер проверяет их, генерирует JWT токен и возвращает его клиенту. Клиент при каждом запросе отправляет этот токен в заголовке Authorization: Bearer <token>. Сервер проверяет подпись и извлекает данные пользователя.
Настройка проекта на Rust
Создадим новый проект:
cargo new rust_oauth_jwt
cd rust_oauth_jwt
Добавим необходимые зависимости в Cargo.toml:
[dependencies]
actix-web = "4"
actix-rt = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
jsonwebtoken = "9"
chrono = { version = "0.4", features = ["serde"] }
dotenv = "0.15"
env_logger = "0.11"
log = "0.4"
Создадим файл .env в корне проекта для хранения секретного ключа:
JWT_SECRET=my_super_secret_key_change_in_production
JWT_EXPIRATION_HOURS=24
Загрузим переменные окружения в main.rs:
use dotenv::dotenv;
use std::env;
fn main() {
dotenv().ok();
env_logger::init();
// ... остальной код
}
Создание структуры пользователя и JWT токена
Сначала определим модель пользователя и структуру claims для JWT. Создадим файл models.rs:
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct User {
pub id: i32,
pub username: String,
pub password_hash: String, // В реальном проекте хранить хеш, не пароль!
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String, // subject (user id)
pub username: String,
pub exp: usize, // expiration timestamp
}
Теперь создадим утилиту для работы с JWT в jwt_utils.rs:
use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use crate::models::Claims;
pub fn create_jwt(user_id: &str, username: &str) -> Result<String, jsonwebtoken::errors::Error> {
let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let expiration_hours: i64 = std::env::var("JWT_EXPIRATION_HOURS")
.unwrap_or_else(|_| "24".to_string())
.parse()
.unwrap();
let expiration = Utc::now()
.checked_add_signed(Duration::hours(expiration_hours))
.expect("valid timestamp")
.timestamp() as usize;
let claims = Claims {
sub: user_id.to_owned(),
username: username.to_owned(),
exp: expiration,
};
encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(secret.as_ref()),
)
}
pub fn verify_jwt(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(secret.as_ref()),
&Validation::default(),
)?;
Ok(token_data.claims)
}
Реализация конечных точек API
Создадим файл handlers.rs с двумя эндпоинтами: регистрация/логин (упрощенная версия) и защищенный маршрут.
use actix_web::{web, HttpRequest, HttpResponse};
use crate::jwt_utils::{create_jwt, verify_jwt};
use crate::models::User;
// Упрощенный логин (в реальности проверяйте пароль через bcrypt)
pub async fn login() -> HttpResponse {
// Имитация поиска пользователя в БД
let user = User {
id: 1,
username: "demo_user".to_string(),
password_hash: "".to_string(),
};
match create_jwt(&user.id.to_string(), &user.username) {
Ok(token) => HttpResponse::Ok().json(serde_json::json!({
"access_token": token,
"token_type": "Bearer"
})),
Err(_) => HttpResponse::InternalServerError().json("Failed to create token"),
}
}
// Защищенный эндпоинт
pub async fn protected_profile(req: HttpRequest) -> HttpResponse {
// Извлекаем токен из заголовка Authorization
let auth_header = req.headers().get("Authorization");
if auth_header.is_none() {
return HttpResponse::Unauthorized().json("Missing authorization header");
}
let auth_str = auth_header.unwrap().to_str().unwrap_or("");
if !auth_str.starts_with("Bearer ") {
return HttpResponse::Unauthorized().json("Invalid authorization format");
}
let token = &auth_str[7..]; // Убираем "Bearer "
match verify_jwt(token) {
Ok(claims) => HttpResponse::Ok().json(serde_json::json!({
"user_id": claims.sub,
"username": claims.username,
"message": "Доступ разрешен!"
})),
Err(_) => HttpResponse::Unauthorized().json("Invalid or expired token"),