Skip to content

الوسيط

يُولّد FORGE خط أنابيب middleware متعدد الطبقات مبني على تكامل tower مع Axum. كل طلب وارد يمر عبر سلسلة من دوال middleware تتعامل مع الاهتمامات المتقاطعة -- المصادقة، التفويض، تتبع الطلبات، و CORS -- قبل الوصول لمعالجك. يُطبّق Middleware على نطاقات مختلفة حسب كون المسار عاماً أو مُصادقاً أو لـ admin فقط.

خط أنابيب Middleware

طلب وارد


┌──────────────────────┐
│   TraceLayer         │   تسجيل طلبات/استجابات HTTP
├──────────────────────┤
│   CORS               │   التحقق من طلبات cross-origin
├──────────────────────┤
│   Request ID         │   تعيين ترويسة X-Request-Id (UUID)
├──────────────────────┤
│   Auth Middleware     │   استخراج والتحقق من JWT token
├──────────────────────┤
│   RBAC Middleware     │   التحقق من الصلاحيات والأدوار
├──────────────────────┤
│   Handler            │   دالة معالج المسار الخاصة بك
└──────────────────────┘

ليس كل middleware يعمل على كل طلب. خط الأنابيب مُعدّ بحيث تتجاوز المسارات العامة المصادقة و RBAC، بينما مسارات admin تُنفّذ كليهما.

Auth Middleware

يستخرج auth middleware الـ JWT token من ترويسة Authorization، يُتحقق منه، ويحقن struct الـ AuthUser في امتدادات الطلب. المعالجات و middleware اللاحقة يمكنها الوصول للمستخدم المُصادق بدون إعادة تحليل الـ token.

كيف يعمل

rust
use axum::{extract::{Request, State}, middleware::Next, response::Response};
use crate::error::AppError;
use crate::utils::jwt::{self, TokenType};
use crate::AppState;

pub async fn auth_middleware(
    State(state): State<AppState>,
    mut request: Request,
    next: Next,
) -> Result<Response, AppError> {
    // 1. Extract Bearer token from Authorization header
    let token = extract_token(&request)?;

    // 2. Validate token signature and expiration
    let token_data = jwt::validate_token(&token, &state.config.auth)?;

    // 3. Ensure it's an access token (not refresh token)
    if token_data.claims.token_type != TokenType::Access {
        return Err(AppError::InvalidToken);
    }

    // 4. Build AuthUser from validated claims
    let auth_user = AuthUser {
        user_id: token_data.claims.sub,
        email: token_data.claims.email,
        token_jti: token_data.claims.jti,
    };

    // 5. Store in request extensions for later access
    request.extensions_mut().insert(auth_user);

    // 6. Continue to next middleware or handler
    Ok(next.run(request).await)
}

بنية AuthUser

struct الـ AuthUser هو كائن الهوية المتاح لكل معالج مُصادق:

rust
#[derive(Debug, Clone)]
pub struct AuthUser {
    /// User ID extracted from JWT subject claim
    pub user_id: Uuid,
    /// User email from token
    pub email: String,
    /// Token ID for blacklist support
    pub token_jti: Uuid,
}

استخدام AuthUser في المعالجات

AuthUser ينفّذ trait الـ FromRequestParts في Axum، لذا يمكنك استخدامه كمعامل للمعالج مباشرة:

rust
use crate::middleware::AuthUser;

pub async fn get_profile(
    auth_user: AuthUser,
    State(state): State<AppState>,
) -> Result<Json<ApiResponse<ProfileResponse>>, AppError> {
    let profile = ProfileService::get_by_id(&state.db, auth_user.user_id).await?;
    Ok(Json(ApiResponse::success(profile)))
}

إذا لم يكن للطلب AuthUser في امتداداته (لأن auth middleware لم يعمل أو كان الـ token غير صالح)، يُرجع المُستخرج خطأ 401 Unauthorized تلقائياً.

