← همه آموزش‌ها

JWT چیست؟

احراز هویت با توکن، به زبان خیلی ساده و همراه با مثال عملی ASP.NET Core

بنر JWT: سه بخش Header، Payload و Signature در کنار کارت‌های ورود و Refresh Token.
توکن سه بخش دارد: مشخصات الگوریتم، ادعاها، و امضایی که سرور بررسی می‌کند.

در این آموزش چه می‌سازیم؟

یک API کوچک داریم. کاربر با نام کاربری و رمز عبور وارد می‌شود، سرور یک JWT به او می‌دهد و کاربر با آن توکن به مسیرهای محافظت‌شده دسترسی پیدا می‌کند.

هدف این مطلب حفظ کردن اصطلاحات نیست؛ می‌خواهیم دقیقاً بفهمیم JWT چیست، چطور کار می‌کند و کجا باید مراقب امنیت باشیم.

۱. JWT چیست؟

JWT مخفف JSON Web Token است. به زبان ساده، JWT مثل یک کارت ورود دیجیتال است که سرور بعد از ورود موفق به کاربر می‌دهد.

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

در یک API نیز تقریباً همین اتفاق می‌افتد:

  • نام کاربری و رمز عبور را یک بار به مسیر ورود می‌فرستید.
  • سرور اطلاعات را بررسی می‌کند و یک JWT می‌سازد.
  • در درخواست‌های بعدی، JWT را همراه درخواست می‌فرستید.
  • سرور اعتبار توکن و سطح دسترسی شما را بررسی می‌کند.
JWT معمولاً برای احراز هویت در Web API، اپلیکیشن موبایل و ارتباط بین سرویس‌ها استفاده می‌شود.

۲. روند ورود چگونه است؟

۱. کاربر نام کاربری و رمز عبور را به /login می‌فرستد.
↓
۲. سرور اطلاعات کاربر را با دیتابیس بررسی می‌کند.
↓
۳. اگر درست بود، سرور یک JWT امضاشده برمی‌گرداند.
↓
۴. برنامه توکن را در هدر درخواست‌های بعدی قرار می‌دهد.
↓
۵. سرور امضا، تاریخ انقضا و دسترسی‌های توکن را بررسی می‌کند.

هدر درخواست محافظت‌شده به این شکل است:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

کلمه Bearer یعنی «دارنده این توکن». پس هر کسی که توکن معتبر را داشته باشد، می‌تواند از آن استفاده کند؛ درست مثل پول نقد. به همین دلیل توکن نباید افشا شود یا به دست دیگران بیفتد.

۳. JWT از چه بخش‌هایی ساخته شده است؟

یک JWT سه بخش دارد که با نقطه از هم جدا می‌شوند:

xxxxx.yyyyy.zzzzz
Header.Payload.Signature

Header — مشخصات فنی

نوع توکن و الگوریتم امضا را مشخص می‌کند:

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload — اطلاعات یا Claimها

اطلاعات لازم درباره کاربر در این قسمت قرار می‌گیرد:

{
  "sub": "42",
  "name": "Sara",
  "role": "Admin",
  "exp": 1784491200
}

هر داده داخل Payload یک Claim نامیده می‌شود. برای مثال sub شناسه کاربر، role نقش و exp زمان انقضای توکن است.

Signature — امضا

سرور دو بخش اول را با یک کلید محرمانه امضا می‌کند. اگر کسی Payload را تغییر دهد، امضا دیگر معتبر نیست و سرور توکن را رد می‌کند.

JWT رمزنگاری‌شده نیست. Header و Payload فقط به Base64Url تبدیل شده‌اند و هر کسی می‌تواند آن‌ها را بخواند. هیچ‌وقت رمز عبور، شماره کارت یا اطلاعات حساس را داخل JWT نگذارید. امضا جلوی تغییر محتوا را می‌گیرد، نه جلوی خواندن آن.

۴. مثال عملی با ASP.NET Core

