Introduction to ASP.NET Core part 15: starting with tag helpers
Introduction to ASP.NET Core part 14: the view start and the layout files
Introduction to ASP.NET Core part 13: the view imports file
Introduction to ASP.NET Core part 12: data annotation of view models
Introduction to ASP.NET Core part 11: inserting a new Book in a form
Introduction to ASP.NET Core part 10: the details page and more on view models
Introduction to ASP.NET Core part 9: MVC continued with routing
Introduction to ASP.NET Core part 8: MVC continued with controller actions and our first view
Introduction to ASP.NET Core part 7: starting with MVC
Introduction to ASP.NET Core part 6: environments and settings
Introduction to ASP.NET Core part 5: static files
Introduction to ASP.NET Core part 4: middleware and the component pipeline
Introduction to ASP.NET Core part 3: the configuration file
Introduction to ASP.NET Core part 2: dependencies and dependency injection
Introduction to ASP.NET Core part 1: anatomy of an empty web project
آشنایی با فریمورک الکترون Electron
Now you can upgrade any .NET application to the latest version of .NET inside of Visual Studio! We are happy to introduce it as a Visual Studio extension and will upgrade your .NET Framework or .NET Core web- and desktop apps. In this video, Olia shows you how to get the extension and start to update your projects to the latest version of NET in minutes.
ثبت برنامهی خود در گوگل و انجام تنظیمات آن
اولین کاری که برای استفاده از نگارش سوم Google Analytics API باید صورت گیرد، ثبت برنامهی خود در Google Developer Console است. برای این منظور ابتدا به آدرس ذیل وارد شوید:
سپس بر روی دکمهی Create project کلیک کنید. نام دلخواهی را وارد کرده و در ادامه بر روی دکمهی Create کلیک نمائید تا پروفایل این پروژه ایجاد شود.
تنها نکتهی مهم این قسمت، بخاطر سپردن نام پروژه است. زیرا از آن جهت اتصال به API گوگل استفاده خواهد شد.
پس از ایجاد پروژه، به صفحهی آن وارد شوید و از منوی سمت چپ صفحه، گزینهی Credentials را انتخاب کنید. در ادامه در صفحهی باز شده، بر روی دکمهی Create new client id کلیک نمائید.
در صفحهی باز شده، گزینهی Service account را انتخاب کنید. اگر سایر گزینهها را انتخاب نمائید، کاربری که قرار است از API استفاده کند، باید بتواند توسط مرورگر نصب شدهی بر روی کامپیوتر اجرا کنندهی برنامه، یکبار به گوگل لاگین نماید که این مورد مطلوب برنامههای وب و همچنین سرویسها نیست.
در اینجا ابتدا یک فایل مجوز p12 را به صورت خودکار دریافت خواهید کرد و همچنین پس از ایجاد client id، نیاز است، ایمیل آنرا جایی یادداشت نمائید:
از این ایمیل و همچنین فایل p12 ارائه شده، جهت لاگین به سرور استفاده خواهد شد.
همچنین نیاز است تا به برگهی APIs پروژهی ایجاد شده رجوع کرد و گزینهی Analytics API آنرا فعال نمود:
تا اینجا کار ثبت و فعال سازی برنامهی خود در گوگل به پایان میرسد.
دادن دسترسی به Client ID ثبت شده در برنامهی Google Analytics
پس از اینکه Client ID سرویس خود را ثبت کردید، نیاز است به اکانت Google Analytics خود وارد شوید. سپس در منوی آن، گزینهی Admin را پیدا کرده و به آن قسمت، وارد شوید:
در ادامه به گزینهی User management آن وارد شده و به ایمیل Client ID ایجاد شده در قسمت قبل، دسترسی خواندن و آنالیز را اعطاء کنید:
در صورت عدم رعایت این مساله، کلاینت API، قادر به دسترسی به Google Analytics نخواهد بود.
استفاده از نگارش سوم Google Analytics API در دات نت
قسمت مهم کار، تنظیمات فوق است که در صورت عدم رعایت آنها، شاید نصف روزی را مشغول به دیباگ برنامه شوید. در ادامه نیاز است پیشنیازهای دسترسی به نگارش سوم Google Analytics API را نصب کنیم. برای این منظور، سه بستهی نیوگت ذیل را توسط کنسول پاورشل نیوگت، به برنامه اضافه کنید:
PM> Install-Package Google.Apis PM> Install-Package Google.Apis.auth PM> Install-Package Google.Apis.Analytics.v3
using System; using System.Linq; using System.Security.Cryptography.X509Certificates; using Google.Apis.Analytics.v3; using Google.Apis.Analytics.v3.Data; using Google.Apis.Auth.OAuth2; using Google.Apis.Services; namespace GoogleAnalyticsAPIv3Tests { public class AnalyticsQueryParameters { public DateTime Start { set; get; } public DateTime End { set; get; } public string Dimensions { set; get; } public string Filters { set; get; } public string Metrics { set; get; } } public class AnalyticsAuthentication { public Uri SiteUrl { set; get; } public string ApplicationName { set; get; } public string ServiceAccountEmail { set; get; } public string KeyFilePath { set; get; } public string KeyFilePassword { set; get; } public AnalyticsAuthentication() { KeyFilePassword = "notasecret"; } } public class GoogleAnalyticsApiV3 { public AnalyticsAuthentication Authentication { set; get; } public AnalyticsQueryParameters QueryParameters { set; get; } public GaData GetData() { var service = createAnalyticsService(); var profile = getProfile(service); var query = service.Data.Ga.Get("ga:" + profile.Id, QueryParameters.Start.ToString("yyyy-MM-dd"), QueryParameters.End.ToString("yyyy-MM-dd"), QueryParameters.Metrics); query.Dimensions = QueryParameters.Dimensions; query.Filters = QueryParameters.Filters; query.SamplingLevel = DataResource.GaResource.GetRequest.SamplingLevelEnum.HIGHERPRECISION; return query.Execute(); } private AnalyticsService createAnalyticsService() { var certificate = new X509Certificate2(Authentication.KeyFilePath, Authentication.KeyFilePassword, X509KeyStorageFlags.Exportable); var credential = new ServiceAccountCredential( new ServiceAccountCredential.Initializer(Authentication.ServiceAccountEmail) { Scopes = new[] { AnalyticsService.Scope.AnalyticsReadonly } }.FromCertificate(certificate)); return new AnalyticsService(new BaseClientService.Initializer { HttpClientInitializer = credential, ApplicationName = Authentication.ApplicationName }); } private Profile getProfile(AnalyticsService service) { var accountListRequest = service.Management.Accounts.List(); var accountList = accountListRequest.Execute(); var site = Authentication.SiteUrl.Host.ToLowerInvariant(); var account = accountList.Items.FirstOrDefault(x => x.Name.ToLowerInvariant().Contains(site)); var webPropertyListRequest = service.Management.Webproperties.List(account.Id); var webPropertyList = webPropertyListRequest.Execute(); var sitePropertyList = webPropertyList.Items.FirstOrDefault(a => a.Name.ToLowerInvariant().Contains(site)); var profileListRequest = service.Management.Profiles.List(account.Id, sitePropertyList.Id); var profileList = profileListRequest.Execute(); return profileList.Items.FirstOrDefault(a => a.Name.ToLowerInvariant().Contains(site)); } } }
مثالی از نحوه استفاده از کلاس GoogleAnalyticsApiV3
در ادامه یک برنامهی کنسول را ملاحظه میکنید که از کلاس GoogleAnalyticsApiV3 استفاده میکند:
using System; using System.Collections.Generic; using System.Linq; namespace GoogleAnalyticsAPIv3Tests { class Program { static void Main(string[] args) { var statistics = new GoogleAnalyticsApiV3 { Authentication = new AnalyticsAuthentication { ApplicationName = "My Project", KeyFilePath = "811e1d9976cd516b55-privatekey.p12", ServiceAccountEmail = "10152bng4j3mq@developer.gserviceaccount.com", SiteUrl = new Uri("https://www.dntips.ir/") }, QueryParameters = new AnalyticsQueryParameters { Start = DateTime.Now.AddDays(-7), End = DateTime.Now, Dimensions = "ga:date", Filters = null, Metrics = "ga:users,ga:sessions,ga:pageviews" } }.GetData(); foreach (var result in statistics.TotalsForAllResults) { Console.WriteLine(result.Key + " -> total:" + result.Value); } Console.WriteLine(); foreach (var row in statistics.ColumnHeaders) { Console.Write(row.Name + "\t"); } Console.WriteLine(); foreach (var row in statistics.Rows) { var rowItems = (List<string>)row; Console.WriteLine(rowItems.Aggregate((s1, s2) => s1 + "\t" + s2)); } Console.ReadLine(); } } }
چند نکته
ApplicationName همان نام پروژهای است که ابتدای کار، در گوگل ایجاد کردیم.
KeyFilePath مسیر فایل مجوز p12 ایی است که گوگل در حین ایجاد اکانت سرویس، در اختیار ما قرار میدهد.
ServiceAccountEmail آدرس ایمیل اکانت سرویس است که در قسمت ادمین Google Analytics به آن دسترسی دادیم.
SiteUrl آدرس سایت شما است که هم اکنون در Google Analytics دارای یک اکانت و پروفایل ثبت شدهاست.
توسط AnalyticsQueryParameters میتوان نحوهی کوئری گرفتن از Google Analytics را مشخص کرد. تاریخ شروع و پایان گزارش گیری در آن مشخص هستند. در مورد پارامترهایی مانند Dimensions و Metrics بهتر است به مرجع کامل آن در گوگل مراجعه نمائید:
Dimensions & Metrics Reference
برای نمونه در مثال فوق، تعداد کاربران، سشنهای آن و همچنین تعداد بار مشاهدهی صفحات، گزارشگیری میشود.
برای مطالعه بیشتر
Using Google APIs in Windows Store Apps
How To Use Google Analytics From C# With OAuth
Google Analytic’s API v3 with C#
.NET Library for Accessing and Querying Google Analytics V3 via Service Account
Google OAuth2 C#
بررسی اجزای Hybrid Flow
در قسمت سوم در حین «انتخاب OpenID Connect Flow مناسب برای یک برنامهی کلاینت از نوع ASP.NET Core» به این نتیجه رسیدیم که Flow مناسب یک برنامهی Mvc Client از نوع Hybrid است. در اینجا هر Flow، شروع به ارسال درخواستی به سمت Authorization Endpoint میکند؛ با یک چنین قالبی:
https://idpHostAddress/connect/authorize? client_id=imagegalleryclient &redirect_uri=https://clientapphostaddress/signin-oidcoidc &scope=openid profile &response_type=code id_token &response_mode=form_post &nonce=63626...n2eNMxA0
- سپس client_id جهت تعیین برنامهای که درخواست را ارسال میکند، ذکر شدهاست؛ از این جهت که یک IDP جهت کار با چندین نوع کلاینت مختلف طراحی شدهاست.
- redirect_uri همان Redirect Endpoint است که در سطح برنامهی کلاینت تنظیم میشود.
- در مورد scope در قسمت قبل در حین راه اندازی IdentityServer توضیح دادیم. در اینجا برنامهی کلاینت، درخواست scopeهای openid و profile را دادهاست. به این معنا که نیاز دارد تا Id کاربر وارد شدهی به سیستم و همچنین Claims منتسب به او را در اختیار داشته باشد.
- response_type نیز به code id_token تنظیم شدهاست. توسط response_type، نوع Flow مورد استفاده مشخص میشود. ذکر code به معنای بکارگیری Authorization code flow است. ذکر id_token و یا id_token token هر دو به معنای استفادهی از implicit flow است. اما برای مشخص سازی Hybrid flow یکی از سه مقدار code id_token و یا code token و یا code id_token token با هم ذکر میشوند:
- در اینجا response_mode مشخص میکند که اطلاعات بازگشتی از سمت IDP که توسط response_type مشخص شدهاند، با چه قالبی به سمت کلاینت بازگشت داده شوند که میتواند از طریق Form POST و یا URI باشد.
در Hybrid flow با response_type از نوع code id_token، ابتدا کلاینت یک درخواست Authentication را به Authorization Endpoint ارسال میکند (با همان قالب URL فوق). سپس در سطح IDP، کاربر برای مثال با ارائهی کلمهی عبور و نام کاربری، تعیین اعتبار میشود. همچنین در اینجا IDP ممکن است رضایت کاربر را از دسترسی به اطلاعات پروفایل او نیز سؤال بپرسد (تحت عنوان مفهوم Consent). سپس IDP توسط یک Redirection و یا Form POST، اطلاعات authorization code و identity token را به سمت برنامهی کلاینت ارسال میکند. این همان اطلاعات مرتبط با response_type ای است که درخواست کردهایم. سپس برنامهی کلاینت این اطلاعات را تعیین اعتبار کرده و در صورت موفقیت آمیز بودن این عملیات، اکنون درخواست تولید توکن هویت را به token endpoint ارسال میکند. برای این منظور کلاینت سه مشخصهی authorization code ،client-id و client-secret را به سمت token endpoint ارسال میکند. در پاسخ یک identity token را دریافت میکنیم. در اینجا مجددا این توکن تعیین اعتبار شده و سپس Id کاربر را از آن استخراج میکند که در برنامهی کلاینت قابل استفاده خواهد بود. این مراحل را در تصویر زیر میتوانید ملاحظه کنید.
البته اگر دقت کرده باشید، یک identity token در همان ابتدای کار از Authorization Endpoint دریافت میشود. اما چرا از آن استفاده نمیکنیم؟ علت اینجا است که token endpoint نیاز به اعتبارسنجی client را نیز دارد. به این ترتیب یک لایهی امنیتی دیگر نیز در اینجا بکار گرفته میشود. همچنین access token و refresh token نیز از همین token endpoint قابل دریافت هستند.
تنظیم IdentityServer جهت انجام عملیات ورود به سیستم بر اساس جزئیات Hybrid Flow
برای افزودن قسمت لاگین به برنامهی MVC قسمت دوم، نیاز است تغییراتی را در برنامهی کلاینت و همچنین IDP اعمال کنیم. برای این منظور کلاس Config پروژهی IDP را که در قسمت قبل ایجاد کردیم، به صورت زیر تکمیل میکنیم:
namespace DNT.IDP { public static class Config { public static IEnumerable<Client> GetClients() { return new List<Client> { new Client { ClientName = "Image Gallery", ClientId = "imagegalleryclient", AllowedGrantTypes = GrantTypes.Hybrid, RedirectUris = new List<string> { "https://localhost:5001/signin-oidc" }, PostLogoutRedirectUris = new List<string> { "https://localhost:5001/signout-callback-oidc" }, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile }, ClientSecrets = { new Secret("secret".Sha256()) } } }; } } }
- ابتدا نام کلاینت را مشخص میکنیم. این نام و عنوان، در صفحهی لاگین و Consent (رضایت دسترسی به اطلاعات پروفایل کاربر)، ظاهر میشود.
- همچنین نیاز است یک Id دلخواه را نیز برای آن مشخص کنیم؛ مانند imagegalleryclient در اینجا.
- AllowedGrantTypes را نیز به Hybrid Flow تنظیم کردهایم. علت آنرا در قسمت سوم این سری بررسی کردیم.
- با توجه به اینکه Hybrid Flow از Redirectها استفاده میکند و اطلاعات نهایی را به کلاینت از طریق Redirection ارسال میکند، به همین جهت آدرس RedirectUris را به آدرس برنامهی Mvc Client تنظیم کردهایم (که در اینجا بر روی پورت 5001 کار میکند). قسمت signin-oidc آنرا در ادامه تکمیل خواهیم کرد.
- در قسمت AllowedScopes، لیست scopeهای مجاز قابل دسترسی توسط این کلاینت مشخص شدهاند که شامل دسترسی به ID کاربر و Claims آن است.
- به ClientSecrets نیز جهت client authenticating نیاز داریم.
تنظیم برنامهی MVC Client جهت انجام عملیات ورود به سیستم بر اساس جزئیات Hybrid Flow
برای افزودن قسمت لاگین به سیستم، کلاس آغازین پروژهی MVC Client را به نحو زیر تکمیل میکنیم:
namespace ImageGallery.MvcClient.WebApp { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; }).AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.SignInScheme = "Cookies"; options.Authority = "https://localhost:6001"; options.ClientId = "imagegalleryclient"; options.ResponseType = "code id_token"; //options.CallbackPath = new PathString("...") //options.SignedOutCallbackPath = new PathString("...") options.Scope.Add("openid"); options.Scope.Add("profile"); options.SaveTokens = true; options.ClientSecret = "secret"; options.GetClaimsFromUserInfoEndpoint = true; });
- ابتدا با فراخوانی AddAuthentication، کار تنظیمات میانافزار استاندارد Authentication برنامههای ASP.NET Core انجام میشود. در اینجا DefaultScheme آن به Cookies تنظیم شدهاست تا عملیات Sign-in و Sign-out سمت کلاینت را میسر کند. سپس DefaultChallengeScheme به oidc تنظیم شدهاست. این مقدار با Scheme ای که در ادامه آنرا تنظیم خواهیم کرد، تطابق دارد.
- سپس متد AddCookie فراخوانی شدهاست که authentication-Scheme را به عنوان پارامتر قبول میکند. به این ترتیب cookie based authentication در برنامه میسر میشود. پس از اعتبارسنجی توکن هویت دریافتی و تبدیل آن به Claims Identity، در یک کوکی رمزنگاری شده برای استفادههای بعدی ذخیره میشود.
- در آخر تنظیمات پروتکل OpenID Connect را ملاحظه میکنید. به این ترتیب مراحل اعتبارسنجی توسط این پروتکل در اینجا که Hybrid flow است، پشتیبانی خواهد شد. اینجا است که کار درخواست Authorization، دریافت و اعتبارسنجی توکن هویت صورت میگیرد. اولین پارامتر آن authentication-Scheme است که به oidc تنظیم شدهاست. به این ترتیب اگر قسمتی از برنامه نیاز به Authentication داشته باشد، OpenID Connect به صورت پیشفرض مورد استفاده قرار میگیرد. به همین جهت DefaultChallengeScheme را نیز به oidc تنظیم کردیم. در اینجا SignInScheme به Cookies تنظیم شدهاست که با DefaultScheme اعتبارسنجی تطابق دارد. به این ترتیب نتیجهی موفقیت آمیز عملیات اعتبارسنجی در یک کوکی رمزنگاری شده ذخیره خواهد شد. مقدار خاصیت Authority به آدرس IDP تنظیم میشود که بر روی پورت 6001 قرار دارد. تنظیم این مسیر سبب خواهد شد تا این میانافزار سمت کلاینت، به discovery endpoint دسترسی یافته و بتواند مقادیر سایر endpoints برنامهی IDP را به صورت خودکار دریافت و استفاده کند. سپس ClientId تنظیم شدهاست که باید با مقدار تنظیم شدهی آن در سمت IDP یکی باشد و همچنین مقدار ClientSecret در اینجا نیز باید با ClientSecrets سمت IDP یکی باشد. ResponseType تنظیم شدهی در اینجا با AllowedGrantTypes سمت IDP تطابق دارد که از نوع Hybrid است. سپس دو scope درخواستی توسط این برنامهی کلاینت که openid و profile هستند در اینجا اضافه شدهاند. به این ترتیب میتوان به مقادیر Id کاربر و claims او دسترسی داشت. مقدار CallbackPath در اینجا به RedirectUris سمت IDP اشاره میکند که مقدار پیشفرض آن همان signin-oidc است. با تنظیم SaveTokens به true امکان استفادهی مجدد از آنها را میسر میکند.
پس از تکمیل قسمت ConfigureServices و انجام تنظیمات میانافزار اعتبارسنجی، نیاز است این میانافزار را نیز به برنامه افزود که توسط متد UseAuthentication انجام میشود:
namespace ImageGallery.MvcClient.WebApp { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseAuthentication();
پس از این تنظیمات، با اعمال ویژگی Authorize، دسترسی به کنترلر گالری برنامهی MVC Client را صرفا محدود به کاربران وارد شدهی به سیستم میکنیم:
namespace ImageGallery.MvcClient.WebApp.Controllers { [Authorize] public class GalleryController : Controller { // .... public async Task WriteOutIdentityInformation() { var identityToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken); Debug.WriteLine($"Identity token: {identityToken}"); foreach (var claim in User.Claims) { Debug.WriteLine($"Claim type: {claim.Type} - Claim value: {claim.Value}"); } }
فراخوانی متد GetTokenAsync با پارامتر IdToken، همان Identity token دریافتی از IDP را بازگشت میدهد. این توکن با تنظیم SaveTokens به true در تنظیمات AddOpenIdConnect که پیشتر انجام دادیم، قابل استخراج از کوکی اعتبارسنجی برنامه شدهاست.
این متد را در ابتدای اکشن متد Index فراخوانی میکنیم:
public async Task<IActionResult> Index() { await WriteOutIdentityInformation(); // ....
اجرای برنامه جهت آزمایش تنظیمات انجام شده
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه با هم در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید:
در این حالت چون فیلتر Authorize به کل اکشن متدهای کنترلر گالری اعمال شده، میانافزار Authentication که در فایل آغازین برنامهی کلاینت MVC تنظیم شدهاست، وارد عمل شده و کاربر را به صفحهی لاگین سمت IDP هدایت میکند (شماره پورت آن 6001 است). لاگ این اعمال را هم در برگهی network مرورگر میتواند مشاهده کنید.
در اینجا نام کاربری و کلمهی عبور اولین کاربر تعریف شدهی در فایل Config.cs برنامهی IDP را که User 1 و password است، وارد میکنیم. پس از آن صفحهی Consent ظاهر میشود:
در اینجا از کاربر سؤال میپرسد که آیا به برنامهی کلاینت اجازه میدهید تا به Id و اطلاعات پروفایل و یا همان Claims شما دسترسی پیدا کند؟
فعلا گزینهی remember my design را انتخاب نکنید تا همواره بتوان این صفحه را در دفعات بعدی نیز مشاهده کرد. سپس بر روی گزینهی Yes, Allow کلیک کنید.
اکنون به صورت خودکار به سمت برنامهی MVC Client هدایت شده و میتوانیم اطلاعات صفحهی اول سایت را کاملا مشاهده کنیم (چون کاربر اعتبارسنجی شدهاست، از فیلتر Authorize رد خواهد شد).
همچنین در اینجا اطلاعات زیادی نیز جهت دیباگ برنامه لاگ میشوند که در آینده جهت عیب یابی آن میتوانند بسیار مفید باشند:
با دنبال کردن این لاگ میتوانید مراحل Hybrid Flow را مرحله به مرحله با مشاهدهی ریز جزئیات آن بررسی کنید. این مراحل به صورت خودکار توسط میانافزار Authentication انجام میشوند و در نهایت اطلاعات توکنهای دریافتی به صورت خودکار در اختیار برنامه برای استفاده قرار میگیرند. یعنی هم اکنون کوکی رمزنگاری شدهی اطلاعات اعتبارسنجی کاربر در دسترس است و به اطلاعات آن میتوان توسط شیء this.User، در اکشن متدهای برنامهی MVC، دسترسی داشت.
تنظیم برنامهی MVC Client جهت انجام عملیات خروج از سیستم
ابتدا نیاز است یک لینک خروج از سیستم را به برنامهی کلاینت اضافه کنیم. برای این منظور به فایل Views\Shared\_Layout.cshtml مراجعه کرده و لینک logout را در صورت IsAuthenticated بودن کاربر جاری وارد شدهی به سیستم، نمایش میدهیم:
<div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li><a asp-area="" asp-controller="Gallery" asp-action="Index">Home</a></li> <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li> @if (User.Identity.IsAuthenticated) { <li><a asp-area="" asp-controller="Gallery" asp-action="Logout">Logout</a></li> } </ul> </div>
شیء this.User، هم در اکشن متدها و هم در Viewهای برنامه، جهت دسترسی به اطلاعات کاربر اعتبارسنجی شده، در دسترس است.
این لینک به اکشن متد Logout، در کنترلر گالری اشاره میکند که آنرا به صورت زیر تکمیل خواهیم کرد:
namespace ImageGallery.MvcClient.WebApp.Controllers { [Authorize] public class GalleryController : Controller { public async Task Logout() { // Clears the local cookie ("Cookies" must match the name of the scheme) await HttpContext.SignOutAsync("Cookies"); await HttpContext.SignOutAsync("oidc"); }
سپس نیاز است از برنامهی IDP نیز logout شویم. به همین جهت سطر دوم SignOutAsync با پارامتر oidc را مشاهده میکنید. بدون وجود این سطر، کاربر فقط از برنامهی کلاینت logout میشود؛ اما اگر به IDP مجددا هدایت شود، مشاهده خواهد کرد که در آن سمت، هنوز نام کاربری او توسط IDP شناسایی میشود.
بهبود تجربهی کاربری Logout
پس از logout، بدون انجام یکسری از تنظیمات، کاربر مجددا به برنامهی کلاینت به صورت خودکار هدایت نخواهد شد و در همان سمت IDP متوقف میشد. برای بهبود این وضعیت و بازگشت مجدد به برنامهی کلاینت، اینکار را یا توسط مقدار دهی خاصیت SignedOutCallbackPath مربوط به متد AddOpenIdConnect میتوان انجام داد و یا بهتر است مقدار پیشفرض آنرا به تنظیمات IDP نسبت داد که پیشتر در تنظیمات متد GetClients آنرا ذکر کرده بودیم:
PostLogoutRedirectUris = new List<string> { "https://localhost:5001/signout-callback-oidc" },
البته هنوز یک مرحلهی انتخاب و کلیک بر روی لینک بازگشت وجود دارد. برای حذف آن و خودکار کردن Redirect نهایی آن، میتوان کدهای IdentityServer4.Quickstart.UI را که در قسمت قبل به برنامهی IDP اضافه کردیم، اندکی تغییر دهیم. برای این منظور فایل src\IDP\DNT.IDP\Quickstart\Account\AccountOptions.cs را گشوده و سپس فیلد AutomaticRedirectAfterSignOut را که false است، به true تغییر دهید.
تنظیمات بازگشت Claims کاربر به برنامهی کلاینت
به صورت پیشفرض، Identity Server اطلاعات Claims کاربر را ارسال نمیکند و Identity token صرفا به همراه اطلاعات Id کاربر است. برای تنظیم آن میتوان در سمت تنظیمات IDP، در متد GetClients، زمانیکه new Client صورت میگیرد، خاصیت AlwaysIncludeUserClaimsInIdToken هر کلاینت را به true تنظیم کرد؛ اما ایده خوبی نیست. Identity token از طریق Authorization endpoint دریافت میشود. در اینجا اگر این اطلاعات از طریق URI دریافت شود و Claims به Identity token افزوده شوند، به مشکل بیش از حد طولانی شدن URL نهایی خواهیم رسید و ممکن است از طرف وب سرور یک چنین درخواستی برگشت بخورد. به همین جهت به صورت پیشفرض اطلاعات Claims به Identity token اضافه نمیشوند.
در اینجا برای دریافت Claims، یک endpoint دیگر در IDP به نام UserInfo endpoint درنظر گرفته شدهاست. در این حالت برنامهی کلاینت، مقدار Access token دریافتی را که به همراه اطلاعات scopes متناظر با Claims است، به سمت UserInfo endpoint ارسال میکند. باید دقت داشت زمانیکه Identity token دوم از Token endpoint دریافت میشود (تصویر ابتدای بحث)، به همراه آن یک Access token نیز صادر و ارسال میگردد. اینجا است که میانافزار oidc، این توکن دسترسی را به سمت UserInfo endpoint ارسال میکند تا user claims را دریافت کند:
در تنظیمات سمت کلاینت AddOpenIdConnect، درخواست openid و profile، یعنی درخواست Id کاربر و Claims آن وجود دارند:
options.Scope.Add("openid"); options.Scope.Add("profile");
options.GetClaimsFromUserInfoEndpoint = true;
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه با هم در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.
مدیریت تغییرات گریدی از اطلاعات به کمک استفاده از الگوی واحد کار مشترک بین ViewModel و لایه سرویس
در این صفحه با کلیک بر روی دکمه به علاوه، یک ردیف به ردیفهای موجود اضافه شده و در اینجا میتوان اطلاعات کاربر جدیدی به همراه سطح دسترسی او را وارد و ذخیره کرد و یا حتی اطلاعات کاربران موجود را ویرایش نمود. اگر بخواهیم مانند مراحلی که در قسمت قبل در مورد تعریف یک صفحه جدید در برنامه توضیح داده شد، عمل کنیم، به صورت خلاصه به ترتیب ذیل عمل شده است:
1) ایجاد صفحه تغییر مشخصات کاربر
ابتدا صفحه Views\Admin\AddNewUser.xaml به پروژه ریشه که Viewهای برنامه در آن تعریف میشوند، اضافه شده است. به همراه دو دکمه و یک ListView که تطابق بهتری با قالب متروی مورد استفاده دارد.
2) تنظیم اعتبارسنجی صفحه اضافه شده
مرحله بعد تعریف هر صفحهای در سیستم، مشخص سازی وضعیت دسترسی به آن است:
/// <summary> /// افزودن و مدیریت کاربران سیستم /// </summary> [PageAuthorization(AuthorizationType.ApplyRequiredRoles, "IsAdmin, CanAddNewUser")]
3) تغییر منوی برنامه جهت اشاره به صفحه جدید
در ادامه در فایل منوی برنامه Views\MainMenu.xaml تعریف دسترسی به صفحه Views\Admin\AddNewUser.xaml قید شده است:
<Button Style="{DynamicResource MetroCircleButtonStyle}" Height="55" Width="55" Command="{Binding DoNavigate}" CommandParameter="\Views\Admin\AddNewUser.xaml" Margin="2"> <Rectangle Width="28" Height="17.25"> <Rectangle.Fill> <VisualBrush Stretch="Fill" Visual="{StaticResource appbar_user_add}" /> </Rectangle.Fill> </Rectangle> </Button>
4) ایجاد ViewModel متناظر با صفحه
مرحله نهایی تعریف صفحه AddNewUser، افزودن ViewModel متناظر با آن است که سورس کامل آنرا در فایل ViewModels\Admin\AddNewUserViewModel.cs پروژه Infrastructure میتوانید ملاحظه کنید.
نکته مهم این ViewModel، ارائه خاصیت لیست کاربران از نوع ObservableCollection به View و گرید برنامه است:
public ObservableCollection<User> UsersList { set; get; }
/// <summary> /// جهت مقاصد انقیاد دادهها در دبلیو پی اف طراحی شده است /// لیستی از کاربران سیستم را باز میگرداند /// </summary> /// <param name="count">تعداد کاربر مد نظر</param> /// <returns>لیستی از کاربران</returns> public ObservableCollection<User> GetSyncedUsersList(int count = 1000) { _users.OrderBy(x => x.FriendlyName).Take(count) .Load(); // For Databinding with WPF. // Before calling this method you need to fill the context by using `Load()` method. return _users.Local; }
در اینجا از قابلیت خاصیتی به نام Local که یک ObservableCollection تحت نظر EF را بازگشت میدهد، استفاده شده است. برای استفاده از این خاصیت، ابتدا باید کوئری خود را تهیه و سپس متد Load را بر روی آن فراخوانی کرد. سپس خاصیت Local بر اساس اطلاعات کوئری قبلی پر و مقدار دهی خواهد شد.
علت انتخاب نام Synced برای این متد، تحت نظر بودن اطلاعات خاصیت Local است تا زمانیکه Context تعریف شده زنده نگه داشته شود. به همین جهت در برنامه جاری از روش زنده نگه داشتن Context به ازای یک ViewModel استفاده شده است.
به Context، توسط اینترفیس IUnitOfWork تزریق شده در سازنده کلاس ViewModel میتوان دسترسی یافت. چون در اینجا از تزریق وابستگیها استفاده شده است، وهلهای که IUnitOfWork کلاس AddNewUserViewModel را تشکیل میدهد، دقیقا همان وهلهای است که در کلاس UsersService لایه سرویس استفاده شده است. در نتیجه، در گرید برنامه هر تغییری اعمال شود، تحت نظر IUnitOfWork خواهد بود و صرفا با فراخوانی متد uow.ApplyAllChanges آن، کلیه تغییرات تمام ردیفهای تحت نظر EF به صورت خودکار در بانک اطلاعاتی درج و یا به روز خواهند شد.
همچنین در مورد ViewModelContextHasChanges نیز در قسمت قبل بحث شد. در اینجا پیاده سازی کننده آن صرفا خاصیت uow.ContextHasChanges است. به این ترتیب اگر کاربر، تغییری را در صفحه داده باشد و بخواهد به صفحه دیگری رجوع کند، با پیام زیر مواجه خواهد شد:
از همین خاصیت برای فعال و غیرفعال کردن دکمه ذخیره سازی اطلاعات نیز استفاده شده است:
/// <summary> /// فعال و غیرفعال سازی خودکار دکمه ثبت /// این متد به صورت خودکار توسط RelayCommand کنترل میشود /// </summary> private bool canDoSave() { // آیا در حین نمایش صفحهای دیگر باید به کاربر پیغام داد که اطلاعات ذخیره نشدهای وجود دارد؟ return ViewModelContextHasChanges; }
/// <summary> /// رخداد ذخیره سازی اطلاعات را دریافت میکند /// </summary> public RelayCommand DoSave { set; get; }
DoSave = new RelayCommand(doSave, canDoSave);
این بررسی نیز بسیار سبک و سریع است. از این جهت که تغییرات Context در حافظه نگهداری میشوند و مراجعه به آن مساوی مراجعه به بانک اطلاعاتی نیست.
اگر امروز با استفاده از دستور dotnet new console یک برنامهی کنسول را شروع کنیم، به یک فایل program.cs تقریبا خالی و یک فایل csproj. مرتبط میرسیم و ... همین! اما نیازهای واقعی ما در یک برنامهی کنسول، شامل این موارد هستند:
- نیاز به انجام تزریق وابستگیها
- نیاز به دسترسی به فایلهای کانفیگ؛ شبیه به برنامههای وب
- نیاز به کار با EF و مدیریت صحیح طول عمر Context آن
- مشخص بودن شماره نگارش دات نت و همچنین ابزارهای EF استفاده شده
- غنی سازی کامپایلر با یک سری آنالایزر و تنظیمات مخصوص آن جهت اعمال سخت گیریهای بیشتر!
- استفاده از مدیریت مرکزی بستههای نیوگت برای پروژههای مختلف در Solution
- نیاز به کار با HttpClient؛ آن هم به نحو صحیحی
- نیاز به دسترسی به سوئیچها و پارامترهای ارسالی به برنامهی کنسول از طریق خط فرمان
- نیاز به دسترسی به سیستم Logging؛ شبیه به برنامههای وب
- امکان انجام آزمونهای واحد و یکپارچگی درون حافظهای
تمام این موارد به صورت از پیش تنظیم شده، در قالب ارائه شدهی توسط این پروژه موجود هستند.
برای نصب آن، ابتدا مخزن کد فوق را clone کرده و سپس فایل register_template.cmd آنرا اجرا کنید. پس از آن دستور dotnet new dntconsole، جهت ایجاد پروژههای جدید مبتنی بر این قالب، در دسترس خواهد بود.
• NotNull/NotEmpty
• Matches (regex)
• InclusiveBetween (range)
• CreditCard
• EqualTo (cross-property equality comparison)
• MaxLength
• MinLength
• Length
اما باید دقت داشت که سایر قابلیتهای این کتابخانه، قابلیت ترجمهی به قواعد unobtrusive java script validation ندارند؛ مانند:
- استفادهی از شرطها توسط متدهای When و یا Unless (برای مثال اگر خاصیت A دارای مقدار 5 بود، آنگاه ورود اطلاعات خاصیت B باید اجباری شود)
- تعریف قواعد اعتبارسنجی سفارشی، برای مثال توسط متد Must
- تعریف RuleSetها (برای مثال تعیین کنید که از یک مدل، فقط تعدادی از خواص آن اعتبارسنجی شوند و نه تمام آنها)
در یک چنین حالتهایی باید کاربر اطلاعات کامل فرم را به سمت سرور ارسال کند و در post-back صورت گرفته، نتایج اعتبارسنجی را دریافت کند.
روش توسعهی اعتبارسنجی سمت کلاینت کتابخانهی FluentValidation جهت سازگاری آن با unobtrusive java script validation
باتوجه به اینکه قواعد سفارشی کتابخانهی FluentValidation، معادل unobtrusive java script validation ای ندارند، در ادامه مثالی را جهت تدارک آنها بررسی میکنیم. برای این منظور، معادل مطلب «ایجاد ویژگیهای اعتبارسنجی سفارشی در ASP.NET Core 3.1 به همراه اعتبارسنجی سمت کلاینت آنها» را بر اساس ویژگیهای کتابخانهی FluentValidation پیاده سازی میکنیم.
1) ایجاد اعتبارسنج سمت سرور
میخواهیم اگر مقدار عددی خاصیتی بیشتر از مقدار عددی خاصیت دیگری بود، اعتبارسنجی آن با شکست مواجه شود. به همین منظور با پیاده سازی یک PropertyValidator سفارشی، LowerThanValidator را به صورت زیر تعریف میکنیم:
using System; using FluentValidation; using FluentValidation.Validators; namespace FluentValidationSample.Models { public class LowerThanValidator : PropertyValidator { public string DependentProperty { get; set; } public LowerThanValidator(string dependentProperty) : base($"باید کمتر از {dependentProperty} باشد") { DependentProperty = dependentProperty; } protected override bool IsValid(PropertyValidatorContext context) { if (context.PropertyValue == null) { return false; } var typeInfo = context.Instance.GetType(); var dependentPropertyValue = Convert.ToInt32(typeInfo.GetProperty(DependentProperty).GetValue(context.Instance, null)); return int.Parse(context.PropertyValue.ToString()) < dependentPropertyValue; } } public static class CustomFluentValidationExtensions { public static IRuleBuilderOptions<T, int> LowerThan<T>( this IRuleBuilder<T, int> ruleBuilder, string dependentProperty) { return ruleBuilder.SetValidator(new LowerThanValidator(dependentProperty)); } } }
- همچنین جهت سهولت کار فراخوانی آن، یک متد الحاقی جدید به نام LowerThan نیز در اینجا تعریف شدهاست.
- البته اگر قصد اعتبارسنجی سمت سرور را ندارید، میتوان در متد IsValid، مقدار true را بازگشت داد.
2) ایجاد متادیتای مورد نیاز جهت unobtrusive java script validation در سمت سرور
در ادامه نیاز است ویژگیهای data-val خاص unobtrusive java script validation را توسط FluentValidation ایجاد کنیم:
using FluentValidation; using FluentValidation.AspNetCore; using FluentValidation.Internal; using FluentValidation.Resources; using FluentValidation.Validators; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; namespace FluentValidationSample.Models { public class LowerThanClientValidator : ClientValidatorBase { private LowerThanValidator LowerThanValidator { get { return (LowerThanValidator)Validator; } } public LowerThanClientValidator(PropertyRule rule, IPropertyValidator validator) : base(rule, validator) { } public override void AddValidation(ClientModelValidationContext context) { MergeAttribute(context.Attributes, "data-val", "true"); MergeAttribute(context.Attributes, "data-val-LowerThan", GetErrorMessage(context)); MergeAttribute(context.Attributes, "data-val-LowerThan-dependentproperty", LowerThanValidator.DependentProperty); } private string GetErrorMessage(ClientModelValidationContext context) { var formatter = ValidatorOptions.MessageFormatterFactory().AppendPropertyName(Rule.GetDisplayName()); string messageTemplate; try { messageTemplate = Validator.Options.ErrorMessageSource.GetString(null); } catch (FluentValidationMessageFormatException) { messageTemplate = ValidatorOptions.LanguageManager.GetStringForValidator<NotEmptyValidator>(); } return formatter.BuildMessage(messageTemplate); } } }
<input dir="rtl" type="number" data-val="true" data-val-lowerthan="باید کمتر از Age باشد" data-val-lowerthan-dependentproperty="Age" data-val-required="'سابقه کار' must not be empty." id="Experience" name="Experience" value="">
3) تعریف مدل کاربران و اعتبارسنجی آن
مدلی که در این مثال از آن استفاده شده، یک چنین تعریفی را دارد و در قسمت اعتبارسنجی خاصیت Experience آن، از متد الحاقی جدید LowerThan استفاده شدهاست:
public class UserModel { [Display(Name = "نام کاربری")] public string Username { get; set; } [Display(Name = "سن")] public int Age { get; set; } [Display(Name = "سابقه کار")] public int Experience { get; set; } } public class UserValidator : AbstractValidator<UserModel> { public UserValidator() { RuleFor(x => x.Username).NotNull(); RuleFor(x => x.Age).NotNull(); RuleFor(x => x.Experience).LowerThan(nameof(UserModel.Age)).NotNull(); } }
4) افزودن اعتبارسنجهای تعریف شده به تنظیمات برنامه
پس از تعریف LowerThanValidator و LowerThanClientValidator، روش افزودن آنها به تنظیمات FluentValidation به صورت زیر است:
namespace FluentValidationSample.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews().AddFluentValidation( fv => { fv.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly()); fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>(); fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false; fv.ConfigureClientsideValidation(clientSideValidation => { clientSideValidation.Add( validatorType: typeof(LowerThanValidator), factory: (context, rule, validator) => new LowerThanClientValidator(rule, validator)); }); } ); }
5) تعریف کدهای جاوا اسکریپتی مورد نیاز
پیش از هرکاری، اسکریپتهای فایل layout برنامه باید چنین تعریفی را داشته باشند:
<script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script> <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script> <script src="~/js/site.js" asp-append-version="true"></script>
$.validator.unobtrusive.adapters.add('LowerThan', ['dependentproperty'], function (options) { options.rules['LowerThan'] = { dependentproperty: options.params['dependentproperty'] }; options.messages['LowerThan'] = options.message; }); $.validator.addMethod('LowerThan', function (value, element, parameters) { var dependentProperty = '#' + parameters['dependentproperty']; var dependentControl = $(dependentProperty); if (dependentControl) { var targetvalue = dependentControl.val(); if (parseInt(targetvalue) > parseInt(value)) { return true; } return false; } return true; });
6) آزمایش برنامه
@using FluentValidationSample.Models @model UserModel @{ ViewData["Title"] = "Home Page"; } <div dir="rtl"> <form asp-controller="Home" asp-action="RegisterUser" method="post"> <fieldset class="form-group"> <legend>ثبت نام</legend> <div class="form-group row"> <label asp-for="Username" class="col-md-2 col-form-label text-md-left"></label> <div class="col-md-10"> <input dir="rtl" asp-for="Username" class="form-control" /> <span asp-validation-for="Username" class="text-danger"></span> </div> </div> <div class="form-group row"> <label asp-for="Age" class="col-md-2 col-form-label text-md-left"></label> <div class="col-md-10"> <input dir="rtl" asp-for="Age" class="form-control" /> <span asp-validation-for="Age" class="text-danger"></span> </div> </div> <div class="form-group row"> <label asp-for="Experience" class="col-md-2 col-form-label text-md-left"></label> <div class="col-md-10"> <input dir="rtl" asp-for="Experience" class="form-control" /> <span asp-validation-for="Experience" class="text-danger"></span> </div> </div> <div class="form-group row"> <label class="col-md-2 col-form-label text-md-left"></label> <div class="col-md-10 text-md-right"> <button type="submit" class="btn btn-info col-md-2">ارسال</button> </div> </div> </fieldset> </form> </div>