المصادقة الاختيارية

بعض المسارات تتصرف مختلفاً للمستخدمين المُصادقين مقابل المجهولين. استخدم optional_auth_middleware لهذه الحالات:

rust
pub async fn optional_auth_middleware(
    State(state): State<AppState>,
    mut request: Request,
    next: Next,
) -> Response {
    // Try to validate token, but continue regardless
    if let Ok(token) = extract_token(&request) {
        if let Ok(token_data) = jwt::validate_token(&token, &state.config.auth) {
            if token_data.claims.token_type == TokenType::Access {
                let auth_user = AuthUser { /* ... */ };
                request.extensions_mut().insert(auth_user);
            }
        }
    }

    // Always continue -- never return error
    next.run(request).await
}

نصيحة

استخدم optional_auth_middleware لنقاط النهاية العامة مثل قوائم القوائم حيث قد يرى المستخدمون المُصادقون عناصر إضافية (مثل عناصر القائمة مع visibility: "auth").

RBAC Middleware

يتحقق RBAC middleware مما إذا كان للمستخدم المُصادق الصلاحيات أو الأدوار المطلوبة قبل السماح بالوصول. يوفر FORGE ست دوال مصنع middleware تغطي كل نمط تفويض شائع.

فحص الصلاحية

النمط الأكثر شيوعاً -- التحقق من أن للمستخدم صلاحية واحدة:

rust
use axum::{Router, routing::get, middleware};
use crate::middleware::require_permission;

fn user_routes(state: AppState) -> Router<AppState> {
    Router::new()
        .route(
            "/",
            get(list_users)
                .layer(middleware::from_fn_with_state(
                    state.clone(),
                    require_permission("users.view"),
                )),
        )
        .route(
            "/",
            post(create_user)
                .layer(middleware::from_fn_with_state(
                    state.clone(),
                    require_permission("users.create"),
                )),
        )
}

جميع مصانع Middleware

الدالةالفحصحالة الاستخدام
require_permission("perm")للمستخدم صلاحية محددةمعظم المسارات
require_any_permission(&["a", "b"])للمستخدم واحدة على الأقلوصول عرض أو تعديل
require_all_permissions(&["a", "b"])للمستخدم كل الصلاحياتوصول مُركّب
require_role("role")للمستخدم دور محددميزات محددة بدور
require_any_role(&["a", "b"])للمستخدم دور واحد على الأقلطبقات admin متعددة
require_all_roles(&["a", "b"])للمستخدم كل الأدوارفحوصات أدوار مُركّبة

مثال: صلاحيات متعددة

rust
use crate::middleware::require_any_permission;

// User needs contents.edit or contents.create
.route(
    "/contents/draft",
    post(save_draft)
        .layer(middleware::from_fn_with_state(
            state.clone(),
            require_any_permission(&["contents.edit", "contents.create"]),
        )),
)

تجاوز Super Admin

جميع RBAC middleware تتجاوز تلقائياً فحوصات الصلاحيات لـ super admins. المستخدم يُعتبر super admin إذا كان لديه دور super_admin أو صلاحية wildcard *:

rust
impl UserPermissions {
    pub fn is_super_admin(&self) -> bool {
        self.roles.contains("super_admin") || self.permissions.contains("*")
    }
}

تحذير

تجاوز super admin يحدث بصمت في middleware. هذا يعني أن super admins يمكنهم الوصول لكل مسار بغض النظر عن إعدادات الصلاحيات الفردية. استخدم دور super_admin باعتدال وفقط للمديرين على مستوى الجذر.

تخزين الصلاحيات مؤقتاً

تحميل الصلاحيات من قاعدة البيانات في كل طلب سيكون مكلفاً. يتضمن FORGE كاش صلاحيات في الذاكرة مع TTL قابل للتعديل (الافتراضي: 5 دقائق):

rust
use crate::middleware::PermissionCache;
use std::time::Duration;

