مطالب
سیستم‌های توزیع شده در NET. - بخش دوم - چرا یک سخت افزار به تنهایی پاسخگوی نیازمندی‌های ما نیست؟
 قبل از شروع به بحث در مورد سیستم‌های توزیع شده، بهتر است ابتدا به سوالی اساسی که اساس بوجود آمدن سیستم‌های توزیع شده است، پاسخ دهیم:

چرا یک سخت افزار به تنهایی پاسخگوی همه نیاز‌های ما نیست؟

همه می‌دانیم که در یک هسته‌ی از پردازنده، چیزی بعنوان پردازش موازی وجود ندارد. هر هسته در هر لحظه می‌تواند یک پردازش را انجام دهد و این سرعت بالای در پردازش عملیات جاری و سوئیچ کردن بین دیگر عملیات است که حس موازی اجرا شدن آنها را به ما می‌دهد. یعنی در صورتیکه بخواهیم در یک سخت افزار با پردازنده‌ی تک هسته‌ای، برنامه نویسی موازی انجام بدهیم، در واقع هیچیک از عملیات ما بصورت موازی انجام نمی‌شوند. زمان پردازشی پردازنده، بر اساس تعداد عملیات و اولویت آنها، بین آنها تقسیم می‌شود. هر لحظه یکی از آنها اجرا می‌شود و با اتمام زمان اجرایش، نوبت به بعدی می‌رسد تا جاییکه تمام آنها به اتمام برسند. در این حالت پردازنده تک هسته‌ای، برای 2 کار زمان صرف می‌کند؛ اول اجرای عملیات جاری و دوم سوئیچ کردن به عملیات بعدی.

حال در صورتیکه تعداد عملیاتی که باید در سیستم بصورت همزمان انجام شوند بیشتر شود، زمانیکه پردازنده باید برای سوئیچ صرف کند نیز بیشتر شده و در نتیجه زمان اجرای عملیات بیشتر می‌شود و آنها دیر‌تر به اتمام میرسند. با بیشتر شدن تعداد این عملیات، کار به جایی می‌رسد که دیگر پردازنده هیچ زمانی را برای پردازش یک عملیات ندارد و باید تمام وقت خود را با سوئیچ کردن بین آنها صرف کند. بله؛ ما سیستمی را طراحی کرد‌ه‌ایم که شامل مجموعه‌ای از عملیات است که هیچ یک اجرا نمی‌شوند!

راه حل چیست؟

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


اینجا بود که نیازمندی‌های ما باعث شدند سخت افزارها پیچیده‌تر شوند. البته پیچیدگی بود که باعث تکامل آنها شد. تا اینجا برای انجام تعداد عملیات بیشتر می‌توانیم سخت افزار را ارتقاء دهیم. همچنین در اینجا بود که مفهوم Parallel Systems تکامل پیدا کرد؛ سیستمهایی که توانایی اجرای همزمان چند عملیات را داشتند که همه آنها از یک حافظه، بصورت مشترک استفاده می‌کردند.

مشکل سیستمهای Parallel مشخص است. کارآیی این نوع سیستم، کاملا به سخت افزار و نوع پیاده سازی آنها وابسته است. یعنی در صورت نیاز به کارآیی بیشتر، تنها راه ارتقاء سخت افزار و بهینه کردن کدهاست. اما این روال را تا کجا می‌توانیم انجام دهیم؟

برای روشن شدن مشکل بالا بیایید یک Web Application را بر روی یک سخت افزار اجرا کنیم. در یک Web Application یک Thread Pool شامل مجموعه‌ای از Threadها می‌باشد که هر Thread وظیفه اجرای یک درخواست را بر عهده دارد. یعنی با دریافت یک درخواست، یک Thread از این مجموعه کم می‌شود و وظیفه پاسخ دهی به آن در خواست را بر عهده می‌گیرد. تعداد Thread هایی که در یک Thread Pool می‌باشند نیز وابسته به تعداد هسته‌های پردازنده می‌باشد. برای این تعداد بصورت پیشفرض مقداری در نظر گرفته می‌شود که بیشترین کارآیی را در یک هسته داشته باشد؛ مثلا در ASP.NET بصورت پیشفرض به ازای هر هسته‌ی از CPU، تعداد 20 Thread به این مجموعه اضافه می‌شود. یعنی ما در یک پردازنده 2 هسته‌ای تنها می‌توانیم تعداد 40 درخواست را بصورت همزمان دریافت کنیم. در صورتیکه تعداد در خواستها در یک لحظه بیشتر از این تعداد باشد، تمام درخواست‌های اضافی در صف دریافت قرار می‌گیرند تا یکی از این Thread‌ها به درخواست خودش پاسخ دهد و به Thread Pool بازگردد و آماده اجرای درخواست بعدی باشد.

