Would you like help creating single-page applications (SPAs)? Maybe you've worked with tools like jQuery and AJAX, but what about Ember? With a simple syntax and an emphasis on reuse and components, this JavaScript framework can make it very easy to create interactive pages. Get a close look at Ember, plus lots of demos and practical tips with popular experts Adam Tuliper and Christopher Harrison, in this three-hour event for the MVA community
بررسی Claims Transformations
میخواهیم Claims بازگشت داده شدهی توسط IDP را به یکسری Claims که کار کردن با آنها در برنامهی MVC Client سادهتر است، تبدیل کنیم.
زمانیکه اطلاعات Claim، توسط میانافزار oidc دریافت میشود، ابتدا بررسی میشود که آیا دیکشنری نگاشتها وجود دارد یا خیر؟ اگر بله، کار نگاشتها از یک claim type به claim type دیگر انجام میشود.
برای مثال لیست claims اصلی بازگشت داده شدهی توسط IDP، پس از تبدیلات و نگاشتهای آن در برنامهی کلاینت، یک چنین شکلی را پیدا میکند:
Claim type: sid - Claim value: f3940d6e58cbb576669ee49c90e22cb1 Claim type: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 Claim type: http://schemas.microsoft.com/identity/claims/identityprovider - Claim value: local Claim type: http://schemas.microsoft.com/claims/authnmethodsreferences - Claim value: pwd Claim type: given_name - Claim value: Vahid Claim type: family_name - Claim value: N
namespace ImageGallery.MvcClient.WebApp { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); }
در ادامه اگر مجددا لیست claims را پس از logout و login، بررسی کنیم، به این صورت تبدیل شدهاست:
• Claim type: sid - Claim value: 91f5a09da5cdbbe18762526da1b996fb • Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 • Claim type: idp - Claim value: local • Claim type: given_name - Claim value: Vahid • Claim type: family_name - Claim value: N
کار با مجموعهی User Claims
تا اینجا لیست this.User.Claims، به همراه تعدادی Claims است که به آنها نیازی نداریم؛ مانند sid که بیانگر session id در سمت IDP است و یا idp به معنای identity provider میباشد. حذف آنها حجم کوکی نگهداری کنندهی آنها را کاهش میدهد. همچنین میخواهیم تعدادی دیگر را نیز به آنها اضافه کنیم.
علاوه بر اینها میانافزار oidc، یکسری از claims دریافتی را راسا فیلتر و حذف میکند؛ مانند زمان منقضی شدن توکن و امثال آن که در عمل واقعا به تعدادی از آنها نیازی نداریم. اما میتوان این سطح تصمیم گیری فیلتر claims رسیده را نیز کنترل کرد. در تنظیمات متد AddOpenIdConnect، خاصیت options.ClaimActions نیز وجود دارد که توسط آن میتوان بر روی حذف و یا افزوده شدن Claims، کنترل بیشتری را اعمال کرد:
namespace ImageGallery.MvcClient.WebApp { public void ConfigureServices(IServiceCollection services) { // ... .AddOpenIdConnect("oidc", options => { // ... options.ClaimActions.Remove("amr"); options.ClaimActions.DeleteClaim("sid"); options.ClaimActions.DeleteClaim("idp"); }); }
اکنون اگر پس از logout و login، لیست this.User.Claims را بررسی کنیم، دیگر خبری از sid و idp در آن نیست. همچنین claim از نوع amr نیز به صورت پیشفرض حذف نشدهاست:
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 • Claim type: amr - Claim value: pwd • Claim type: given_name - Claim value: Vahid • Claim type: family_name - Claim value: N
افزودن Claim جدید آدرس کاربر
برای افزودن Claim جدید آدرس کاربر، به کلاس src\IDP\DNT.IDP\Config.cs مراجعه کرده و آنرا به صورت زیر تکمیل میکنیم:
namespace DNT.IDP { public static class Config { // identity-related resources (scopes) public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Address() }; }
همین مورد را به لیست AllowedScopes متد GetClients نیز اضافه میکنیم:
AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Address },
namespace DNT.IDP { public static class Config { public static List<TestUser> GetUsers() { return new List<TestUser> { new TestUser { // ... Claims = new List<Claim> { // ... new Claim("address", "Main Road 1") } }, new TestUser { // ... Claims = new List<Claim> { // ... new Claim("address", "Big Street 2") } } }; }
پس از آن به کلاس ImageGallery.MvcClient.WebApp\Startup.cs مراجعه میکنیم تا درخواست این claim را به لیست scopes میانافزار oidc اضافه کنیم:
namespace ImageGallery.MvcClient.WebApp { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... .AddOpenIdConnect("oidc", options => { // ... options.Scope.Add("address"); // … options.ClaimActions.DeleteClaim("address"); }); }
یک نکته: فراخوانی DeleteClaim بر روی address غیر ضروری است و میشود این سطر را حذف کرد. از این جهت که اگر به سورس OpenID Connect Options مایکروسافت مراجعه کنیم، مشاهده خواهیم کرد که میانافزار اعتبارسنجی استاندارد ASP.NET Core، تنها تعداد معدودی از claims را نگاشت میکند. به این معنا که هر claim ای که در token وجود داشته باشد، اما اینجا نگاشت نشده باشد، در claims نهایی حضور نخواهند داشت و address claim یکی از اینها نیست. بنابراین در لیست نهایی this.User.Claims حضور نخواهد داشت؛ مگر اینکه مطابق همین سورس، با استفاده از متد options.ClaimActions.MapUniqueJsonKey، یک نگاشت جدید را برای آن تهیه کنیم و البته چون نمیخواهیم آدرس در لیست claims وجود داشته باشد، این نگاشت را تعریف نخواهیم کرد.
دریافت اطلاعات بیشتری از کاربران از طریق UserInfo Endpoint
همانطور که در قسمت قبل با بررسی «تنظیمات بازگشت Claims کاربر به برنامهی کلاینت» عنوان شد، میانافزار oidc با UserInfo Endpoint کار میکند که تمام عملیات آن خودکار است. در اینجا امکان کار با آن از طریق برنامه نویسی مستقیم نیز جهت دریافت اطلاعات بیشتری از کاربران، وجود دارد. برای مثال شاید به دلایل امنیتی نخواهیم آدرس کاربر را در لیست Claims او قرار دهیم. این مورد سبب کوچکتر شدن کوکی متناظر با این اطلاعات و همچنین دسترسی به اطلاعات به روزتری از کاربر میشود.
درخواستی که به سمت UserInfo Endpoint ارسال میشود، باید یک چنین فرمتی را داشته باشد:
GET idphostaddress/connect/userinfo Authorization: Bearer R9aty5OPlk
اکنون برای دریافت دستی اطلاعات آدرس از IDP و UserInfo Endpoint آن، ابتدا نیاز است بستهی نیوگت IdentityModel را به پروژهی Mvc Client اضافه کنیم:
dotnet add package IdentityModel
namespace ImageGallery.MvcClient.WebApp.Controllers { [Authorize] public class GalleryController : Controller { public async Task<IActionResult> OrderFrame() { var discoveryClient = new DiscoveryClient(_configuration["IDPBaseAddress"]); var metaDataResponse = await discoveryClient.GetAsync(); var userInfoClient = new UserInfoClient(metaDataResponse.UserInfoEndpoint); var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); var response = await userInfoClient.GetAsync(accessToken); if (response.IsError) { throw new Exception("Problem accessing the UserInfo endpoint.", response.Exception); } var address = response.Claims.FirstOrDefault(c => c.Type == "address")?.Value; return View(new OrderFrameViewModel(address)); }
OrderFrameViewModel ارسالی به View، یک چنین شکلی را دارد:
namespace ImageGallery.MvcClient.ViewModels { public class OrderFrameViewModel { public string Address { get; } = string.Empty; public OrderFrameViewModel(string address) { Address = address; } } }
@model ImageGallery.MvcClient.ViewModels.OrderFrameViewModel <div class="container"> <div class="h3 bottomMarginDefault">Order a framed version of your favorite picture.</div> <div class="text bottomMarginSmall">We've got this address on record for you:</div> <div class="text text-info bottomMarginSmall">@Model.Address</div> <div class="text">If this isn't correct, please contact us.</div> </div>
سپس به Shared\_Layout.cshtml مراجعه کرده و لینکی را به این اکشن متد و View، اضافه میکنیم:
<li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
اکنون اگر برنامه را اجرا کنیم، پس از login، یک چنین خروجی قابل مشاهده است:
همانطور که ملاحظه میکنید، آدرس شخص به صورت مستقیم از UserInfo Endpoint دریافت و نمایش داده شدهاست.
بررسی Authorization مبتنی بر نقشها
تا اینجا مرحلهی Authentication را که مشخص میکند کاربر وار شدهی به سیستم کیست، بررسی کردیم که اطلاعات آن از Identity token دریافتی از IDP استخراج میشود. مرحلهی پس از ورود به سیستم، مشخص کردن سطوح دسترسی کاربر و تعیین این مورد است که کاربر مجاز به انجام چه کارهایی میباشد. به این مرحله Authorization میگویند و روشهای مختلفی برای مدیریت آن وجود دارد:
الف) RBAC و یا Role-based Authorization و یا تعیین سطوح دسترسی بر اساس نقشهای کاربر
در این حالت claim ویژهی role، از IDP دریافت شده و توسط آن یکسری سطوح دسترسی کاربر مشخص میشوند. برای مثال کاربر وارد شدهی به سیستم میتواند تصویری را اضافه کند و یا آیا مجاز است نگارش قاب شدهی تصویری را درخواست دهد؟
ب) ABAC و یا Attribute based access control روش دیگر مدیریت سطوح دسترسی است و عموما آنرا نسبت به حالت الف ترجیح میدهند که آنرا در قسمتهای بعدی بررسی خواهیم کرد.
در اینجا روش «تعیین سطوح دسترسی بر اساس نقشهای کاربر» را بررسی میکنیم. برای این منظور به تنظیمات IDP در فایل src\IDP\DNT.IDP\Config.cs مراجعه کرده و claims جدیدی را تعریف میکنیم:
namespace DNT.IDP { public static class Config { public static List<TestUser> GetUsers() { return new List<TestUser> { new TestUser { //... Claims = new List<Claim> { //... new Claim("role", "PayingUser") } }, new TestUser { //... Claims = new List<Claim> { //... new Claim("role", "FreeUser") } } }; }
سپس باید برای این claim جدید یک scope جدید را نیز به قسمت GetIdentityResources اضافه کنیم تا توسط client قابل دریافت شود:
namespace DNT.IDP { public static class Config { public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { // ... new IdentityResource( name: "roles", displayName: "Your role(s)", claimTypes: new List<string>() { "role" }) }; }
همچنین باید به کلاینت مجوز درخواست این scope را نیز بدهیم. به همین جهت آنرا به AllowedScopes مشخصات Client نیز اضافه میکنیم:
namespace DNT.IDP { public static class Config { public static IEnumerable<Client> GetClients() { return new List<Client> { new Client { // ... AllowedScopes = { // ... "roles" } // ... } }; }
در ادامه قرار است تنها کاربری که دارای نقش PayingUser است، امکان دسترسی به سفارش نگارش قاب شدهی تصاویر را داشته باشد. به همین جهت به کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامهی کلاینت مراجعه کرده و درخواست scope نقشهای کاربر را به متد تنظیمات AddOpenIdConnect اضافه میکنیم:
options.Scope.Add("roles");
برای آزمایش آن، یکبار از برنامه خارج شده و مجددا به آن وارد شوید. اینبار در صفحهی consent، از کاربر مجوز دسترسی به نقشهای او نیز سؤال پرسیده میشود:
اما اگر به لیست موجود در this.User.Claims در برنامهی کلاینت مراجعه کنیم، نقش او را مشاهده نخواهیم کرد و به این لیست اضافه نشدهاست:
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 • Claim type: amr - Claim value: pwd • Claim type: given_name - Claim value: Vahid • Claim type: family_name - Claim value: N
همانطور که در نکتهای پیشتر نیز ذکر شد، چون role جزو لیست نگاشتهای OpenID Connect Options مایکروسافت نیست، آنرا به صورت خودکار به لیست claims اضافه نمیکند؛ دقیقا مانند آدرسی که بررسی کردیم. برای رفع این مشکل و افزودن نگاشت آن در متد تنظیمات AddOpenIdConnect، میتوان از متد MapUniqueJsonKey به صورت زیر استفاده کرد:
options.ClaimActions.MapUniqueJsonKey(claimType: "role", jsonKey: "role");
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 • Claim type: amr - Claim value: pwd • Claim type: given_name - Claim value: Vahid • Claim type: family_name - Claim value: N • Claim type: role - Claim value: PayingUser
استفاده از نقش تعریف شده جهت محدود کردن دسترسی به سفارش تصاویر قاب شده
در ادامه قصد داریم لینک درخواست تصاویر قاب شده را فقط برای کاربرانی که دارای نقش PayingUser هستند، نمایش دهیم. به همین جهت به فایل Views\Shared\_Layout.cshtml مراجعه کرده و آنرا به صورت زیر تغییر میدهیم:
@if(User.IsInRole("PayingUser")) { <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li> }
options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = JwtClaimTypes.GivenName, RoleClaimType = JwtClaimTypes.Role, };
اکنون برای آزمایش آن یکبار از سیستم خارج شده و مجددا به آن وارد شوید. پس از آن لینک درخواست نگارش قاب شدهی تصاویر، برای کاربر User 1 نمایان خواهد بود و نه برای User 2 که FreeUser است.
البته هرچند تا این لحظه لینک نمایش View متناظر با اکشن متد OrderFrame را امن کردهایم، اما هنوز خود این اکشن متد به صورت مستقیم با وارد کردن آدرس https://localhost:5001/Gallery/OrderFrame در مرورگر قابل دسترسی است. برای رفع این مشکل به کنترلر گالری مراجعه کرده و دسترسی به اکشن متد OrderFrame را توسط فیلتر Authorize و با مقدار دهی خاصیت Roles آن محدود میکنیم:
namespace ImageGallery.MvcClient.WebApp.Controllers { [Authorize] public class GalleryController : Controller { [Authorize(Roles = "PayingUser")] public async Task<IActionResult> OrderFrame() {
برای آزمایش آن، توسط مشخصات کاربر User 2 به سیستم وارد شده و آدرس https://localhost:5001/Gallery/OrderFrame را مستقیما در مرورگر وارد کنید. در این حالت یک چنین تصویری نمایان خواهد شد:
همانطور که مشاهده میکنید، علاوه بر عدم دسترسی به این اکشن متد، به صفحهی Account/AccessDenied که هنوز در برنامهی کلاینت تعریف نشدهاست، هدایت شدهایم. به همین جهت خطای 404 و یا یافت نشد، نمایش داده شدهاست.
برای تغییر مقدار پیشفرض صفحهی عدم دسترسی، ابتدا Controllers\AuthorizationController.cs را با این محتوا ایجاد میکنیم:
using Microsoft.AspNetCore.Mvc; public class AuthorizationController : Controller { public IActionResult AccessDenied() { return View(); } }
<div class="container"> <div class="h3">Woops, looks like you're not authorized to view this page.</div> <div>Would you prefer to <a asp-controller="Gallery" asp-action="Logout">log in as someone else</a>?</div> </div>
اکنون نیاز است تا این آدرس جدید را به کلاس ImageGallery.MvcClient.WebApp\Startup.cs معرفی کنیم.
namespace ImageGallery.MvcClient.WebApp { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(options => { // ... }).AddCookie("Cookies", options => { options.AccessDeniedPath = "/Authorization/AccessDenied"; }) // ...
برای آزمایش آن، یکبار از برنامه خارج شده و مجددا با اکانت User 2 به آن وارد شوید و آدرس https://localhost:5001/Gallery/OrderFrame را مستقیما در مرورگر وارد کنید. اینبار تصویر زیر که همان آدرس جدید تنظیم شدهاست نمایش داده خواهد شد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.
مفهوم زبان مشترک در DDD
Account
A bank account allows us to send and receive money and has its unique number. Anytime we tell about an account in a bank, an account is always a bank account. In the other hand, an account in an information system is used to authorize a user. We have the term "account" meaning something absolutely different in two different domains. Domain has an impact on what we imagine when someone says a concrete term. So we have to learn and specify domain terms first.
Price
Let's speak about e-shop domain. What is a price? For us, as customers, it is how much we pay. A manager can think about price as an amount that his company pays to the supplier. For an accountant, a price is just a number. And e-shop programmer is now confused.
Language is crucial because customers and experts are telling their stories in their language. But it is also natural language, inaccurate, ambiguous, context-aware. And as we can see, language can be tricky even within one domain.
کتابخانه Foundatio.Redis
When building several big cloud applications we found a lack of great solutions (that's not to say there isn't solutions out there) for many key pieces to building scalable distributed applications while keeping the development experience simple. Here are a few examples of why we built and use Foundatio.
تشخیص تغییرات در Angular2
تکمیل فرم ثبت نام کاربران
در ادامه کدهای کامل کامپوننت فرم ثبت نام کاربران را مشاهده میکنید:
@page "/registration" @inject IClientAuthenticationService AuthenticationService @inject NavigationManager NavigationManager <EditForm Model="UserForRegistration" OnValidSubmit="RegisterUser" class="pt-4"> <DataAnnotationsValidator /> <div class="py-4"> <div class=" row form-group "> <div class="col-6 offset-3 "> <div class="card border"> <div class="card-body px-lg-5 pt-4"> <h3 class="col-12 text-success text-center py-2"> <strong>Sign Up</strong> </h3> @if (ShowRegistrationErrors) { <div> @foreach (var error in Errors) { <p class="text-danger text-center">@error</p> } </div> } <hr style="background-color:aliceblue" /> <div class="py-2"> <InputText @bind-Value="UserForRegistration.Name" class="form-control" placeholder="Name..." /> <ValidationMessage For="(()=>UserForRegistration.Name)" /> </div> <div class="py-2"> <InputText @bind-Value="UserForRegistration.Email" class="form-control" placeholder="Email..." /> <ValidationMessage For="(()=>UserForRegistration.Email)" /> </div> <div class="py-2 input-group"> <div class="input-group-prepend"> <span class="input-group-text"> +1</span> </div> <InputText @bind-Value="UserForRegistration.PhoneNo" class="form-control" placeholder="Phone number..." /> <ValidationMessage For="(()=>UserForRegistration.PhoneNo)" /> </div> <div class="form-row py-2"> <div class="col"> <InputText @bind-Value="UserForRegistration.Password" type="password" id="password" placeholder="Password..." class="form-control" /> <ValidationMessage For="(()=>UserForRegistration.Password)" /> </div> <div class="col"> <InputText @bind-Value="UserForRegistration.ConfirmPassword" type="password" id="confirm" class="form-control" placeholder="Confirm Password..." /> <ValidationMessage For="(()=>UserForRegistration.ConfirmPassword)" /> </div> </div> <hr style="background-color:aliceblue" /> <div class="py-2"> @if (IsProcessing) { <button type="submit" class="btn btn-success btn-block disabled"><i class="fas fa-sign-in-alt"></i> Please Wait...</button> } else { <button type="submit" class="btn btn-success btn-block"><i class="fas fa-sign-in-alt"></i> Register</button> } </div> </div> </div> </div> </div> </div> </EditForm> @code{ UserRequestDTO UserForRegistration = new UserRequestDTO(); bool IsProcessing; bool ShowRegistrationErrors; IEnumerable<string> Errors; private async Task RegisterUser() { ShowRegistrationErrors = false; IsProcessing = true; var result = await AuthenticationService.RegisterUserAsync(UserForRegistration); if (result.IsRegistrationSuccessful) { IsProcessing = false; NavigationManager.NavigateTo("/login"); } else { IsProcessing = false; Errors = result.Errors; ShowRegistrationErrors = true; } } }
- مدل این فرم بر اساس UserRequestDTO تشکیل شدهاست که همان شیءای است که اکشن متد ثبت نام سمت Web API انتظار دارد.
- در این کامپوننت به کمک سرویس IClientAuthenticationService که آنرا در قسمت قبل تهیه کردیم، شیء نهایی متصل به فرم، به سمت Web API Endpoint ثبت نام ارسال میشود.
- در اینجا روشی را جهت غیرفعال کردن یک دکمه، پس از کلیک بر روی آن مشاهده میکنید. میتوان پس از کلیک بر روی دکمهی ثبت نام، با true کردن یک فیلد مانند IsProcessing، بلافاصله دکمهی جاری را برای مثال با ویژگی disabled در صفحه درج کرد و یا حتی آنرا از صفحه حذف کرد. این روش، یکی از روشهای جلوگیری از کلیک چندبارهی کاربر، بر روی یک دکمهاست.
- فرم جاری، خطاهای اعتبارسنجی مخصوص Identity سمت سرور را نیز نمایش میدهد که حاصل از ارسال آنها توسط اکشن متد ثبت نام است:
- پس از پایان موفقیت آمیز ثبت نام، کاربر را به سمت فرم لاگین هدایت میکنیم.
تکمیل فرم ورود به سیستم کاربران
در ادامه کدهای کامل کامپوننت فرم ثبت نام کاربران را مشاهده میکنید:
@page "/login" @inject IClientAuthenticationService AuthenticationService @inject NavigationManager NavigationManager <div id="logreg-forms"> <h1 class="h3 mb-3 pt-3 font-weight-normal text-primary" style="text-align:center;">Sign In</h1> <EditForm Model="UserForAuthentication" OnValidSubmit="LoginUser"> <DataAnnotationsValidator /> @if (ShowAuthenticationErrors) { <p class="text-center text-danger">@Errors</p> } <InputText @bind-Value="UserForAuthentication.UserName" id="email" placeholder="Email..." class="form-control mb-2" /> <ValidationMessage For="(()=>UserForAuthentication.UserName)"></ValidationMessage> <InputText @bind-Value="UserForAuthentication.Password" type="password" placeholder="Password..." id="password" class="form-control mb-2" /> <ValidationMessage For="(()=>UserForAuthentication.Password)"></ValidationMessage> @if (IsProcessing) { <button type="submit" class="btn btn-success btn-block disabled"><i class="fas fa-sign-in-alt"></i> Please Wait...</button> } else { <button type="submit" class="btn btn-success btn-block"><i class="fas fa-sign-in-alt"></i> Sign in</button> } <a href="/registration" class="btn btn-primary text-white mt-3"><i class="fas fa-user-plus"></i> Register as a new user</a> </EditForm> </div> @code { AuthenticationDTO UserForAuthentication = new AuthenticationDTO(); bool IsProcessing = false; bool ShowAuthenticationErrors; string Errors; string ReturnUrl; private async Task LoginUser() { ShowAuthenticationErrors = false; IsProcessing = true; var result = await AuthenticationService.LoginAsync(UserForAuthentication); if (result.IsAuthSuccessful) { IsProcessing = false; var absoluteUri = new Uri(NavigationManager.Uri); var queryParam = HttpUtility.ParseQueryString(absoluteUri.Query); ReturnUrl = queryParam["returnUrl"]; if (string.IsNullOrEmpty(ReturnUrl)) { NavigationManager.NavigateTo("/"); } else { NavigationManager.NavigateTo("/" + ReturnUrl); } } else { IsProcessing = false; Errors = result.ErrorMessage; ShowAuthenticationErrors = true; } } }
- مدل این فرم بر اساس AuthenticationDTO تشکیل شدهاست که همان شیءای است که اکشن متد لاگین سمت Web API انتظار دارد.
- در این کامپوننت به کمک سرویس IClientAuthenticationService که آنرا در قسمت قبل تهیه کردیم، شیء نهایی متصل به فرم، به سمت Web API Endpoint ثبت نام ارسال میشود.
- در اینجا نیز همانند فرم ثبت نام، پس از کلیک بر روی دکمهی ورود به سیستم، با true کردن یک فیلد مانند IsProcessing، بلافاصله دکمهی جاری را با ویژگی disabled در صفحه درج کردهایم تا از کلیک چندبارهی کاربر، جلوگیری شود.
- این فرم، خطاهای اعتبارسنجی مخصوص Identity سمت سرور را نیز نمایش میدهد که حاصل از ارسال آنها توسط اکشن متد لاگین است:
همانطور که مشاهده میکنید، مقدار JWT تولید شدهی پس از لاگین و همچنین مشخصات کاربر دریافتی از Web Api، جهت استفادههای بعدی، در Local Storage مرورگر درج شدهاند.
تغییر منوی راهبری سایت، بر اساس وضعیت لاگین شخص
تا اینجا قسمتهای ثبت نام و ورود به سیستم را تکمیل کردیم. در ادامه نیاز داریم تا منوی سایت را هم بر اساس وضعیت اعتبارسنجی شخص، تغییر دهیم. برای مثال اگر شخصی به سیستم وارد شدهاست، باید در منوی سایت، لینک خروج و نام خودش را مشاهده کند و نه مجددا لینکهای ثبتنام و لاگین را. جهت تغییر منوی راهبری سایت، کامپوننت Shared\NavMenu.razor را گشوده و لینکهای قبلی ثبتنام و لاگین را با محتوای زیر جایگزین میکنیم:
<AuthorizeView> <Authorized> <li class="nav-item p-0"> <NavLink class="nav-link" href="#"> <span class="p-2"> Hello, @context.User.Identity.Name! </span> </NavLink> </li> <li class="nav-item p-0"> <NavLink class="nav-link" href="logout"> <span class="p-2"> Logout </span> </NavLink> </li> </Authorized> <NotAuthorized> <li class="nav-item p-0"> <NavLink class="nav-link" href="registration"> <span class="p-2"> Register </span> </NavLink> </li> <li class="nav-item p-0"> <NavLink class="nav-link" href="login"> <span class="p-2"> Login </span> </NavLink> </li> </NotAuthorized> </AuthorizeView>
تکمیل قسمت خروج از سیستم
اکنون که لینک logout را در منوی سایت، پس از ورود به سیستم نمایش میدهیم، میتوان کدهای کامپوننت آنرا (Pages\Authentication\Logout.razor) به صورت زیر تکمیل کرد:
@page "/logout" @inject IClientAuthenticationService AuthenticationService @inject NavigationManager NavigationManager @code { protected async override Task OnInitializedAsync() { await AuthenticationService.LogoutAsync(); NavigationManager.NavigateTo("/"); } }
مشکل! پس از کلیک بر روی logout، هرچند میتوان مشاهده کرد که اطلاعات Local Storage به درستی حذف شدهاند، اما ... پس از هدایت به صفحهی اصلی برنامه، هنوز هم لینک logout و نام کاربری شخص نمایان هستند و به نظر هیچ اتفاقی رخ ندادهاست!
علت اینجا است که AuthenticationStateProvider سفارشی را که تهیه کردیم، فاقد اطلاع رسانی تغییر وضعیت است:
namespace BlazorWasm.Client.Services { public class AuthStateProvider : AuthenticationStateProvider { // ... public void NotifyUserLoggedIn(string token) { var authenticatedUser = new ClaimsPrincipal( new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType") ); var authState = Task.FromResult(new AuthenticationState(authenticatedUser)); base.NotifyAuthenticationStateChanged(authState); } public void NotifyUserLogout() { var authState = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); base.NotifyAuthenticationStateChanged(authState); } } }
namespace BlazorWasm.Client.Services { public class ClientAuthenticationService : IClientAuthenticationService { private readonly HttpClient _client; private readonly ILocalStorageService _localStorage; private readonly AuthenticationStateProvider _authStateProvider; public ClientAuthenticationService( HttpClient client, ILocalStorageService localStorage, AuthenticationStateProvider authStateProvider) { _client = client; _localStorage = localStorage; _authStateProvider = authStateProvider; } public async Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication) { // ... if (response.IsSuccessStatusCode) { //... ((AuthStateProvider)_authStateProvider).NotifyUserLoggedIn(result.Token); return new AuthenticationResponseDTO { IsAuthSuccessful = true }; } //... } public async Task LogoutAsync() { //... ((AuthStateProvider)_authStateProvider).NotifyUserLogout(); } } }
- ابتدا AuthenticationStateProvider به سازندهی کلاس تزریق شدهاست.
- سپس در حین لاگین موفق، متد NotifyUserLoggedIn آن فراخوانی شدهاست.
- در آخر پس از خروج از سیستم، متد NotifyUserLogout فراخوانی شدهاست.
پس از این تغییرات اگر بر روی لینک logout کلیک کنیم، این گزینه به درستی عمل کرده و اینبار شاهد نمایش مجدد لینکهای لاگین و ثبت نام خواهیم بود.
محدود کردن دسترسی به صفحات برنامه بر اساس نقشهای کاربران
پس از ورود کاربر به سیستم و تامین AuthenticationState، اکنون میخواهیم تنها این نوع کاربران اعتبارسنجی شده بتوانند جزئیات اتاقها (برای شروع رزرو) و یا صفحهی نمایش نتیجهی پرداخت را مشاهده کنند. البته نمیخواهیم صفحهی نمایش لیست اتاقها را محدود کنیم. برای این منظور ویژگی Authorize را به ابتدای تعاریف کامپوننتهای PaymentResult.razor و RoomDetails.razor، اضافه میکنیم:
@attribute [Authorize(Roles = ConstantRoles.Customer)]
@using Microsoft.AspNetCore.Authorization
تکمیل مشخصات هویتی شخصی که قرار است اتاقی را رزرو کند
پیشتر در فرم RoomDetails.razor، اطلاعات ابتدایی کاربر را مانند نام او، دریافت میکردیم. اکنون با توجه به محدود شدن این کامپوننت به کاربران لاگین کرده، میتوان اطلاعات کاربر وارد شدهی به سیستم را نیز به صورت خودکار بارگذاری و تکمیل کرد:
@page "/hotel-room-details/{Id:int}" // ... @code { // ... protected override async Task OnInitializedAsync() { try { HotelBooking.OrderDetails = new RoomOrderDetailsDTO(); if (Id != null) { // ... if (await LocalStorage.GetItemAsync<UserDTO>(ConstantKeys.LocalUserDetails) != null) { var userInfo = await LocalStorage.GetItemAsync<UserDTO>(ConstantKeys.LocalUserDetails); HotelBooking.OrderDetails.UserId = userInfo.Id; HotelBooking.OrderDetails.Name = userInfo.Name; HotelBooking.OrderDetails.Email = userInfo.Email; HotelBooking.OrderDetails.Phone = userInfo.PhoneNo; } } } catch (Exception e) { await JsRuntime.ToastrError(e.Message); } }
public async Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details) { // details.UserId = "unknown user!";
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-32.zip
به همراه NET Core 2.1.، یک HttpClientFactory توکار توسط مایکروسافت ارائه شدهاست:
HttpClientFactory in ASP.NET Core 2.1 (Part 2) Defining Named and Typed Clients
به این ترتیب برای مثال جهت کار با یک آدرس مشخص، میتوان تنظیمات آنرا یکبار در آغاز برنامه ثبت کرد:
public void ConfigureServices(IServiceCollection services) { services.AddHttpClient("github", c => { c.BaseAddress = new Uri("https://api.github.com/"); c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // Github requires a user-agent }); services.AddHttpClient(); }
IHttpClientFactory _httpClientFactory; public MyController(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } public IActionResult Index() { //This client doesn’t have any special configuration applied var defaultClient = _httpClientFactory.CreateClient(); //This client has the header and base address configured for the “github” client above. var gitHubClient = _httpClientFactory.CreateClient("github"); return View(); }
حمله به بدهیهای فنی
Technical Debt is a metaphor for shortcuts and hacks in software that make it more difficult to change and maintain than it could be with an optimal design. Many applications have accumulated a large amount of technical debt, and figuring out how to deal with it is a fairly common challenge for many developers
کتاب رایگان Power BI Succinctly
Power BI is a cloud-based business analytics service that gives us a single view of the most critical business data. A tool for creating live dashboards and rich, interactive reports, Power BI allows access to data for monitoring the health of a business. Pierstefano Tucci will take readers through the Power BI interface, query creation, publishing your work, and more in Power BI Succinctly.
- What is Power BI?
- Service
- Desktop
- Mobile Apps
- Solution Template
- Devoloper
- Power BI Embedded
- Power BI Gateway: Data Security