// Created during application startup
let permission_cache = PermissionCache::new(Duration::from_secs(300));

الكاش يُخزّن في AppState ويُستخدم تلقائياً من جميع RBAC middleware. العمليات الرئيسية:

rust
// Cache is checked automatically before hitting database
let perms = get_user_permissions(&state.db, &state.permission_cache, user_id).await?;

// Invalidate specific user's cache (e.g., after role change)
state.permission_cache.invalidate(user_id).await;

// Invalidate all cached permissions (e.g., after running permission seeder)
state.permission_cache.invalidate_all().await;

// Clean up expired entries (call periodically)
state.permission_cache.cleanup_expired().await;

نصيحة

عندما تُحدّث أدوار مستخدم عبر admin API، أبطل كاش صلاحياته ليأخذ التغيير مفعوله فوراً بدلاً من انتظار انتهاء TTL.

مُستخرج UserPermissions

لفحوصات صلاحيات دقيقة داخل معالج (أبعد مما يوفره middleware مستوى المسار)، استخدم UserPermissionsExtractor:

rust
use crate::middleware::UserPermissionsExtractor;

pub async fn complex_handler(
    auth_user: AuthUser,
    perms: UserPermissionsExtractor,
    State(state): State<AppState>,
) -> Result<impl IntoResponse, AppError> {
    // Check specific permission in handler logic
    if perms.has_permission("users.create") {
        // Allow bulk creation
    }

    // Check role
    if perms.has_role("editor") {
        // Show editor-specific options
    }

    // Check multiple permissions
    if perms.has_all_permissions(&["contents.edit", "media.upload"]) {
        // Allow content with media
    }

    Ok(Json(ApiResponse::success(result)))
}

API Key Middleware

يُولّد FORGE أيضاً مصادقة بمفتاح API للوصول البرمجي. مفاتيح API تُرسل عبر ترويسة X-API-Key وتتبع الصيغة forge_sk_...:

rust
pub async fn api_key_middleware(
    State(state): State<AppState>,
    mut request: Request,
    next: Next,
) -> Result<Response, AppError> {
    let api_key = extract_api_key(&request)?;  // From X-API-Key header
    let key_row = service.validate_key(&api_key).await?;

    // Track usage asynchronously (non-blocking)
    tokio::spawn(async move {
        service.update_usage(key_id, client_ip).await;
    });

    request.extensions_mut().insert(AuthApiKey::from(key_row));
    Ok(next.run(request).await)
}

مفاتيح API لها نظام صلاحيات خاص بها، منفصل عن أدوار المستخدمين:

rust
use crate::middleware::require_api_key_permission;

// Protect external endpoint with API key + permission
.route(
    "/external/users",
    get(list_users_external)
        .layer(middleware::from_fn(require_api_key_permission("users.view")))
        .layer(middleware::from_fn_with_state(state.clone(), api_key_middleware)),
)

تحذير

API key middleware و auth middleware مستقلان. المسار يجب أن يستخدم واحداً أو الآخر، ليس كليهما. للمسارات التي تقبل أي طريقة مصادقة، استخدم optional_auth_middleware مع optional_api_key_middleware.

تتبع الطلبات

يستخدم FORGE TraceLayer من Tower لتسجيل طلبات واستجابات HTTP. يُطبّق عالمياً في إعداد الـ router:

rust
use tower_http::trace::TraceLayer;

fn create_router(state: AppState) -> Router {
    Router::new()
        .nest("/api/v1", routes::api_routes(state.clone()))
        .route("/health", get(health_check))
        .layer(TraceLayer::new_for_http())  // Logs every request
        .with_state(state)
}

طبقة التتبع تُسجّل طريقة الطلب والمسار ورمز الحالة والتأخير باستخدام crate الـ tracing. عدّل مستوى التسجيل عبر متغير البيئة RUST_LOG:

bash
# Show debug logs for API and tower_http debug logs
RUST_LOG=myapp_api=debug,tower_http=debug cargo run

# Production: warnings and errors only
RUST_LOG=myapp_api=warn cargo run

إعداد CORS

CORS يُعالج على مستوى reverse proxy (Caddy) في نشر FORGE القياسي. ومع ذلك، إذا شغّلت API بدون proxy، يمكنك إضافة CORS layer من tower_http:

rust
use tower_http::cors::{CorsLayer, Any};
use axum::http::{HeaderName, Method};

fn create_router(state: AppState) -> Router {
    let cors = CorsLayer::new()
        .allow_origin(Any)
        .allow_methods([
            Method::GET,
            Method::POST,
            Method::PUT,
            Method::PATCH,
            Method::DELETE,
        ])
        .allow_headers([
            HeaderName::from_static("content-type"),
            HeaderName::from_static("authorization"),
            HeaderName::from_static("x-api-key"),
        ]);

    Router::new()
        .nest("/api/v1", routes::api_routes(state.clone()))
        .layer(cors)
        .with_state(state)
}

نصيحة

في نشر FORGE الافتراضي مع Caddy، ترويسات CORS تُعيّن على مستوى الـ proxy. إضافة CorsLayer لخادم Axum ستُسبب ترويسات مكررة. أضفها فقط إذا كنت تُشغّل API مباشرة بدون reverse proxy.

تطبيق Middleware على المسارات

يوفر Axum طريقتين لتطبيق middleware، و FORGE يستخدم كليهما حسب النطاق:

Middleware على مستوى المجموعة

يُطبّق على مجموعة كاملة من المسارات باستخدام .layer():

rust
pub fn admin_routes(state: AppState) -> Router<AppState> {
    Router::new()
        .nest("/users", user_routes(state.clone()))
        .nest("/roles", role_routes(state.clone()))
        .nest("/contents", content_routes(state.clone()))
        // Auth middleware applies to all admin routes
        .layer(middleware::from_fn_with_state(state, auth_middleware))
}

Middleware على مستوى المسار

يُطبّق على مسارات فردية باستخدام .layer() على المسار:

rust
fn content_routes(state: AppState) -> Router<AppState> {
    Router::new()
        .route(
            "/",
            get(list_contents)
                .layer(middleware::from_fn_with_state(
                    state.clone(),
                    require_permission("contents.view"),
                )),
        )
        .route(
            "/",
            post(create_content)
                .layer(middleware::from_fn_with_state(
                    state.clone(),
                    require_permission("contents.create"),
                )),
        )
}

تحذير

Middleware في Axum يُنفّذ بـ ترتيب عكسي لكيفية إضافته. آخر استدعاء .layer() يعمل أولاً. هذا يعني auth middleware (المُضاف أخيراً لمجموعة admin) يعمل قبل RBAC middleware (المُضاف لكل مسار)، وهو الترتيب الصحيح -- يجب المصادقة قبل فحص الصلاحيات.

Middleware مخصص

لإضافة middleware خاص بك، اتبع هذا النمط:

rust
use axum::{extract::Request, middleware::Next, response::Response};

pub async fn my_custom_middleware(
    // Optional: inject application state
    State(state): State<AppState>,
    // The incoming request
    request: Request,
    // The next middleware or handler in the chain
    next: Next,
) -> Result<Response, AppError> {
    // Before handler: inspect/modify request
    let start = std::time::Instant::now();

    // Pass request to next middleware
    let response = next.run(request).await;

    // After handler: inspect/modify response
    let duration = start.elapsed();
    tracing::info!("Request took {:?}", duration);

    Ok(response)
}

سجّله في الـ router:

rust
use axum::middleware;

let app = Router::new()
    .route("/api/data", get(handler))
    .layer(middleware::from_fn_with_state(state, my_custom_middleware));

انظر أيضاً

Released under the MIT License.