یک پروژه Minimal API بسازید و پکیج احراز هویت JWT را نصب کنید:

dotnet new webapi -n JwtDemo
cd JwtDemo
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

تنظیم کلید در appsettings.json

{
  "Jwt": {
    "Key": "A-DEMO-SECRET-KEY-AT-LEAST-32-CHARS!",
    "Issuer": "JwtDemoApi",
    "Audience": "JwtDemoClient"
  }
}
این کلید فقط برای اجرای مثال است. در محیط واقعی، کلید را داخل فایل پروژه یا Git ذخیره نکنید؛ از environment variable، Secret Manager یا سرویس مدیریت رمز استفاده کنید.

کد کامل Program.cs

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);

var jwtKey = builder.Configuration["Jwt:Key"]
    ?? throw new InvalidOperationException("JWT key is missing");
var issuer = builder.Configuration["Jwt:Issuer"];
var audience = builder.Configuration["Jwt:Audience"];

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = issuer,
            ValidAudience = audience,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(jwtKey)),
            ClockSkew = TimeSpan.Zero
        };
    });

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapPost("/login", (LoginRequest request) =>
{
    // فقط برای آموزش؛ در پروژه واقعی کاربر را از دیتابیس بخوانید
    // و رمز هش‌شده را با PasswordHasher بررسی کنید.
    if (request.Username != "sara" || request.Password != "123456")
        return Results.Unauthorized();

    var claims = new[]
    {
        new Claim(JwtRegisteredClaimNames.Sub, "42"),
        new Claim(ClaimTypes.Name, "Sara"),
        new Claim(ClaimTypes.Role, "Admin"),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
    };

    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
    var credentials = new SigningCredentials(
        key, SecurityAlgorithms.HmacSha256);

    var token = new JwtSecurityToken(
        issuer: issuer,
        audience: audience,
        claims: claims,
        expires: DateTime.UtcNow.AddMinutes(15),
        signingCredentials: credentials);

    var tokenText = new JwtSecurityTokenHandler().WriteToken(token);
    return Results.Ok(new { accessToken = tokenText });
});

app.MapGet("/profile", (ClaimsPrincipal user) =>
{
    return Results.Ok(new
    {
        Name = user.Identity?.Name,
        Message = "شما وارد شده‌اید."
    });
}).RequireAuthorization();

app.MapGet("/admin", () => "فقط مدیر این متن را می‌بیند.")
   .RequireAuthorization(policy => policy.RequireRole("Admin"));

app.Run();

record LoginRequest(string Username, string Password);

این کد چه می‌کند؟

  • AddAuthentication می‌گوید روش احراز هویت ما Bearer Token است.
  • TokenValidationParameters قوانین اعتبارسنجی توکن را تعریف می‌کند.
  • مسیر /login بعد از بررسی کاربر، یک توکن ۱۵ دقیقه‌ای می‌سازد.
  • RequireAuthorization اجازه دسترسی بدون توکن معتبر را نمی‌دهد.
  • مسیر /admin علاوه بر توکن معتبر، نقش Admin می‌خواهد.
ترتیب UseAuthentication و UseAuthorization مهم است: اول باید هویت کاربر مشخص شود، بعد سطح دسترسی او بررسی شود.

۵. تست کردن API

برنامه را اجرا کنید:

dotnet run

آدرس HTTPS نمایش‌داده‌شده در ترمینال را جایگزین https://localhost:7001 کنید. ابتدا وارد شوید:

curl -k -X POST https://localhost:7001/login \
  -H "Content-Type: application/json" \
  -d '{"username":"sara","password":"123456"}'

پاسخ شامل توکن است:

{
  "accessToken": "eyJhbGciOiJIUzI1NiIs..."
}

حالا مقدار توکن را در درخواست بعدی قرار دهید:

curl -k https://localhost:7001/profile \
  -H "Authorization: Bearer YOUR_TOKEN"