حال با فرض اینکه بصورت میانگین به هر درخواست در مدت 2 ثانیه پاسخ داده شود و در طول هر 2 ثانیه ما تعداد 200 درخواست جدید دریافت کنیم، یعنی در هر 2 ثانیه تعداد 160 درخواست در صف پردازش درخواست باقی می‌مانند. یعنی در مدت 10 ثانیه تعداد 800 درخواست پردازش نشده در این صف وجود دارند. در صورتیکه این روال ادامه پیدا کند، صف پردازش بزرگتر و بزرگتر می‌شود؛ تا جایی که دیگر حافظه‌ای برای دریافت درخواستهای جدید نباشد. اینجاست که سیستم ما از دسترس خارج می‌شود. پس تصمیم می‌گیریم سخت افزار خود را ارتقاء دهیم و کدهای خود را نیز بهینه کنیم. مثلا جاییکه عملیات I/O را انجام می‌دهیم، برای استفاده‌ی بهینه از Thread‌های موجود، کدهای خود را بصورت Async اجرا کنیم.

تا حدودی مشکل ما فعلا حل شده‌است. بعد از مدتی بدلیل اضافه شدن نیازمندی‌های جدید، تعدادکاربران فعال سیستم زیاد می‌شود و دوباره مشکل پوشش دادن تعداد بیشتر درخواست بوجود می‌آید. مجبوریم دوباره عملیات Scale-up یا Vertical scaling را انجام دهیم. بله؛ عملیاتی که ما در این سیستم‌ها برای مقیاس‌پذیری انجام می‌دهیم، اصطلاحا  Vertical scaling یا Scale-up نام دارد. یعنی با افزایش تعداد کاربران یا تعداد درخواست، کدها بهینه‌تر و سخت افزار ارتقاء پیدا می‌کند.

البته مثالی که ذکر شد به هیچ وجه با دنیای واقعی قابل مقایسه نیست. ممکن است شما سرویسی بسیار حیاتی را پیاده سازی کرده باشید که در شرایط خاص، هزاران یا میلیون‌ها کاربر بصورت همزمان بخواهند درخواستهای خود را برای شما ارسال کنند. در این حالت شما دو راه دارید؛ اول اینکه مرتبا سرویس بسیار حیاتی خود را از دسترس خارج کنید و منتظر بمانید تا حجم تعداد درخواست‌های کاربران کاهش یابد و یا مجبورید سخت افزار سرور خود را آنقدر ارتقاء دهید، تا این تعداد درخواست را بصورت همزمان پوشش دهد. واقعا هزینه تهیه کردن این سرور چقدر است؟

فرض کنید از سمت پایگاه داده نیز با مشکل روبرو شده‌ایم. حجم داده‌های ما روبه افزایش است. فضای حافظه‌ی آزاد تنها Server ی که داده‌های ما را ذخیره می‌کند، رو به اتمام است. چاره چیست؟ آن را ارتقا دهیم؟ بله برای مدتی سرور را از دسترس خارج کرده و فضای آزاد را افزایش می‌دهیم. در این بین تمام سرویس‌های ما که وابسته به این سرور هستند، از دسترس خارج می‌شوند. این کار برای داده‌هایی که ذاتا همیشه رو به افزایش هستند، چقدر باید تکرار شوند؟ چقدر باید هزینه کنیم تا این داده‌ها که تمام سرویس‌های ما به آنها وابسته هستند، با مشکل مواجه نشوند، یا کارآیی بازیابی آنها پایین نیاید؟

