۲۰ نکته کلیدی برای امنیت WebAPI در دات‌نت

امنیت WebAPI فقط به احراز هویت و استفاده از HTTPS محدود نمی‌شود. بسیاری از آسیب‌پذیری‌های جدی از جایی شروع می‌شوند که ورودی‌ها درست اعتبارسنجی نشده‌اند، خطاها اطلاعات داخلی را فاش می‌کنند، توکن‌ها با تنظیمات نامناسب صادر شده‌اند یا محدودیت مناسبی روی منابع اعمال نشده است. در این مقاله، ۲۰ نکته کاملاً عملی و پرتکرار در WebAPIهای .NET را با مثال‌های صحیح، مثال‌های اشتباه و توضیح ریسک هر کدام مرور می‌کنیم تا بتوانید API خود را پایدارتر و امن‌تر پیاده‌سازی کنید.

۱) کنترل ورودی‌ها در سطح DTO

ورودی‌ها باید از طریق مدل‌های DTO اعتبارسنجی شوند؛ اتصال مستقیم Entity به اکشن باعث می‌شود کاربر بتواند فیلدهای حساس را نیز مقداردهی کند و این یکی از رایج‌ترین اشتباهات امنیتی در WebAPI است.

اشتباه:


[HttpPost("register")]
public IActionResult Register(User user)
{
    _db.Users.Add(user);
    _db.SaveChanges();
    return Ok();
}

در این روش، هر فیلدی که در موجودیت User وجود داشته باشد قابل مقداردهی از سمت کلاینت است؛ از جمله نقش‌ها، وضعیت حساب، تاریخ‌ها و حتی فیلدهای سیستمی.

مثال صحیح:


public class RegisterRequest
{
    [Required, MaxLength(50)]
    public string FullName { get; set; }

    [Required, EmailAddress]
    public string Email { get; set; }

    [Required, MinLength(6)]
    public string Password { get; set; }
}

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly AppDbContext _db;
    private readonly PasswordHasher<User> _hasher = new();

    public AuthController(AppDbContext db)
    {
        _db = db;
    }

    [HttpPost("register")]
    public IActionResult Register([FromBody] RegisterRequest model)
    {
        if (!ModelState.IsValid)
            return BadRequest(ModelState);

        var user = new User
        {
            FullName = model.FullName,
            Email = model.Email,
            PasswordHash = _hasher.HashPassword(null, model.Password),
            CreatedAt = DateTime.UtcNow,
            IsAdmin = false // فیلد حساس، فقط در سرور مقداردهی می‌شود
        };

        _db.Users.Add(user);
        _db.SaveChanges();

        return Ok();
    }
}

با این رویکرد فقط فیلدهای موردنیاز از سمت کاربر گرفته می‌شود و فیلدهای حساس خارج از دسترس او باقی می‌مانند. در غیر این صورت، کاربر می‌تواند از طریق بدنه درخواست، نقش خود را تغییر دهد یا روی فیلدهای امنیتی اثر بگذارد.

۲) جلوگیری از Mass Assignment

Mass Assignment زمانی رخ می‌دهد که تمامی فیلدهای یک مدل بدون محدودیت از روی ورودی کاربر مقداردهی شوند. اگر فیلدهای حساسی مثل IsAdmin، Role یا IsActive در مدل وجود داشته باشد، کاربر می‌تواند آن‌ها را به نفع خود تغییر دهد.

ارسال اشتباه از سمت کلاینت:


{
  "fullName": "Test User",
  "email": "user@example.com",
  "isAdmin": true
}

کد اشتباه در سمت سرور:


[HttpPost("update")]
public IActionResult Update([FromBody] User user)
{
    _db.Users.Update(user);
    _db.SaveChanges();
    return Ok();
}

در این حالت اگر کلاینت فیلد isAdmin را در بدنه JSON ارسال کند، روی موجودیت اعمال می‌شود و کاربر معمولی می‌تواند دسترسی مدیریتی بگیرد.

