استفاده از Middleware برای اعتبارسنجی درخواست‌ها در ASP.NET Core

در دنیای توسعه وب مدرن، به ویژه در ساخت API‌ها و سرویس‌های پشتیبان (Back-end)، برنامه‌ها به طور مداوم در معرض انواع مختلفی از درخواست‌ها قرار دارند. این درخواست‌ها می‌توانند از منابع معتبر (مانند یک اپلیکیشن موبایل یا وب‌سایت رسمی) یا از منابع مخرب (مانند اسکریپت‌های یک هکر) نشأت بگیرند. اعتبارسنجی درخواست (Request Validation) فرآیندی است که طی آن اطمینان حاصل می‌کنیم که داده‌های ورودی به برنامه، قبل از هرگونه پردازش، با فرمت، ساختار و قوانین مورد انتظار ما مطابقت دارند.
کینگتو - آموزش برنامه نویسی تخصصصی - دات نت - سی شارپ - بانک اطلاعاتی و امنیت

استفاده از Middleware برای اعتبارسنجی درخواست‌ها در ASP.NET Core

45 بازدید 0 نظر ۱۴۰۴/۰۷/۲۶

اهمیت این فرآیند را نمی‌توان نادیده گرفت:

  1. امنیت (Security): این اولین و مهم‌ترین دلیل است. اعتبارسنجی نادرست یا عدم اعتبارسنجی می‌تواند درب‌های برنامه را به روی حملاتی مانند SQL Injection، Cross-Site Scripting (XSS)، Mass Assignment و Parameter Tampering باز کند. با اعتبارسنجی ورودی، ما اطمینان حاصل می‌کنیم که داده‌های مخرب هرگز به لایه‌های پردازش داده یا پایگاه داده نمی‌رسند.

  2. یکپارچگی داده (Data Integrity): اصل "Garbage In, Garbage Out" (ورودی بی‌ارزش، خروجی بی‌ارزش) در اینجا کاملاً صادق است. اگر داده‌های نامعتبر (مثلاً یک ایمیل بدون فرمت صحیح یا یک سن منفی) اجازه ورود به سیستم را پیدا کنند، پایگاه داده شما به سرعت با داده‌های نادرست و غیرقابل استفاده پر می‌شود که پاکسازی آن بسیار پرهزینه خواهد بود.

  3. تجربه کاربری (User Experience): اگرچه اعتبارسنجی در بک‌اند اتفاق می‌افتد، اما ارسال پاسخ‌های واضح و مشخص در مورد داده‌های نادرست (مانند 400 Bad Request به همراه جزئیات خطا) به فرانت‌اند کمک می‌کند تا پیام‌های مناسبی را به کاربر نهایی نمایش دهد.

  4. کاهش بار پردازشی (Reduced Processing Load): رد کردن سریع درخواست‌های نامعتبر از هدر رفتن منابع سیستمی (CPU، حافظه، اتصالات پایگاه داده) برای پردازش درخواستی که در نهایت با شکست مواجه می‌شود، جلوگیری می‌کند.

در اکوسیستم ASP.NET Core، روش‌های مختلفی برای اعتبارسنجی وجود دارد، از جمله Data Annotations روی مدل‌ها، کتابخانه‌های قدرتمندی مانند FluentValidation و استفاده از Action Filters. با این حال، یکی از قدرتمندترین و انعطاف‌پذیرترین ابزارها برای این کار، استفاده از Middleware است.

 

درک خط لوله (Pipeline) و Middleware در ASP.NET Core

برای درک اینکه چرا Middleware برای اعتبارسنجی مناسب است، ابتدا باید مفهوم خط لوله درخواست (Request Pipeline) در ASP.NET Core را درک کنیم.

تصور کنید هر درخواست HTTP که به سرور شما می‌رسد، باید از یک خط مونتاژ عبور کند. این خط مونتاژ، همان Pipeline است. هر ایستگاه در این خط مونتاژ، یک Middleware (میان‌افزار) نامیده می‌شود.

هر Middleware وظیفه‌ای مشخص دارد:

  • برخی ممکن است درخواست را بررسی کنند (مانند Middleware احراز هویت).

  • برخی ممکن است پاسخ را تغییر دهند (مانند Middleware فشرده‌سازی).

  • برخی ممکن است خطاها را مدیریت کنند (مانند Middleware مدیریت استثنا).

هر Middleware تصمیم می‌گیرد که آیا درخواست را به ایستگاه بعدی (Middleware بعدی) در خط لوله ارسال کند یا خیر. این کار از طریق یک RequestDelegate به نام next انجام می‌شود. اگر یک Middleware تصمیم بگیرد که next را فراخوانی نکند، به اصطلاح خط لوله را "Short-Circuit" (اتصال کوتاه) کرده است. این بدان معناست که پردازش درخواست در همان نقطه متوقف شده و یک پاسخ مستقیماً به کلاینت بازگردانده می‌شود.

