در دنیای دیجیتال امروز، تأمین امنیت برنامه‌های وب و اطمینان از اینکه فقط کاربران مجاز می‌توانند به منابع حساس دسترسی داشته باشند، از اهمیت بالایی برخوردار است. یکی از روش‌های محبوب و کارآمد برای انجام این کار، استفاده از توکن‌های وب JSON یا به اختصار JWT (جِی دبلیو تی) است. JWT یک استاندارد باز (RFC 7519) برای ایجاد توکن‌های دسترسی است که اطلاعات را به صورت امن به عنوان یک شیء JSON منتقل می‌کنند. این شیء JSON به صورت دیجیتالی امضا شده است و می‌توان آن را برای تأیید هویت کاربر و اعطای مجوز دسترسی به منابع محافظت‌شده استفاده کرد.
کینگتو - آموزش برنامه نویسی تخصصصی - دات نت - سی شارپ - بانک اطلاعاتی و امنیت

توکن وب و یا JWT چیست؟ کلید امن و کارآمد برای مجوزدهی

38 بازدید 0 نظر ۱۴۰۴/۰۲/۰۲

چیستی JWT: یک گذرنامه دیجیتالی امن
به زبان ساده، JWT مانند یک گذرنامه دیجیتالی عمل می‌کند. این گذرنامه حاوی اطلاعاتی در مورد هویت کاربر و مجوزهای او است که توسط یک نهاد معتبر (معمولاً سرور برنامه) امضا شده است. هنگامی که کاربر می‌خواهد به یک منبع محافظت‌شده دسترسی پیدا کند، این گذرنامه را ارائه می‌دهد و سرور با بررسی امضای آن، هویت کاربر و مجوزهایش را تأیید کرده و در صورت معتبر بودن، اجازه دسترسی را صادر می‌کند.

JWT از سه بخش اصلی تشکیل شده است که با نقطه (.) از هم جدا می‌شوند:

Header (هدر):

این بخش شامل اطلاعاتی در مورد خود توکن، از جمله نوع توکن (که معمولاً JWT است) و الگوریتم امضای استفاده شده (مانند HS256 یا RS256) است. این اطلاعات به صورت JSON کدگذاری شده و سپس با استفاده از الگوریتم Base64Url به یک رشته تبدیل می‌شود.

برای مثال، یک هدر ساده می‌تواند به شکل زیر باشد:

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


که پس از کدگذاری Base64Url به چیزی شبیه به:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

تبدیل می‌شود.

Payload (بار):

این بخش شامل ادعاها (Claims) است. ادعاها اظهاراتی در مورد یک موجودیت (معمولاً کاربر) و داده‌های اضافی هستند.

سه نوع ادعا وجود دارد:

  • ادعاهای ثبت‌شده (Registered Claims): این ادعاها مجموعه‌ای از کلیدهای از پیش تعریف‌شده هستند که برای قابلیت همکاری بهتر توصیه می‌شوند، مانند iss (صادرکننده)، sub (موضوع)، aud (مخاطب)، exp (زمان انقضا)، nbf (قبل از)، iat (صادر شده در)، و jti (شناسه توکن JWT).
  • ادعاهای عمومی (Public Claims): این ادعاها می‌توانند توسط هر کسی تعریف شوند، اما برای جلوگیری از تصادم نام‌ها، توصیه می‌شود که آنها را با یک URI مرتبط کنید.
  • ادعاهای خصوصی (Private Claims): این ادعاها توسط طرفین توافق‌کننده تعریف می‌شوند و برای انتقال اطلاعات سفارشی بین آنها استفاده می‌شوند.

برای مثال، یک payload می‌تواند به شکل زیر باشد:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "exp": 1682345678
}


که پس از کدگذاری Base64Url به چیزی شبیه به:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTY4MjM0NTY3OH0

تبدیل می‌شود.

Signature (امضا):

این بخش برای تأیید اصالت توکن و اطمینان از اینکه هیچ کس محتوای آن را پس از صدور تغییر نداده است، استفاده می‌شود. امضا با ترکیب هدر کدگذاری شده، payload کدگذاری شده و یک کلید مخفی (برای الگوریتم‌های متقارن مانند HS256) یا یک کلید خصوصی (برای الگوریتم‌های نامتقارن مانند RS256) با استفاده از الگوریتم مشخص شده در هدر، ایجاد می‌شود.

برای مثال، اگر از الگوریتم HS256 و یک کلید مخفی به نام your-secret-key استفاده شود، امضا به صورت زیر محاسبه می‌شود:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-secret-key
)