حال بیاید از زاویه دیگری به ماجرا نگاه کنیم ما یک سرویس بسیار حساس با تعداد کاربران زیادی را داریم. از دسترس خارج شدن این سرویس برای ما بسیار هزینه بر است. اما تنها سرور بسیار قوی ما که برای آن هزینه‌ی بسیار زیادی را پرداخت کرده‌ایم، با مشکلی مواجه شده که ممکن است زمان زیادی برای رفع مشکل آن صرف شود. بله باز سرویس از دسترس خارج شده و ما با مشکل بسیار جدی مواجه شده‌ایم که ممکن است آینده‌ی سرویس بسیار مهم را به خطر بیاندازد. چاره چیست؟ مثلا تعدادی سرور مشابه سرور اصلی را خریداری کنیم و در صورتیکه سرور اصلی با مشکل جدی مواجه شد از آنها استفاده کنیم. بله هزینه چند برابر شد. فرض کنید به هر دلیل، برق قسمتی که شما این سرورها را نگهداری می‌کنید، قطع شد! چه راهکاری را دارید؟ واقعیتی که باید بپذیریم این است که چون ما یک سرور را برای اجرای Application خودمان داریم، در هرصورت اگر این سرور با مشکلی مواجه شود، تمام سرویس‌های ما با خطری جدی مواجه می‌شوند و ما نیز در صورتیکه بخواهیم این چرخه‌ی معیوب را ادامه دهیم، تنها هر بار صورت مسئله را پاک می‌کنیم. بهتر است روش جدیدی را برای این مشکل بیابیم.

اینجاست که مفهوم سیستمهای توزیع شده به کمک سیستمهای Parallel می‌آید و مفهوم Scale-up یا Vertical scaling  با مفهموم Horizontal Scaling یا Scale-out ادغام می‌شود. در قسمت بعدی، با مفاهیم، خصوصیات و اصطلاحات موجود در این سیستم‌ها آشنا می‌شویم.
مطالب
اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
ASP.NET Core 2.0 به همراه یک AuthenticationMiddleware است که یکی از قابلیت‌های این میان‌افزار، افزودن اطلاعات کاربر و یا همان HttpContext.User به یک کوکی رمزنگاری شده و سپس اعتبارسنجی این کوکی در درخواست‌های بعدی کاربر است. این مورد سبب خواهد شد تا بتوان بدون نیاز به پیاده سازی سیستم کامل ASP.NET Core Identity، یک سیستم اعتبارسنجی سبک، ساده و سفارشی را تدارک دید.



تعریف موجودیت‌های مورد نیاز جهت طراحی یک سیستم اعتبارسنجی

در اینجا کنترل کامل سیستم در اختیار ما است و در این حالت می‌توان طراحی تمام قسمت‌ها را از ابتدا و مطابق میل خود انجام داد. برای مثال سیستم اعتبارسنجی ساده‌ی ما، شامل جدول کاربران و نقش‌های آن‌ها خواهد بود و این دو با هم رابطه‌ی many-to-many دارند. به همین جهت جدول UserRole نیز در اینجا پیش بینی شده‌است.