بدون هدر Authorization پاسخ 401 Unauthorized دریافت می‌کنید. اگر توکن معتبر باشد ولی نقش لازم را نداشته باشید، معمولاً پاسخ 403 Forbidden دریافت می‌کنید.

کدمعنی ساده
401هویت مشخص نیست؛ توکن وجود ندارد، خراب است یا منقضی شده.
403هویت مشخص است، اما برای این کار اجازه کافی ندارید.

۶. Access Token و Refresh Token

بهتر است Access Token عمر کوتاهی، مثلاً ۵ تا ۱۵ دقیقه، داشته باشد. اما نمی‌خواهیم کاربر هر ۱۵ دقیقه دوباره رمز عبور وارد کند. اینجا Refresh Token به کار می‌آید.

  • Access Token: عمر کوتاه دارد و همراه درخواست‌های API ارسال می‌شود.
  • Refresh Token: عمر طولانی‌تر دارد و فقط برای گرفتن Access Token جدید استفاده می‌شود.
Access Token منقضی می‌شود.
↓
برنامه Refresh Token را به مسیر مخصوص تمدید می‌فرستد.
↓
سرور Refresh Token را با رکورد امن دیتابیس بررسی می‌کند.
↓
یک Access Token جدید صادر و Refresh Token قبلی تعویض می‌شود.

Refresh Token معمولاً یک رشته تصادفی است، نه لزوماً JWT. آن را به‌صورت هش‌شده در دیتابیس نگه دارید و امکان لغو، انقضا و rotation را برای آن فراهم کنید.

۷. نکات امنیتی مهم

  • همیشه HTTPS: بدون HTTPS، توکن ممکن است در مسیر شبکه دزدیده شود.
  • عمر کوتاه: Access Token کوتاه‌عمر، خسارت ناشی از سرقت توکن را کاهش می‌دهد.
  • کلید قوی: برای HS256 از یک کلید تصادفی و طولانی استفاده کنید و آن را از کد و Git بیرون نگه دارید.
  • اعتبارسنجی کامل: امضا، صادرکننده، مخاطب و تاریخ انقضا را بررسی کنید.
  • اطلاعات کم: فقط Claimهای لازم را داخل توکن بگذارید؛ JWT جای دیتابیس نیست.
  • رمز عبور واقعی: رمز را هرگز به شکل ساده ذخیره نکنید؛ از ASP.NET Core Identity یا PasswordHasher استفاده کنید.
  • ذخیره در مرورگر: نگه‌داری طولانی‌مدت توکن در localStorage در برابر حملات XSS آسیب‌پذیر است. برای وب‌اپ‌ها، معماری مبتنی بر کوکی HttpOnly، Secure و SameSite را بررسی کنید.
  • لغو دسترسی: JWT تا پایان عمر خود معمولاً معتبر می‌ماند. برای خروج فوری، تغییر رمز یا مسدود کردن کاربر، راهکار revocation یا توکن کوتاه‌عمر لازم است.
برای سیستم‌های واقعی، به‌خصوص ورود کاربران عمومی، بهتر است از راهکارهای استاندارد مثل ASP.NET Core Identity و سرویس‌های مبتنی بر OAuth 2.0 / OpenID Connect استفاده کنید؛ سیستم احراز هویت را از صفر نسازید.

۸. جمع‌بندی

  • JWT یک کارت ورود دیجیتال و امضاشده است.
  • سه بخش دارد: Header، Payload و Signature.
  • Payload قابل خواندن است؛ اطلاعات محرمانه داخل آن نگذارید.
  • توکن با هدر Authorization: Bearer ... به API فرستاده می‌شود.
  • سرور باید امضا، صادرکننده، مخاطب و انقضا را بررسی کند.
  • Access Token را کوتاه‌عمر نگه دارید و برای تمدید امن از Refresh Token استفاده کنید.

اگر JWT را «کارت ورود امضاشده» در نظر بگیرید، بیشتر مفاهیم آن ساده و قابل فهم می‌شوند.