الگوی صحیح:


public class UpdateProfileRequest
{
    [Required, MaxLength(50)]
    public string FullName { get; set; }

    [Required, EmailAddress]
    public string Email { get; set; }
}

[HttpPost("update-profile")]
public IActionResult UpdateProfile([FromBody] UpdateProfileRequest dto)
{
    var userId = GetCurrentUserId();
    var user = _db.Users.Find(userId);

    if (user == null)
        return NotFound();

    user.FullName = dto.FullName;
    user.Email = dto.Email;

    _db.SaveChanges();

    return Ok();
}

در این ساختار فقط فیلدهای مجاز از سمت کاربر دریافت می‌شود و نقش‌ها، وضعیت حساب و سایر فیلدهای حساس از کنترل او خارج است؛ همین موضوع جلوی بسیاری از نفوذهای منطقی را می‌گیرد.

۳) مدیریت صحیح JWT

استفاده از JWT بدون توجه به عمر توکن، قدرت کلید و تنظیمات اعتبارسنجی، می‌تواند امنیت احراز هویت را کاملاً بی‌اثر کند. توکن باید کوتاه‌عمر باشد و امضای آن با کلیدی امن و مخفی انجام شود، همچنین Issuer و Audience باید به‌درستی بررسی شوند.

کد اشتباه:


var token = new JwtSecurityToken(
    issuer: null,          // بدون issuer
    audience: null,        // بدون audience
    expires: DateTime.UtcNow.AddDays(7), // عمر بسیار زیاد
    signingCredentials: new SigningCredentials(
        new SymmetricSecurityKey(Encoding.UTF8.GetBytes("weak-key")),
        SecurityAlgorithms.HmacSha256)
);

مثال صحیح – ایجاد توکن:


var key = Encoding.UTF8.GetBytes(configuration["Jwt:Key"]);

var tokenDescriptor = new SecurityTokenDescriptor
{
    Subject = new ClaimsIdentity(new[]
    {
        new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
        new Claim(ClaimTypes.Email, user.Email)
    }),
    Expires = DateTime.UtcNow.AddMinutes(15),
    Issuer = "learnwise",
    Audience = "learnwise-api",
    SigningCredentials = new SigningCredentials(
        new SymmetricSecurityKey(key),
        SecurityAlgorithms.HmacSha256)
};

var tokenHandler = new JwtSecurityTokenHandler();
var securityToken = tokenHandler.CreateToken(tokenDescriptor);
var jwt = tokenHandler.WriteToken(securityToken);

تنظیمات اعتبارسنجی در Program.cs:


builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "learnwise",
            ValidateAudience = true,
            ValidAudience = "learnwise-api",
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])
            ),
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromMinutes(1)
        };
    });

در صورت استفاده از کلید ضعیف، عمر طولانی یا غیرفعال بودن اعتبارسنجی‌ها، سرقت یک توکن مساوی با دسترسی طولانی‌مدت مهاجم به سیستم است. کوتاه کردن عمر توکن و اعتبارسنجی کامل، این ریسک را به حداقل می‌رساند.

۴) محدودسازی تلاش‌های Login

اگر endpoint ورود بدون محدودیت تعداد تلاش باشد، مهاجم می‌تواند میلیون‌ها رمز عبور را تست کند و علاوه بر احتمال نفوذ، بار سنگینی روی سیستم ایجاد کند.

کد اشتباه:


[HttpPost("login")]
public async Task<IActionResult> Login(LoginRequest dto)
{
    var user = await _userManager.FindByEmailAsync(dto.Email);
    if (user == null) return Unauthorized();

    if (!await _userManager.CheckPasswordAsync(user, dto.Password))
        return Unauthorized();

    // تولید توکن...
    return Ok();
}

مثال صحیح – استفاده از قابلیت Lockout در Identity:


builder.Services.Configure<IdentityOptions>(options =>
{
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
    options.Lockout.AllowedForNewUsers = true;
});

[HttpPost("login")]
public async Task<IActionResult> Login(LoginRequest dto)
{
    var result = await _signInManager.PasswordSignInAsync(
        dto.Email,
        dto.Password,
        isPersistent: false,
        lockoutOnFailure: true);

    if (result.IsLockedOut)
        return BadRequest("حساب شما به‌صورت موقت قفل شده است.");

    if (!result.Succeeded)
        return Unauthorized("نام کاربری یا رمز عبور نامعتبر است.");

    // تولید توکن...
    return Ok();
}

با فعال‌کردن Lockout، پس از چند تلاش ناموفق، حساب برای مدت مشخصی قفل می‌شود و حملات brute-force عملاً بی‌اثر خواهد شد.

۵) عدم افشای وجود یا عدم وجود کاربر

endpointهای بازیابی رمز عبور و ثبت‌نام نباید مشخص کنند که ایمیل یا شماره موبایل واردشده از قبل ثبت شده است یا خیر؛ این اطلاعات برای مهاجمان بسیار ارزشمند است.

کد اشتباه:


[HttpPost("forgot-password")]
public async Task<IActionResult> ForgotPassword(string email)
{
    var user = await _userManager.FindByEmailAsync(email);
    if (user == null)
        return BadRequest("ایمیل در سیستم ثبت نشده است.");

    // ارسال لینک...
    return Ok("لینک بازیابی ارسال شد.");
}

مثال صحیح:


[HttpPost("forgot-password")]
public async Task<IActionResult> ForgotPassword(string email)
{
    var user = await _userManager.FindByEmailAsync(email);
    if (user != null)
    {
        // ارسال لینک بازیابی برای کاربر
    }

    // پاسخ یکسان در هر دو حالت:
    return Ok("در صورت وجود حساب فعال، لینک بازیابی ارسال خواهد شد.");
}

با این رویکرد، مهاجم حتی با تست ده‌ها هزار ایمیل نمی‌تواند تشخیص دهد کدام‌یک در سیستم شما وجود دارد و امکان استخراج فهرست کاربران از بین می‌رود.

۶) ذخیره رمز عبور با Hash امن

ذخیره رمز عبور به‌صورت ساده یا استفاده از الگوریتم‌های سریع مانند SHA و MD5، در عمل به مهاجم اجازه می‌دهد پس از دسترسی به دیتابیس، رمزها را ظرف چند دقیقه بازیابی کند. برای این منظور باید از الگوریتم‌های کندتر و مقاوم‌تر مثل PBKDF2 استفاده کرد.

کد اشتباه:


// اشتباه: استفاده از SHA256 برای رمز عبور
using var sha = SHA256.Create();
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(model.Password));
user.PasswordHash = Convert.ToBase64String(hash);

مثال صحیح – استفاده از PasswordHasher:


var hasher = new PasswordHasher<User>();
user.PasswordHash = hasher.HashPassword(user, model.Password);

var result = hasher.VerifyHashedPassword(user, user.PasswordHash, model.Password);
// result بر اساس صحت رمز عبور ارزیابی می‌شود

PasswordHasher از PBKDF2 با تعداد تکرار بالا استفاده می‌کند و حتی در صورت افشای دیتابیس، کرک کردن رمزهای عبور را بسیار دشوار و پرهزینه می‌کند.

۷) مدیریت صحیح RefreshToken

RefreshToken باید منحصربه‌فرد، دارای تاریخ انقضا، قابل ابطال و در دیتابیس ذخیره شود. نگه‌داشتن آن فقط در مدل کاربر یا بدون انقضا، ریسک جدی امنیتی ایجاد می‌کند و می‌تواند به دسترسی طولانی‌مدت مهاجم منجر شود.

کد اشتباه:


