کار با Authentication State از طریق کدنویسی
فرض کنید در کامپوننت HotelRoomUpsert.razor نمیخواهیم دسترسیها را به کمک اعمال ویژگی attribute [Authorize]@ محدود کنیم؛ میخواهیم اینکار را از طریق کدنویسی مستقیم انجام دهیم:
// ... @*@attribute [Authorize]*@ @code { [CascadingParameter] public Task<AuthenticationState> AuthenticationState { get; set; } protected override async Task OnInitializedAsync() { var authenticationState = await AuthenticationState; if (!authenticationState.User.Identity.IsAuthenticated) { var uri = new Uri(NavigationManager.Uri); NavigationManager.NavigateTo($"/identity/account/login?returnUrl={uri.LocalPath}"); } // ...
- سپس یک پارامتر ویژه را از نوع CascadingParameter، به نام AuthenticationState تعریف کردیم. این خاصیت از طریق کامپوننت CascadingAuthenticationState که در قسمت قبل به فایل BlazorServer.App\App.razor اضافه کردیم، تامین میشود.
- در آخر در روال رویدادگردان OnInitializedAsync، بر اساس آن میتوان به اطلاعات User جاری وارد شدهی به سیستم دسترسی یافت و برای مثال اگر اعتبارسنجی نشده بود، با استفاده از NavigationManager، او را به صفحهی لاگین هدایت میکنیم.
- در اینجا روش ارسال آدرس صفحهی فعلی را نیز مشاهده میکنید. این امر سبب میشود تا پس از لاگین، کاربر مجددا به همین صفحه هدایت شود.
authenticationState، امکانات بیشتری را نیز در اختیار ما قرار میدهد؛ برای مثال با استفاده از متد ()authenticationState.User.IsInRole آن میتوان دسترسی به قسمتی را بر اساس نقشهای خاصی محدود کرد.
ثبت کاربر ادمین Identity
در ادامه میخواهیم دسترسی به کامپوننتهای مختلف را بر اساس نقشها، محدود کنیم. به همین جهت نیاز است تعدادی نقش و یک کاربر ادمین را به بانک اطلاعاتی برنامه اضافه کنیم. برای اینکار به پروژهی BlazorServer.Common مراجعه کرده و تعدادی نقش ثابت را تعریف میکنیم:
namespace BlazorServer.Common { public static class ConstantRoles { public const string Admin = nameof(Admin); public const string Customer = nameof(Customer); public const string Employee = nameof(Employee); } }
سپس در فایل BlazorServer.App\appsettings.json، مشخصات ابتدایی کاربر ادمین را ثبت میکنیم:
{ "AdminUserSeed": { "UserName": "vahid@dntips.ir", "Password": "123@456#Pass", "Email": "vahid@dntips.ir" } }
namespace BlazorServer.Models { public class AdminUserSeed { public string UserName { get; set; } public string Password { get; set; } public string Email { get; set; } } }
namespace BlazorServer.App { public class Startup { public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddOptions<AdminUserSeed>().Bind(Configuration.GetSection("AdminUserSeed")); // ...
using System; using System.Linq; using System.Threading.Tasks; using BlazorServer.Common; using BlazorServer.DataAccess; using BlazorServer.Models; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; namespace BlazorServer.Services { public class IdentityDbInitializer : IIdentityDbInitializer { private readonly ApplicationDbContext _dbContext; private readonly UserManager<IdentityUser> _userManager; private readonly RoleManager<IdentityRole> _roleManager; private readonly IOptions<AdminUserSeed> _adminUserSeedOptions; public IdentityDbInitializer( ApplicationDbContext dbContext, UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<AdminUserSeed> adminUserSeedOptions) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _roleManager = roleManager ?? throw new ArgumentNullException(nameof(roleManager)); _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); _adminUserSeedOptions = adminUserSeedOptions ?? throw new ArgumentNullException(nameof(adminUserSeedOptions)); } public async Task SeedDatabaseWithAdminUserAsync() { if (_dbContext.Roles.Any(role => role.Name == ConstantRoles.Admin)) { return; } await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Admin)); await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Customer)); await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Employee)); await _userManager.CreateAsync( new IdentityUser { UserName = _adminUserSeedOptions.Value.UserName, Email = _adminUserSeedOptions.Value.Email, EmailConfirmed = true }, _adminUserSeedOptions.Value.Password); var user = await _dbContext.Users.FirstAsync(u => u.Email == _adminUserSeedOptions.Value.Email); await _userManager.AddToRoleAsync(user, ConstantRoles.Admin); } } }
پس از تعریف این سرویس، نیاز است آنرا به سیستم تزریق وابستگیهای برنامه اضافه کرد:
namespace BlazorServer.App { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddScoped<IIdentityDbInitializer, IdentityDbInitializer>(); // ...
using System; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Polly; namespace BlazorServer.DataAccess.Utils { public static class MigrationHelpers { public static void MigrateDbContext<TContext>( this IServiceProvider serviceProvider, Action<IServiceProvider> postMigrationAction ) where TContext : DbContext { using var scope = serviceProvider.CreateScope(); var scopedServiceProvider = scope.ServiceProvider; var logger = scopedServiceProvider.GetRequiredService<ILogger<TContext>>(); using var context = scopedServiceProvider.GetService<TContext>(); logger.LogInformation($"Migrating the DB associated with the context {typeof(TContext).Name}"); var retry = Policy.Handle<Exception>().WaitAndRetry(new[] { TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(15) }); retry.Execute(() => { context.Database.Migrate(); postMigrationAction(scopedServiceProvider); }); logger.LogInformation($"Migrated the DB associated with the context {typeof(TContext).Name}"); } } }
کار متد الحاقی فوق، دریافت یک IServiceProvider است که به سرویسهای اصلی برنامه اشاره میکند. سپس بر اساس آن، یک Scoped ServiceProvider را ایجاد میکند تا درون آن بتوان با Context برنامه در طی مدت کوتاهی کار کرد و در پایان آن، سرویسهای ایجاد شده را Dispose کرد.
در این متد ابتدا Database.Migrate فراخوانی میشود تا اگر مرحلهای از Migrations برنامه هنوز به بانک اطلاعاتی اعمال نشده، کار اجرا و اعمال آن انجام شود. سپس یک متد سفارشی را از فراخوان دریافت کرده و اجرا میکند. برای مثال توسط آن میتوان IIdentityDbInitializer در فایل BlazorServer.App\Program.cs به صوت زیر فراخوانی کرد:
public static void Main(string[] args) { var host = CreateHostBuilder(args).Build(); host.Services.MigrateDbContext<ApplicationDbContext>( scopedServiceProvider => scopedServiceProvider.GetRequiredService<IIdentityDbInitializer>() .SeedDatabaseWithAdminUserAsync() .GetAwaiter() .GetResult() ); host.Run(); }
و همچنین کاربر پیشفرض سیستم را نیز میتوان مشاهده کرد:
که نقش ادمین و کاربر پیشفرض، به این صورت به هم مرتبط شدهاند (یک رابطهی many-to-many برقرار است):
محدود کردن دسترسی کاربران بر اساس نقشها
پس از ایجاد کاربر ادمین و تعریف نقشهای پیشفرض، اکنون محدود کردن دسترسی به کامپوننتهای برنامه بر اساس نقشها، سادهاست. برای این منظور فقط کافی است لیست نقشهای مدنظر را که میتوانند توسط کاما از هم جدا شوند، به ویژگی Authorize کامپوننتها معرفی کرد:
@attribute [Authorize(Roles = ConstantRoles.Admin)]
protected override async Task OnInitializedAsync() { var authenticationState = await AuthenticationState; if (!authenticationState.User.Identity.IsAuthenticated || !authenticationState.User.IsInRole(ConstantRoles.Admin)) { var uri = new Uri(NavigationManager.Uri); NavigationManager.NavigateTo($"/identity/account/login?returnUrl={uri.LocalPath}"); }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-23.zip