اندازهی قلم متن
تخمین مدت زمان مطالعهی مطلب:
ده دقیقه
با به پایان رسیدن مرحلهی توسعهی ASP.NET Identity 2.x مخصوص نگارشهای ASP.NETایی که از Full .NET Framework استفاده میکنند، نگارش جدید آن صرفا بر پایهی ASP.NET Core تهیه شدهاست و در طی یک سری، نحوهی سفارشی سازی تقریبا تمام اجزای آنرا بررسی خواهیم کرد. جهت سهولت پیگیری این سری، پروژهی کامل سفارشی سازی شدهی ASP.NET Core Identity را از مخزن کد DNT Identity میتوانید دریافت کنید.
پیشنیازهای اجرای پروژهی DNT Identity
- ابتدا نیاز است حداقل ASP.NET Core Identity 1.1 را نصب کرده باشید.
- همچنین بانک اطلاعاتی پایهی آن که به صورت خودکار در اولین بار اجرای برنامه تشکیل میشود، مبتنی بر LocalDB است. بنابراین اگر قصد تغییری را در تنظیمات Context آن ندارید، بهتر است LocalDB را نیز بر روی سیستم نصب کنید. هرچند با تغییر تنظیم ActiveDatabase به SqlServer در فایل appsettings.json، برنامه به صورت خودکار از نگارش کامل SqlServer استفاده خواهد کرد. رشتهی اتصالی آن نیز در مدخل ConnectionStrings فایل appsettings.json ذکر شدهاست و قابل تغییر است. برای شروع به کار، نیازی به اجرای مراحل Migrations را نیز ندارید و همینقدر که برنامه را اجرا کنید، بانک اطلاعاتی آن نیز تشکیل خواهد شد.
- کاربر پیش فرض Admin سیستم و کلمهی عبور آن از مدخل AdminUserSeed فایل appsettings.json خوانده میشوند.
- تنظیمات ایمیل پیش فرض برنامه به استفادهی از PickupFolder در مدخل Smtp فایل appsettings.json تنظیم شدهاست. بنابراین تمام ایمیلهای برنامه را جهت آزمایش محلی میتوانید در مسیر PickupFolder آن یا همان c:\smtppickup مشاهده کنید. محتوای این ایمیلها را نیز توسط مرورگر (drag&drop بر روی یک tab جدید) و یا برنامهی Outlook میتوان مشاهده کرد.
سفارشی سازی کلید اصلی موجودیتهای ASP.NET Core Identity
ASP.NET Core Identity به همراه دو سری موجودیت است. یک سری سادهی آن که از یک string به عنوان نوع کلید اصلی استفاده میکنند و سری دوم، حالت جنریک که در آن میتوان نوع کلید اصلی را به صورت صریحی قید کرد و تغییر داد. در اینجا نیز قصد داریم از حالت جنریک استفاده کرده و نوع کلید اصلی جداول را تغییر دهیم. تمام این موجودیتهای تغییر یافته را در پوشهی src\ASPNETCoreIdentitySample.Entities\Identity نیز میتوانید مشاهده کنید و شامل موارد ذیل هستند:
جدول نقشهای سیستم
کار با ارث بری از نگارش جنریک کلاس IdentityRole شروع میشود. این کلاس پایه، حاوی تعاریف اصلی فیلدهای جدول نقشهای سیستم است که اولین آرگومان جنریک آن، نوع کلید اصلی جدول مرتبط را نیز مشخص میکند و در اینجا به int تنظیم شدهاست. همچنین یک اینترفیس جدید IAuditableEntity را نیز در انتهای این تعریفها مشاهده میکنید. در مورد این اینترفیس و Shadow properties متناظر با آن، در ادامهی بحث با سفارشی سازی DbContext برنامه بیشتر توضیح داده خواهد شد.
در اولین بار اجرای برنامه، نقش Admin در این جدول ثبت خواهد شد.
جدول کاربران منتسب به نقشها
کلاس پایهی جدول کاربران منتسب به نقشها، کلاس جنریک IdentityUserRole است که در اینجا با تغییر آرگومان جنریک آن به int، نوع فیلدهای UserId و RoleId آن به int تنظیم میشوند. در کلاس سفارشی سازی شدهی فوق، دو خاصیت اضافهتر User و Role نیز را مشاهده میکنید. مزیت تعریف آنها، دسترسی سادهتر به اطلاعات کاربران و نقشها توسط EF Core است.
در اولین بار اجرای برنامه، کاربر شماره 1 یا همان Admin به نقش شماره 1 یا همان Admin، انتساب داده میشود.
جدول جدید IdentityRoleClaim سیستم
در ASP.NET Core Identity، جدول جدیدی به نام RoleClaim نیز اضافه شدهاست. در این سری از آن برای پیاده سازی سطوح دسترسی پویای به صفحات استفاده خواهیم کرد. ابتدا یک سری نقش ثابت در جدول Roles ثبت خواهند شد. سپس تعدادی کاربر به هر نقش نسبت داده میشوند. اکنون میتوان به هر نقش نیز تعدادی Claim را انتساب داد. برای مثال یک Claim سفارشی که شامل ID سفارشی area:controller:action باشد. به این ترتیب و با بررسی سفارشی آن میتوان سطوح دسترسی پویا را نیز پیاده سازی کرد و مزیت آن این است که تمام این Claims به صورت خودکار به کوکی شخص نیز اضافه شده و دسترسی به اطلاعات آن بسیار سریع است و نیازی به مراجعهی به بانک اطلاعاتی را ندارد.
جدول UserClaim سیستم
میتوان به هر کاربر یک سری Claim مخصوص را نیز انتساب داد. برای مثال مسیر عکس ذخیره شدهی او در سرور، چه موردی است و این اطلاعات به صورت خودکار به کوکی او نیز توسط ASP.NET Core Identity اضافه میشوند. البته ما در این سری روش دیگری را برای سفارشی سازی Claims عمومی کاربران بکار خواهیم گرفت (با سفارشی سازی کلاس ApplicationClaimsPrincipalFactory آن).
جداول توکن و لاگینهای کاربران
دراینجا نیز نحوهی سفارشی سازی و تغییر جداول لاگینهای کاربران و توکنهای مرتبط با آنها را مشاهده میکنید. این جداول بیشتر جهت دسترسی به حالتهایی مانند لاگین با حساب کاربری جیمیل مورد استفاده قرار میگیرند و کاربرد پیش فرضی ندارند (اگر از تامین کنندههای لاگین خارجی نمیخواهید استفاده کنید).
جدول کاربران سیستم
در اینجا علاوه بر نحوهی تغییر نوع کلید اصلی جدول کاربران سیستم، نحوهی افزودن خواص اضافهتری مانند نام، تاریخ تولد، مکان، تصویر و غیره را نیز مشاهده میکنید. به علاوه جدولی نیز جهت ثبت سابقهی کلمات عبور هش شدهی کاربران نیز تدارک دیده شدهاست تا کاربران نتوانند از 5 کلمهی عبور اخیر خود (تنظیم NotAllowedPreviouslyUsedPasswords در فایل appsettings.json) استفاده کنند.
فیلد IsActive نیز از این جهت اضافه شدهاست تا بجای حذف فیزیکی یک کاربر، بتوان اکانت او را غیرفعال کرد.
تعریف Shadow properties ثبت تغییرات رکوردها
در #C ارثبری چندگانهی کلاسها ممنوع است؛ مگر اینکه از اینترفیسها استفاده شود. برای مثال IdentityUser یک کلاس است و در اینجا دیگر نمیتوان کلاس دومی را به نام BaseEntity جهت اعمال خواص اضافهتری اعمال کرد. به همین جهت است که اعمال اینترفیس خالی IAuditableEntity را در اینجا مشاهده میکنید. این اینترفیس کار علامتگذاری کلاسهایی را انجام میدهد که قصد داریم به آنها به صورت خودکار، خواصی مانند تاریخ ثبت رکورد، تاریخ ویرایش آن و غیره را اعمال کنیم.
در Context برنامه، به اطلاعات src\ASPNETCoreIdentitySample.Entities\AuditableEntity مراجعه شده و متد AddAuditableShadowProperties بر روی تمام کلاسهایی از نوع IAuditableEntity اعمال میشود. این متد خواص مدنظر ما را مانند ModifiedDateTime به صورت Shadow properties به موجودیتهای علامتگذاری شده اضافه میکند.
همچنین متد SetAuditableEntityPropertyValues، کار مقدار دهی خودکار این خواص را انجام خواهد داد. بنابراین دیگر نیازی نیست در برنامه برای مثال IP شخص ثبت کننده یا ویرایش کننده را به صورت دستی مقدار دهی کرد. هم تعریف و هم مقدار دهی آن توسط Change tracker سیستم به صورت خودکار انجام خواهند شد.
تاثیر افزودن Shadow properties را بر روی کلاس نقشهای سیستم، در تصویر فوق ملاحظه میکنید. خواصی که به صورت معمول در کلاسهای برنامه ظاهر نمیشوند و صرفا هدف بازبینی سیستم را برآورده میکنند و مدیریت آنها نیز در اینجا کاملا خودکار است.
سفارشی سازی DbContext برنامه
نحوهی سفارشی سازی DbContext برنامه را در پوشهی src\ASPNETCoreIdentitySample.DataLayer\Context و src\ASPNETCoreIdentitySample.DataLayer\Mappings ملاحظه میکنید. پوشهی Context حاوی کلاس ApplicationDbContextBase است که تمام سفارشی سازیهای لازم بر روی آن انجام شدهاست؛ شامل:
- تغییر نوع کلید اصلی موجودیتها به همراه معرفی موجودیتهای تغییر یافته:
ما در ابتدای بحث، برای مثال کلاس Role را سفارشی سازی کردیم. اما برنامه از وجود آن بیاطلاع است. با ارث بری از IdentityDbContext و ذکر این کلاسهای سفارشی به همراه نوع int کلید اصلی مورد استفاده، کار معرفی موجودیتهای سفارشی سازی شده انجام میشود.
- اعمال متد BeforeSaveTriggers به تمام نگارشهای مختلف SaveChanges
در اینجا پیش از ذخیرهی اطلاعات، ابتدا موجودیتها اعتبارسنجی میشوند. سپس مقادیر Shadow properties تنظیم شده و دست آخر، ی و ک فارسی نیز به اطلاعات ثبت شده، جهت یک دست سازی اطلاعات سیستم، اعمال میشوند.
- انتخاب نوع بانک اطلاعاتی مورد استفاده در متد OnConfiguring
در اینجا است که خاصیت ActiveDatabase تنظیم شدهی در فایل appsettings.json خوانده شده و اعمال میشوند. تعریف متد GetDbConnectionString را در کلاس SiteSettingsExtesnsions مشاهده میکنید. کار آن استفادهی از بانک اطلاعاتی درون حافظهای، جهت انجام آزمونهای واحد و یا استفادهی از LocalDb و یا نگارش کامل SQL Server میباشد. اگر علاقمند بودید تا بانک اطلاعاتی دیگری (مثلا SQLite) را نیز اضافه کنید، ابتدا enum ایی به نام ActiveDatabase را تغییر داده و سپس متد GetDbConnectionString و متد OnConfiguring را جهت درج اطلاعات این بانک اطلاعاتی جدید، اصلاح کنید.
پس از تعریف این DbContext پایهی سفارشی سازی شده، کلاس جدید ApplicationDbContext را مشاهده میکنید. این کلاس Context ایی است که در برنامه از آن استفاده میشود و از کلاس پایه ApplicationDbContextBase مشتق شدهاست:
تعاریف موجودیتهای جدید خود را به این کلاس اضافه کنید.
تنظیمات mapping آنها نیز به متد OnModelCreating این کلاس اضافه خواهند شد. فقط نحوهی استفادهی از آن را بهخاطر داشته باشید:
ابتدا باید base.OnModelCreating را ذکر کنید. در غیراینصورت تمام سفارشی سازیهای شما بازنویسی میشوند.
سپس متد AddCustomIdentityMappings ذکر شدهاست. این متد اطلاعات src\ASPNETCoreIdentitySample.DataLayer\Mappings را به صورت خودکار و یکجا اضافه میکند که در آن برای مثال نام جداول پیش فرض Identity سفارشی سازی شدهاند.
در آخر باید AddAuditableShadowProperties فراخوانی شود تا خواص سایهای که پیشتر در مورد آنها بحث شد، به سیستم به صورت خودکار اضافه شوند.
تمام نگاشتهای سفارشی شما باید در این میان و در قسمت «Custom application mappings» درج شوند.
در قسمت بعدی، نحوهی سفارشی سازی سرویسهای پایهی Identity را بررسی خواهیم کرد. بدون این سفارشی سازی و اطلاعات رسانی به سرویسهای پایه که از چه موجودیتهای جدید سفارشی سازی شدهایی در حال استفاده هستیم، کار Migrations انجام نخواهد شد.
کدهای کامل این سری را در مخزن کد DNT Identity میتوانید ملاحظه کنید.
پیشنیازهای اجرای پروژهی DNT Identity
- ابتدا نیاز است حداقل ASP.NET Core Identity 1.1 را نصب کرده باشید.
- همچنین بانک اطلاعاتی پایهی آن که به صورت خودکار در اولین بار اجرای برنامه تشکیل میشود، مبتنی بر LocalDB است. بنابراین اگر قصد تغییری را در تنظیمات Context آن ندارید، بهتر است LocalDB را نیز بر روی سیستم نصب کنید. هرچند با تغییر تنظیم ActiveDatabase به SqlServer در فایل appsettings.json، برنامه به صورت خودکار از نگارش کامل SqlServer استفاده خواهد کرد. رشتهی اتصالی آن نیز در مدخل ConnectionStrings فایل appsettings.json ذکر شدهاست و قابل تغییر است. برای شروع به کار، نیازی به اجرای مراحل Migrations را نیز ندارید و همینقدر که برنامه را اجرا کنید، بانک اطلاعاتی آن نیز تشکیل خواهد شد.
- کاربر پیش فرض Admin سیستم و کلمهی عبور آن از مدخل AdminUserSeed فایل appsettings.json خوانده میشوند.
- تنظیمات ایمیل پیش فرض برنامه به استفادهی از PickupFolder در مدخل Smtp فایل appsettings.json تنظیم شدهاست. بنابراین تمام ایمیلهای برنامه را جهت آزمایش محلی میتوانید در مسیر PickupFolder آن یا همان c:\smtppickup مشاهده کنید. محتوای این ایمیلها را نیز توسط مرورگر (drag&drop بر روی یک tab جدید) و یا برنامهی Outlook میتوان مشاهده کرد.
سفارشی سازی کلید اصلی موجودیتهای ASP.NET Core Identity
ASP.NET Core Identity به همراه دو سری موجودیت است. یک سری سادهی آن که از یک string به عنوان نوع کلید اصلی استفاده میکنند و سری دوم، حالت جنریک که در آن میتوان نوع کلید اصلی را به صورت صریحی قید کرد و تغییر داد. در اینجا نیز قصد داریم از حالت جنریک استفاده کرده و نوع کلید اصلی جداول را تغییر دهیم. تمام این موجودیتهای تغییر یافته را در پوشهی src\ASPNETCoreIdentitySample.Entities\Identity نیز میتوانید مشاهده کنید و شامل موارد ذیل هستند:
جدول نقشهای سیستم
public class Role : IdentityRole<int, UserRole, RoleClaim>, IAuditableEntity { public Role() { } public Role(string name) : this() { Name = name; } public Role(string name, string description) : this(name) { Description = description; } public string Description { get; set; } }
در اولین بار اجرای برنامه، نقش Admin در این جدول ثبت خواهد شد.
جدول کاربران منتسب به نقشها
public class UserRole : IdentityUserRole<int>, IAuditableEntity { public virtual User User { get; set; } public virtual Role Role { get; set; } }
در اولین بار اجرای برنامه، کاربر شماره 1 یا همان Admin به نقش شماره 1 یا همان Admin، انتساب داده میشود.
جدول جدید IdentityRoleClaim سیستم
public class RoleClaim : IdentityRoleClaim<int>, IAuditableEntity { public virtual Role Role { get; set; } }
جدول UserClaim سیستم
public class UserClaim : IdentityUserClaim<int>, IAuditableEntity { public virtual User User { get; set; } }
جداول توکن و لاگینهای کاربران
public class UserToken : IdentityUserToken<int>, IAuditableEntity { public virtual User User { get; set; } } public class UserLogin : IdentityUserLogin<int>, IAuditableEntity { public virtual User User { get; set; } }
جدول کاربران سیستم
public class User : IdentityUser<int, UserClaim, UserRole, UserLogin>, IAuditableEntity { public User() { UserUsedPasswords = new HashSet<UserUsedPassword>(); UserTokens = new HashSet<UserToken>(); } [StringLength(450)] public string FirstName { get; set; } [StringLength(450)] public string LastName { get; set; } [NotMapped] public string DisplayName { get { var displayName = $"{FirstName} {LastName}"; return string.IsNullOrWhiteSpace(displayName) ? UserName : displayName; } } [StringLength(450)] public string PhotoFileName { get; set; } public DateTimeOffset? BirthDate { get; set; } public DateTimeOffset? CreatedDateTime { get; set; } public DateTimeOffset? LastVisitDateTime { get; set; } public bool IsEmailPublic { get; set; } public string Location { set; get; } public bool IsActive { get; set; } = true; public virtual ICollection<UserUsedPassword> UserUsedPasswords { get; set; } public virtual ICollection<UserToken> UserTokens { get; set; } } public class UserUsedPassword : IAuditableEntity { public int Id { get; set; } public string HashedPassword { get; set; } public virtual User User { get; set; } public int UserId { get; set; } }
فیلد IsActive نیز از این جهت اضافه شدهاست تا بجای حذف فیزیکی یک کاربر، بتوان اکانت او را غیرفعال کرد.
تعریف Shadow properties ثبت تغییرات رکوردها
در #C ارثبری چندگانهی کلاسها ممنوع است؛ مگر اینکه از اینترفیسها استفاده شود. برای مثال IdentityUser یک کلاس است و در اینجا دیگر نمیتوان کلاس دومی را به نام BaseEntity جهت اعمال خواص اضافهتری اعمال کرد. به همین جهت است که اعمال اینترفیس خالی IAuditableEntity را در اینجا مشاهده میکنید. این اینترفیس کار علامتگذاری کلاسهایی را انجام میدهد که قصد داریم به آنها به صورت خودکار، خواصی مانند تاریخ ثبت رکورد، تاریخ ویرایش آن و غیره را اعمال کنیم.
در Context برنامه، به اطلاعات src\ASPNETCoreIdentitySample.Entities\AuditableEntity مراجعه شده و متد AddAuditableShadowProperties بر روی تمام کلاسهایی از نوع IAuditableEntity اعمال میشود. این متد خواص مدنظر ما را مانند ModifiedDateTime به صورت Shadow properties به موجودیتهای علامتگذاری شده اضافه میکند.
همچنین متد SetAuditableEntityPropertyValues، کار مقدار دهی خودکار این خواص را انجام خواهد داد. بنابراین دیگر نیازی نیست در برنامه برای مثال IP شخص ثبت کننده یا ویرایش کننده را به صورت دستی مقدار دهی کرد. هم تعریف و هم مقدار دهی آن توسط Change tracker سیستم به صورت خودکار انجام خواهند شد.
تاثیر افزودن Shadow properties را بر روی کلاس نقشهای سیستم، در تصویر فوق ملاحظه میکنید. خواصی که به صورت معمول در کلاسهای برنامه ظاهر نمیشوند و صرفا هدف بازبینی سیستم را برآورده میکنند و مدیریت آنها نیز در اینجا کاملا خودکار است.
سفارشی سازی DbContext برنامه
نحوهی سفارشی سازی DbContext برنامه را در پوشهی src\ASPNETCoreIdentitySample.DataLayer\Context و src\ASPNETCoreIdentitySample.DataLayer\Mappings ملاحظه میکنید. پوشهی Context حاوی کلاس ApplicationDbContextBase است که تمام سفارشی سازیهای لازم بر روی آن انجام شدهاست؛ شامل:
- تغییر نوع کلید اصلی موجودیتها به همراه معرفی موجودیتهای تغییر یافته:
public abstract class ApplicationDbContextBase : IdentityDbContext<User, Role, int, UserClaim, UserRole, UserLogin, RoleClaim, UserToken>, IUnitOfWork
- اعمال متد BeforeSaveTriggers به تمام نگارشهای مختلف SaveChanges
protected void BeforeSaveTriggers() { ValidateEntities(); SetShadowProperties(); this.ApplyCorrectYeKe(); }
- انتخاب نوع بانک اطلاعاتی مورد استفاده در متد OnConfiguring
در اینجا است که خاصیت ActiveDatabase تنظیم شدهی در فایل appsettings.json خوانده شده و اعمال میشوند. تعریف متد GetDbConnectionString را در کلاس SiteSettingsExtesnsions مشاهده میکنید. کار آن استفادهی از بانک اطلاعاتی درون حافظهای، جهت انجام آزمونهای واحد و یا استفادهی از LocalDb و یا نگارش کامل SQL Server میباشد. اگر علاقمند بودید تا بانک اطلاعاتی دیگری (مثلا SQLite) را نیز اضافه کنید، ابتدا enum ایی به نام ActiveDatabase را تغییر داده و سپس متد GetDbConnectionString و متد OnConfiguring را جهت درج اطلاعات این بانک اطلاعاتی جدید، اصلاح کنید.
پس از تعریف این DbContext پایهی سفارشی سازی شده، کلاس جدید ApplicationDbContext را مشاهده میکنید. این کلاس Context ایی است که در برنامه از آن استفاده میشود و از کلاس پایه ApplicationDbContextBase مشتق شدهاست:
public class ApplicationDbContext : ApplicationDbContextBase
تنظیمات mapping آنها نیز به متد OnModelCreating این کلاس اضافه خواهند شد. فقط نحوهی استفادهی از آن را بهخاطر داشته باشید:
protected override void OnModelCreating(ModelBuilder builder) { // it should be placed here, otherwise it will rewrite the following settings! base.OnModelCreating(builder); // Adds all of the ASP.NET Core Identity related mappings at once. builder.AddCustomIdentityMappings(SiteSettings.Value); // Custom application mappings // This should be placed here, at the end. builder.AddAuditableShadowProperties(); }
سپس متد AddCustomIdentityMappings ذکر شدهاست. این متد اطلاعات src\ASPNETCoreIdentitySample.DataLayer\Mappings را به صورت خودکار و یکجا اضافه میکند که در آن برای مثال نام جداول پیش فرض Identity سفارشی سازی شدهاند.
در آخر باید AddAuditableShadowProperties فراخوانی شود تا خواص سایهای که پیشتر در مورد آنها بحث شد، به سیستم به صورت خودکار اضافه شوند.
تمام نگاشتهای سفارشی شما باید در این میان و در قسمت «Custom application mappings» درج شوند.
در قسمت بعدی، نحوهی سفارشی سازی سرویسهای پایهی Identity را بررسی خواهیم کرد. بدون این سفارشی سازی و اطلاعات رسانی به سرویسهای پایه که از چه موجودیتهای جدید سفارشی سازی شدهایی در حال استفاده هستیم، کار Migrations انجام نخواهد شد.
کدهای کامل این سری را در مخزن کد DNT Identity میتوانید ملاحظه کنید.