// اشتباه: نگهداری RefreshToken بدون انقضا و بدون تاریخچه
user.RefreshToken = Guid.NewGuid().ToString();
_db.SaveChanges();

مدل پیشنهادی برای سشن کاربر:


public class UserSession
{
    public int Id { get; set; }
    public int UserId { get; set; }
    public string RefreshToken { get; set; } = string.Empty;
    public DateTime ExpiresAt { get; set; }
    public bool IsRevoked { get; set; }
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

صدور RefreshToken جدید:


var session = new UserSession
{
    UserId = user.Id,
    RefreshToken = Guid.NewGuid().ToString("N"),
    ExpiresAt = DateTime.UtcNow.AddDays(7),
    IsRevoked = false
};

_db.UserSessions.Add(session);
_db.SaveChanges();

با این ساختار، می‌توانید در هر زمان RefreshToken را باطل کنید، تاریخچه سشن‌ها را داشته باشید و در صورت مشاهده رفتار مشکوک، دسترسی را محدود کنید.

۸) جلوگیری از افشای اطلاعات در خطاها

پیام‌های خطا نباید شامل Exception، StackTrace، Query، نام جدول یا هر نوع اطلاعات داخلی باشند. این اطلاعات می‌تواند مسیر حمله را برای مهاجم به‌وضوح مشخص کند، در حالی که کاربر نهایی به این سطح از جزئیات نیازی ندارد.

کد اشتباه:


catch (SqlException ex)
{
    return StatusCode(500, ex.ToString());
}

الگوی بهتر – استفاده از پاسخ استاندارد و لاگ داخلی:


catch (Exception ex)
{
    _logger.LogError(ex, "Error while processing request {Path}", HttpContext.Request.Path);

    return StatusCode(StatusCodes.Status500InternalServerError, new
    {
        success = false,
        message = "خطا در انجام عملیات. لطفاً بعداً دوباره تلاش کنید.",
        code = 500
    });
}

اطلاعات دقیق خطا فقط در لاگ‌ها ذخیره می‌شود و پاسخ کاربر عمومی و قابل‌پیش‌بینی باقی می‌ماند. این کار مانع از افشای ساختار داخلی سیستم برای مهاجم می‌شود.

۹) محدود کردن اندازه درخواست

ارسال فایل‌ها یا بدنه‌های بسیار بزرگ می‌تواند حافظه و پردازنده سرور را مصرف کند و عملاً باعث حملات DoS شود. بهتر است محدودیتی منطقی برای اندازه بدنه درخواست تعریف شود تا فقط حجم معقولی از داده پذیرفته شود.

Middleware ساده برای محدودسازی:


app.Use(async (context, next) =>
{
    const long maxRequestBodySize = 2 * 1024 * 1024; // 2MB

    if (context.Request.ContentLength.HasValue &&
        context.Request.ContentLength.Value > maxRequestBodySize)
    {
        context.Response.StatusCode = StatusCodes.Status413PayloadTooLarge;
        await context.Response.WriteAsync("حجم داده ارسالی بیش از حد مجاز است.");
        return;
    }

    await next();
});

بدون چنین محدودیتی، چند درخواست حجیم می‌تواند سرور شما را از دسترس خارج کند و عملکرد سیستم را به شدت کاهش دهد.

۱۰) غیرفعال‌سازی روش‌های خطرناک HTTP

روش‌هایی مانند TRACE معمولاً در برنامه‌های کاربردی نیاز نیست و فعال بودن آن‌ها می‌تواند اطلاعات درخواست را برای مهاجم آشکار کند. بهتر است این روش‌ها در وب‌سرور غیرفعال شوند تا Attack Surface کاهش یابد.

مثال در nginx برای رد کردن TRACE:


if ($request_method = TRACE) {
    return 405;
}

با این تنظیم، هیچ درخواست TRACE پردازش نمی‌شود و امکان مشاهده مستقیم headerها توسط مهاجم از بین می‌رود.

