المصادقة والترخيص في Blazor: دليل عملي
إذا كنت قد عملت مع ASP.NET MVC أو Razor Pages، فمن المحتمل أن يكون لديك نموذج عقلي لكيفية عمل المصادقة: تعترض البرامج الوسيطة الطلب، وتتحقق من ملف تعريف الارتباط أو الرمز المميز، وتملأ HttpContext.User، وأنت خارج السباقات. يقوم Blazor بتغيير هذا النموذج العقلي بطرق دقيقة ولكنها مهمة - وإذا لم تفهم هذه الاختلافات مبكرًا، فسوف ينتهي بك الأمر إلى تصحيح مشكلات المصادقة التي تبدو مستحيلة.
في هذا المنشور، أريد أن أتعرف على كيفية عمل المصادقة والتفويض فعليًا في Blazor، والذي يغطي نماذج استضافة الخادم وWebAssembly. سننتقل من الأساسيات إلى موفري الخدمات المخصصين وبروتوكول OAuth الخارجي والمزالق التي رأيتها حتى مطوري .NET ذوي الخبرة.
لماذا تختلف المصادقة في Blazor
في ASP.NET التقليدي، كل تفاعل للمستخدم هو طلب HTTP. يتحقق الخادم من صحة بيانات الاعتماد، ويعين ملف تعريف الارتباط، وكل طلب لاحق يحمل ملف تعريف الارتباط هذا. خط أنابيب المصادقة خطي ويمكن التنبؤ به.
يعمل Blazor Server عبر اتصال SignalR المستمر. بعد أن يقوم طلب HTTP الأولي بتحميل الصفحة، تحدث جميع التفاعلات اللاحقة عبر WebSockets. لا يوجد طلب HTTP جديد لكل نقرة على الزر، لذلك لا يتم إعادة تنفيذ البرامج الوسيطة في كل تفاعل. يتوفر HttpContext أثناء الاتصال الأولي، ولكن الاعتماد عليه طوال عمر الدائرة يعد بمثابة وصفة للأخطاء.
يعمل Blazor WebAssembly بالكامل في المتصفح. لا يوجد HttpContext من جانب الخادم على الإطلاق. يجب جلب حالة المصادقة من واجهة برمجة التطبيقات (API)، وتخزينها من جانب العميل، وإدارتها من خلال الرموز المميزة - عادةً JWTs. يثق الخادم بالعميل فقط فيما يتعلق بالتحقق من صحة الرمز المميز.
وهذا يعني أن Blazor يحتاج إلى تجريد خاص به لحالة المصادقة، وهو تجريد يعمل بغض النظر عن طراز الاستضافة. هذا التجريد هو AuthenticationStateProvider.
حالة المصادقة: المؤسسة
يقع AuthenticationStateProvider في قلب نظام مصادقة Blazor. هذه فئة مجردة تكشف عن طريقة حاسمة واحدة:
public abstract Task<AuthenticationState> GetAuthenticationStateAsync();
يلتف الكائن AuthenticationState بـ ClaimsPrincipal — وهو نفس نموذج الهوية المستخدم في .NET. لا تتصل المكونات بملفات تعريف الارتباط أو الرموز المميزة مباشرةً؛ يسألون AuthenticationStateProvider عن الحالة الحالية.
لجعل هذه الحالة متاحة لشجرة المكونات بأكملها، يوفر Blazor CascadingAuthenticationState. عادةً ما تقوم بتغليف جهاز التوجيه الخاص بك به في App.razor أو في تخطيطك:
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
<p>You're not authorized to view this page.</p>
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Page not found.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
يقوم AuthorizeRouteView بمهمة مزدوجة هنا: فهو يتحقق مما إذا كان المستخدم قد تمت مصادقته وتفويضه قبل عرض مكون الصفحة المطابق، ويوفر واجهة مستخدم احتياطية عندما لا يكون الأمر كذلك.
في .NET 8 والإصدارات الأحدث مع نموذج Blazor الموحد، ستقوم بتكوين هذا في App.razor وسيتعامل إطار العمل مع المعلمة المتتالية تلقائيًا عند استخدام AddCascadingAuthenticationState() في تسجيل الخدمة الخاصة بك.
تكامل هوية ASP.NET
بالنسبة لمعظم المشاريع، لا تحتاج إلى إنشاء مصادقة من الصفر. تمنحك هوية ASP.NET إدارة المستخدم، وتجزئة كلمة المرور، والمصادقة الثنائية، وتأكيد الحساب خارج الصندوق.يبدأ إعداده باستخدام Blazor في Program.cs:
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
});
باستخدام قالب Blazor Web App في .NET 8+، تستخدم واجهة مستخدم الهوية المدعومة مكونات Razor مباشرةً. يمكنك الحصول على صفحات تسجيل الدخول والتسجيل وإدارة الحساب التي تتكامل بشكل طبيعي مع بقية تطبيق Blazor الخاص بك - لم يعد هناك مزيج غريب من صفحات Razor ومكونات Blazor.
يرث ApplicationDbContext من IdentityDbContext وستحتاج إلى تشغيل عمليات الترحيل لإنشاء جداول الهوية:
public class ApplicationDbContext : IdentityDbContext<IdentityUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options) { }
}
dotnet ef migrations add InitialIdentity
dotnet ef database update
مكون AuthorizeView
بمجرد تدفق حالة المصادقة عبر شجرة المكونات الخاصة بك، يتيح لك AuthorizeView عرض واجهة المستخدم بشكل مشروط:
<AuthorizeView>
<Authorized>
<p>Welcome, @context.User.Identity?.Name!</p>
<a href="/account/manage">Manage Account</a>
<form method="post" action="/account/logout">
<button type="submit">Log Out</button>
</form>
</Authorized>
<NotAuthorized>
<a href="/account/login">Log In</a>
<a href="/account/register">Register</a>
</NotAuthorized>
</AuthorizeView>
تتيح لك المعلمة context داخل <Authorized> الوصول إلى AuthenticationState، حتى تتمكن من فحص المطالبات والأدوار وهوية المستخدم مباشرة في الترميز الخاص بك.
يمكنك أيضًا استخدام AuthorizeView مع الأدوار والسياسات:
<AuthorizeView Roles="Admin,Moderator">
<Authorized>
<button @onclick="DeletePost">Delete Post</button>
</Authorized>
</AuthorizeView>
<AuthorizeView Policy="CanEditArticles">
<Authorized>
<button @onclick="EditArticle">Edit</button>
</Authorized>
</AuthorizeView>
هناك شيء واحد يجب أخذه في الاعتبار: AuthorizeView هو أحد اهتمامات واجهة المستخدم. فهو يخفي العناصر أو يظهرها، لكنه لا يحمي المنطق الأساسي. إذا كان بإمكان شخص ما الاتصال بنقطة نهاية واجهة برمجة التطبيقات (API) الخاصة بك أو استدعاء طريقتك مباشرة، فسيتجاوز AuthorizeView بالكامل. قم دائمًا بفرض الترخيص على جانب الخادم أيضًا.
السمة [التفويض]
لحماية صفحة بأكملها، قم بتطبيق السمة [Authorize]:
@page "/admin/dashboard"
@attribute [Authorize(Roles = "Admin")]
<h1>Admin Dashboard</h1>
<p>Only administrators can see this page.</p>
عندما ينتقل مستخدم غير مصادق إلى هذه الصفحة، يبدأ AuthorizeRouteView ويعرض القالب <NotAuthorized> الذي حددته مسبقًا. يمكنك إعادة التوجيه إلى صفحة تسجيل الدخول بدلاً من ذلك عن طريق التعامل مع حالة NotAuthorized مع التنقل:
<NotAuthorized>
@if (!context.User.Identity?.IsAuthenticated ?? true)
{
<RedirectToLogin />
}
else
{
<p>You don't have permission to access this page.</p>
}
</NotAuthorized>
قد يبدو مكون RedirectToLogin البسيط كما يلي:
@inject NavigationManager Navigation
@code {
protected override void OnInitialized()
{
var returnUrl = Uri.EscapeDataString(Navigation.Uri);
Navigation.NavigateTo($"/account/login?returnUrl={returnUrl}", forceLoad: true);
}
}
يعد forceLoad: true مهمًا هنا - فأنت تريد تنقلًا فعليًا عبر HTTP حتى تتمكن البرامج الوسيطة للمصادقة من جانب الخادم من التعامل مع تدفق تسجيل الدخول بشكل صحيح.
التفويض المستند إلى الدور والسياسة
الأدوار هي أبسط نموذج: قم بتعيين المستخدمين إلى مجموعات مثل “المسؤول” أو “المحرر”، ثم تحقق من العضوية. لكن السياسات تمنحك مرونة أكبر بكثير.
تسجيل السياسات في Program.cs:
builder.Services.AddAuthorizationCore(options =>
{
options.AddPolicy("CanPublish", policy =>
policy.RequireClaim("Permission", "Publish"));
options.AddPolicy("MinimumAge", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(18)));
options.AddPolicy("PremiumUser", policy =>
policy.RequireRole("Premium")
.RequireClaim("Subscription", "Active"));
});
المتطلبات المخصصة تحتاج إلى معالج:
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge) => MinimumAge = minimumAge;
}
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
var dateOfBirthClaim = context.User.FindFirst("DateOfBirth");
if (dateOfBirthClaim is null)
return Task.CompletedTask;
var dateOfBirth = DateOnly.Parse(dateOfBirthClaim.Value);
var age = DateOnly.FromDateTime(DateTime.Today).Year - dateOfBirth.Year;
if (age >= requirement.MinimumAge)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
تسجيل المعالج:
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
في المكونات، يمكنك أيضًا التحقق من الترخيص برمجيًا عندما تحتاج إلى منطق ديناميكي:
@inject IAuthorizationService AuthorizationService
@inject AuthenticationStateProvider AuthStateProvider
@code {
private bool canPublish;
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var result = await AuthorizationService.AuthorizeAsync(
authState.User, "CanPublish");
canPublish = result.Succeeded;
}
}
موفري OAuth الخارجيين
يعد دعم “تسجيل الدخول باستخدام Google” أو “تسجيل الدخول باستخدام GitHub” أمرًا مباشرًا باستخدام البرنامج الوسيط لمصادقة ASP.NET. تم تكوينها من جانب الخادم نظرًا لأن تدفق OAuth يتطلب عمليات إعادة توجيه HTTP.
builder.Services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = builder.Configuration["Auth:Google:ClientId"]!;
options.ClientSecret = builder.Configuration["Auth:Google:ClientSecret"]!;
options.Scope.Add("profile");
})
.AddMicrosoftAccount(options =>
{
options.ClientId = builder.Configuration["Auth:Microsoft:ClientId"]!;
options.ClientSecret = builder.Configuration["Auth:Microsoft:ClientSecret"]!;
})
.AddGitHub(options =>
{
options.ClientId = builder.Configuration["Auth:GitHub:ClientId"]!;
options.ClientSecret = builder.Configuration["Auth:GitHub:ClientSecret"]!;
});
بالنسبة إلى GitHub، ستحتاج إلى حزمة AspNet.Security.OAuth.GitHub NuGet، لأنها غير مضمنة في مكتبات ASP.NET الافتراضية.
توفر واجهة مستخدم تسجيل الدخول بعد ذلك روابط تؤدي إلى التحدي الخارجي:
@page "/account/external-login"
<h2>Sign in with an external provider</h2>
<form method="post" action="/api/auth/external-login">
<button type="submit" name="provider" value="Google">
Sign in with Google
</button>
<button type="submit" name="provider" value="Microsoft">
Sign in with Microsoft
</button>
<button type="submit" name="provider" value="GitHub">
Sign in with GitHub
</button>
</form>
تقوم نقطة نهاية واجهة برمجة التطبيقات (API) بتشغيل التحدي وتتعامل مع رد الاتصال:
app.MapPost("/api/auth/external-login", (string provider, HttpContext context) =>
{
var properties = new AuthenticationProperties
{
RedirectUri = "/api/auth/external-callback"
};
return Results.Challenge(properties, [provider]);
});
تتطلب المصادقة الخارجية في Blazor دائمًا التنقل الكامل للصفحة - لا يمكنك إكمال إعادة توجيه OAuth داخل دائرة SignalR أو تطبيق WebAssembly دون المرور عبر الخادم.
المصادقة المستندة إلى الرمز المميز في Blazor WebAssemblyيتم تشغيل Blazor WebAssembly على العميل، لذا لا يتم تطبيق المصادقة المستندة إلى ملفات تعريف الارتباط بنفس الطريقة. بدلاً من ذلك، عادةً ما تستخدم JWTs المخزنة في الذاكرة والمرفقة بطلبات HTTP الصادرة.
يوفر إطار العمل AuthorizationMessageHandler لإرفاق الرموز المميزة تلقائيًا:
builder.Services.AddHttpClient("API",
client => client.BaseAddress = new Uri("https://api.example.com"))
.AddHttpMessageHandler<AuthorizationMessageHandler>();
builder.Services.AddScoped(sp =>
sp.GetRequiredService<IHttpClientFactory>().CreateClient("API"));
بالنسبة لتطبيقات Blazor WASM المستقلة التي تقوم بالمصادقة مقابل واجهة برمجة التطبيقات الخاصة بها، فسوف تقوم بتنفيذ AuthenticationStateProvider المخصص الذي يوزع JWT:
public class JwtAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly ILocalStorageService _localStorage;
private readonly HttpClient _httpClient;
public JwtAuthenticationStateProvider(
ILocalStorageService localStorage,
HttpClient httpClient)
{
_localStorage = localStorage;
_httpClient = httpClient;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var token = await _localStorage.GetItemAsync<string>("authToken");
if (string.IsNullOrWhiteSpace(token))
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var claims = ParseClaimsFromJwt(token);
var identity = new ClaimsIdentity(claims, "jwt");
return new AuthenticationState(new ClaimsPrincipal(identity));
}
public void NotifyAuthStateChanged()
{
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var payload = jwt.Split('.')[1];
var padded = payload.Length % 4 switch
{
2 => payload + "==",
3 => payload + "=",
_ => payload
};
var bytes = Convert.FromBase64String(padded);
var json = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(bytes);
return json?.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()))
?? Enumerable.Empty<Claim>();
}
}
بعد تسجيل الدخول بنجاح، يمكنك تخزين الرمز المميز وإخطار حالة المصادقة:
public class AuthService
{
private readonly HttpClient _httpClient;
private readonly ILocalStorageService _localStorage;
private readonly JwtAuthenticationStateProvider _authStateProvider;
public AuthService(
HttpClient httpClient,
ILocalStorageService localStorage,
AuthenticationStateProvider authStateProvider)
{
_httpClient = httpClient;
_localStorage = localStorage;
_authStateProvider = (JwtAuthenticationStateProvider)authStateProvider;
}
public async Task<bool> LoginAsync(string email, string password)
{
var response = await _httpClient.PostAsJsonAsync("/api/auth/login",
new { Email = email, Password = password });
if (!response.IsSuccessStatusCode)
return false;
var result = await response.Content
.ReadFromJsonAsync<LoginResponse>();
await _localStorage.SetItemAsync("authToken", result!.Token);
_authStateProvider.NotifyAuthStateChanged();
return true;
}
public async Task LogoutAsync()
{
await _localStorage.RemoveItemAsync("authToken");
_authStateProvider.NotifyAuthStateChanged();
}
}
كلمة تحذير: تخزين JWTs في localStorage يعرضهم لهجمات XSS. بالنسبة للتطبيقات ذات الأمان الأعلى، فكر في الاحتفاظ بالرموز المميزة في الذاكرة فقط واستخدام الرموز المميزة للتحديث، أو اعتماد نمط الواجهة الخلفية للواجهة (BFF) حيث يدير الخادم الرموز المميزة ويستخدم العميل ملفات تعريف ارتباط HTTP فقط.
خادم Blazor مقابل WebAssembly: الاعتبارات الأمنية
يغير نموذج الاستضافة وضع الأمان لديك بشكل أساسي.
**يحتفظ Blazor Server بكل منطق المكونات لديك على الخادم. يرى العميل فقط اختلافات HTML المقدمة عبر SignalR. هذا يعني:
- المنطق الحساس لا يترك الخادم أبدًا
- يمكنك الوصول إلى قواعد البيانات والخدمات الداخلية مباشرة من المكونات
- تأتي حالة المصادقة من
HttpContextللخادم عند الاتصال الأولي - يمكن للدائرة أن تعيش أكثر من ملف تعريف ارتباط المصادقة - إذا انتهت صلاحية ملف تعريف الارتباط الخاص بالمستخدم، تظل الدائرة حية حتى يتم قطع اتصالها
- يجب عليك التعامل مع انقطاع الدائرة بأمان وإعادة التحقق من صحة حالة المصادقة عند إعادة الاتصال
Blazor WebAssembly يعمل بالكامل في المتصفح. هذا يعني:
- كل كود المكون الخاص بك قابل للتنزيل والفحص
- لا تضع أبدًا أسرارًا أو سلاسل اتصال أو منطق عمل حساس في مكونات WASM
- يتم فرض المصادقة فقط على العميل لتجربة المستخدم؛ يجب أن يحدث التنفيذ الحقيقي في طبقة API الخاصة بك
- إدارة الرمز المميز هي مسؤوليتك
- فكر في استخدام النموذج المستضاف حيث يتعامل مشروع الخادم مع المصادقة ويخدم تطبيق WASM
أحد الأنماط التي أوصي بها لتطبيقات WebAssembly هو التعامل مع كل مكون كما لو كان “واجهة مستخدم غير موثوق بها” وكل نقطة نهاية لواجهة برمجة التطبيقات كما لو كان يتم استدعاؤها بواسطة عميل غير معروف. التحقق من صحة كل شيء من جانب الخادم بغض النظر عما يتحقق منه العميل.
إنشاء AuthenticationStateProvider مخصص
في بعض الأحيان لا يتناسب الموفرون المضمنون مع بنيتك. ربما تقوم بالتكامل مع نظام مصادقة قديم، أو تحتاج إلى استطلاع تغييرات حالة المصادقة. فيما يلي موفر مخصص أكثر اكتمالاً لخادم Blazor:
public class CustomAuthStateProvider : AuthenticationStateProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IUserService _userService;
public CustomAuthStateProvider(
IHttpContextAccessor httpContextAccessor,
IUserService userService)
{
_httpContextAccessor = httpContextAccessor;
_userService = userService;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext?.User?.Identity?.IsAuthenticated != true)
return new AuthenticationState(
new ClaimsPrincipal(new ClaimsIdentity()));
var userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId is null)
return new AuthenticationState(
new ClaimsPrincipal(new ClaimsIdentity()));
var user = await _userService.GetUserWithClaimsAsync(userId);
if (user is null)
return new AuthenticationState(
new ClaimsPrincipal(new ClaimsIdentity()));
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id),
new(ClaimTypes.Name, user.DisplayName),
new(ClaimTypes.Email, user.Email)
};
claims.AddRange(user.Roles.Select(r => new Claim(ClaimTypes.Role, r)));
claims.AddRange(user.Permissions.Select(p => new Claim("Permission", p)));
var identity = new ClaimsIdentity(claims, "Custom");
return new AuthenticationState(new ClaimsPrincipal(identity));
}
public void MarkUserAsAuthenticated(string userId)
{
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public void MarkUserAsLoggedOut()
{
var anonymous = new ClaimsPrincipal(new ClaimsIdentity());
var authState = Task.FromResult(new AuthenticationState(anonymous));
NotifyAuthenticationStateChanged(authState);
}
}
سجله في Program.cs:
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
builder.Services.AddScoped<CustomAuthStateProvider>();
الفكرة الأساسية هنا هي NotifyAuthenticationStateChanged — يؤدي استدعاؤها إلى تشغيل المعلمة المتتالية للتحديث، والتي تعيد تقييم كل AuthorizeView وAuthorizeRouteView في شجرة المكونات الخاصة بك. هذه هي الطريقة التي تجعل واجهة المستخدم تتفاعل مع أحداث تسجيل الدخول/الخروج دون تحديث كامل للصفحة.
المخاطر والحلول الشائعة
بعد العمل مع Blazor auth عبر العديد من المشاريع، إليك المشكلات التي أراها في أغلب الأحيان:
1. استخدام HttpContext في مكونات خادم BlazorHttpContext متاح أثناء طلب HTTP الأولي، ولكنه null أو قديم أثناء تفاعلات SignalR. لا تقم بإدخال IHttpContextAccessor في المكونات التي تعمل بعد العرض الأولي.
الحل: التقط ما تحتاجه من HttpContext أثناء التهيئة وقم بتخزينه في خدمة محددة النطاق:
public class UserContext
{
public string? UserId { get; set; }
public string? AccessToken { get; set; }
}
// In a component that renders during the initial HTTP request:
@inject IHttpContextAccessor HttpContextAccessor
@inject UserContext UserContext
@code {
protected override void OnInitialized()
{
var context = HttpContextAccessor.HttpContext;
UserContext.UserId = context?.User.FindFirstValue(ClaimTypes.NameIdentifier);
UserContext.AccessToken = context?.Request.Headers.Authorization
.ToString().Replace("Bearer ", "");
}
}
2. عدم تحديث حالة المصادقة بعد تسجيل الدخول
يمكنك استدعاء واجهة برمجة تطبيقات تسجيل الدخول الخاصة بك، وقد نجحت، ولكن واجهة المستخدم لا تزال تعرض “تسجيل الدخول”.
الحل: يجب عليك الاتصال بـ NotifyAuthenticationStateChanged على AuthenticationStateProvider بعد تغيير حالة المصادقة. لا يكتشف إطار العمل بطريقة سحرية أنه تم تخزين الرمز المميز أو تعيين ملف تعريف الارتباط.
3. سمة الترخيص لا تعمل على المكونات
يمكنك إضافة [Authorize] إلى أحد المكونات ولكنه لا يمنع المستخدمين غير المصادقين.
الحل: تأكد من أنك تستخدم AuthorizeRouteView بدلاً من RouteView العادي في App.razor. يتجاهل المعيار RouteView سمات الترخيص بالكامل.
4. العرض المسبق يكسر حالة المصادقة
أثناء العرض المسبق من جانب الخادم في Blazor WebAssembly، لا يوجد رمز مصادقة متاح. يتم عرض المكونات على أنها غير مصادقة، ثم تومض إلى حالة المصادقة بعد تحميل WASM.
الحل: إما تعطيل العرض المسبق للصفحات الحساسة للمصادقة باستخدام @rendermode InteractiveWebAssembly (بدون العرض المسبق)، أو التعامل مع حالة التحميل بأمان:
<AuthorizeView>
<Authorized>
<p>Welcome back, @context.User.Identity?.Name</p>
</Authorized>
<Authorizing>
<p>Loading...</p>
</Authorizing>
<NotAuthorized>
<a href="/login">Sign in</a>
</NotAuthorized>
</AuthorizeView>
5. انتهاء صلاحية الرمز المميز في الدوائر طويلة التشغيل
يمكن لدوائر Blazor Server البقاء على قيد الحياة لساعات. إذا انتهت صلاحية الرمز المميز أو الجلسة، فسيظل المستخدم “مصادقًا عليه” في واجهة المستخدم ولكن تبدأ استدعاءات واجهة برمجة التطبيقات بالفشل.
الحل: قم بإجراء فحص دوري أو استخدم RevalidatingServerAuthenticationStateProvider:
public class RevalidatingAuthStateProvider
: RevalidatingServerAuthenticationStateProvider
{
private readonly IServiceScopeFactory _scopeFactory;
public RevalidatingAuthStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory)
: base(loggerFactory)
{
_scopeFactory = scopeFactory;
}
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
await using var scope = _scopeFactory.CreateAsyncScope();
var userManager = scope.ServiceProvider
.GetRequiredService<UserManager<IdentityUser>>();
var user = await userManager.GetUserAsync(authenticationState.User);
return user is not null;
}
}
يؤدي هذا إلى التحقق من أن المستخدم لا يزال موجودًا (وأن ختم الأمان الخاص به لم يتغير) كل 30 دقيقة.
الخلاصة
تتطلب المصادقة والترخيص في Blazor تحولًا في التفكير من ASP.NET التقليدي للاستجابة للطلب. إن تجريد AuthenticationStateProvider هو المفتاح لفهم كيفية تناسب كل شيء معًا - بمجرد استيعاب ذلك، يتبع الباقي بشكل طبيعي.
بالنسبة لمعظم التطبيقات، ابدأ بهوية ASP.NET والقوالب المضمنة. إنهم يتعاملون مع الأعباء الثقيلة لإدارة المستخدم وتجزئة كلمة المرور وإنشاء الرمز المميز. طبقة من السياسات والتفويض المبني على المطالبات مع نمو متطلباتك. قم بإضافة موفري OAuth خارجيين عندما يتوقعهم المستخدمون.
نموذج الاستضافة مهم: يمنحك Blazor Server وضعًا أمنيًا أكثر تقليدية حيث يبقى الكود على الخادم، بينما يدفعك WebAssembly نحو التفكير بواجهة برمجة التطبيقات أولاً حيث يكون العميل غير موثوق به حسب التصميم. ولا يعتبر أي منهما أكثر أمانًا بطبيعته، بل لديهم فقط نماذج تهديد مختلفة.
أيًا كان النهج الذي تختاره، تذكر القاعدة الذهبية: التفويض في واجهة المستخدم مخصص لتجربة المستخدم، أما التفويض على الخادم فهو للأمان. قم دائمًا بتنفيذ كليهما.