اشتراکها
معرفی کامپوننت EasyQuery
از کامپوننت EasyQuery برای ارائه یک رابط کاربر پسند (UserFriendly) جهت جستجو و فیلترینگ پیشرفته بر روی اطلاعات توسط کاربران استفاده میشود. این کامپوننت به کاربران اجازه میدهد تا اطلاعاتی را که نیاز دارند به راحتی و بدون نیاز به کمک شما یا تیم پشتیبانی، فراهم نمایند.
نسخه آزمایشی گزارش گیری ساخته شده با EasyQuery تحت وب
نسخههای آزمایشی دیگر بر روی ASP.NET MVC و Silverlight و ASP.NET AJAX و Win Forms
دانلود کدهای نسخه آزمایشی تحت وب
ASP.NET Core 2.0 به همراه یک AuthenticationMiddleware است که یکی از قابلیتهای این میانافزار، افزودن اطلاعات کاربر و یا همان HttpContext.User به یک کوکی رمزنگاری شده و سپس اعتبارسنجی این کوکی در درخواستهای بعدی کاربر است. این مورد سبب خواهد شد تا بتوان بدون نیاز به پیاده سازی سیستم کامل ASP.NET Core Identity، یک سیستم اعتبارسنجی سبک، ساده و سفارشی را تدارک دید.
تعریف موجودیتهای مورد نیاز جهت طراحی یک سیستم اعتبارسنجی
در اینجا کنترل کامل سیستم در اختیار ما است و در این حالت میتوان طراحی تمام قسمتها را از ابتدا و مطابق میل خود انجام داد. برای مثال سیستم اعتبارسنجی سادهی ما، شامل جدول کاربران و نقشهای آنها خواهد بود و این دو با هم رابطهی many-to-many دارند. به همین جهت جدول UserRole نیز در اینجا پیش بینی شدهاست.
جدول کاربران
در اینجا SerialNumber فیلدی است که با هر بار ویرایش اطلاعات کاربران باید از طرف برنامه به روز رسانی شود. از آن جهت غیرمعتبر سازی کوکی کاربر استفاده خواهیم کرد. برای مثال اگر خاصیت فعال بودن او تغییر کرد و یا نقشهای او را تغییر دادیم، کاربر در همان لحظه باید logout شود. به همین جهت چنین فیلدی در اینجا در نظر گرفته شدهاست تا با بررسی آن بتوان وضعیت معتبر بودن کوکی او را تشخیص داد.
جدول نقشهای کاربران
البته این سیستم ساده دارای یک سری نقش ثابت و مشخص است.
که این نقشها را در ابتدای کار برنامه به بانک اطلاعات اضافه خواهیم کرد.
جدول ارتباط نقشها با کاربران و برعکس
وجود این جدول در EF Core جهت تعریف یک رابطهی many-to-many ضروری است.
تعریف Context برنامه و فعالسازی Migrations در EF Core 2.0
DbContext برنامه را به صورت ذیل در یک اسمبلی دیگر اضافه خواهیم کرد:
در اینجا موجودیتهای برنامه به صورت 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 ذیل دقیقا مشخص سازی همین مساله است:
کاری که در اینجا انجام شده، خواندن DefaultConnection از قسمت ConnectionStrings فایل appsettings.json است:
و سپس استفادهی از آن جهت تنظیم رشتهی اتصالی متد UseSqlServer و در آخر وهله سازی ApplicationDbContext.
کار یافتن این کلاس در حین تدارک و اعمال مهاجرتها توسط EF Core 2.0 خودکار بوده و باید محل قرارگیری آن دقیقا در اسمبلی باشد که DbContext برنامه در آن تعریف شدهاست.
تدارک لایه سرویسهای برنامه
پس از مشخص شدن ساختار موجودیتها و همچنین Context برنامه، اکنون میتوان لایه سرویس برنامه را به صورت ذیل تکمیل کرد:
سرویس کاربران
کار این سرویس ابتدایی کاربران، یافتن یک یک کاربر بر اساس Id او و یا کلمهی عبور و نام کاربری او است. از این امکانات در حین لاگین و یا اعتبارسنجی کوکی کاربر استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا میتوانید مشاهده کنید.
سرویس نقشهای کاربران
از این سرویس برای یافتن نقشهای کاربر لاگین شدهی به سیستم و افزودن آنها به کوکی رمزنگاری شدهی اعتبارسنجی او استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا میتوانید مشاهده کنید.
سرویس آغاز بانک اطلاعاتی
از این سرویس در آغاز کار برنامه برای اعمال خودکار مهاجرتهای تولیدی و همچنین ثبت نقشهای آغازین سیستم به همراه افزودن کاربر Admin استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا میتوانید مشاهده کنید.
سرویس اعتبارسنجی کوکیهای کاربران
یکی از قابلیتهای میانافزار اعتبارسنجی ASP.NET Core 2.0، رخدادی است که در آن اطلاعات کوکی دریافتی از کاربر، رمزگشایی شده و در اختیار برنامه جهت تعیین اعتبار قرار میگیرد:
کار این سرویس، تعیین اعتبار موارد ذیل است:
- آیا کوکی دریافت شده دارای اطلاعات HttpContext.User است؟
- آیا این کوکی به همراه اطلاعات فیلد SerialNumber است؟
- آیا این کوکی به همراه Id کاربر است؟
- آیا کاربری که بر اساس این Id یافت میشود غیرفعال شدهاست؟
- آیا کاربری که بر اساس این Id یافت میشود دارای SerialNumber یکسانی با نمونهی موجود در بانک اطلاعاتی است؟
اگر خیر، این اعتبارسنجی رد شده و بلافاصله کوکی کاربر نیز معدوم خواهد شد.
تنظیمات ابتدایی میانافزار اعتبارسنجی کاربران در ASP.NET Core 2.0
تنظیمات کامل ابتدایی میانافزار اعتبارسنجی کاربران در ASP.NET Core 2.0 را در فایل Startup.cs میتوانید مشاهده کنید.
ابتدا سرویسهای برنامه معرفی شدهاند:
سپس تنظیمات مرتبط با ترزیق وابستگیهای ApplicationDbContext برنامه انجام شدهاست. در اینجا رشتهی اتصالی، از فایل appsettings.json خوانده شده و سپس در اختیار متد UseSqlServer قرار میگیرد:
در ادامه تعدادی Policy مبتنی بر نقشهای ثابت سیستم را تعریف کردهایم. این کار اختیاری است اما روش توصیه شدهی در ASP.NET Core، کار با Policyها است تا کار مستقیم با نقشها. Policyها انعطاف پذیری بیشتری را نسبت به نقشها ارائه میدهند و در اینجا به سادگی میتوان چندین نقش و یا حتی Claim را با هم ترکیب کرد و به صورت یک Policy ارائه داد:
قسمت اصلی تنظیمات میان افزار اعتبارسنجی مبتنی بر کوکیها در اینجا قید شدهاست:
ابتدا مشخص شدهاست که روش مدنظر ما، اعتبارسنجی مبتنی بر کوکیها است و سپس تنظیمات مرتبط با کوکی رمزنگاری شدهی برنامه مشخص شدهاند. تنها قسمت مهم آن CookieAuthenticationEvents است که نحوهی پیاده سازی آنرا با معرفی سرویس ICookieValidatorService پیشتر بررسی کردیم. این قسمت جائی است که پس از هر درخواست به سرور اجرا شده و کوکی رمزگشایی شده، در اختیار برنامه جهت اعتبارسنجی قرار میگیرد.
کار نهایی تنظیمات میان افزار اعتبارسنجی در متد Configure با فراخوانی UseAuthentication صورت میگیرد. اینجا است که میان افزار، به برنامه معرفی خواهد شد:
همچنین پس از آن، کار اجرای سرویس آغاز بانک اطلاعاتی نیز انجام شدهاست تا نقشها و کاربر Admin را به سیستم اضافه کند:
پیاده سازی ورود و خروج به سیستم
پس از این مقدمات به مرحلهی آخر پیاده سازی این سیستم اعتبارسنجی میرسیم.
پیاده سازی Login
در اینجا از سرویس کاربران استفاده شده و بر اساس نام کاربری و کلمهی عبور ارسالی به سمت سرور، این کاربر یافت خواهد شد.
در صورت وجود این کاربر، مرحلهی نهایی کار، فراخوانی متد الحاقی HttpContext.SignInAsync است:
مهمترین کار متد HttpContext.SignInAsync علاوه بر تنظیم طول عمر کوکی کاربر، قسمت createCookieClaimsAsync است که به صورت ذیل پیاده سازی شدهاست:
در اینجا است که اطلاعات اضافی کاربر مانند Id او یا نقشهای او به کوکی رمزنگاری شدهی تولیدی توسط HttpContext.SignInAsync اضافه شده و در دفعات بعدی و درخواستهای بعدی او، به صورت خودکار توسط مرورگر به سمت سرور ارسال خواهند شد. این کوکی است که امکان کار با MyProtectedApiController و یا MyProtectedAdminApiController را فراهم میکند. اگر شخص لاگین کرده باشد، بلافاصله قابلیت دسترسی به امکانات محدود شدهی توسط فیلتر Authorize را خواهد یافت. همچنین در این مثال چون کاربر Admin لاگین میشود، امکان دسترسی به Policy مرتبطی را نیز خواهد یافت:
پیاده سازی Logout
متد الحاقی HttpContext.SignOutAsync کار Logout کاربر را تکمیل میکند.
آزمایش نهایی برنامه
در فایل index.html ، نمونهای از متدهای لاگین، خروج و فراخوانی اکشن متدهای محافظت شده را مشاهده میکنید. این روش برای برنامههای تک صفحهای وب یا SPA نیز میتواند مفید باشد و به همین نحو کار میکنند.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
تعریف موجودیتهای مورد نیاز جهت طراحی یک سیستم اعتبارسنجی
در اینجا کنترل کامل سیستم در اختیار ما است و در این حالت میتوان طراحی تمام قسمتها را از ابتدا و مطابق میل خود انجام داد. برای مثال سیستم اعتبارسنجی سادهی ما، شامل جدول کاربران و نقشهای آنها خواهد بود و این دو با هم رابطهی 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; } }
جدول نقشهای کاربران
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; } }
تعریف 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); }); } }
سازندهی کلاس به همراه پارامتر 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); } }
{ "ConnectionStrings": { "DefaultConnection": "Data Source=(LocalDB)\\MSSQLLocalDB;Initial Catalog=ASPNETCore2CookieAuthenticationDB;Integrated Security=True;MultipleActiveResultSets=True;" }, "LoginCookieExpirationDays": 30 }
کار یافتن این کلاس در حین تدارک و اعمال مهاجرتها توسط 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); }
پیاده سازی کامل این سرویس را در اینجا میتوانید مشاهده کنید.
سرویس نقشهای کاربران
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(); }
پیاده سازی کامل این سرویس را در اینجا میتوانید مشاهده کنید.
سرویس اعتبارسنجی کوکیهای کاربران
یکی از قابلیتهای میانافزار اعتبارسنجی 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); } }; });
کار نهایی تنظیمات میان افزار اعتبارسنجی در متد 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(); }
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); }
[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 نیز میتواند مفید باشد و به همین نحو کار میکنند.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
نظرات مطالب
معرفی JSON Web Token
خیلی ممنون از مطلب مفیدتون.
روش token base authentication روش بسیار خوبی برای جایگزین شدن با روشهای پیشین است، به خصوص در برنامههای تک صفحه ای. سوالی که برای بنده مطرح شد. بنده در پروژه ای token و اطلاعات تکمیلی کاربر را در Local Storage ذخیره میکنم. حال اینکه وقتی token به کاربر داده شد، اگر اطلاعات را کپی کنم و در مرورگر دیگری این اطلاعات را وارد کنم من را تایید صلاحیت میکنه. در صورتی که این نکته میتونه خیلی خطر ساز باشه، چون token که به کاربر اعطا میشه کلید refresh token را هم به همراه خودش داره و در نهایت کاربر تا هر زمانی که بخواد میتونه لاگین شده باقی بمونه. ممنون میشم اگر راه حلی پیشنهاد بدید به بنده.
یک سوالی در مورد عملکرد برنامه تلگرام هم دیده بودم، در تلگرام هم فکر میکنم از token استفاده میشه. اما خیلی جالبه که شما میتونید sessionهای فعالتون را ببینید که از چند دستگاه لاگین شده اید. آیا اگر اطلاعات token را در دیتابیس نگه دارم میتونم مثل تلگرام این وضعیت را هم کنترل کنم؟ مثلا آیا میتونم از سمت سرور یک token که تایید صلاحیت هست را expire کنم؟
کار با Areas را تا ASP.NET MVC 5.x میتوانید در مطلب «ASP.NET MVC #14» مطالعه کنید. در ASP.NET Core، کلیات آن ثابت ماندهاست و تنظیمات ابتدایی آن اندکی تغییر کردهاند.
مفهوم Areas
Areas یکی از روشهای ساماندهی برنامههای بزرگ، به نواحی کوچکتری مانند قسمتهای مدیریتی، پشتیبانی از کاربران و غیره است. به این ترتیب میتوان کنترلرها، Viewها و مدلهای هر قسمت را از قسمتی دیگر، جدا کرد و مدیریت پروژه را سادهتر نمود. هر Area دارای ساختار پوشههای مرتبط به خود میباشد و به این نحو است که جداسازی این نواحی مختلف را میسر میکند؛ تا بهتر مشخص باشد که هر المانی متعلق است به چه ناحیهای. به علاوه در این حالت میتوان پروژه را بین چندین توسعه دهندهی مختلف نیز تقسیم کرد؛ بدون اینکه در کار یکدیگر تداخلی ایجاد کنند.
ایجاد Areas
اگر با ASP.NET MVC 5.x کار کرده باشید، میدانید که ویژوال استودیو با کلیک راست بر روی پروژهی جاری، گزینه افزودن یک Area جدید را به همراه دارد. یک چنین قابلیتی تا ASP.NET Core 1.1 به ابزارهای همراه آن افزوده نشدهاست. بنابراین تمام مراحل ذیل را باید دستی ایجاد کنید و هنوز قالب از پیش تعریف شده و ساده کنندهای برای اینکار وجود ندارد:
همانطور که در تصویر نیز ملاحظه میکنید، نیاز است در ریشهی پروژه، پوشهی جدیدی را به نام Areas ایجاد کرد. سپس در داخل این پوشه میتوان نواحی مختلفی را با پوشه بندیهای مجزایی ایجاد نمود. برای مثال در اینجا ناحیهی Blog ایجاد شدهاست که در این ناحیه نیز پوشههای Controllers و Views آن باید به صورت دستی ایجاد شوند.
افزودن مسیریابی مرتبط با Areas
پس از اضافه کردن دستی پوشههای Areas و ناحیهی جدید، به همراه ساختار پوشههای کنترلرها و Viewهای آن، اکنون نیاز است این ناحیهی جدید را به سیستم مسیریابی معرفی نمود. برای این منظور به فایل آغازین برنامه مراجعه کرده و در متد Configure آن، تعریف جدید ذیل را اضافه میکنیم:
مسیریابی پیشفرض را در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 9 - بررسی تغییرات مسیریابی» پیشتر بررسی کردهایم. در اینجا پیش از این مسیریابی، مسیریابی جدید areas تعریف شدهاست. قید exists در اینجا به معنای تنها استفادهی از نواحی تعریف شدهی در برنامهی جاری است و الگوی تعریف شده، تمام آنها را شامل میشود و دیگر نیازی به تعریف مسیریابی جداگانهای به ازای ایجاد هر Area جدید نیست (برخلاف ASP.NET MVC 5.x).
علامتگذاری کنترلرهای یک ناحیه
تمام اصول کار کردن با کنترلرهای یک ناحیه، مانند سایر کنترلرهای دیگر برنامهاست؛ با یک تفاوت:
در ASP.NET Core حتما نیاز است توسط ویژگی جدید Area، نام ناحیهی کنترلر را صریحا مشخص کرد؛ در غیراینصورت، صرفنظر از محل تعریف این کنترلر، اطلاعات آن متعلق به هیچ ناحیهای نبوده و وارد سیستم مسیریابی Areas نمیشود.
یک نکته: اگر از Attribute routing استفاده میکنید، توکن مرتبط با نواحی، [area] نام دارد:
فعالسازی Layout و Tag Helpers در Areas
اگر در همین حال، برنامه را اجرا و به مسیر http://localhost/blog مراجعه کنید، هرچند اطلاعات View متناظر با کنترلر Home و اکشن متد Index آن نمایش داده میشوند، اما این View نه layout دارد و نه Tag helpers آن پردازش شدهاند. برای فعالسازی این دو مورد، دو فایل ViewStart.cshtml_ و ViewImports.cshtml_ را از پوشهی views اصلی پروژه، به پوشهی views این Area جدید کپی کنید. فایل ViewStart، نام و مسیر فایل layout پیش فرض ناحیه را مشخص میکند و فایل ViewImports حاوی تعاریف فعالسازی Tag helpers است:
هر Area میتواند layout خاص خودش را داشته باشد؛ اما اگر فایل ViewStart آن به نحو ذیل مقدار دهی شود، به فایل اصلی واقع در پوشهی Views/Shared/_Layout.cshtml ریشهی پروژه، اشاره میکند.
و برای تغییر و یا مقداردهی صریح آن میتوان به صورت ذیل عمل کرد:
Areas و تاثیر آنها در حین لینک دهی به قسمتهای مختلف برنامه
اگر قرار است لینکی به قسمتی واقع در همان Area جاری مرتبط شود، نیازی نیست تا هیچ نکتهی خاصی را درنظر گرفت و تولید لینکها به نحو صحیحی صورت میگیرند:
اما اگر میخواهیم به ناحیهی جدیدی به نام Services و کنترلر و اکشن متد خاصی از آن، از یک ناحیهی دیگر لینک دهیم، نیاز است asp-area را صریحا ذکر کرد:
به علاوه اگر قصد تعریف لینکی را به یک اکشن متد واقع در کنترلری که در هیچ ناحیهای قرار ندارد، داشته باشیم، باید asp-area آنرا خالی ذکر کنیم:
تاثیر Areas بر روی تنظیمات توزیع برنامه
فایلهای View موجود در Areas نیز باید در حین توزیع نهایی برنامه ارائه شوند؛ مگر اینکه آنها را از پیش کامپایل کرده باشیم. اگر از حالت از پیش کامپایل کردن Viewها استفاده نمیشود، نیاز است قسمت publishOptions فایل project.json را به نحو ذیل در جهت الحاق فایلهای Viewهای نواحی مختلف، ویرایش و تکمیل کرد:
مفهوم Areas
Areas یکی از روشهای ساماندهی برنامههای بزرگ، به نواحی کوچکتری مانند قسمتهای مدیریتی، پشتیبانی از کاربران و غیره است. به این ترتیب میتوان کنترلرها، Viewها و مدلهای هر قسمت را از قسمتی دیگر، جدا کرد و مدیریت پروژه را سادهتر نمود. هر Area دارای ساختار پوشههای مرتبط به خود میباشد و به این نحو است که جداسازی این نواحی مختلف را میسر میکند؛ تا بهتر مشخص باشد که هر المانی متعلق است به چه ناحیهای. به علاوه در این حالت میتوان پروژه را بین چندین توسعه دهندهی مختلف نیز تقسیم کرد؛ بدون اینکه در کار یکدیگر تداخلی ایجاد کنند.
ایجاد Areas
اگر با ASP.NET MVC 5.x کار کرده باشید، میدانید که ویژوال استودیو با کلیک راست بر روی پروژهی جاری، گزینه افزودن یک Area جدید را به همراه دارد. یک چنین قابلیتی تا ASP.NET Core 1.1 به ابزارهای همراه آن افزوده نشدهاست. بنابراین تمام مراحل ذیل را باید دستی ایجاد کنید و هنوز قالب از پیش تعریف شده و ساده کنندهای برای اینکار وجود ندارد:
همانطور که در تصویر نیز ملاحظه میکنید، نیاز است در ریشهی پروژه، پوشهی جدیدی را به نام Areas ایجاد کرد. سپس در داخل این پوشه میتوان نواحی مختلفی را با پوشه بندیهای مجزایی ایجاد نمود. برای مثال در اینجا ناحیهی Blog ایجاد شدهاست که در این ناحیه نیز پوشههای Controllers و Views آن باید به صورت دستی ایجاد شوند.
افزودن مسیریابی مرتبط با Areas
پس از اضافه کردن دستی پوشههای Areas و ناحیهی جدید، به همراه ساختار پوشههای کنترلرها و Viewهای آن، اکنون نیاز است این ناحیهی جدید را به سیستم مسیریابی معرفی نمود. برای این منظور به فایل آغازین برنامه مراجعه کرده و در متد Configure آن، تعریف جدید ذیل را اضافه میکنیم:
app.UseMvc(routes => { routes.MapRoute( name: "areas", template: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
علامتگذاری کنترلرهای یک ناحیه
تمام اصول کار کردن با کنترلرهای یک ناحیه، مانند سایر کنترلرهای دیگر برنامهاست؛ با یک تفاوت:
[Area("Blog")] public class HomeController : Controller { public IActionResult Index() { return View(); } }
یک نکته: اگر از Attribute routing استفاده میکنید، توکن مرتبط با نواحی، [area] نام دارد:
[Route("[area]/app/[controller]/actions/[action]/{id:weekday?}")]
فعالسازی Layout و Tag Helpers در Areas
اگر در همین حال، برنامه را اجرا و به مسیر http://localhost/blog مراجعه کنید، هرچند اطلاعات View متناظر با کنترلر Home و اکشن متد Index آن نمایش داده میشوند، اما این View نه layout دارد و نه Tag helpers آن پردازش شدهاند. برای فعالسازی این دو مورد، دو فایل ViewStart.cshtml_ و ViewImports.cshtml_ را از پوشهی views اصلی پروژه، به پوشهی views این Area جدید کپی کنید. فایل ViewStart، نام و مسیر فایل layout پیش فرض ناحیه را مشخص میکند و فایل ViewImports حاوی تعاریف فعالسازی Tag helpers است:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{ Layout = "_Layout"; }
@{ Layout = "~/Areas/Blog/Views/Shared/_Layout.cshtml"; }
Areas و تاثیر آنها در حین لینک دهی به قسمتهای مختلف برنامه
اگر قرار است لینکی به قسمتی واقع در همان Area جاری مرتبط شود، نیازی نیست تا هیچ نکتهی خاصی را درنظر گرفت و تولید لینکها به نحو صحیحی صورت میگیرند:
<a asp-action="Index" asp-controller="Home">Link</a>
<a asp-area="Services" asp-controller="Home" asp-action="Index">Go to Services’ Home Page</a>
<a asp-action="Index" asp-controller="Home" asp-area="">Link</a>
تاثیر Areas بر روی تنظیمات توزیع برنامه
فایلهای View موجود در Areas نیز باید در حین توزیع نهایی برنامه ارائه شوند؛ مگر اینکه آنها را از پیش کامپایل کرده باشیم. اگر از حالت از پیش کامپایل کردن Viewها استفاده نمیشود، نیاز است قسمت publishOptions فایل project.json را به نحو ذیل در جهت الحاق فایلهای Viewهای نواحی مختلف، ویرایش و تکمیل کرد:
"publishOptions": { "include": [ "Areas/**/*.cshtml", .... .... ]
مدتها از کلاس DelegateCommand معرفی شده در این آدرس استفاده میکردم. این کلاس یک مشکل جزئی دارد و آن هم عدم بررسی مجدد قسمت canExecute به صورت خودکار هست.
خلاصهای برای کسانی که بار اول هست با این مباحث برخورد میکنند؛ یا MVVM به زبان بسیار ساده:
در برنامه نویسی متداول سیستم مایکروسافتی، در هر سیستمی که ایجاد کرده و در هر فناوری که ارائه داده از زمان VB6 تا امروز، شما روی یک دکمه مثلا دوبار کلیک میکنید و در فایل اصطلاحا code behind این فرم و در روال رخدادگردان آن شروع به کد نویسی خواهید کرد. این مورد تقریبا در همه جا صادق است؛ از WinForms تا WPF تا Silverlight تا حتی ASP.NET Webforms . به عمد هم این طراحی صورت گرفته تا برنامه نویسها در این محیطها زیاد احساس غریبی نکنند. اما این روش یک مشکل مهم دارد و آن هم «توهم» جداسازی رابط کاربر از کدهای برنامه است. به ظاهر یک فایل فرم وجود دارد و یک فایل جدای code behind ؛ اما در عمل هر دوی اینها یک partial class یا به عبارتی «یک کلاس» بیشتر نیستند. «فکر میکنیم» که از هم جدا شدند اما واقعا یکی هستند. شما در code behind صفحه به صورت مستقیم با عناصر رابط کاربری سروکار دارید و کدهای شما به این عناصر گره خوردهاند.
شاید بپرسید که چه اهمیتی دارد؟
مشکل اول: امکان نوشتن آزمونها واحد برای این متدها وجود ندارد یا بسیار سخت است. این متدها فقط با وجود فرم و رابط کاربری متناظر با آنها هست که معنا پیدا میکنند و تک تک عناصر آنها وهله سازی میشوند.
مشکل دوم: کد نوشته فقط برای همین فرم جاری آن قابل استفاده است؛ چون به صورت صریح به عناصر موجود در فرم اشاره میکند. نمیتونید این فایل code behind رو بردارید بدون هیچ تغییری برای فرم دیگری استفاده کنید.
مشکل سوم: نمیتونید طراحی فرم رو بدید به یک نفر، کد نویسی اون رو به شخصی دیگر. چون ایندو لازم و ملزوم یکدیگرند.
این سیستم کد نویسی دهه 90 است.
چند سالی است که طراحان سعی کردهاند این سیستم رو دور بزنند و روشهایی رو ارائه بدن که در آنها فرمهای برنامه و فایلهای پیاده سازی کنندهی منطق آن هیچگونه ارتباط مستقیمی باهم نداشته باشند؛ به هم گره نخورده باشند؛ ارجاعی به هیچیک از عناصر بصری فرم را در خود نداشته باشند. به همین دلیل ASP.NET MVC به وجود آمده و در همان سالها مثلا MVVM .
سؤال:
الان که رابط کاربری از فایل پیاده سازی کننده منطق آن جدا شده و دیگر Code behind هم نیست (همان partial class های متداول)، این فایلها چطور متوجه میشوند که مثلا روی یک فرم، شیءایی قرار گرفته؟ از کجا متوجه خواهند شد که روی دکمهای کلیک شده؟ اینها که ارجاعی از فرم را در درون خود ندارند.
در الگوی MVVM این سیم کشی توسط امکانات قوی Binding موجود در WPF میسر میشود. در ASP.NET MVC چیزی شبیه به آن به نام Model binder و همان مکانیزمهای استاندارد HTTP این کار رو میکنه. در MVVM شما بجای code behind خواهید داشت ViewModel (اسم جدید آن). در ASP.NET MVC این اسم شده Controller. بنابراین اگر این اسامی رو شنیدید زیاد تعجب نکنید. اینها همان Code behind قدیمی هستند اما ... بدون داشتن ارجاعی از رابط کاربری در خود که ... اطلاعات موجود در فرم به نحوی به آنها Bind و ارسال میشوند.
این سیم کشیها هم نامرئی هستند. یعنی فایل ViewModel یا فایل Controller نمیدونند که دقیقا از چه کنترلی در چه فرمی این اطلاعات دریافت شده.
این ایده هم جدید نیست. شاید بد نباشه به دوران طلایی Win32 برگردیم. همان توابع معروف PostMessage و SendMessage را به خاطر دارید؟ شما در یک ترد میتونید با مثلا PostMessage شیءایی رو به یک فرم که در حال گوش فرا دادن به تغییرات است ارسال کنید (این سیم کشی هم نامرئی است). بنابراین پیاده سازی این الگوها حتی در Win32 و کلیه فریم ورکهای ساخته شده بر پایه آنها مانند VCL ، VB6 ، WinForms و غیره ... «از روز اول» وجود داشته و میتونستند بعد از 10 سال نیان بگن که اون روشهای RAD ایی رو که ما پیشنهاد دادیم، میشد خیلی بهتر از همان ابتدا، طور دیگری پیاده سازی بشه.
ادامه بحث!
این سیم کشی یا اصطلاحا Binding ، در مورد رخدادها هم در WPF وجود داره و اینبار به نام Commands معرفی شدهاست. به این معنا که بجای اینکه بنویسید:
<Button Click="btnClick_Event">Last</Button>
بنویسید:
<Button Command="{Binding GoLast}">Last</Button>
حالا باید مکانیزمی وجود داشته باشه تا این پیغام رو به ViewModel برنامه برساند. اینکار با پیاده سازی اینترفیس ICommand قابل انجام است که معرفی یک کلاس عمومی از پیاده سازی آنرا در ابتدای بحث مشاهده نمودید.
در یک DelegateCommand، توسط متد منتسب به executeAction، مشخص خواهیم کرد که اگر این سیم کشی برقرار شد (که ما دقیقا نمیدانیم و نمیخواهیم که بدانیم از کجا و کدام فرم دقیقا)، لطفا این اعمال را انجام بده و توسط متد منتسب به canExecute به سیستم Binding خواهیم گفت که آیا مجاز هستی این اعمال را انجام دهی یا خیر. اگر این متد false برگرداند، مثلا دکمه یاد شده به صورت خودکار غیرفعال میشود.
اما مشکل کلاس DelegateCommand ذکر شده هم دقیقا همینجا است. این دکمه تا ابد غیرفعال خواهد ماند. در WPF کلاسی وجود دارد به نام CommandManager که حاوی متدی استاتیکی است به نام InvalidateRequerySuggested. اگر این متد به صورت دستی فراخوانی شود، یکبار دیگر کلیه متدهای منتسب به تمام canExecute های تعریف شده، به صورت خودکار اجرا میشوند و اینجا است که میتوان دکمهای را که باید مجددا بر اساس شرایط جاری تغییر وضعیت پیدا کند، فعال کرد. بنابراین فراخوانی متد InvalidateRequerySuggested یک راه حل کلی رفع نقیصهی ذکر شده است.
راه حل دومی هم برای حل این مشکل وجود دارد. میتوان از رخدادگردان CommandManager.RequerySuggested استفاده کرد. روال منتسب به این رخدادگردان هر زمانی که احساس کند تغییری در UI رخ داده، فراخوانی میشود. بنابراین پیاده سازی بهبود یافته کلاس DelegateCommand به صورت زیر خواهد بود:
using System;
using System.Windows.Input;
namespace MvvmHelpers
{
// Ref.
// - http://johnpapa.net/silverlight/5-simple-steps-to-commanding-in-silverlight/
// - http://joshsmithonwpf.wordpress.com/2008/06/17/allowing-commandmanager-to-query-your-icommand-objects/
public class DelegateCommand<T> : ICommand
{
readonly Func<T, bool> _canExecute;
bool _canExecuteCache;
readonly Action<T> _executeAction;
public DelegateCommand(Action<T> executeAction, Func<T, bool> canExecute = null)
{
if (executeAction == null)
throw new ArgumentNullException("executeAction");
_executeAction = executeAction;
_canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
add { if (_canExecute != null) CommandManager.RequerySuggested += value; }
remove { if (_canExecute != null) CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute((T)parameter);
}
public void Execute(object parameter)
{
_executeAction((T)parameter);
}
}
}
استفاده از آن هم در ViewModel ساده است. یکبار خاصیتی به این نام تعریف میشود. سپس در سازنده کلاس مقدار دهی شده و متدهای متناظر آن تعریف خواهند شد:
public DelegateCommand<string> GoLast { set; get; }
//in ctor
GoLast = new DelegateCommand<string>(goLast, canGoLast);
private bool canGoLast(string data)
{
//ex.
return ListViewGuiData.CurrentPage != ListViewGuiData.TotalPage - 1;
}
private void goLast(string data)
{
//do something
}
مزیت کلاس DelegateCommand جدید هم این است که مثلا متد canGoLast فوق، به صورت خودکار با به روز رسانی UI ، فراخوانی و تعیین اعتبار مجدد میشود.
jQuery یا کلا JavaScript یعنی کدهای سمت کاربر. بنابراین این کدها باید دسترسی رسیدن به مدخل سمت سرور را داشته باشند و اگر عموم از آن منع شده باشند، یک اسکریپت معمولی هم بدیهی است که دسترسی خاصی نخواهد داشت چون سمت سرور اجرا نمیشود. فقط نتیجه عملیات آن به سمت سرور Post میشود.
یکی از کارهای متداول سمت سرور جهت منع عموم و فقط دسترسی دادن به jQuery مورد زیر است:
«بررسی امنیتی، حین استفاده از jQuery Ajax»
یکی از کارهای متداول سمت سرور جهت منع عموم و فقط دسترسی دادن به jQuery مورد زیر است:
«بررسی امنیتی، حین استفاده از jQuery Ajax»
امروزه اهمیت یادگیری JavaScript بر هیچ کس پوشیده نیست ، APIهای جدید HTML 5 و امکانات جدید وب مثل Geo Location ، Offline storage ، Canvas ، Offline Web applications و ... همه نیاز به دانش JavaScript دارند. در نتیجه نیاز است علاوه بر یادگیری JavaScript به تمیز کد نوشتن جهت سهولت نگهداری آگاه بود. همانطور که در کدهای سمت سرور مثل C# و یا PHP نیاز به استفاده از الگوهای طراحی (Design Patterns)است در JavaScript هم اوضاع به همین منوال است.
الگوی طراحی یک راه حل قابل استفاده مجدد است که برای حل مشکلات متداول در طراحی نرم افزار به کار میرود.
چرا به الگویهای طراحی JavaScript نیازمندیم ؟
- می خواهیم کد هایی با قابلیت استفادهی مجدد بنویسیم ، استفاده از عملکردهای مشابه در سطح صفحات یک Web application یا چند Web Application.
- می خواهیم کد هایی با قابلیت نگهداری بنویسیم ، هر چه قدر در فاز توسعه کدهای با کیفیت بنویسیم در فاز نگهداری از آن بهره میبریم. باید کد هایی بنویسیم که قابل Debug و خواندن توسط دیگر افراد تیم باشند.
- کدهای ما نباید با توابع و متغیرهای دیگر پلاگینها تداخل نامگزاری داشته باشند. در برنامههای امروزی بسیار مرسوم است که از پلاگینهای Third party استفاده شود. میخواهیم با رعایت Encapsulation and modularization در کدهایمان از این تداخل جلوگیری کنیم.
معمولا کدهای JavaScript که توسط اکثر ما نوشته میشود یک سری تابع پشت سرهم هست ، بدون هیچ کپسوله سازی :
function getDate() { var now = new Date(); var utc = now.getTime() + (now.getTimezoneOffset() * 60000); var est; est = new Date(utc + (3600000 * -4)); return dateFormat(est, "dddd, mmmm dS, yyyy, h:MM:ss TT") + " EST"; } function initiate_geolocationToTextbox() { navigator.geolocation.getCurrentPosition(handle_geolocation_queryToTextBox); } function handle_geolocation_queryToTextBox(position) { var longitude = position.coords.longitude; var latitude = position.coords.latitude; $("#IncidentLocation").val(latitude + " " + longitude); }
به این روش کدنویسی Function Spaghetti Code گفته میشود که معایبی دارد :
- توابع و متغیرها به Global scope برنامه افزوده میشوند.
- کد Modular نیست.
- احتمال رخ دادن Conflict در اسامی متغیرها و توابع بالا میرود.
- نگهداری کد به مرور زمان سخت میشود.
با شبیه سازی یک مثال مشکلات احتمالی را بررسی میکنیم :
// file1.js function saveState(obj) { // write code here to saveState of some object alert('file1 saveState'); } // file2.js (remote team or some third party scripts) function saveState(obj, obj2) { // further code... alert('file2 saveState"); }
همانطور که میبینید در این مثال در 2 فایل متفاوت در برنامه مان از 2 تابع با اسامی یکسان و امضای متفاوت استفاده کرده ایم . اگر فایلها را اینگونه در برنامه آدرس دهی کنیم :
<script src="file1.js" type="text/javascript"></script> <script src="file2.js" type="text/javascript"></script>
متد saveState در فایلی که دیرتر آدرس داده شده (file2.js) ، متد saveState در file1.js را Override میکند ، در نتیجه عملکردی که از متد saveState در فایل اول انتظار داریم اتفاق نمیافتد.
در پست بعدی به راه حل این مشکلات و کپسوله سازی خواهم پرداخت.
برای مطالعهی بیشتر کتاب (Learning JavaScript Design Patterns) را از دست ندهید.
مورد مدنظر شما اصطلاحا paging نام دارد و در گزارش گیریهای خصوصا برنامههای تحت وب که گرید نهایی را برنامه نویس با کدنویسی و ارائه منبع داده مناسبی طراحی و پیاده سازی میکند، بسیار مرسوم است (یک Take و Skip است در سمت کوئری LINQ نوشته شده). مثلا:
«واکشی اطلاعات به صورت chunk chunk (تکه تکه) و نمایش در ListView»
این قابلیت اگر در نرم افزارهای گزارشگیری یاد شده، پیاده سازی شدهاست (مانند مثال یاد شده MaximumRows و StartRowIndex را هربار در اختیار برنامه نویس قرار میدهند)، آنگاه قابل استفاده و پیاده سازی خواهد بود. در غیراینصورت، کار خاصی را نمیتوان انجام داد و باید مطابق نیاز تجاری آنها رفتار کرد.
«واکشی اطلاعات به صورت chunk chunk (تکه تکه) و نمایش در ListView»
این قابلیت اگر در نرم افزارهای گزارشگیری یاد شده، پیاده سازی شدهاست (مانند مثال یاد شده MaximumRows و StartRowIndex را هربار در اختیار برنامه نویس قرار میدهند)، آنگاه قابل استفاده و پیاده سازی خواهد بود. در غیراینصورت، کار خاصی را نمیتوان انجام داد و باید مطابق نیاز تجاری آنها رفتار کرد.
ASP.NET Core به همراه یک فریم ورک توکار ثبت وقایع (Logging) ارائه شدهی توسط تزریق وابستگیها است که به صورت پیش فرض نیز فعال است.
این تصویر را پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 6 - سرویسها و تزریق وابستگیها» مشاهده کردهاید. در اینجا لیست سرویسهایی را مشاهده میکنید که به صورت پیش فرض، ثبت شدهاند و فعال هستند و ILogger و ILoggerFactory نیز جزئی از آنها هستند. بنابراین نیازی به فعال سازی آنها نیست؛ اما برای استفادهی از آنها نیاز به انجام یک سری تنظیمات است.
پیاده سازی ثبت وقایع در ASP.NET Core
اولین قدم کار با فریم ورک ثبت وقایع ASP.NET Core، معرفی ILoggerFactory به متد Configure کلاس آغازین برنامه است:
متد Configure امضای مشخصی را ندارد و در اینجا به هر تعداد سرویسی که نیاز باشد، میتوان اینترفیسهای آنها را جهت تزریق وابستگیهای متناظر توسط IoC Containser توکار ASP.NET Core، معرفی کرد. در اینجا برای تنظیم ویژگیهای سرویس ثبت وقایع، تزریق وابستگی ILoggerFactory صورت گرفتهاست.
سطر اول متد، تنظیمات ثبت وقایع را از خاصیت Logging فایل appsettings.json برنامه میخواند (در مورد خاصیت Configuration، در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» بیشتر بحث شد) و لاگ کردن ویژهی در کنسول NET Core. را فعال میکند:
در مورد Log Level و یا سطوح ثبت وقایع، در ادامهی مطلب بحث خواهد شد.
و سطر دوم سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو میشود.
متد AddDebug برای شناسایی، نیاز به افزودن وابستگیهای ذیل در فایل project.json برنامه را دارد:
پس از این تنظیمات، برنامه را اجرا کنید.
در اینجا میتوانید ریز وقایعی را که توسط ASP.NET Core لاگ شدهاست، مشاهده کنید. برای مثال چه درخواستی صورت گرفتهاست و چقدر اجرای آن زمانبردهاست.
این فعال سازی مرتبط است به متد AddDebug که اضافه شد. اگر میخواهید خروجی AddConsole را هم مشاهده کنید، از طریق خط فرمان، به پوشهی اصلی پروژه وارد شده و سپس دستور dotnet run را اجرا کنید:
دستور dotnet run سبب راه اندازی وب سرور برنامه بر روی پورت 5000 شدهاست که در تصویر نیز مشخص است.
بنابراین اینبار برای دسترسی به برنامه باید مسیر http://localhost:5000 را در مرورگر خود طی کنید. در اینجا نیز میتوان حالتهای مختلف اطلاعات لاگ شده را مشاهده کرد و تمام اینها مرتبط هستند به ذکر متد AddConsole .
کار با سرویس ثبت وقایع ASP.NET Core از طریق تزریق وابستگیها
برای کار با سرویس ثبت وقایع توکار ASP.NET Core در قسمتهای مختلف برنامه، میتوان از ترزیق وابستگی ILogger آن استفاده کرد:
در این کنترلر، وابستگی اینترفیس ILogger با پارامتری از نوع کنترلر جاری به سازندهی کلاس تزریق شدهاست. علت ذکر این پارامتر جنریک این است که ILoggerFactory بداند چگونه باید متد CreateLogger خود را در پشت صحنه وهله سازی کند.
سپس با توجه به اینکه این سرویس جزو سرویسهای از پیش ثبت شدهی ASP.NET Core است، امکانات آن بدون نیاز به تنظیمات بیشتری در دسترس است. برای مثال از متد LogInformation آن در اکشن متد Hello استفاده شدهاست و خروجی عبارت لاگ شدهی آنرا در اینجا میتوانید مشاهده کنید:
سطوح مختلف ثبت وقایع
اینترفیس ILogger به همراه متدهای مختلفی است؛ مانند LogError، LogDebug و غیره. معانی آنها به شرح زیر هستند:
Debug (1): ثبت واقعهای است با بیشترین حد جزئیات ممکن که عموما شامل اطلاعات حساسی نیز میباشد. بنابراین نباید در حالت ارائهی نهایی برنامه فعال شود.
(2) Verbose: ثبت وقایعی مفصل، جهت بررسی مشکلات در حین توسعهی برنامه. تنها باید حاوی اطلاعاتی برای دیباگ برنامه باشند.
(3) Information: عموما برای ردیابی قسمتهای مختلف برنامه مورد استفاده قرار میگیرند.
(4) Warning: جهت ثبت واقعهای نامطلوب در سیستم بکار میرود و سبب قطع اجرای برنامه نمیشود.
(5) Errors: مشکلات برنامه را که سبب قطع سرویس دهی آن شدهاند را ثبت میکند. هدف آن ثبت مشکلات واحد جاری است و نه کل برنامه.
Critical (6): هدف آن ثبت مشکلات بحرانی کل سیستم است که سبب از کار افتادن آن شدهاند.
برای مثال در حین تنظیم متد AddDebug که سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو میشود، میتوان حداقل سطح ثبت وقایع را نیز ذکر کرد:
این حداقل مرتبط است با اعدادی که در کنار سطوح فوق ملاحظه میکنید. برای مثال اگر حداقل سطح ثبت وقایع به Information تنظیم شود، چون سطح آن 3 است، دیگر سطوح پایینتر از آن لاگ نخواهند شد. اهمیت این مساله در اینجا است که اگر صرفا نیاز به اطلاعات Critical داشتیم، نیازی نیست تا با انبوهی از اطلاعات لاگ شده سر و کار داشته باشیم و به این ترتیب میتوان حجم اطلاعات نمایش داده شده را کاهش داد.
البته ترتیب واقعی این سطوح را در enum مرتبط با آنها بهتر میتوان مشاهده کرد:
یک نکته: زمانیکه متد AddDebug را بدون پارامتر فراخوانی میکنید، حداقل سطح ثبت وقایع آن به Information تنظیم شدهاست. یعنی در این لاگ، خبری از اطلاعات Debug نخواهد بود (چون سطح دیباگ پایینتر است از Information). بنابراین اگر میخواهید این اطلاعات را هم مشاهده کنید باید پارامتر minLevel آنرا به LogLevel.Debug تنظیم نمائید.
امکان استفادهی از پروایدرهای ثبت وقایع ثالث
تا اینجا، دو نمونه از پروایدرهای توکار ثبت وقایع ASP.NET Core را بررسی کردیم. اگر نیاز به ثبت این اطلاعات با فرمتهای مختلف و یا در بانک اطلاعاتی وجود دارد، میتوان به تامین کنندههای ثالثی که قابلیت کار با ILoggerFactory را دارند نیز مراجعه کرد. برای مثال:
- elmah.io - provider for the elmah.io service
- Loggr - provider for the Loggr service
- NLog - provider for the NLog library
- Serilog - provider for the Serilog library
این تصویر را پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 6 - سرویسها و تزریق وابستگیها» مشاهده کردهاید. در اینجا لیست سرویسهایی را مشاهده میکنید که به صورت پیش فرض، ثبت شدهاند و فعال هستند و ILogger و ILoggerFactory نیز جزئی از آنها هستند. بنابراین نیازی به فعال سازی آنها نیست؛ اما برای استفادهی از آنها نیاز به انجام یک سری تنظیمات است.
پیاده سازی ثبت وقایع در ASP.NET Core
اولین قدم کار با فریم ورک ثبت وقایع ASP.NET Core، معرفی ILoggerFactory به متد Configure کلاس آغازین برنامه است:
public void Configure(ILoggerFactory loggerFactory, IApplicationBuilder app, IHostingEnvironment env) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug();
سطر اول متد، تنظیمات ثبت وقایع را از خاصیت Logging فایل appsettings.json برنامه میخواند (در مورد خاصیت Configuration، در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» بیشتر بحث شد) و لاگ کردن ویژهی در کنسول NET Core. را فعال میکند:
{ "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } } }
و سطر دوم سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو میشود.
متد AddDebug برای شناسایی، نیاز به افزودن وابستگیهای ذیل در فایل project.json برنامه را دارد:
{ "dependencies": { //same as before "Microsoft.Extensions.Logging": "1.0.0", "Microsoft.Extensions.Logging.Console": "1.0.0", "Microsoft.Extensions.Logging.Debug": "1.0.0" } }
در اینجا میتوانید ریز وقایعی را که توسط ASP.NET Core لاگ شدهاست، مشاهده کنید. برای مثال چه درخواستی صورت گرفتهاست و چقدر اجرای آن زمانبردهاست.
این فعال سازی مرتبط است به متد AddDebug که اضافه شد. اگر میخواهید خروجی AddConsole را هم مشاهده کنید، از طریق خط فرمان، به پوشهی اصلی پروژه وارد شده و سپس دستور dotnet run را اجرا کنید:
دستور dotnet run سبب راه اندازی وب سرور برنامه بر روی پورت 5000 شدهاست که در تصویر نیز مشخص است.
بنابراین اینبار برای دسترسی به برنامه باید مسیر http://localhost:5000 را در مرورگر خود طی کنید. در اینجا نیز میتوان حالتهای مختلف اطلاعات لاگ شده را مشاهده کرد و تمام اینها مرتبط هستند به ذکر متد AddConsole .
کار با سرویس ثبت وقایع ASP.NET Core از طریق تزریق وابستگیها
برای کار با سرویس ثبت وقایع توکار ASP.NET Core در قسمتهای مختلف برنامه، میتوان از ترزیق وابستگی ILogger آن استفاده کرد:
[Route("[controller]")] public class AboutController : Controller { private readonly ILogger<AboutController> _logger; public AboutController(ILogger<AboutController> logger) { _logger = logger; } [Route("")] public ActionResult Hello() { _logger.LogInformation("Running Hello"); return Content("Hello from DNT!"); }
سپس با توجه به اینکه این سرویس جزو سرویسهای از پیش ثبت شدهی ASP.NET Core است، امکانات آن بدون نیاز به تنظیمات بیشتری در دسترس است. برای مثال از متد LogInformation آن در اکشن متد Hello استفاده شدهاست و خروجی عبارت لاگ شدهی آنرا در اینجا میتوانید مشاهده کنید:
سطوح مختلف ثبت وقایع
اینترفیس ILogger به همراه متدهای مختلفی است؛ مانند LogError، LogDebug و غیره. معانی آنها به شرح زیر هستند:
Debug (1): ثبت واقعهای است با بیشترین حد جزئیات ممکن که عموما شامل اطلاعات حساسی نیز میباشد. بنابراین نباید در حالت ارائهی نهایی برنامه فعال شود.
(2) Verbose: ثبت وقایعی مفصل، جهت بررسی مشکلات در حین توسعهی برنامه. تنها باید حاوی اطلاعاتی برای دیباگ برنامه باشند.
(3) Information: عموما برای ردیابی قسمتهای مختلف برنامه مورد استفاده قرار میگیرند.
(4) Warning: جهت ثبت واقعهای نامطلوب در سیستم بکار میرود و سبب قطع اجرای برنامه نمیشود.
(5) Errors: مشکلات برنامه را که سبب قطع سرویس دهی آن شدهاند را ثبت میکند. هدف آن ثبت مشکلات واحد جاری است و نه کل برنامه.
Critical (6): هدف آن ثبت مشکلات بحرانی کل سیستم است که سبب از کار افتادن آن شدهاند.
برای مثال در حین تنظیم متد AddDebug که سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو میشود، میتوان حداقل سطح ثبت وقایع را نیز ذکر کرد:
loggerFactory.AddDebug(minLevel: LogLevel.Information);
البته ترتیب واقعی این سطوح را در enum مرتبط با آنها بهتر میتوان مشاهده کرد:
public enum LogLevel { Trace, Debug, Information, Warning, Error, Critical, None, }
یک نکته: زمانیکه متد AddDebug را بدون پارامتر فراخوانی میکنید، حداقل سطح ثبت وقایع آن به Information تنظیم شدهاست. یعنی در این لاگ، خبری از اطلاعات Debug نخواهد بود (چون سطح دیباگ پایینتر است از Information). بنابراین اگر میخواهید این اطلاعات را هم مشاهده کنید باید پارامتر minLevel آنرا به LogLevel.Debug تنظیم نمائید.
امکان استفادهی از پروایدرهای ثبت وقایع ثالث
تا اینجا، دو نمونه از پروایدرهای توکار ثبت وقایع ASP.NET Core را بررسی کردیم. اگر نیاز به ثبت این اطلاعات با فرمتهای مختلف و یا در بانک اطلاعاتی وجود دارد، میتوان به تامین کنندههای ثالثی که قابلیت کار با ILoggerFactory را دارند نیز مراجعه کرد. برای مثال:
- elmah.io - provider for the elmah.io service
- Loggr - provider for the Loggr service
- NLog - provider for the NLog library
- Serilog - provider for the Serilog library