جدول کاربران

    public class User
    {
        public User()
        {
            UserRoles = new HashSet<UserRole>();
        }

        public int Id { get; set; }

        public string Username { get; set; }

        public string Password { get; set; }

        public string DisplayName { get; set; }

        public bool IsActive { get; set; }

        public DateTimeOffset? LastLoggedIn { get; set; }

        /// <summary>
        /// every time the user changes his Password,
        /// or an admin changes his Roles or stat/IsActive,
        /// create a new `SerialNumber` GUID and store it in the DB.
        /// </summary>
        public string SerialNumber { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
    }
در اینجا SerialNumber فیلدی است که با هر بار ویرایش اطلاعات کاربران باید از طرف برنامه به روز رسانی شود. از آن جهت غیرمعتبر سازی کوکی کاربر استفاده خواهیم کرد. برای مثال اگر خاصیت فعال بودن او تغییر کرد و یا نقش‌های او را تغییر دادیم، کاربر در همان لحظه باید logout شود. به همین جهت چنین فیلدی در اینجا در نظر گرفته شده‌است تا با بررسی آن بتوان وضعیت معتبر بودن کوکی او را تشخیص داد.

جدول نقش‌های کاربران

    public class Role
    {
        public Role()
        {
            UserRoles = new HashSet<UserRole>();
        }

        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
    }
البته این سیستم ساده دارای یک سری نقش ثابت و مشخص است.
    public static class CustomRoles
    {
        public const string Admin = nameof(Admin);
        public const string User = nameof(User);
    }
که این نقش‌ها را در ابتدای کار برنامه به بانک اطلاعات اضافه خواهیم کرد.


جدول ارتباط نقش‌ها با کاربران و برعکس

    public class UserRole
    {
        public int UserId { get; set; }
        public int RoleId { get; set; }

        public virtual User User { get; set; }
        public virtual Role Role { get; set; }
    }
وجود این جدول در EF Core جهت تعریف یک رابطه‌ی many-to-many ضروری است.


تعریف Context برنامه و فعالسازی Migrations در EF Core 2.0

DbContext برنامه را به صورت ذیل در یک اسمبلی دیگر اضافه خواهیم کرد:
    public interface IUnitOfWork : IDisposable
    {
        DbSet<TEntity> Set<TEntity>() where TEntity : class;

        int SaveChanges(bool acceptAllChangesOnSuccess);
        int SaveChanges();
        Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken());
        Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken());
    }

    public class ApplicationDbContext : DbContext, IUnitOfWork
    {
        public ApplicationDbContext(DbContextOptions options) : base(options)
        { }

        public virtual DbSet<User> Users { set; get; }
        public virtual DbSet<Role> Roles { set; get; }
        public virtual DbSet<UserRole> UserRoles { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            // it should be placed here, otherwise it will rewrite the following settings!
            base.OnModelCreating(builder);

            // Custom application mappings
            builder.Entity<User>(entity =>
            {
                entity.Property(e => e.Username).HasMaxLength(450).IsRequired();
                entity.HasIndex(e => e.Username).IsUnique();
                entity.Property(e => e.Password).IsRequired();
                entity.Property(e => e.SerialNumber).HasMaxLength(450);
            });

            builder.Entity<Role>(entity =>
            {
                entity.Property(e => e.Name).HasMaxLength(450).IsRequired();
                entity.HasIndex(e => e.Name).IsUnique();
            });

            builder.Entity<UserRole>(entity =>
            {
                entity.HasKey(e => new { e.UserId, e.RoleId });
                entity.HasIndex(e => e.UserId);
                entity.HasIndex(e => e.RoleId);
                entity.Property(e => e.UserId);
                entity.Property(e => e.RoleId);
                entity.HasOne(d => d.Role).WithMany(p => p.UserRoles).HasForeignKey(d => d.RoleId);
                entity.HasOne(d => d.User).WithMany(p => p.UserRoles).HasForeignKey(d => d.UserId);
            });
        }
    }
در اینجا موجودیت‌های برنامه به صورت DbSet در معرض دید EF Core 2.0 قرار گرفته‌اند. همچنین رابطه‌ی Many-to-Many بین نقش‌ها و کاربران نیز تنظیم شده‌است.
سازنده‌ی کلاس به همراه پارامتر DbContextOptions است تا بتوان آن‌را در آغاز برنامه تغییر داد.


فعالسازی مهاجرت‌ها در EF Core 2.0

EF Core 2.0 برخلاف نگارش‌های قبلی آن به دنبال کلاسی مشتق شده‌ی از IDesignTimeDbContextFactory می‌گردد تا بتواند نحوه‌ی وهله سازی ApplicationDbContext را دریافت کند. در اینجا چون DbContext تعریف شده دارای یک سازنده‌ی با پارامتر است، EF Core 2.0 نمی‌داند که چگونه باید آن‌را در حین ساخت مهاجرت‌ها و اعمال آن‌ها، وهله سازی کند. کار کلاس ApplicationDbContextFactory ذیل دقیقا مشخص سازی همین مساله است:
    /// <summary>
    /// Only used by EF Tooling
    /// </summary>
    public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
    {
        public ApplicationDbContext CreateDbContext(string[] args)
        {
            var basePath = Directory.GetCurrentDirectory();
            Console.WriteLine($"Using `{basePath}` as the BasePath");
            var configuration = new ConfigurationBuilder()
                                    .SetBasePath(basePath)
                                    .AddJsonFile("appsettings.json")
                                    .Build();
            var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
            var connectionString = configuration.GetConnectionString("DefaultConnection");
            builder.UseSqlServer(connectionString);
            return new ApplicationDbContext(builder.Options);
        }
    }