این قابلیت "اتصال کوتاه" دقیقاً همان چیزی است که Middleware را به ابزاری ایده‌آل برای اعتبارسنجی تبدیل می‌کند.

 

چرا از Middleware برای اعتبارسنجی استفاده کنیم؟

همانطور که گفته شد، روش‌های دیگری مانند Action Filters یا اعتبارسنجی خودکار مدل در کنترلرهای [ApiController] وجود دارد. پس چرا باید به سراغ Middleware برویم؟

پاسخ در "زمان" و "مکان" اجرای اعتبارسنجی نهفته است.

  1. اجرای بسیار زود (Early Execution): Middleware‌ها معمولاً در ابتدای خط لوله اجرا می‌شوند. شما می‌توانید یک Middleware اعتبارسنجی را طوری پیکربندی کنید که قبل از هر چیز دیگری اجرا شود—قبل از Routing (مسیریابی)، قبل از Authentication (احراز هویت)، و قطعاً قبل از Model Binding (اتصال مدل) و اجرای منطق کنترلر.

  2. استراتژی "Fail Fast" (شکست سریع): با اجرای زود هنگام، شما می‌توانید درخواست‌های آشکارا نامعتبر را در همان نطفه خفه کنید. چرا باید منابع سیستم را صرف مسیریابی یک درخواست، بررسی توکن احراز هویت آن، و تلاش برای اتصال مدل آن کنیم، در حالی که می‌توانیم در 1 میلی‌ثانیه اول تشخیص دهیم که درخواست فاقد یک Header حیاتی (مانند X-Api-Key) است؟

  3. اعتبارسنجی سراسری (Global Validation): Middleware‌ها ذاتاً سراسری هستند. آن‌ها روی هر درخواستی که وارد سیستم می‌شود اعمال می‌گردند. این برای قوانینی که باید در کل برنامه صادق باشند، عالی است.

  4. نگرانی‌های سطح پایین (Low-Level Concerns): Middleware برای اعتبارسنجی‌هایی که به منطق تجاری (Business Logic) خاص یک کنترلر وابسته نیستند، ایده‌آل است. این‌ها معمولاً "نگرانی‌های زیرساختی" یا "Cross-Cutting Concerns" نامیده می‌شوند.

چه نوع اعتبارسنجی‌هایی برای Middleware مناسب هستند؟

  • بررسی Header‌های ضروری: مانند Content-Type (آیا کلاینت JSON ارسال می‌کند؟)، Accept (آیا کلاینت JSON می‌پذیرد؟)، یا Header‌های سفارشی مانند X-Api-Key یا X-Tenant-Id.

  • بررسی Query String‌های الزامی: اطمینان از وجود پارامترهای خاص در URL.

  • اعتبارسنجی‌های امنیتی اولیه: مانند بررسی فرمت IP یا مسدود کردن User-Agent‌های خاص.

  • بررسی حجم درخواست (Request Size): جلوگیری از حملات Denial of Service با مسدود کردن درخواست‌های با بدنه (Body) بسیار بزرگ، قبل از اینکه به طور کامل بارگذاری شوند.

 

ساخت یک Middleware اعتبارسنجی سفارشی (Custom Validation Middleware)

بیایید یک سناریوی عملی را پیاده‌سازی کنیم. فرض کنید می‌خواهیم یک Middleware بسازیم که دو قانون را اجرا کند:

  1. هر درخواست POST یا PUT باید دارای Header Content-Type با مقدار application/json باشد.

  2. هر درخواست به API ما باید دارای یک Header سفارشی به نام X-Api-Key با یک مقدار ثابت (مثلاً MySecretKey123) باشد.

 

گام اول: ایجاد کلاس Middleware

ابتدا، کلاس Middleware خود را ایجاد می‌کنیم. یک Middleware در ساده‌ترین حالت، کلاسی است که یک سازنده (Constructor) با پارامتر RequestDelegate و یک متد InvokeAsync با پارامتر HttpContext دارد.

public class ApiKeyValidationMiddleware
{
    private readonly RequestDelegate _next;
    private const string ApiKeyHeaderName = "X-Api-Key";
    private const string ApiKeyValue = "MySecretKey123"; // این باید از Configuration خوانده شود

    public ApiKeyValidationMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 1. اعتبارسنجی API Key
        if (!context.Request.Headers.TryGetValue(ApiKeyHeaderName, out var extractedApiKey))
        {
            // کلید API وجود ندارد
            await HandleErrorAsync(context, 401, "API Key was not provided."); // 401 Unauthorized
            return; // اتصال کوتاه (Short-circuit)
        }