۱۱) افزودن Security Headers

حتی اگر API شما خروجی HTML تولید نکند، برخی هدرهای امنیتی مانند X-Frame-Options و X-Content-Type-Options می‌توانند از حملات مرورگرمحور جلوگیری کنند و برای سطوح مختلف کلاینت (وب، موبایل، PWA) مفید هستند.

Middleware ساده برای هدرهای امنیتی:


app.Use(async (context, next) =>
{
    context.Response.Headers["X-Frame-Options"] = "DENY";
    context.Response.Headers["X-Content-Type-Options"] = "nosniff";
    context.Response.Headers["Referrer-Policy"] = "no-referrer";

    await next();
});

این هدرها از قرار گرفتن خروجی شما در iframe (برای جلوگیری از Clickjacking) و از تفسیر اشتباه نوع فایل‌ها توسط مرورگر جلوگیری می‌کنند و یک لایه امنیتی ساده اما مؤثر اضافه می‌کنند.

۱۲) جلوگیری از Log Injection

اگر متن‌های ورودی کاربر بدون هیچ پردازشی وارد لاگ شوند، مهاجم می‌تواند با ورود کاراکترهای کنترلی، ساختار لاگ را بشکند، سطرهای مختلف را در هم ادغام کند یا برخی رخدادها را پنهان کند. این موضوع تحلیل رخداد و مانیتورینگ را دشوار می‌کند.

کد اشتباه:


logger.LogInformation($"User input: {input}");

الگوی بهتر:


logger.LogInformation("User input: {Input}", Sanitize(input));

private static string Sanitize(string value)
{
    if (string.IsNullOrEmpty(value)) return value;
    return value.Replace("\r", string.Empty).Replace("\n", string.Empty);
}

با حذف کاراکترهای کنترل و جداسازی پارامترهای لاگ، ساختار لاگ قابل اطمینان باقی می‌ماند و تحلیل رخدادها ساده‌تر خواهد بود.

۱۳) جلوگیری از SQL Injection

استفاده از رشته‌های ترکیبی برای ساخت Query، راه ورود مستقیم برای حملات SQL Injection است. باید همیشه از پارامترها استفاده کرد؛ چه در EF Core و چه در Dapper و ADO.NET، تا ورودی کاربر به‌عنوان داده پردازش شود نه کد.

کد اشتباه:


var sql = $"SELECT * FROM Users WHERE Email = '{email}'";
var user = await connection.QueryFirstOrDefaultAsync<User>(sql);

مثال صحیح در Dapper:


var sql = "SELECT * FROM Users WHERE Email = @Email";
var user = await connection.QueryFirstOrDefaultAsync<User>(sql, new { Email = email });

پارامتری‌سازی باعث می‌شود حتی در صورت ارسال کاراکترهای خاص از سمت کاربر، Query اصلی تغییری نکند و فقط مقدار پارامتر به‌صورت امن در دستور SQL جای‌گذاری شود.

۱۴) ابطال RefreshToken پس از استفاده

پس از صدور RefreshToken جدید، نسخه قبلی باید باطل شود تا در صورت سرقت قابل استفاده مجدد نباشد. نگه‌داشتن چند RefreshToken فعال بدون کنترل، سطح حمله را گسترده می‌کند و Session Fixation را ممکن می‌سازد.

نمونه ساده از ابطال:


public async Task RevokeSessionAsync(string refreshToken)
{
    var session = await _db.UserSessions
        .FirstOrDefaultAsync(x => x.RefreshToken == refreshToken && !x.IsRevoked);

    if (session == null)
        return;

    session.IsRevoked = true;
    await _db.SaveChangesAsync();
}

با ابطال سشن قبلی، حتی اگر Token قدیمی در جایی نگهداری شده باشد، دیگر قابل استفاده نخواهد بود و حملات مبتنی بر سوءاستفاده از سشن‌های قدیمی خنثی می‌شود.

