OAuth 2.0 и JWT авторизация на Rust: пошаговое руководство для начинающих

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение

Современная веб-разработка немыслима без безопасной аутентификации и авторизации пользователей. Два самых популярных стандарта — 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"),                

Блоги

Рекомендаций по книгам