        if (!ApiKeyValue.Equals(extractedApiKey))
        {
            // کلید API نامعتبر است
            await HandleErrorAsync(context, 401, "Invalid API Key.");
            return; // اتصال کوتاه (Short-circuit)
        }

        // 2. اعتبارسنجی Content-Type برای متدهای POST/PUT
        var method = context.Request.Method;
        if ((method == HttpMethods.Post || method == HttpMethods.Put) && 
            context.Request.ContentType != "application/json")
        {
            await HandleErrorAsync(context, 415, "Unsupported Media Type. Please use application/json."); // 415 Unsupported Media Type
            return; // اتصال کوتاه (Short-circuit)
        }

        // اگر همه چیز خوب بود، درخواست را به Middleware بعدی ارسال کن
        await _next(context);
    }

    private static Task HandleErrorAsync(HttpContext context, int statusCode, string message)
    {
        context.Response.StatusCode = statusCode;
        context.Response.ContentType = "application/json";
        var errorResponse = new { Error = message };
        return context.Response.WriteAsJsonAsync(errorResponse);
    }
}

 

 

گام دوم: ثبت Middleware

حالا باید به ASP.NET Core بگوییم که از این Middleware در خط لوله خود استفاده کند. این کار در فایل Program.cs (یا Startup.cs در نسخه‌های قدیمی‌تر) انجام می‌شود.

نکته بسیار مهم: ترتیب ثبت Middleware‌ها حیاتی است. این Middleware باید قبل از Middleware‌هایی مانند app.UseAuthentication()، app.UseAuthorization() و app.MapControllers() ثبت شود تا بتواند درخواست‌های نامعتبر را قبل از رسیدن به آن‌ها رد کند.

var builder = WebApplication.CreateBuilder(args);

// ... سرویس‌های دیگر

var app = builder.Build();

// Middleware مدیریت استثنا باید همیشه در ابتدا باشد
app.UseExceptionHandler("/Error"); 

// Middleware اعتبارسنجی سفارشی ما
app.UseMiddleware();

// ... سایر Middleware‌ها
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

اکنون، هر درخواستی که به سرور ما برسد، ابتدا از فیلتر ApiKeyValidationMiddleware عبور می‌کند. اگر API Key معتبر نباشد یا Content-Type در یک درخواست POST نادرست باشد، بلافاصله یک پاسخ خطا (401 یا 415) برگردانده می‌شود و درخواست هرگز به کنترلرها نمی‌رسد.

 

چالش پیشرفته: اعتبارسنجی بدنه درخواست (Request Body)

اعتبارسنجی Header‌ها و Query String‌ها آسان است. اما اگر بخواهیم بدنه (Body) درخواست را در Middleware اعتبارسنجی کنیم، با چالش بزرگی روبرو می‌شویم: Stream (جریان) بدنه درخواست، فقط یک بار خواندنی (Forward-Only) است.

این بدان معناست که اگر Middleware شما بدنه درخواست را بخواند تا آن را (مثلاً) Deserialize کرده و اعتبارسنجی کند، آن Stream به انتها می‌رسد. سپس، وقتی درخواست به کنترلر می‌رسد و Model Binding تلاش می‌کند تا بدنه را دوباره بخواند (مثلاً برای پارامتر [FromBody])، با یک Stream خالی مواجه شده و شکست می‌خورد.

راه حل: فعال کردن بافرینگ (Buffering).

ما می‌توانیم به ASP.NET Core بگوییم که بدنه درخواست را در حافظه (یا در صورت بزرگ بودن، در دیسک) بافر کند. این به ما امکان می‌دهد Stream را بخوانیم و سپس آن را به ابتدا "Rewind" (عقب برگردانیم) تا Middleware‌ها یا کنترلرهای بعدی نیز بتوانند آن را بخوانند.

بیایید یک Middleware بنویسیم که بدنه JSON را می‌خواند و بررسی می‌کند که آیا فیلد Id در آن منفی است یا خیر.

// یک DTO ساده برای مثال
public class SampleModel { public int Id { get; set; } }

public class RequestBodyValidationMiddleware
{
    private readonly RequestDelegate _next;