۱۵) جلوگیری از دسترسی به داده‌های دیگر کاربران (BOLA)

Broken Object Level Authorization زمانی رخ می‌دهد که دسترسی به یک شیء (مثلاً سفارش) فقط بر اساس شناسه آن کنترل شود و مالکیت آن بررسی نشود. این یکی از شایع‌ترین ضعف‌های امنیتی APIها است و می‌تواند به افشای داده‌های حساس منجر شود.

کد اشتباه:


[HttpGet("orders/{id:int}")]
public async Task<IActionResult> GetOrder(int id)
{
    var order = await _db.Orders.FindAsync(id);
    if (order == null) return NotFound();
    return Ok(order);
}

مثال صحیح با بررسی مالکیت:


[Authorize]
[HttpGet("orders/{id:int}")]
public async Task<IActionResult> GetOrder(int id)
{
    var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));

    var order = await _db.Orders
        .FirstOrDefaultAsync(o => o.Id == id && o.UserId == userId);

    if (order == null)
        return NotFound();

    return Ok(order);
}

با این الگو، کاربر تنها به رکوردهای متعلق به خود دسترسی خواهد داشت و امکان مشاهده یا دستکاری داده‌های دیگران از بین می‌رود.

۱۶) جلوگیری از Replay Attack

در عملیات حساس مانند پرداخت، ثبت سفارش یا ارسال OTP، نباید اجازه داد همان درخواست معتبر چندبار تکرار شود. استفاده از شناسه یکتا (requestId) یا nonce راه‌حل رایج این مشکل است.

مثال ساده با requestId:


public class PaymentRequest
{
    public string RequestId { get; set; } = string.Empty;
    public int OrderId { get; set; }
    public decimal Amount { get; set; }
}

[HttpPost("pay")]
public async Task<IActionResult> Pay([FromBody] PaymentRequest dto)
{
    if (await _db.PaymentRequests.AnyAsync(x => x.RequestId == dto.RequestId))
        return BadRequest("این درخواست قبلاً پردازش شده است.");

    _db.PaymentRequests.Add(new PaymentRequestEntity
    {
        RequestId = dto.RequestId,
        OrderId = dto.OrderId,
        Amount = dto.Amount,
        CreatedAt = DateTime.UtcNow
    });

    // ادامه فرایند پرداخت...

    await _db.SaveChangesAsync();
    return Ok();
}

با این روش، هر شناسه درخواست فقط یکبار پردازش می‌شود و مهاجم نمی‌تواند یک درخواست معتبر را چند بار تکرار کند و تراکنش‌های تکراری ایجاد کند.

۱۷) امنیت فایل Upload

آپلود فایل بدون کنترل پسوند، نوع MIME و اندازه، می‌تواند به آپلود اسکریپت‌های مخرب و در نهایت اجرای کد روی سرور منجر شود. باید تمام ورودی‌های مرتبط با فایل با دقت بررسی شوند.

کد اشتباه:


[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile file)
{
    using var stream = System.IO.File.Create("wwwroot/uploads/" + file.FileName);
    await file.CopyToAsync(stream);
    return Ok();
}

مثال صحیح (ساده‌سازی‌شده):


[HttpPost("upload-image")]
public async Task<IActionResult> UploadImage(IFormFile file)
{
    if (file == null || file.Length == 0)
        return BadRequest("فایل ارسال نشده است.");

    if (!file.ContentType.StartsWith("image/"))
        return BadRequest("فقط فایل تصویری مجاز است.");

    const long maxSize = 2 * 1024 * 1024;
    if (file.Length > maxSize)
        return BadRequest("حجم فایل بیش از حد مجاز است.");

    var uploadsPath = Path.Combine("uploads", "images");
    Directory.CreateDirectory(uploadsPath);

    var extension = Path.GetExtension(file.FileName);
    var fileName = $"{Guid.NewGuid():N}{extension}";
    var filePath = Path.Combine(uploadsPath, fileName);

    using (var stream = System.IO.File.Create(filePath))
    {
        await file.CopyToAsync(stream);
    }

    return Ok(new { fileName });
}

