برای انتقال جداول احراز هویت (Identity) از SQL Server به بانک اطلاعاتی MongoDB و نحوه استفاده از آن از سورس نمونه در لینک بالا استفاده کنید.
همچنین در این پروژه از پکیج AspNet.Identity.MongoDB 2.0.8 که بر روی Nuget قرار دارد استفاده شده است.
Full-Text Search در MongoDB
MongoDB, one of the leading NoSQL databases, is well known for its fast performance, flexible schema, scalability and great indexing capabilities. At the core of this fast performance lies MongoDB indexes, which support efficient execution of queries by avoiding full-collection scans and hence limiting the number of documents MongoDB searches.
Starting from version 2.4, MongoDB began with an experimental feature supporting Full-Text Search using Text Indexes .شروع به کار با MVC 6 و MongoDB
ایجاد یک request bin جدید
برای مشاهدهی محتوای ارسالی توسط postman بدون برپایی وب سرویس خاصی، از سایت requestbin استفاده خواهیم کرد. در اینجا با کلیک بر روی دکمهی create a request bin، یک آدرس موقتی را مانند http://requestbin.fullcontact.com/1gdduy21 برای شما تولید میکند. میتوان انواع و اقسام درخواستها را به این آدرس ارسال کرد و سپس با ریفرش کردن آن در مرورگر، دقیقا محتوای ارسالی به سمت سرور را بررسی نمود.
برای مثال اگر همین آدرس را در postman وارد کرده و سپس بر روی دکمهی send آن کلیک کنیم، پس از ریفرش صفحه، چنین تصویری حاصل میشود:
Encoding کوئری استرینگها در postman
مثال زیر را درنظر بگیرید:
در اینجا با استفاده از گرید ساخت Query Params، دو کوئری استرینگ جدید را ایجاد کردهایم که در دومی، مقدار وارد شده، دارای فاصله است. اگر این درخواست را ارسال کنیم، مشاهده خواهیم کرد که مقدار ارسالی توسط آن encoded نیست:
برای رفع این مشکل میتوان بر روی تکستباکس ورود مقدار یک کلید، کلیک راست کرد و از منوی باز شده، گزینهی encode URI component را انتخاب نمود:
البته برای اینکه این گزینه درست عمل کند، نیاز است یکبار کل متن را انتخاب کرد و سپس بر روی آن کلیک راست نمود، تا انتخاب گزینهی encode URI component، به درستی اعمال شود:
امکان تعریف متغیرها در آدرسهای HTTP
Postman از امکان تعریف path variables پشتیبانی میکند. برای مثال مسیر api.example.com/users/5/contracts/2 را در نظر بگیرید که میتواند سبب نمایش اطلاعات قرارداد دوم کاربر پنجم شود. برای پویا کردن یک چنین آدرسی در postman میتوان از مسیری مانند api.example.com/users/:userid/contracts/:contractid استفاده کرد:
اگر به تصویر فوق دقت کنیم، متغیرهای شروع شدهی با :، در قسمت path variables ذکر شدهاند و به سادگی قابل تغییر و مقدار دهی میباشند (در گریدی همانند گرید کوئری استرینگها) که برای آزمایش دستی، بسیار مفید هستند.
امکان ارسال فایلها به سمت سرور
زمانیکه برای مثال، نوع درخواست به Post تغییر میکند، امکان تنظیم بدنهی آن نیز مسیر میشود. در این حالت اگر گزینهی form-data را انتخاب کنیم، با نزدیک کردن اشارهگر ماوس به هر ردیف جدید (پیش از ورود کلید آن ردیف)، میتوان نوع Text و یا File را انتخاب کرد:
در اینجا پس از انتخاب گزینهی File، میتوان علاوه بر تعیین کلید این ردیف، با استفاده از دکمهی select files، چندین فایل را نیز برای ارسال انتخاب کرد:
روش انتقال درخواستهای پیچیده به Postman
تا اینجا روش ساخت درخواستهای متداولی را بررسی کردیم که آنچنان پیچیده، طولانی و به همراه جزئیات زیادی (مانند کوکیها،انواع و اقسام هدرها و ...) نبودند. فرض کنید میخواهید درخواست ارسال یک امتیاز جدید را به مطلبی در سایت جاری، توسط Postman شبیه سازی کنیم. برای اینکار، توسط مرورگر کروم به سایت وارد شده و پس از لاگین و تنظیم خودکار کوکیهای اعتبارسنجی، برگهی developer tools مرورگر را باز کرده و در آن، قسمت network را انتخاب کنید:
در اینجا گزینهی preserve log را نیز انتخاب کنید تا پس از ارسال درخواستی، سابقهی عملیات، پاک نشود. سپس به صورت معمولی به مطلبی امتیاز دهید. اکنون بر روی مدخل درخواست آن کلیک راست کرده و از منوی ظاهر شده، گزینهی Copy->Copy as cURL را انتخاب کنید تا این عملیات و تمام جزئیات مرتبط با آنرا تبدیل به یک دستور cURL کرده و به حافظه کپی کند.
سپس در postman، از منوی بالای صفحه، سمت چپ آن، گزینهی Import را انتخاب کنید:
در ادامه این دستور کپی شدهی در حافظه را در قسمت Paste Raw Text، قرار دهید و بر روی دکمهی import کلیک کنید:
در این حالت postman این دستور را پردازش کرده و فیلدهای ساخت یک درخواست را به صورت خودکار پر میکند.
یک نکتهی مهم: در حالت انتخاب Copy->Copy as cURL در مرورگر کروم، دو گزینهی cmd و bash موجود هستند. حالت bash را باید انتخاب کنید تا توسط postman به دسترسی parse شود. حالت cmd یک چنین خروجی مشکل داری را در postman تولید میکند که قابل ارسال به سمت سرور نیست:
اما جزئیات حالت bash آن، به درستی parse شدهاست و قابلیت send مجدد را دارد:
کار با کوکیها در Postman
کوکیها نیز در اصل به صورت یک هدر HTTP به صورت خودکار توسط مرورگرها به سمت سرور ارسال میشوند؛ اما Postman روش سادهتری را برای تعریف و کار با آنها ارائه میدهد (و ترجیح میدهد که بین کوکیها و هدرها تفاوت قائل شود؛ هم در زمان ارسال و هم در زمان نمایش response که در کنار قسمت هدرهای دریافتی از سرور، لیست کوکیهای دریافتی نیز به صورت مجزایی نمایش داده میشوند).
با کلیک بر روی لینک Cookies، در ذیل قسمتی که آدرس یک درخواست تنظیم میشود، قسمت مدیریت کوکیها نیز ظاهر خواهد شد که با انتخاب هر نامی در اینجا، میتوان مقدار آنرا ویرایش و یا حتی حذف کرد. در این لیست، تمام کوکیهایی را که تاکنون تنظیم کرده باشید، میتوانید مشاهده کنید (و مختص به برگهی خاصی نیست) و همانند قسمت مدیریت کوکیهای یک مرورگر رفتار میکند.
روش کار با آن نیز به این صورت است: ابتدا باید یک دومین را اضافه کنید. سپس ذیل دومین اضافه شده، دکمهی Add cookie ظاهر میشود و به هر تعدادی که لازم باشد، میتوان برای آن دومین کوکی تعریف کرد:
پس از تعریف کوکیها، در حین کلیک بر روی دکمهی send، کوکیهای متعلق به دومین وارد شدهی در قسمت آدرس درخواست، از قسمت مدیریت کوکیها به صورت خودکار دریافت شده و به سمت سرور ارسال خواهند شد.
به اشتراک گذاری سابقهی درخواستها
در قسمت اول مشاهده کردیم که برای ذخیره سازی درخواستها، باید آنها را در مجموعههای Postman، ذخیره و ساماندهی کرد. برای گرفتن خروجی از این مجموعهها و به اشتراک گذاشتن آنها، اگر اشارهگر ماوس را بر روی نام یک مجموعه حرکت دهیم، یک دکمهی جدید با برچسب ... ظاهر میشود:
با کلیک بر روی آن، یکی از گزینههای آن، export نام دارد که جزئیات تمام درخواستهای یک مجموعه را به صورت یک فایل JSON ذخیره میکند. برای نمونه فایل JSON خروجی دو قسمت قبل این سری را میتوانید از اینجا دریافت کنید: httpbin.postman_collection.json
پس از تولید این فایل JSON، برای بازیابی آن میتوان از دکمهی Import که در منوی سمت چپ، بالای postman قرار دارد، استفاده کرد که نمونهای از آنرا برای Import جزئیات درخواستهای cURL پیشتر مشاهده کردید. در اینجا، همان اولین گزینهی دیالوگ Import که Import file نام دارد، دقیقا برای همین منظور تدارک دیده شدهاست.
بررسی 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 وارد کنید.
ALTER SYSTEM SET recyclebin = ON;
ALTER SESSION SET recyclebin = ON;
ALTER SYSTEM SET recyclebin = OFF; یا ALTER SESSION SET recyclebin = OFF;
Create Table Test (ID int, FirstName varchar(255), LastName Varchar(255))
Drop table Test
FLASHBACK TABLE Test TO BEFORE DROP;
Select * From recyclebin; یا SELECT * FROM USER_RECYCLEBIN;
برنامه نویسی شیء گرا
در این بخش میخواهیم به بررسی یکسری از ویژگیها و نکات ریز برنامه نویسی شیء گرا در جاوا اسکریپت بپردازیم که یک برنامه نویس حرفهای جاوا اسکریپت حتما باید بر آنها واقف باشد تا بتواند کتابخانهها و Framework های موثرتر و بهینهتری را ایجاد کند. لازم به ذکر است که در این مجموعه مقالات، پیادهسازی اشیاء و شیوهی کد نویسی، بر اساس استاندارد ECMAScript 5 یا ES5 انجام خواهد شد. بنابراین از قابلیتهای جدیدی که در ES6 اضافه شدهاست، صحبت نخواهیم کرد. پس از پایان این مجموعه مقالات و پس از آگاهی کامل از قابلیتهای جاوا اسکریپت، در مجموعه مقالاتی به بررسی قابلیتهای جدید ES6 خواهیم پرداخت که مرتبط به مقالات جاری است.
همانطور که قبلا اشاره شد، در زبانهای برنامه نویسی شیء گرا، مفهومی به نام کلاس وجود دارد که ساختاری را جهت ایجاد اشیاء معرفی میکند و میتوانیم اشیاء مختلفی را از این کلاسها ایجاد نماییم. اما در جاوا اسکریپت مفهوم کلاس وجود ندارد و فقط میتوانیم از اشیاء استفاده کنیم که نسبت به زبانهای مبتنی بر کلاس متفاوت میباشد.
بر اساس تعریفی که از اشیاء در استاندارد ECMAScript صورت گرفته است، هرشیء، شامل مجموعهای از ویژگیهاست، که هر یک از آنها میتواند حاوی یک مقدار پایه، شیء و یا تابع باشد. به عبارت دیگر هر شیء شامل آرایهای از مقادیر است. هر ویژگی ( Property ) یا تابع (که در برنامه نویسی شیء گرا متد نیز نامیده میشود) توسط نام خود شناسایی میشوند که به یک مقدار دادهای نگاشت یا Map شدهاند. به همین دلیل میتوان هر شیء را به عنوان یک Hash Table تصور کرد که دادهها را به صورت یک زوج کلید مقدار یا key-value pairs نگهداری مینماید. در اینصورت نام ویژگیها و متدها به عنوان key و مقدار آنها به عنوان value در نظر گرفته میشوند.
مفهوم شیء
همانطور که قبلا اشاره شد، جهت تعریف اشیاء میتوان از دو روش استفاده نمود. در روش اول، ایجاد شیء با استفاده از شیء Object و در روش دوم، با استفاده از Object Literal Notation انجام خواهد شد. روش دوم جدیدتر و بین برنامه نویسان جاوا اسکریپت محبوبتر است. مثال دیگری را جهت یادآوری در این مورد ذکر میکنم:
var person = new Object(); person.firstName = "Meysam"; person.birth = new Date(1982, 11, 8); person.getAge = function () { var now = new Date(); return now.getFullYear() - this.birth.getFullYear(); } alert(person.firstName + ": " + person.getAge()); // Meysam: 34
var person = { firstName: "Meysam", birth: new Date(1982, 11, 8), getAge: function () { var now = new Date(); return now.getFullYear() - this.birth.getFullYear(); } }; alert(person.firstName + ": " + person.getAge()); // Meysam: 34
انواع Property ها
در ECMAScript 5 ، صفاتی برای Property ها معرفی شده است که از طریق Attribute های داخلی به Property ها اختصاص مییابد. این Attribute ها توسط موتور جاوا اسکریپت بر روی Property ها پیاده سازی میشوند و به صورت مستقیم قابل دسترسی نمیباشند. در طی فرآیند آموزش این مطالب، Attribute های داخلی را در [[]] قرار میدهیم، مثل [[Enumarable]] ، تا از سایر دستورات تفکیک شوند. به صورت کلی دو نوع ویژگی داریم که شامل Data Properties و Accessor Properties میباشند که به شرح آنها میپردازیم.
Data Properties
Data Property ها، 4 صفت یا Attribute را توصیف میکنند که عبارتند از:
[[Configurable]]
مشخص میکند یک Property اجازه حذف، تعریف مجدد و یا تغییر نوع را دارد یا خیر. بصورت پیش فرض، زمانی که یک شیء بصورت مستقیم ساخته میشود، مقدار این ویژگی True میباشد.
[[Enumarable]]
مشخص میکند که آیا امکان پیمایش یک Property توسط حلقه for-in وجود دارد یا خیر. بصورت پیش فرض، زمانیکه یک شیء بصورت مستقیم ساخته میشود، مقدار این ویژگی True میباشد.
[[Writable]]
مشخص میکند که آیا مقدار یک Property قابل تغییر میباشد یا خیر. بصورت پیش فرض، زمانیکه یک شیء بصورت مستقیم ساخته میشود، مقدار این ویژگی True میباشد.
[[Value]]
شامل مقدار واقعی یک Property و محل مقداردهی یا برگرداندن مقدار Property ها میباشد. مقدار پیش فرض آن نیز undefined میباشد.
زمانیکه یک Property به صورت عادی به یک شیء اضافه میشود، مانند مثالهای قبلی، سه Attribute اول به true تنظیم میشوند و [[Value]] با مقدار اولیه Property تنظیم میگردد. در این حالت آن Property ، قابل بروزرسانی و پیمایش میباشد. جهت تغییر ساختار یک Property و تنظیم Attribute های آن، باید آن Property را با استفاده از متد defineProperty() تعریف نماییم . شکل کلی تعریف Property با استفاده از این متد به صورت زیر میباشد:
Object.defineProperty(obj, prop, descriptor)
var person = {}; Object.defineProperty(person, "name", { writable: false, value:"Meysam" }); alert(person.name); // Meysam person.name = "Arash"; alert(person.name); // Meysam
var person = {}; Object.defineProperty(person, "name", { configurable: false, value: "Meysam" }); alert(person.name); // Meysam delete person.name; alert(person.name); // Meysam
لازم به ذکر است که میتوانید متد defineProperty() را چندین بار برای یک Property فراخوانی نموده و در هر مرحله صفات متفاوتی را تنظیم و یا صفات قبلی را تغییر دهید.
علاوه بر متد فوق، متد دیگری به نام defineProperties() وجود دارد که میتوان چند Property را بصورت همزمان تعریف و صفات آن را تنظیم نمود. شکل کلی این متد به صورت زیر است:
Object.defineProperties(obj, props)
آرگومان props یک شیء میباشد که ویژگیهای آن، نام همان Property هایی هستند که باید به obj اضافه شوند. همچنین هر ویژگی خود یک شیء میباشد که میتوان صفات آن ویژگی را تنظیم نمود. به مثال زیر توجه کنید:
var person = {}; Object.defineProperties(person, { "name": { configurable: false, value: "Meysam" }, "age": { writable:false, value:34 } });
Accessor Properties
این صفات شامل توابع getter و setter میباشند که یک یا هر دوی آنها میتوانند برای یک Property تنظیم شوند. زمانی که مقداری را از یک Property میخوانید، تابع getter فراخوانی میشود و مقدار Property مربوطه را بر میگرداند. این تابع میتواند قبل از برگرداندن مقدار، پردازش هایی را بر روی آن Property انجام داده و یک نتیجهی معتبر را برگرداند. زمانیکه Property را مقداردهی مینمایید، تابع setter فراخوانی میشود و Property را با مقدار جدید تنظیم مینماید. این تابع میتواند قبل از مقداردهی به Property ، دادهی مورد نظر را اعتبارسنجی نماید تا از ورود مقادیر نامعتبر جلوگیری کند. Accessor Properties شامل 2 صفت زیر میباشد:
[[Get]]
یک تابع میباشد و زمانی فراخوانی میگردد که مقدار یک Property را بخوانیم و مقدار پیش فرض آن undefined میباشد.
[[Set]]
یک تابع میباشد و زمانی فراخوانی میگردد که یک Property را مقداردهی نماییم و مقدار پیش فرض آن undefined میباشد. این تابع شامل یک آرگومان ورودی است که حاوی مقدار ارسالی به Property است.
مثال زیر یک پیاده سازی ساده از شیء تاریخ شمسی میباشد که هنوز از لحاظ طراحی دارای نواقصی هست و در ادامه کارآیی و کد آن را بهبود میبخشیم.
var date = { _year: 1, _month: 1, _day: 1, isLeap: function () { switch (this.year % 33) { case 1: case 5: case 9: case 13: case 17: case 22: case 26: case 30: return true; default: return false; } } }; Object.defineProperties(date, { "year": { "get": function () { return this._year; }, "set": function (newValue) { if (newValue < 1 || newValue > 9999) throw new Error("Year must be between 1 and 9999"); this._year = newValue; } }, "month": { "get": function () { return this._month; }, "set": function (newValue) { if (newValue < 1 || newValue > 12) throw new Error("Month must be between 1 and 12"); this._month = newValue; } }, "day": { "get": function () { return this._day; }, "set": function (newValue) { if (newValue < 1 || newValue > 31) throw new Error("Day must be between 1 and 31"); if (this.month === 12 && !this.isLeap() && newValue > 29) throw new Error("Day must be between 1 and 29"); if (this.month > 6 && newValue > 30) throw new Error("Day must be between 1 and 30"); this._day = newValue; } } });
دقت داشته باشید که لازم نیست حتما accessor های getter و setter با هم برای یک Property تنظیم شوند و شما میتوانید فقط یکی از آنها را برای Property به کار ببرید. اگر فقط تابع getter به یک Property اختصاص یابد، آن Property فقط خواندنی میشود و امکان تغییر مقدار آن وجود ندارد. در این صورت هر دستوری که اقدام به تغییر Property نماید، بیتاثیر خواهد بود. همچنین اگر فقط تابع setter به یک Property اختصاص یابد، آن Property فقط نوشتنی میشود و امکان خواندن مقدار آن وجود ندارد. در این صورت هر دستوری که اقدام به خواندن Property نماید، مقدار undefined برای آن برگردانده میشود.
نکتهی دیگری که باید به آن توجه کنید این است که اگر یک Property با استفاده از متد defineProperty() تعریف گردد، Attribute هایی که مقداردهی نشدهاند، مثل [[Configurable]] ، [[Enumarable]] و [[Writable]] با false مقداردهی میگردند و [[Value]] ، [[Get]] و [[Set]] مقدار undefined را بر میگردانند. در مبحث بعدی، در مورد این نکته مثالی ارائه شده است.
خواندن Attribute های مربوط به یک Property
با استفاده از متد getOwnPropertyDescriptor() میتوان، Attribute های اختصاص داده شده به Property ها را خواند و از مقدار آنها مطلع شد. این متد شامل 2 آرگومان میباشد، که آرگومان اول، شیء ای است که میخواهیم Attribute آن را بخوانیم و آرگومان دوم، نام Attribute میباشد. خروجی متد getOwnPropertyDescriptor() یک شیء از نوع PropertyDescriptor میباشد که ویژگیهای آن، همان Attribute هایی هستند که برای یک Property تنظیم شدهاند. به مثال زیر جهت خواندن Attribute های شیء تاریخ شمسی توجه کنید:
var descriptor = Object.getOwnPropertyDescriptor(date, "_year"); alert(descriptor.value); // 1 alert(descriptor.configurable); // true alert(typeof descriptor.get); // undefined descriptor = Object.getOwnPropertyDescriptor(date, "year"); alert(descriptor.value); // undefined alert(descriptor.configurable); // false alert(typeof descriptor.get); // function
var userId= User.Identity.GetUserId(); var user = _context.Users.Find(userId); var user = int.Parse(User.Identity.GetUserId());
public class CommonASPNETRegistry : StructureMap.Configuration.DSL.Registry { public CommonASPNETRegistry() { For<IIdentity>().Use(() => HttpContext.Current.User.Identity); // Other dependencies } }
public static class SmObjectFactory { private static readonly Lazy<Container> _containerBuilder = new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication); public static IContainer Container { get { return _containerBuilder.Value; } } private static Container defaultContainer() { return new Container(ioc => { // Other settings ioc.AddRegistry(new CommonASPNETRegistry()); }); } }
var user = int.Parse(User.Identity.GetUserId());
public interface ICurrentUser { ApplicationUser User { get; } }
public class CurrentUser : ICurrentUser { private readonly IIdentity _identity; private readonly IApplicationUserManager _userManager; private ApplicationUser _user; public CurrentUser(IIdentity identity, IApplicationUserManager userManager) { _identity = identity; _userManager = userManager; } public ApplicationUser User { get { return _user ?? (_user = _userManager.FindById(int.Parse(_identity.GetUserId()))); } } }
public class HomeController : BaseController { private readonly ICurrentUser _currentUser; public HomeController(ICurrentUser user) { _user = user; } public ActionResult Index() { // user var user = _currentUser.User; // user id var userId = _currentUser.User.Id; } }