    public RequestBodyValidationMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // فقط درخواست‌های POST با JSON را بررسی می‌کنیم
        if (context.Request.Method == HttpMethods.Post && context.Request.ContentType?.StartsWith("application/json") == true)
        {
            // 1. فعال کردن بافرینگ تا بتوانیم Stream را دوباره بخوانیم
            context.Request.EnableBuffering();

            // 2. خواندن بدنه درخواست
            string bodyAsText;
            using (var reader = new StreamReader(context.Request.Body, leaveOpen: true))
            {
                bodyAsText = await reader.ReadToEndAsync();
            }

            // 3. بازگرداندن Stream به ابتدا برای خوانندگان بعدی (مثل Model Binding)
            context.Request.Body.Position = 0;

            if (string.IsNullOrWhiteSpace(bodyAsText))
            {
                await HandleErrorAsync(context, 400, "Request body cannot be empty.");
                return;
            }

            try
            {
                // 4. اعتبارسنجی (در اینجا Deserialization)
                var model = System.Text.Json.JsonSerializer.Deserialize(bodyAsText);
                if (model != null && model.Id < 0)
                {
                    await HandleErrorAsync(context, 400, "ID cannot be negative.");
                    return;
                }
            }
            catch (System.Text.Json.JsonException)
            {
                await HandleErrorAsync(context, 400, "Invalid JSON format.");
                return;
            }
        }

        // ادامه خط لوله
        await _next(context);
    }
    
    // (متد HandleErrorAsync مانند مثال قبلی)
    private static Task HandleErrorAsync(HttpContext context, int statusCode, string message)
    {
        context.Response.StatusCode = statusCode;
        context.Response.ContentType = "application/json";
        var errorResponse = new { Error = message };
        return context.Response.WriteAsJsonAsync(errorResponse);
    }
}

هشدار عملکردی: خواندن و Deserialization بدنه درخواست در Middleware می‌تواند هزینه‌بر باشد، به خصوص برای درخواست‌های بزرگ. شما در حال انجام کاری هستید که Model Binding بعداً دوباره آن را انجام خواهد داد. این الگو باید با احتیاط و فقط زمانی استفاده شود که اعتبارسنجی بدنه قبل از منطق اصلی (مانند احراز هویت) ضروری باشد.

 

مقایسه Middleware با سایر روش‌های اعتبارسنجی

Middleware ابزاری در جعبه ابزار شماست، اما همیشه بهترین ابزار نیست.

روش (Method) زمان اجرا سطح (Scope) مورد استفاده ایده‌آل
Middleware بسیار زود (در خط لوله) سراسری (Global) اعتبارسنجی‌های زیرساختی و امنیتی (API Keys, Headers, Content-Type).
Action Filters دیرتر (قبل/بعد از اجرای Action) کنترلر یا Action خاص اعتبارسنجی‌های وابسته به منطق Action، دستکاری پارامترها.
Model Validation (Data Annotations) دیر (در زمان Model Binding) روی مدل (DTO) اعتبارسنجی‌های ساده و اعلامی روی فیلدهای مدل ([Required], [Range]).
FluentValidation دیر (ادغام با Model Binding) روی مدل (DTO) اعتبارسنجی‌های پیچیده و قوانین تجاری روی فیلدهای مدل، با جداسازی کامل قوانین.

قانون کلی:

  • از Middleware برای اعتبارسنجی "پوسته" درخواست (Headers, Method, URL) و قوانین امنیتی سراسری استفاده کنید.

  • از Model Validation (Data Annotations یا FluentValidation) برای اعتبارسنجی "محتوای" درخواست (یعنی داده‌های داخل بدنه JSON) استفاده کنید.

 

نتیجه‌گیری

Middleware در ASP.NET Core یک ابزار فوق‌العاده قدرتمند برای پیاده‌سازی اعتبارسنجی‌های سراسری، زود هنگام و زیرساختی است. با استفاده از قابلیت "اتصال کوتاه" (Short-Circuiting)، Middleware به ما اجازه می‌دهد تا استراتژی "Fail Fast" را پیاده‌سازی کرده و درخواست‌های نامعتبر را قبل از هدر دادن منابع سیستمی، در همان ابتدای خط لوله رد کنیم.

این روش برای بررسی‌هایی مانند کلیدهای API، انواع محتوا (Content-Type) و سایر Header‌های امنیتی ایده‌آل است. اگرچه اعتبارسنجی بدنه درخواست در Middleware ممکن است (و چالش‌های عملکردی خود را دارد)، اما برای اعتبارسنجی‌های سطح پایین‌تر، Middleware یک جایگزین تمیزتر و کارآمدتر نسبت به تکرار کد اعتبارسنجی در هر کنترلر یا Action Filter ارائه می‌دهد. در نهایت، یک معماری قوی، از ترکیبی از Middleware (برای اعتبارسنجی زیرساخت) و اعتبارسنجی مدل (برای اعتبارسنجی منطق تجاری) بهره می‌برد.

 
 
لینک استاندارد شده: tpTqkKO

0 نظر

    هنوز نظری برای این مقاله ثبت نشده است.
جستجوی مقاله و آموزش
دوره‌ها با تخفیفات ویژه