JWT چیست؟
احراز هویت با توکن، به زبان خیلی ساده و همراه با مثال عملی ASP.NET Core
در این آموزش چه میسازیم؟
یک API کوچک داریم. کاربر با نام کاربری و رمز عبور وارد میشود، سرور یک JWT به او میدهد و کاربر با آن توکن به مسیرهای محافظتشده دسترسی پیدا میکند.
هدف این مطلب حفظ کردن اصطلاحات نیست؛ میخواهیم دقیقاً بفهمیم JWT چیست، چطور کار میکند و کجا باید مراقب امنیت باشیم.
۱. JWT چیست؟
JWT مخفف JSON Web Token است. به زبان ساده، JWT مثل یک کارت ورود دیجیتال است که سرور بعد از ورود موفق به کاربر میدهد.
فرض کنید وارد یک شرکت میشوید. نگهبان هویت شما را بررسی میکند و یک کارت مهمان میدهد. از آن لحظه برای ورود به اتاقهای مجاز، هر بار مدارک هویتی نشان نمیدهید؛ همان کارت را نشان میدهید.
در یک API نیز تقریباً همین اتفاق میافتد:
- نام کاربری و رمز عبور را یک بار به مسیر ورود میفرستید.
- سرور اطلاعات را بررسی میکند و یک JWT میسازد.
- در درخواستهای بعدی، JWT را همراه درخواست میفرستید.
- سرور اعتبار توکن و سطح دسترسی شما را بررسی میکند.
۲. روند ورود چگونه است؟
/login میفرستد.هدر درخواست محافظتشده به این شکل است:
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 را تغییر دهد، امضا دیگر معتبر نیست و سرور توکن را رد میکند.
۴. مثال عملی با 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"
}
}
کد کامل 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 جدید استفاده میشود.
Refresh Token معمولاً یک رشته تصادفی است، نه لزوماً JWT. آن را بهصورت هششده در دیتابیس نگه دارید و امکان لغو، انقضا و rotation را برای آن فراهم کنید.
۷. نکات امنیتی مهم
- همیشه HTTPS: بدون HTTPS، توکن ممکن است در مسیر شبکه دزدیده شود.
- عمر کوتاه: Access Token کوتاهعمر، خسارت ناشی از سرقت توکن را کاهش میدهد.
- کلید قوی: برای HS256 از یک کلید تصادفی و طولانی استفاده کنید و آن را از کد و Git بیرون نگه دارید.
- اعتبارسنجی کامل: امضا، صادرکننده، مخاطب و تاریخ انقضا را بررسی کنید.
- اطلاعات کم: فقط Claimهای لازم را داخل توکن بگذارید؛ JWT جای دیتابیس نیست.
- رمز عبور واقعی: رمز را هرگز به شکل ساده ذخیره نکنید؛ از ASP.NET Core Identity یا
PasswordHasherاستفاده کنید. - ذخیره در مرورگر: نگهداری طولانیمدت توکن در
localStorageدر برابر حملات XSS آسیبپذیر است. برای وباپها، معماری مبتنی بر کوکیHttpOnly،SecureوSameSiteرا بررسی کنید. - لغو دسترسی: JWT تا پایان عمر خود معمولاً معتبر میماند. برای خروج فوری، تغییر رمز یا مسدود کردن کاربر، راهکار revocation یا توکن کوتاهعمر لازم است.
۸. جمعبندی
- JWT یک کارت ورود دیجیتال و امضاشده است.
- سه بخش دارد: Header، Payload و Signature.
- Payload قابل خواندن است؛ اطلاعات محرمانه داخل آن نگذارید.
- توکن با هدر
Authorization: Bearer ...به API فرستاده میشود. - سرور باید امضا، صادرکننده، مخاطب و انقضا را بررسی کند.
- Access Token را کوتاهعمر نگه دارید و برای تمدید امن از Refresh Token استفاده کنید.
اگر JWT را «کارت ورود امضاشده» در نظر بگیرید، بیشتر مفاهیم آن ساده و قابل فهم میشوند.