بهتر است مسیر ذخیره‌سازی فایل خارج از wwwroot باشد و فقط از طریق endpointهای کنترل‌شده در دسترس قرار گیرد تا امکان اجرای مستقیم فایل‌ها از بین برود.

۱۸) تنظیم Timeout روی HttpClient

اگر درخواست‌های خروجی شما به سرویس‌های دیگر Timeout مشخصی نداشته باشند، در صورت کندی یا عدم پاسخ آن سرویس‌ها، WebAPI شما ممکن است در وضعیت Hang قرار بگیرد و منابعش قفل شود. این موضوع می‌تواند به کندی شدید یا عدم پاسخ‌دهی منجر شود.

کد اشتباه:


var client = new HttpClient();
var response = await client.GetAsync("https://external-service/api/data");
// بدون Timeout، این درخواست می‌تواند بسیار طولانی شود

مثال صحیح:


var client = new HttpClient
{
    Timeout = TimeSpan.FromSeconds(5)
};

try
{
    var response = await client.GetAsync("https://external-service/api/data");
    response.EnsureSuccessStatusCode();
    var content = await response.Content.ReadAsStringAsync();
    // پردازش پاسخ...
}
catch (TaskCanceledException)
{
    // Timeout یا لغو درخواست
}

تنظیم Timeout منطقی، مانع از این می‌شود که کندی سرویس‌های بیرونی کل WebAPI شما را از کار بیندازد و از انباشته شدن اتصال‌ها و Threadها جلوگیری می‌کند.

۱۹) اجبار HTTPS

استفاده از HTTP ساده (بدون TLS) برای API به این معناست که تمام توکن‌ها، کوکی‌ها و داده‌ها در مسیر قابل شنود و دستکاری هستند. باید تمام ترافیک به HTTPS هدایت شود تا داده‌ها در حین انتقال رمزنگاری شوند.

مثال در .NET:


// در Program.cs
app.UseHttpsRedirection();

مثال در nginx:


server {
    listen 80;
    server_name api.example.com;

    return 301 https://$host$request_uri;
}

با این تنظیمات، حتی اگر کاربر به‌صورت دستی از HTTP استفاده کند، به‌صورت خودکار به HTTPS ریدایرکت می‌شود و ارتباط همیشه رمزنگاری‌شده باقی می‌ماند.

۲۰) نسخه‌بندی API

اعمال تغییرات breaking روی همان endpoint بدون نسخه‌بندی، کلاینت‌های قدیمی را از کار می‌اندازد و برای کاربران مشکلات جدی ایجاد می‌کند. نسخه‌بندی به شما اجازه می‌دهد رفتارهای جدید را کنار نسخه‌های قدیمی ارائه کنید و مهاجرت کلاینت‌ها را تدریجی انجام دهید.

نمونه استفاده از API Versioning در .NET:


// Program.cs
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
});

// کنترلر نسخه 1
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/orders")]
[ApiController]
public class OrdersV1Controller : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return Ok(new { version = "v1", items = new string[] { "item1", "item2" } });
    }
}

// کنترلر نسخه 2
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/orders")]
[ApiController]
public class OrdersV2Controller : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return Ok(new { version = "v2", items = new string[] { "item1", "item2" }, totalCount = 2 });
    }
}

با این ساختار، کلاینت‌های قدیمی می‌توانند از مسیر /api/v1/orders استفاده کنند و کلاینت‌های جدید به /api/v2/orders سوئیچ می‌کنند؛ بدون آن‌که تغییری در رفتار نسخه قبلی رخ دهد.