نتیجه این محاسبات یک رشته امضا خواهد بود که به انتهای هدر و payload کدگذاری شده اضافه می‌شود.

بنابراین رشته تولیده شده طبق الگوی زیر خواهد بود:

[Base64Url(Header)].[Base64Url(Payload)].[Signature]

مکانیسم مجوزدهی با JWT: گام به گام به سوی امنیت
اکنون بیایید به بررسی دقیق مکانیسم مجوزدهی با استفاده از JWT بپردازیم. این فرآیند معمولاً شامل مراحل زیر است:

  • احراز هویت کاربر: هنگامی که یک کاربر می‌خواهد به یک برنامه وب دسترسی پیدا کند، ابتدا باید هویت خود را تأیید کند. این کار معمولاً از طریق وارد کردن نام کاربری و رمز عبور انجام می‌شود. برنامه وب (به عنوان سرور احراز هویت) اعتبار کاربر را بررسی می‌کند.
  • صدور JWT: اگر اعتبار کاربر با موفقیت تأیید شود، سرور احراز هویت یک JWT جدید ایجاد می‌کند. این JWT شامل ادعاهایی در مورد هویت کاربر (مانند شناسه کاربری، نام کاربری) و احتمالاً مجوزهای او (مانند نقش‌ها) است. سرور سپس این JWT را با استفاده از یک کلید مخفی یا کلید خصوصی امضا می‌کند. کلید مورد استفاده برای امضا فقط در اختیار سرور است و برای جلوگیری از جعل توکن بسیار مهم است.
  • ارسال JWT به کلاینت: سرور JWT امضا شده را به کلاینت (معمولاً مرورگر وب یا برنامه موبایل) ارسال می‌کند. این ارسال می‌تواند از طریق روش‌های مختلفی انجام شود، اما معمولاً از طریق هدر Authorization با پیشوند Bearer (به عنوان مثال، Authorization: Bearer ) یا از طریق یک کوکی انجام می‌شود.
  • ذخیره سازی JWT در کلاینت: کلاینت JWT را ذخیره می‌کند. رایج‌ترین روش‌ها شامل ذخیره در حافظه مرورگر (برای مدت کوتاه)، Local Storage یا Session Storage (برای برنامه‌های وب) یا در حافظه برنامه (برای برنامه‌های موبایل) است. توجه به امنیت محل ذخیره سازی JWT در کلاینت بسیار مهم است.
  • ارسال JWT در درخواست‌های بعدی: هر بار که کلاینت می‌خواهد به یک منبع محافظت‌شده در سرور دسترسی پیدا کند، JWT را در درخواست HTTP خود ارسال می‌کند. این معمولاً از طریق هدر Authorization با پیشوند Bearer انجام می‌شود.

 

دریافت و اعتبارسنجی JWT در سرور:

سرور دریافت‌کننده درخواست، JWT موجود در هدر Authorization را استخراج می‌کند. سپس، سرور باید اصالت JWT را با انجام مراحل زیر تأیید کند:

  • بررسی امضا: سرور از کلید مخفی (یا کلید عمومی مربوطه در صورت استفاده از الگوریتم نامتقارن) برای تأیید امضای JWT استفاده می‌کند. اگر امضا معتبر نباشد، به این معنی است که توکن دستکاری شده است و درخواست باید رد شود.
  • بررسی ادعاها: سرور ادعاهای موجود در payload JWT را بررسی می‌کند. این شامل موارد زیر است:
    • تاریخ انقضا (exp): بررسی می‌کند که آیا توکن هنوز منقضی نشده است.
    • صادرکننده (iss): بررسی می‌کند که آیا توکن توسط یک نهاد معتبر صادر شده است.
    • مخاطب (aud): بررسی می‌کند که آیا توکن برای این سرور در نظر گرفته شده است.
    • ادعاهای سفارشی: بررسی ادعاهای خصوصی برای اطمینان از اینکه کاربر مجوزهای لازم برای دسترسی به منبع مورد نظر را دارد.
  • اعطای مجوز دسترسی: اگر امضا معتبر باشد و تمام ادعاهای لازم نیز تأیید شوند، سرور هویت کاربر را تأیید کرده و بر اساس مجوزهای موجود در JWT (یا با جستجو در پایگاه داده بر اساس اطلاعات موجود در JWT)، اجازه دسترسی به منبع محافظت‌شده را صادر می‌کند.

 