کاری که در اینجا انجام شده، خواندن DefaultConnection از قسمت ConnectionStrings فایل appsettings.json است:
{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=(LocalDB)\\MSSQLLocalDB;Initial Catalog=ASPNETCore2CookieAuthenticationDB;Integrated Security=True;MultipleActiveResultSets=True;"
  },
  "LoginCookieExpirationDays": 30
}
و سپس استفاده‌ی از آن جهت تنظیم رشته‌ی اتصالی متد UseSqlServer و در آخر وهله سازی ApplicationDbContext.
کار یافتن این کلاس در حین تدارک و اعمال مهاجرت‌ها توسط EF Core 2.0 خودکار بوده و باید محل قرارگیری آن دقیقا در اسمبلی باشد که DbContext برنامه در آن تعریف شده‌است.


تدارک لایه سرویس‌های برنامه

پس از مشخص شدن ساختار موجودیت‌ها و همچنین Context برنامه، اکنون می‌توان لایه سرویس برنامه را به صورت ذیل تکمیل کرد:

سرویس کاربران
    public interface IUsersService
    {
        Task<string> GetSerialNumberAsync(int userId);
        Task<User> FindUserAsync(string username, string password);
        Task<User> FindUserAsync(int userId);
        Task UpdateUserLastActivityDateAsync(int userId);
    }
کار این سرویس ابتدایی کاربران، یافتن یک یک کاربر بر اساس Id او و یا کلمه‌ی عبور و نام کاربری او است. از این امکانات در حین لاگین و یا اعتبارسنجی کوکی کاربر استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس نقش‌های کاربران
    public interface IRolesService
    {
        Task<List<Role>> FindUserRolesAsync(int userId);
        Task<bool> IsUserInRole(int userId, string roleName);
        Task<List<User>> FindUsersInRoleAsync(string roleName);
    }
از این سرویس برای یافتن نقش‌های کاربر لاگین شده‌ی به سیستم و افزودن آن‌ها به کوکی رمزنگاری شده‌ی اعتبارسنجی او استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس آغاز بانک اطلاعاتی

    public interface IDbInitializerService
    {
        void Initialize();
        void SeedData();
    }
از این سرویس در آغاز کار برنامه برای اعمال خودکار مهاجرت‌های تولیدی و همچنین ثبت نقش‌های آغازین سیستم به همراه افزودن کاربر Admin استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس اعتبارسنجی کوکی‌های کاربران

یکی از قابلیت‌های میان‌افزار اعتبارسنجی ASP.NET Core 2.0، رخ‌دادی است که در آن اطلاعات کوکی دریافتی از کاربر، رمزگشایی شده و در اختیار برنامه جهت تعیین اعتبار قرار می‌گیرد:
    public interface ICookieValidatorService
    {
        Task ValidateAsync(CookieValidatePrincipalContext context);
    }

    public class CookieValidatorService : ICookieValidatorService
    {
        private readonly IUsersService _usersService;
        public CookieValidatorService(IUsersService usersService)
        {
            _usersService = usersService;
            _usersService.CheckArgumentIsNull(nameof(usersService));
        }

        public async Task ValidateAsync(CookieValidatePrincipalContext context)
        {
            var userPrincipal = context.Principal;

            var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
            if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var serialNumberClaim = claimsIdentity.FindFirst(ClaimTypes.SerialNumber);
            if (serialNumberClaim == null)
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var userIdString = claimsIdentity.FindFirst(ClaimTypes.UserData).Value;
            if (!int.TryParse(userIdString, out int userId))
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var user = await _usersService.FindUserAsync(userId).ConfigureAwait(false);
            if (user == null || user.SerialNumber != serialNumberClaim.Value || !user.IsActive)
            {
                // user has changed his/her password/roles/stat/IsActive
                await handleUnauthorizedRequest(context);
            }

            await _usersService.UpdateUserLastActivityDateAsync(userId).ConfigureAwait(false);
        }

        private Task handleUnauthorizedRequest(CookieValidatePrincipalContext context)
        {
            context.RejectPrincipal();
            return context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
کار این سرویس، تعیین اعتبار موارد ذیل است:
- آیا کوکی دریافت شده دارای اطلاعات HttpContext.User است؟
- آیا این کوکی به همراه اطلاعات فیلد SerialNumber است؟
- آیا این کوکی به همراه Id کاربر است؟
- آیا کاربری که بر اساس این Id یافت می‌شود غیرفعال شده‌است؟
- آیا کاربری که بر اساس این Id یافت می‌شود دارای SerialNumber یکسانی با نمونه‌ی موجود در بانک اطلاعاتی است؟

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


تنظیمات ابتدایی میان‌افزار اعتبارسنجی کاربران در ASP.NET Core 2.0

تنظیمات کامل ابتدایی میان‌افزار اعتبارسنجی کاربران در ASP.NET Core 2.0 را در فایل Startup.cs می‌توانید مشاهده کنید.

ابتدا سرویس‌های برنامه معرفی شده‌اند:
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUnitOfWork, ApplicationDbContext>();
            services.AddScoped<IUsersService, UsersService>();
            services.AddScoped<IRolesService, RolesService>();
            services.AddScoped<ISecurityService, SecurityService>();
            services.AddScoped<ICookieValidatorService, CookieValidatorService>();
            services.AddScoped<IDbInitializerService, DbInitializerService>();

سپس تنظیمات مرتبط با ترزیق وابستگی‌های ApplicationDbContext برنامه انجام شده‌است. در اینجا رشته‌ی اتصالی، از فایل appsettings.json خوانده شده و سپس در اختیار متد UseSqlServer قرار می‌گیرد:
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection"),
                    serverDbContextOptionsBuilder =>
                        {
                            var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
                            serverDbContextOptionsBuilder.CommandTimeout(minutes);
                            serverDbContextOptionsBuilder.EnableRetryOnFailure();
                        });
            });

