Введение в OAuth 2.0 и JWT на Rust
Современные веб-приложения требуют надёжной и безопасной аутентификации. Два ключевых стандарта в этой области — OAuth 2.0 (протокол авторизации) и JWT (JSON Web Token, формат токена доступа). Rust, благодаря своей производительности и безопасности памяти, становится отличным выбором для построения API, где критически важны скорость и надёжность.
В этой статье мы разберём, как реализовать OAuth 2.0 и JWT авторизацию на Rust с использованием популярных крейтов. Вы узнаете, как настроить сервер, выпускать и проверять JWT-токены, а также интегрировать OAuth 2.0-поток. Материал рассчитан на разработчиков, знакомых с основами Rust и веб-фреймворков.
Основные концепции OAuth 2.0 и JWT
Что такое OAuth 2.0?
OAuth 2.0 — это протокол делегированного доступа. Он позволяет приложению получать ограниченный доступ к ресурсам пользователя без передачи его учётных данных (логина и пароля). Основные участники:
- Resource Owner — пользователь, владеющий данными.
- Client — наше приложение (например, веб-сервис на Rust).
- Authorization Server — сервер, выдающий токены после успешной аутентификации.
- Resource Server — API, предоставляющий защищённые данные.
Что такое JWT?
JWT — это компактный, URL-безопасный формат токена, состоящий из трёх частей: заголовка (header), полезной нагрузки (payload) и подписи (signature). Подпись гарантирует целостность данных. JWT часто используется как токен доступа (access token) в OAuth 2.0, так как он самодостаточен: сервер может проверить его без обращения к базе данных.
Практическая реализация на Rust
Для реализации нам понадобятся следующие крейты:
actix-web— веб-фреймворк.serdeиserde_json— для сериализации/десериализации.jsonwebtoken— для работы с JWT.dotenv— для управления конфигурацией.chrono— для работы с датами и временем.
Шаг 1: Настройка проекта и структуры
Создайте новый проект Cargo и добавьте зависимости в Cargo.toml:
[dependencies]
actix-web = "4"
actix-rt = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
jsonwebtoken = "9"
dotenv = "0.15"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4"] }
Создайте файл .env для хранения секретного ключа:
JWT_SECRET=my_super_secret_key_12345
Шаг 2: Создание модели данных и JWT-функций
Определим структуры для JWT и реализуем функции создания и проверки токенов.
use chrono::{Utc, Duration};
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use serde::{Deserialize, Serialize};
use std::env;
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String, // идентификатор пользователя
pub exp: usize, // время истечения (timestamp)
pub iat: usize, // время выпуска (timestamp)
}
pub fn create_jwt(user_id: &str) -> Result<String, jsonwebtoken::errors::Error> {
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let now = Utc::now();
let expiration = now + Duration::hours(24); // токен живёт 24 часа
let claims = Claims {
sub: user_id.to_owned(),
exp: expiration.timestamp() as usize,
iat: now.timestamp() as usize,
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(secret.as_bytes()),
)?;
Ok(token)
}
pub fn validate_jwt(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(secret.as_bytes()),
&Validation::default(),
)?;
Ok(token_data.claims)
}
Шаг 3: Реализация эндпоинтов OAuth 2.0 (симуляция)
В реальном OAuth 2.0 клиент перенаправляется на сервер авторизации. Мы упростим процесс и создадим эндпоинт для входа, который возвращает JWT.
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use serde::Deserialize;
#[derive(Deserialize)]
pub struct LoginRequest {
pub username: String,
pub password: String,
}
pub async fn login(body: web::Json<LoginRequest>) -> impl Responder {
// В реальном приложении здесь проверка пароля в БД
if body.username == "admin" && body.password == "password" {
match create_jwt(&body.username) {
Ok(token) => HttpResponse::Ok().json(serde_json::json!({"access_token": token, "token_type": "Bearer"})),
Err(_) => HttpResponse::InternalServerError().json(serde_json::json!({"error": "Token creation failed"})),
}
} else {
HttpResponse::Unauthorized().json(serde_json::json!({"error": "Invalid credentials"}))
}
}
pub async fn protected_resource(req: HttpRequest) -> impl Responder {
// Извлекаем токен из заголовка Authorization
let auth_header = req.headers().get("Authorization");
if let Some(header_value) = auth_header {
let token_str = header_value.to_str().unwrap_or("");
if token_str.starts_with("Bearer ") {
let token = &token_str[7..];
match validate_jwt(token) {
Ok(claims) => HttpResponse::Ok().json(serde_json::json!({
"message": "Access granted",
"user_id": claims.sub,
"exp": claims.exp
})),
Err(_) => HttpResponse::Unauthorized().json(serde_json::json!({"error": "Invalid token"})),
}
} else {
HttpResponse::BadRequest().json(serde_json::json!({"error": "Invalid Authorization header format"}))
}
} else {
HttpResponse::Una