کاربردهای مجوزدهی با JWT: فراتر از احراز هویت
اگرچه JWT اغلب برای احراز هویت استفاده می‌شود، اما کاربردهای آن فراتر از این است:

  • مجوزدهی (Authorization): همانطور که در بالا توضیح داده شد، ادعاهای موجود در JWT می‌توانند برای تعیین مجوزهای کاربر و کنترل دسترسی او به منابع مختلف استفاده شوند.
  • انتقال اطلاعات امن: از آنجایی که JWT امضا شده است، می‌توان از آن برای انتقال امن اطلاعات بین طرفین استفاده کرد. گیرنده می‌تواند مطمئن باشد که اطلاعات از منبع معتبری آمده و دستکاری نشده است.
  • احراز هویت یکپارچه (Single Sign-On - SSO): JWT می‌تواند برای پیاده‌سازی SSO استفاده شود. پس از اینکه کاربر یک بار احراز هویت شد، یک JWT صادر می‌شود که می‌تواند برای دسترسی به چندین برنامه مختلف بدون نیاز به ورود مجدد استفاده شود.
  • برنامه‌های تک صفحه‌ای (Single-Page Applications - SPAs) و برنامه‌های موبایل: JWT به خوبی با معماری‌های مدرن وب و موبایل سازگار است، زیرا مبتنی بر HTTP بوده و نیازی به حفظ وضعیت در سمت سرور ندارد (Stateless).


مثال در ASP.NET Core MVC: پیاده‌سازی گام به گام
برای درک بهتر نحوه استفاده از JWT در یک برنامه ASP.NET Core MVC، یک مثال ساده را در نظر می‌گیریم:

نصب بسته‌های NuGet: ابتدا باید بسته‌های NuGet مورد نیاز برای کار با JWT را نصب کنیم. رایج‌ترین بسته System.IdentityModel.Tokens.Jwt و همچنین بسته Microsoft.AspNetCore.Authentication.JwtBearer برای فعال کردن احراز هویت مبتنی بر JWT در ASP.NET Core است. می‌توانید این بسته‌ها را از طریق NuGet Package Manager در Visual Studio یا با استفاده از دستورات .NET CLI نصب کنید:

dotnet add package System.IdentityModel.Tokens.Jwt
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer


ایجاد یک سرویس برای تولید JWT: یک کلاس ایجاد کنید که مسئولیت تولید JWT را بر عهده داشته باشد. این سرویس به یک کلید مخفی برای امضای توکن نیاز دارد. بهتر است این کلید را از طریق تنظیمات برنامه (appsettings.json) پیکربندی کنید.

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
public class JwtService
{
    private readonly string _secretKey;
    private readonly string _issuer;
    private readonly string _audience;
    public JwtService(IConfiguration configuration)
    {
        _secretKey = configuration["Jwt:SecretKey"];
        _issuer = configuration["Jwt:Issuer"];
        _audience = configuration["Jwt:Audience"];
    }
    public string GenerateToken(string userId, string username, List roles)
    {
        var claims = new List
        {
            new Claim(ClaimTypes.NameIdentifier, userId),
            new Claim(ClaimTypes.Name, username)
        };
        foreach (var role in roles)
        {
            claims.Add(new Claim(ClaimTypes.Role, role));
        }
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secretKey));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        var token = new JwtSecurityToken(
            issuer: _issuer,
            audience: _audience,
            claims: claims,
            expires: DateTime.Now.AddHours(1), // زمان انقضا را تنظیم کنید
            signingCredentials: creds
        );
        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

پیکربندی احراز هویت JWT در Startup.cs: در متد ConfigureServices کلاس Startup.cs، سرویس احراز هویت JWT را پیکربندی کنید.

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
public void ConfigureServices(IServiceCollection services)
{
    // ... سایر پیکربندی‌های سرویس
    services.AddSingleton(); // ثبت سرویس JwtService
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = Configuration["Jwt:Issuer"],
                ValidAudience = Configuration["Jwt:Audience"],
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:SecretKey"]))
            };
        });
    services.AddAuthorization(); // برای استفاده از ویژگی‌های [Authorize]
    services.AddControllersWithViews();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ... سایر پیکربندی‌های pipeline
    app.UseAuthentication(); // فعال کردن مکانیزم احراز هویت
    app.UseAuthorization();  // فعال کردن مکانیزم مجوزدهی
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}