در ادامه تعدادی Policy مبتنی بر نقش‌های ثابت سیستم را تعریف کرده‌ایم. این کار اختیاری است اما روش توصیه شده‌ی در ASP.NET Core، کار با Policyها است تا کار مستقیم با نقش‌ها. Policy‌ها انعطاف پذیری بیشتری را نسبت به نقش‌ها ارائه می‌دهند و در اینجا به سادگی می‌توان چندین نقش و یا حتی Claim را با هم ترکیب کرد و به صورت یک Policy ارائه داد:
            // Only needed for custom roles.
            services.AddAuthorization(options =>
                    {
                        options.AddPolicy(CustomRoles.Admin, policy => policy.RequireRole(CustomRoles.Admin));
                        options.AddPolicy(CustomRoles.User, policy => policy.RequireRole(CustomRoles.User));
                    });

قسمت اصلی تنظیمات میان افزار اعتبارسنجی مبتنی بر کوکی‌ها در اینجا قید شده‌است:
            // Needed for cookie auth.
            services
                .AddAuthentication(options =>
                {
                    options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                })
                .AddCookie(options =>
                {
                    options.SlidingExpiration = false;
                    options.LoginPath = "/api/account/login";
                    options.LogoutPath = "/api/account/logout";
                    //options.AccessDeniedPath = new PathString("/Home/Forbidden/");
                    options.Cookie.Name = ".my.app1.cookie";
                    options.Cookie.HttpOnly = true;
                    options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
                    options.Cookie.SameSite = SameSiteMode.Lax;
                    options.Events = new CookieAuthenticationEvents
                    {
                        OnValidatePrincipal = context =>
                        {
                            var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
                            return cookieValidatorService.ValidateAsync(context);
                        }
                    };
                });
ابتدا مشخص شده‌است که روش مدنظر ما، اعتبارسنجی مبتنی بر کوکی‌ها است و سپس تنظیمات مرتبط با کوکی رمزنگاری شده‌ی برنامه مشخص شده‌اند. تنها قسمت مهم آن CookieAuthenticationEvents است که نحوه‌ی پیاده سازی آن‌را با معرفی سرویس ICookieValidatorService پیشتر بررسی کردیم. این قسمت جائی است که پس از هر درخواست به سرور اجرا شده و کوکی رمزگشایی شده، در اختیار برنامه جهت اعتبارسنجی قرار می‌گیرد.

کار نهایی تنظیمات میان افزار اعتبارسنجی در متد Configure با فراخوانی UseAuthentication صورت می‌گیرد. اینجا است که میان افزار، به برنامه معرفی خواهد شد:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   app.UseAuthentication();

همچنین پس از آن، کار اجرای سرویس آغاز بانک اطلاعاتی نیز انجام شده‌است تا نقش‌ها و کاربر Admin را به سیستم اضافه کند:
            var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
            using (var scope = scopeFactory.CreateScope())
            {
                var dbInitializer = scope.ServiceProvider.GetService<IDbInitializerService>();
                dbInitializer.Initialize();
                dbInitializer.SeedData();
            }