ایجاد یک کنترلر برای احراز هویت و صدور توکن: یک کنترلر ایجاد کنید که endpoint برای احراز هویت کاربر و صدور JWT را ارائه دهد.

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly JwtService _jwtService;
    public AuthController(JwtService jwtService)
    {
        _jwtService = jwtService;
    }
    [HttpPost("login")]
    public IActionResult Login([FromBody] UserLoginModel model)
    {
        // در اینجا باید منطق احراز هویت کاربر را پیاده سازی کنید
        // برای مثال، بررسی نام کاربری و رمز عبور در پایگاه داده
        if (model.Username == "testuser" && model.Password == "password")
        {
            var userId = "1"; // شناسه کاربر
            var username = model.Username;
            var roles = new List { "User" }; // نقش‌های کاربر
            var token = _jwtService.GenerateToken(userId, username, roles);
            return Ok(new { Token = token });
        }
        return Unauthorized();
    }
}
public class UserLoginModel
{
    public string Username { get; set; }
    public string Password { get; set; }
}

محافظت از Endpointها با [Authorize]: برای محافظت از اکشن‌های کنترلر یا کل کنترلر، می‌توانید از attribute [Authorize] استفاده کنید.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
[Authorize] // برای دسترسی به این کنترلر نیاز به احراز هویت است
public class ProtectedController : ControllerBase
{
    [HttpGet("data")]
    public IActionResult GetData()
    {
        return Ok(new { Message = "این داده محافظت شده است و فقط کاربران احراز هویت شده می‌توانند به آن دسترسی داشته باشند." });
    }
    [HttpGet("admin-data")]
    [Authorize(Roles = "Admin")] // فقط کاربرانی با نقش "Admin" می‌توانند به این اکشن دسترسی داشته باشند
    public IActionResult GetAdminData()
    {
        return Ok(new { Message = "این داده فقط برای مدیران قابل دسترسی است." });
    }
}

ارسال توکن از طرف کلاینت: کلاینت پس از دریافت توکن از endpoint /api/Auth/login، باید آن را در هدر Authorization در درخواست‌های بعدی به endpointهای محافظت‌شده ارسال کند. برای مثال، با استفاده از JavaScript در یک برنامه وب:

fetch('/api/protected/data', {
    headers: {
        'Authorization': 'Bearer ' + localStorage.getItem('authToken') // فرض بر اینکه توکن در localStorage ذخیره شده است
    }
})
.then(response => response.json())
.then(data => console.log(data));


نکات امنیتی مهم در استفاده از JWT
از یک کلید مخفی قوی و ایمن استفاده کنید: کلید مخفی (یا کلید خصوصی) برای امضای JWT بسیار حساس است. آن را در یک مکان امن نگهداری کنید و هرگز آن را در سمت کلاینت قرار ندهید

 

پیاده سازی اختصاصی

حال بیاید فرض کنیم که میخواهیم میکانسیم JWT را بصورت اختصاصی پیاده سازی کنیم بدون آنکه از کتابخانه های رایج و قابل دانلود استفاده کنیم.

پیاده سازی اختصاصی نیازمند فهم و دریافت کامل پروسه از پیش گفته شده در این مقاله خواهد بود.

صرفا جهت یادآوری باید اشاره ای به ساختار JWT داشت:

[Base64Url(Header)].[Base64Url(Payload)].[Signature]

بنابراین جهت ایجاد Payload که حاوی اطلاعات کاربر می باشد نیازمند کلاسی هستیم:

public class UserToken
{
    public string UserId { get; set; }
    public string Role { get; set; }
    public long Exp { get; set; } // زمان انقضا بر اساس Unix 
}

ایجاد امضای مخصوص:

public string GenerateKingToken(CustomToken payload, string secretKey)
{
    string header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
    string encodedHeader = Base64UrlEncode(Encoding.UTF8.GetBytes(header));

    string payloadJson = JsonConvert.SerializeObject(payload);
    string encodedPayload = Base64UrlEncode(Encoding.UTF8.GetBytes(payloadJson));

    string signature = ComputeHMACSHA256($"{encodedHeader}.{encodedPayload}", secretKey);
    return $"{encodedHeader}.{encodedPayload}.{signature}";
}

توضیحاتی در خصوص چرایی استفاده از لاین:

    string header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
  • alg: این کلید، الگورتیم استفاده شده را تعیین میکند.
    • که از الگورتیم های رایج میتوان به hs256 - rs256 - es256 اشاره داشت.
alg Description Type Symmetric?
HS256 HMAC using SHA-256 HMAC ✅ Yes
RS256
RSA SHA-256 (Public/Private key)  
Asymmetric ❌ No
ES256
ECDSA using P-256 and SHA-256  
Asymmetric     ❌ No

 

  • و مشخصات الگورتیم ها:

  Type Security     Use Case
HS256 Symmetric Shared secret key     Simpler setup, fast
RS256 Asymmetric Public/private key     Better separation of roles
ES256 Asymmetric Smaller, efficient     Mobile, IoT, modern security

 

  • اگر هم برای frontend و هم برای backend قصد استفاده از JWT را دارید، بهترین و سریعترین روش hs256 خواهد بود
  • در صورت علاقه شما را دعوت به مطالعه مقاله بررسی عمیق الگوریتم های HS256، RS256 و ES256: کاربردها و ملاحظات امنیتی در همین سایت دعوت میکنم.
  • typ: این کلید به ما میگوید: «هی این JWT ماست» و کارکرد بیشتری ندارد! بنابراین این گزینه کاملا اختیاری است.

سپس کلاسی جهت بررسی صلاحیت jwt مان مینویسیم:

public bool ValidateKingToken(string token, string secretKey, out UserToken customPayload)
{
    customPayload = null;

    var parts = token.Split('.');
    if (parts.Length != 3) return false;

    var header = parts[0];
    var payload = parts[1];
    var signature = parts[2];

    string expectedSignature = ComputeHMACSHA256($"{header}.{payload}", secretKey);
    if (signature != expectedSignature) return false;

    var jsonPayload = Encoding.UTF8.GetString(Base64UrlDecode(payload));
    customPayload = JsonConvert.DeserializeObject<UserToken>(jsonPayload);

    var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
    if (customPayload.Exp < now) return false;

    return true;
}

کلاسی جهت تولید الگورتیم: HMAC Helper

private string ComputeHMACSHA256(string text, string key)
{
    var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key));
    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(text));
    return Base64UrlEncode(hash);
}

کلاسی جهت تولید Base64Url Helpers:

public string Base64UrlEncode(byte[] input)
{
    return Convert.ToBase64String(input)
        .TrimEnd('=')
        .Replace('+', '-')
        .Replace('/', '_');
}

public byte[] Base64UrlDecode(string input)
{
    string padded = input
        .Replace('-', '+')
        .Replace('_', '/');

    switch (padded.Length % 4)
    {
        case 2: padded += "=="; break;
        case 3: padded += "="; break;
    }

    return Convert.FromBase64String(padded);
}

در این اختصاصی سازی قصد استفاده از Cookie را دارم، ولی شما میتوانید از روش های دیگری چون localstorage نیز استفاده کنید:

var tokenPayload = new CustomToken
{
    UserId = user.Id.ToString(),
    Role = user.Role,
    Exp = DateTimeOffset.UtcNow.AddMinutes(30).ToUnixTimeSeconds()
};

string token = GenerateCustomToken(tokenPayload, secretKey);

Response.Cookies.Append("customAuth", token, new CookieOptions
{
    HttpOnly = true,
    Secure = false, // Set to true in production
    SameSite = SameSiteMode.Strict,
    Expires = DateTime.UtcNow.AddMinutes(30)
});

و در نهایت نیاز به یک middleware جهت بررسی همیشگی jwt را داریم. البته میتوانید از روش های بیشماری نظیر روش مورد علاقه بنده یعنی Attribute-Based نیز استفاده کنید:

public class CustomTokenMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _secretKey;

    public CustomTokenMiddleware(RequestDelegate next, IConfiguration config)
    {
        _next = next;
        _secretKey = config["JwtSettings:Key"];
    }

    public async Task Invoke(HttpContext context)
    {
        var token = context.Request.Cookies["customAuth"];

        if (!string.IsNullOrEmpty(token))
        {
            if (ValidateCustomToken(token, _secretKey, out var payload))
            {
                var claims = new[]
                {
                    new Claim(ClaimTypes.NameIdentifier, payload.UserId),
                    new Claim(ClaimTypes.Role, payload.Role)
                };

                var identity = new ClaimsIdentity(claims, "CustomScheme");
                context.User = new ClaimsPrincipal(identity);
            }
        }

        await _next(context);
    }
}

نکته: فعال سازی Middleware در program.cs فراموش نشود:

app.UseMiddleware<CustomTokenMiddleware>();

حال میتوانید از قابلیت های چون [Authorize] و User.Identity.IsAuthenticated بصورت دستی نیز استفاده کنید.

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

0 نظر

    هنوز نظری برای این مقاله ثبت نشده است.

نظر خود را اینجا بگذارید

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