پیاده سازی ورود و خروج به سیستم

پس از این مقدمات به مرحله‌ی آخر پیاده سازی این سیستم اعتبارسنجی می‌رسیم.

پیاده سازی Login
در اینجا از سرویس کاربران استفاده شده و بر اساس نام کاربری و کلمه‌ی عبور ارسالی به سمت سرور، این کاربر یافت خواهد شد.
در صورت وجود این کاربر، مرحله‌ی نهایی کار، فراخوانی متد الحاقی HttpContext.SignInAsync است:
        [AllowAnonymous]
        [HttpPost("[action]")]
        public async Task<IActionResult> Login([FromBody]  User loginUser)
        {
            if (loginUser == null)
            {
                return BadRequest("user is not set.");
            }

            var user = await _usersService.FindUserAsync(loginUser.Username, loginUser.Password).ConfigureAwait(false);
            if (user == null || !user.IsActive)
            {
                await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                return Unauthorized();
            }

            var loginCookieExpirationDays = _configuration.GetValue<int>("LoginCookieExpirationDays", defaultValue: 30);
            var cookieClaims = await createCookieClaimsAsync(user).ConfigureAwait(false);
            await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                cookieClaims,
                new AuthenticationProperties
                {
                    IsPersistent = true, // "Remember Me"
                    IssuedUtc = DateTimeOffset.UtcNow,
                    ExpiresUtc = DateTimeOffset.UtcNow.AddDays(loginCookieExpirationDays)
                });

            await _usersService.UpdateUserLastActivityDateAsync(user.Id).ConfigureAwait(false);

            return Ok();
        }
مهم‌ترین کار متد HttpContext.SignInAsync علاوه بر تنظیم طول عمر کوکی کاربر، قسمت createCookieClaimsAsync است که به صورت ذیل پیاده سازی شده‌است:
        private async Task<ClaimsPrincipal> createCookieClaimsAsync(User user)
        {
            var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
            identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
            identity.AddClaim(new Claim("DisplayName", user.DisplayName));

            // to invalidate the cookie
            identity.AddClaim(new Claim(ClaimTypes.SerialNumber, user.SerialNumber));

            // custom data
            identity.AddClaim(new Claim(ClaimTypes.UserData, user.Id.ToString()));

            // add roles
            var roles = await _rolesService.FindUserRolesAsync(user.Id).ConfigureAwait(false);
            foreach (var role in roles)
            {
                identity.AddClaim(new Claim(ClaimTypes.Role, role.Name));
            }

            return new ClaimsPrincipal(identity);
        }
در اینجا است که اطلاعات اضافی کاربر مانند Id او یا نقش‌های او به کوکی رمزنگاری شده‌ی تولیدی توسط HttpContext.SignInAsync اضافه شده و در دفعات بعدی و درخواست‌های بعدی او، به صورت خودکار توسط مرورگر به سمت سرور ارسال خواهند شد. این کوکی است که امکان کار با MyProtectedApiController و یا MyProtectedAdminApiController را فراهم می‌کند. اگر شخص لاگین کرده باشد، بلافاصله قابلیت دسترسی به امکانات محدود شده‌ی توسط فیلتر Authorize را خواهد یافت. همچنین در این مثال چون کاربر Admin لاگین می‌شود، امکان دسترسی به Policy مرتبطی را نیز خواهد یافت:
[Route("api/[controller]")]
[Authorize(Policy = CustomRoles.Admin)]
public class MyProtectedAdminApiController : Controller

پیاده سازی Logout

متد الحاقی HttpContext.SignOutAsync کار Logout کاربر را تکمیل می‌کند.
        [AllowAnonymous]
        [HttpGet("[action]"), HttpPost("[action]")]
        public async Task<bool> Logout()
        {
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            return true;
        }


آزمایش نهایی برنامه

در فایل index.html ، نمونه‌ای از متدهای لاگین، خروج و فراخوانی اکشن متدهای محافظت شده را مشاهده می‌کنید. این روش برای برنامه‌های تک صفحه‌ای وب یا SPA نیز می‌تواند مفید باشد و به همین نحو کار می‌کنند.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
نظرات اشتراک‌ها
معرفی Mozilla Firefox Accounts
این امکان رو گوگل کروم قبلا اضافه کرده بود.