اشتراکها
اشتراکها
کنفرانس Lang.NEXT 2014
نظرات مطالب
نمونه سوالات مصاحبه استخدامی
کتاب اصول برنامه نویسی (Foundation of Programming) که به فارسی هم ترجمه شده هم منبع خوبی برای شروع میتونه باشه.
نظرات مطالب
اشتباهات متداول برنامهنویسهای دات نت
با تشکر.
البته در تایتل لینک هم قرار گرفته که فید نهایی را ناخوانا کرده. لینک و تایتل باید مجزا تعریف شوند.
http://feeds2.feedburner.com/PersianBloggers/Programming
البته در تایتل لینک هم قرار گرفته که فید نهایی را ناخوانا کرده. لینک و تایتل باید مجزا تعریف شوند.
http://feeds2.feedburner.com/PersianBloggers/Programming
نظرات مطالب
سخنان بزرگان!
مآخذی که استفاده شد:
http://www.devtopics.com/101-more-great-computer-quotes/
http://stackoverflow.com/questions/58640/great-programming-quotes
http://www.devtopics.com/101-more-great-computer-quotes/
http://stackoverflow.com/questions/58640/great-programming-quotes
اشتراکها
مسیرراه فراگیری Rust در سال 2024
دراین قسمت قصد داریم عملگر nested loop حاصل از نوشتن جوینها را دقیقتر بررسی کنیم. یک حلقهی تو در تو، از هر ردیف ورودی (دیتاست خارجی) برای یافتن ردیفهایی (دیتاست درونی) که نوع جوین را برآورده میکنند، استفاده میکند.
بررسی مفهوم دیتاست خارجی و درونی
ابتدا در management studio از منوی Query، گزینهی Include actual execution plan را انتخاب میکنیم. سپس کوئریهای زیر را اجرا میکنیم:
این کوئری یک جوین بین جداول OrderLines و Orders را تشکیل دادهاست؛ به همراه کوئری پلن زیر:
در اینجا دیتاست خارجی، همان index seek بالایی است که بر روی جدول Orders انجام شدهاست. اولین ردیف بازگشت داده شدهی توسط آن به همراه OrderID مربوطه را به حلقهی تو در توی Inner Join ارسال میکند. سپس index seek دوم بر روی جدول OrderLines، بر اساس OrderID دیتاست خارجی، ردیف مرتبطی را در صورت وجود یافته و به حلقهی تو در توی Inner Join بازگشت میدهد که در نهایت به select ارسال میشود و این عملیات به همین ترتیب ادامه پیدا میکند. این خلاصهی کاری است که یک حلقهی تو در تو انجام میدهد.
سؤال: اگر جای این دیتاستها را عوض کنیم چه اتفاقی رخ خواهد داد؟
در کوئری زیر توسط گزینهی FORCE ORDER سبب شدهایم تا جای دیتاستهای OUTER/INNER تغییر کند (البته این query hint، کاربرد عملی ندارد و صرفا جهت نمایش دیتاستها از آن استفاده کردهایم):
اینبار در کوئری پلن تولید شده، index seek بالایی بر روی جدول OrderLines، دیتاست خارجی را تشکیل میدهد و index seek دوم بر روی جدول Orders، دیتاست درونی را:
یک نکته: در این تصاویر بجای nested loop، از عملگر Hash Match استفاده شدهاست. اگر بخواهیم بهینه سازی کوئری را وادار کنیم تا از nested loop استفاده کند، میتوان کوئری فوق را توسط یک INNER LOOP JOIN به صورت زیر نوشت:
که یک چنین کوئری پلنی را تولید میکند:
همانطور که مشاهده میکنید اینبار به علت بالا رفتن تعداد ردیفهایی که باید پردازش کند، به یک پلن بسیار غیر بهینه رسیدهاست که برای بهبود آن مجبور شدهاست Parallelism را نیز فعال کند.
در این حالت اگر هر سه کوئری فوق را با هم اجرا کنیم، تا بتوانیم هزینهی آنها را در کوئری پلن نهایی تولید شده، با یکدیگر مقایسه کنیم، هزینهی کوئری اول صفر درصد، کوئری دوم 1 درصد و کوئری سوم 99 درصد نسبت به کل batch محاسبه میشود. علت آن را نیز در برگهی messages، با مشاهدهی logical reads 477304 مربوط به کوئری سوم میتوان مشاهده کرد که نسبت به سایر کوئریها بسیار بیشتر است. بنابراین بهتر است در کار بهینه ساز کوئریها به صورت دستی دخالت نکنیم!
بهبود کارآیی یک کوئری، با حذف حلقهی تو در توی کوئری پلن آن در حالت Key lookup
کوئری زیر را با فرض انتخاب گزینهی Include actual execution plan در منوی کوئری، اجرا میکنیم:
این کوئری هرچند به همراه یک جوین نیست، اما دارای کوئری پلنی دارای یک nested loop است:
ایندکسهایی که در این کوئری پلن استفاده شدهاند، شامل موارد پیشفرض زیر هستند؛ یکی بر روی OrderID که کلید اصلی جدول است، تشکیل شده و دیگری بر روی ContactPersonID که در قسمت where کوئری فوق مورد استفاده قرار گرفتهاست:
علت وجود عملگر key lookup بر روی ایندکس PK_Sales_Orders در اینجا این است که ایندکس FK_Sales_Orders_ContactPersonID، ستونهای کوئری نوشته شده را include نکردهاست. به همین جهت مجبور شدهاست آنها را از clustered index تعریف شده دریافت کند.
برای بهبود این وضعیت، NONCLUSTERED INDEX تعریف شده را به صورت زیر تغییر میدهیم تا ستونهای OrderDate و CustomerPurchaseOrderNumber را INCLUDE کند:
اکنون اگر مجددا کوئری قبلی را اجرا کنیم:
به این کوئری پلن دارای index seek بدون nested loop میرسیم:
چون ایندکس جدید تعریف شده کاملا کوئری ما را پوشش میدهد، دیگر نیازی به ایجاد یک nested loop، جهت کار با چندین index متفرقه نیست.
بهبود کارآیی یک کوئری، با حذف حلقهی تو در توی کوئری پلن آن در حالت RID lookup
در اینجا یک جدول کپی را از روی جدول اصلی Orders ایجاد کردهایم؛ به همراه تعریف یک NONCLUSTERED INDEX بر روی ستون ContactPersonID آن:
سپس کوئری زیر را که همانند کوئری مثال قبلی است، بر روی این جدول کپی اجرا میکنیم:
نتیجهی آن تولید کوئری پلن زیر است:
در اینجا یک nested loop را به همراه RID lookup داریم (RID به معنای row id است). همچنین واژهی heap نیز ذکر شدهاست. در این حالت اطلاعات یک چنین جدولی بدون هیچگونه ترتیبی ذخیره شدهاند؛ بنابراین نیاز به شماره ردیف آن (RID) برای برقراری ارتباطات میباشد. Key lookup زمانی رخ میدهند که یک جدول دارای یک clustered index باشد و RID lookup، در حالت عکس آن رخ میدهد. دقیقا مانند جدول کپی ایجاد شده، که دارای یک clustered index نیست.
در صورت مشاهدهی RID lookup نیز میتوانیم ستونهایی از کوئری را که در NONCLUSTERED INDEX ذکر نشدهاند، include کنیم:
و در این حالت اگر همان کوئری قبلی را مجددا اجرا کنیم، به کوئری پلن دارای index seek زیر خواهیم رسید:
بررسی مفهوم دیتاست خارجی و درونی
ابتدا در management studio از منوی Query، گزینهی Include actual execution plan را انتخاب میکنیم. سپس کوئریهای زیر را اجرا میکنیم:
USE [WideWorldImporters]; GO SET STATISTICS IO ON; GO /* What's are the inner and outer data sets? */ SELECT [ol].[OrderLineID], [o].[CustomerID] FROM [Sales].[OrderLines] [ol] INNER JOIN [Sales].[Orders] [o] ON [ol].[OrderID] = [o].[OrderID] WHERE [o].[CustomerID] = 185; GO
در اینجا دیتاست خارجی، همان index seek بالایی است که بر روی جدول Orders انجام شدهاست. اولین ردیف بازگشت داده شدهی توسط آن به همراه OrderID مربوطه را به حلقهی تو در توی Inner Join ارسال میکند. سپس index seek دوم بر روی جدول OrderLines، بر اساس OrderID دیتاست خارجی، ردیف مرتبطی را در صورت وجود یافته و به حلقهی تو در توی Inner Join بازگشت میدهد که در نهایت به select ارسال میشود و این عملیات به همین ترتیب ادامه پیدا میکند. این خلاصهی کاری است که یک حلقهی تو در تو انجام میدهد.
سؤال: اگر جای این دیتاستها را عوض کنیم چه اتفاقی رخ خواهد داد؟
در کوئری زیر توسط گزینهی FORCE ORDER سبب شدهایم تا جای دیتاستهای OUTER/INNER تغییر کند (البته این query hint، کاربرد عملی ندارد و صرفا جهت نمایش دیتاستها از آن استفاده کردهایم):
SELECT [ol].[OrderLineID], [o].[CustomerID] FROM [Sales].[OrderLines] [ol] INNER JOIN [Sales].[Orders] [o] ON [ol].[OrderID] = [o].[OrderID] WHERE [o].[CustomerID] = 185 OPTION (FORCE ORDER);
یک نکته: در این تصاویر بجای nested loop، از عملگر Hash Match استفاده شدهاست. اگر بخواهیم بهینه سازی کوئری را وادار کنیم تا از nested loop استفاده کند، میتوان کوئری فوق را توسط یک INNER LOOP JOIN به صورت زیر نوشت:
SELECT [ol].[OrderLineID], [o].[CustomerID] FROM [Sales].[OrderLines] [ol] INNER LOOP JOIN [Sales].[Orders] [o] ON [ol].[OrderID] = [o].[OrderID] WHERE [o].[CustomerID] = 185 OPTION (FORCE ORDER); GO
همانطور که مشاهده میکنید اینبار به علت بالا رفتن تعداد ردیفهایی که باید پردازش کند، به یک پلن بسیار غیر بهینه رسیدهاست که برای بهبود آن مجبور شدهاست Parallelism را نیز فعال کند.
در این حالت اگر هر سه کوئری فوق را با هم اجرا کنیم، تا بتوانیم هزینهی آنها را در کوئری پلن نهایی تولید شده، با یکدیگر مقایسه کنیم، هزینهی کوئری اول صفر درصد، کوئری دوم 1 درصد و کوئری سوم 99 درصد نسبت به کل batch محاسبه میشود. علت آن را نیز در برگهی messages، با مشاهدهی logical reads 477304 مربوط به کوئری سوم میتوان مشاهده کرد که نسبت به سایر کوئریها بسیار بیشتر است. بنابراین بهتر است در کار بهینه ساز کوئریها به صورت دستی دخالت نکنیم!
بهبود کارآیی یک کوئری، با حذف حلقهی تو در توی کوئری پلن آن در حالت Key lookup
کوئری زیر را با فرض انتخاب گزینهی Include actual execution plan در منوی کوئری، اجرا میکنیم:
SELECT [ContactPersonID], [OrderDate], [CustomerPurchaseOrderNumber] FROM [Sales].[Orders] WHERE [ContactPersonID] = 3144;
ایندکسهایی که در این کوئری پلن استفاده شدهاند، شامل موارد پیشفرض زیر هستند؛ یکی بر روی OrderID که کلید اصلی جدول است، تشکیل شده و دیگری بر روی ContactPersonID که در قسمت where کوئری فوق مورد استفاده قرار گرفتهاست:
ALTER TABLE [Sales].[Orders] ADD CONSTRAINT [PK_Sales_Orders] PRIMARY KEY CLUSTERED ( [OrderID] ASC ) GO CREATE NONCLUSTERED INDEX [FK_Sales_Orders_ContactPersonID] ON [Sales].[Orders] ( [ContactPersonID] ASC )
برای بهبود این وضعیت، NONCLUSTERED INDEX تعریف شده را به صورت زیر تغییر میدهیم تا ستونهای OrderDate و CustomerPurchaseOrderNumber را INCLUDE کند:
CREATE NONCLUSTERED INDEX [FK_Sales_Orders_ContactPersonID] ON [Sales].[Orders] ( [ContactPersonID] ASC ) INCLUDE ( [OrderDate], [CustomerPurchaseOrderNumber] ) WITH (DROP_EXISTING = ON) ON [USERDATA]; GO
SELECT [ContactPersonID], [OrderDate], [CustomerPurchaseOrderNumber] FROM [Sales].[Orders] WHERE [ContactPersonID] = 3144;
چون ایندکس جدید تعریف شده کاملا کوئری ما را پوشش میدهد، دیگر نیازی به ایجاد یک nested loop، جهت کار با چندین index متفرقه نیست.
بهبود کارآیی یک کوئری، با حذف حلقهی تو در توی کوئری پلن آن در حالت RID lookup
در اینجا یک جدول کپی را از روی جدول اصلی Orders ایجاد کردهایم؛ به همراه تعریف یک NONCLUSTERED INDEX بر روی ستون ContactPersonID آن:
USE [WideWorldImporters] GO DROP TABLE [Sales].[Copy_Orders] GO SELECT * INTO [Sales].[Copy_Orders] FROM [Sales].[Orders]; GO CREATE NONCLUSTERED INDEX [NCI_Copy_Orders_ContactPersonID] ON [Sales].[Copy_Orders] ( [ContactPersonID] ); GO
SELECT [ContactPersonID], [OrderDate], [CustomerPurchaseOrderNumber] FROM [Sales].[Copy_Orders] WHERE [ContactPersonID] = 3144;
در اینجا یک nested loop را به همراه RID lookup داریم (RID به معنای row id است). همچنین واژهی heap نیز ذکر شدهاست. در این حالت اطلاعات یک چنین جدولی بدون هیچگونه ترتیبی ذخیره شدهاند؛ بنابراین نیاز به شماره ردیف آن (RID) برای برقراری ارتباطات میباشد. Key lookup زمانی رخ میدهند که یک جدول دارای یک clustered index باشد و RID lookup، در حالت عکس آن رخ میدهد. دقیقا مانند جدول کپی ایجاد شده، که دارای یک clustered index نیست.
در صورت مشاهدهی RID lookup نیز میتوانیم ستونهایی از کوئری را که در NONCLUSTERED INDEX ذکر نشدهاند، include کنیم:
CREATE NONCLUSTERED INDEX [NCI_Copy_Orders_ContactPersonID] ON [Sales].[Copy_Orders] ( [ContactPersonID] ASC ) INCLUDE ( [OrderDate], [CustomerPurchaseOrderNumber] ) WITH (DROP_EXISTING = ON) ON [USERDATA]; GO
همیشه نمیتوان کاربران را وادار به استفادهی از صفحهی لاگین برنامهی IDP کرد. ممکن است کاربران بخواهند توسط سطوح دسترسی خود در یک شبکهی ویندوزی به سیستم وارد شوند و یا از Social identity providers مانند تلگرام، گوگل، فیسبوک، توئیتر و امثال آنها برای ورود به سیستم استفاده کنند. برای مثال شاید کاربری بخواهد توسط اکانت گوگل خود به سیستم وارد شود. همچنین مباحث two-factor authentication را نیز باید مدنظر داشت؛ برای مثال ارسال یک کد موقت از طریق ایمیل و یا SMS و ترکیب آن با روش فعلی ورود به سیستم جهت بالا بردن میزان امنیت برنامه.
در این مطلب نحوهی یکپارچه سازی Windows Authentication دومینهای ویندوزی را با IdentityServer بررسی میکنیم.
کار با تامین کنندههای هویت خارجی
اغلب کاربران، دارای اکانت ثبت شدهای در جای دیگری نیز هستند و شاید آنچنان نسبت به ایجاد اکانت جدیدی در IDP ما رضایت نداشته باشند. برای چنین حالتی، امکان یکپارچه سازی IdentityServer با انواع و اقسام IDPهای دیگر نیز پیش بینی شدهاست. در اینجا تمام اینها، روشهای مختلفی برای ورود به سیستم، توسط یک کاربر هستند. کاربر ممکن است توسط اکانت خود در شبکهی ویندوزی به سیستم وارد شود و یا توسط اکانت خود در گوگل، اما در نهایت از دیدگاه سیستم ما، یک کاربر مشخص بیشتر نیست.
نگاهی به شیوهی پشتیبانی از تامین کنندههای هویت خارجی توسط Quick Start UI
Quick Start UI ای را که در «قسمت چهارم - نصب و راه اندازی IdentityServer» به IDP اضافه کردیم، دارای کدهای کار با تامین کنندههای هویت خارجی نیز میباشد. برای بررسی آن، کنترلر DNT.IDP\Controllers\Account\ExternalController.cs را باز کنید:
زمانیکه کاربر بر روی یکی از تامین کنندههای لاگین خارجی در صفحهی لاگین کلیک میکند، اکشن Challenge، نام provider مدنظر را دریافت کرده و پس از آن returnUrl را به اکشن متد Callback به صورت query string ارسال میکند. اینجا است که کاربر به تامین کنندهی هویت خارجی مانند گوگل منتقل میشود. البته مدیریت حالت Windows Authentication و استفاده از اکانت ویندوزی در اینجا متفاوت است؛ از این جهت که از returnUrl پشتیبانی نمیکند. در اینجا اطلاعات کاربر از اکانت ویندوزی او به صورت خودکار استخراج شده و به لیست Claims او اضافه میشود. سپس یک کوکی رمزنگاری شده از این اطلاعات تولید میشود تا در ادامه از محتویات آن استفاده شود.
در اکشن متد Callback، اطلاعات کاربر از کوکی رمزنگاری شدهی متد Challenge استخراج میشود و بر اساس آن هویت کاربر در سطح IDP شکل میگیرد.
فعالسازی Windows Authentication برای ورود به IDP
در ادامه میخواهیم برنامه را جهت استفادهی از اکانت ویندوزی کاربران جهت ورود به IDP تنظیم کنیم. برای این منظور باید نکات مطلب «فعالسازی Windows Authentication در برنامههای ASP.NET Core 2.0» را پیشتر مطالعه کرده باشید.
پس از فعالسازی Windows Authentication در برنامه، اگر برنامهی IDP را توسط IIS و یا IIS Express و یا HttpSys اجرا کنید، دکمهی جدید Windows را در قسمت External Login مشاهده خواهید کرد:
یک نکته: برچسب این دکمه را در حالت استفادهی از مشتقات IIS، به صورت زیر میتوان تغییر داد:
اتصال کاربر وارد شدهی از یک تامین کنندهی هویت خارجی به کاربران بانک اطلاعاتی برنامه
سازندهی کنترلر DNT.IDP\Controllers\Account\ExternalController.cs نیز همانند کنترلر Account که آنرا در قسمت قبل تغییر دادیم، از TestUserStore استفاده میکند:
بنابراین در ابتدا آنرا با IUsersService تعویض خواهیم کرد:
و سپس تمام ارجاعات قبلی به users_ را نیز توسط امکانات این سرویس اصلاح میکنیم:
الف) در متد FindUserFromExternalProvider
سطر قدیمی
به این صورت تغییر میکند:
در این حالت امضای این متد نیز باید اصلاح شود تا async شده و همچنین User را بجای TestUser بازگشت دهد:
ب) متد AutoProvisionUser قبلی
نیز باید حذف شود؛ زیرا در ادامه آنرا با صفحهی ثبت نام کاربر، جایگزین میکنیم.
مفهوم «Provisioning a user» در اینجا به معنای درخواست از کاربر، جهت ورود اطلاعاتی مانند نام و نام خانوادگی او است که پیشتر صفحهی ثبت کاربر جدید را برای این منظور در قسمت قبل ایجاد کردهایم و از آن میشود در اینجا استفادهی مجدد کرد. بنابراین در ادامه، گردش کاری ورود کاربر از طریق تامین کنندهی هویت خارجی را به نحوی اصلاح میکنیم که کاربر جدید، ابتدا به صفحهی ثبت نام وارد شود و اطلاعات تکمیلی خود را وارد کند؛ سپس به صورت خودکار به متد Callback بازگشته و ادامهی مراحل را طی نماید:
در اکشن متد نمایش صفحهی ثبت نام کاربر جدید، متد RegisterUser تنها آدرس بازگشت به صفحهی قبلی را دریافت میکند:
اکنون نیاز است اطلاعات Provider و ProviderUserId را نیز در اینجا دریافت کرد. به همین جهت ViewModel زیر را به برنامه اضافه میکنیم:
سپس با داشتن اطلاعات FindUserFromExternalProvider که آنرا در قسمت الف اصلاح کردیم، اگر خروجی آن null باشد، یعنی کاربری که از سمت تامین کنندهی هویت خارجی به برنامهی ما وارد شدهاست، دارای اکانتی در سمت IDP نیست. به همین جهت او را به صفحهی ثبت نام کاربر هدایت میکنیم. همچنین پس از پایان کار ثبت نام نیاز است مجددا به همینجا، یعنی متد Callback که فراخوان FindUserFromExternalProvider است، بازگشت:
در اینجا نحوهی اصلاح اکشن متد Callback را جهت هدایت یک کاربر جدید به صفحهی ثبت نام و تکمیل اطلاعات مورد نیاز IDP را مشاهده میکنید.
returnUrl ارسالی به اکشن متد RegisterUser، به همین اکشن متد جاری اشاره میکند. یعنی کاربر پس از تکمیل اطلاعات و اینبار نال نبودن user او، گردش کاری جاری را ادامه خواهد داد.
در ادامه نیاز است امضای متد نمایش صفحهی ثبت نام را نیز بر این اساس اصلاح کنیم:
به این ترتیب اطلاعات provider نیز علاوه بر ReturnUrl در اختیار View آن قرار خواهد گرفت. البته RegisterUserViewModel هنوز شامل این خواص اضافی نیست. به همین جهت با ارث بری از RegistrationInputModel، این خواص در اختیار RegisterUserViewModel نیز قرار میگیرند:
اکنون نیاز است RegisterUser.cshtml را اصلاح کنیم:
- ابتدا دو فیلد مخفی دیگر Provider و ProviderUserId را نیز به این فرم اضافه میکنیم؛ از این جهت که در حین postback به سمت سرور به مقادیر آنها نیاز داریم:
- با توجه به اینکه کاربر از طریق یک تامین کنندهی هویت خارجی وارد شدهاست، دیگر نیازی به ورود کلمهی عبور ندارد. به همین جهت خاصیت آنرا در ViewModel مربوطه به صورت Required تعریف نکردهایم:
مابقی این فرم ثبت نام مانند قبل خواهد بود.
پس از آن نیاز است اطلاعات اکانت خارجی این کاربر را در حین postback و ارسال اطلاعات به اکشن متد RegisterUser، ثبت کنیم:
که اینکار را با مقدار دهی UserLogins کاربر در حال ثبت، انجام دادهایم.
همچنین در ادامهی این اکشن متد، کار لاگین خودکار کاربر نیز انجام میشود. با توجه به اینکه پس از ثبت اطلاعات کاربر نیاز است مجددا گردش کاری اکشن متد Callback طی شود، این لاگین خودکار را نیز برای حالت ورود از طریق تامین کنندهی خارجی، غیرفعال میکنیم:
بررسی ورود به سیستم توسط دکمهی External Login -> Windows
پس از این تغییرات، اکنون در حین ورود به سیستم (تصویر ابتدای بحث در قسمت فعالسازی اعتبارسنجی ویندوزی)، گزینهی External Login -> Windows را انتخاب میکنیم. بلافاصله به صفحهی ثبتنام کاربر هدایت خواهیم شد:
همانطور که مشاهده میکنید، IDP اکانت ویندوزی جاری را تشخیص داده و فعال کردهاست. همچنین در اینجا خبری از ورود کلمهی عبور هم نیست.
پس از تکمیل این فرم، بلافاصله کار ثبت اطلاعات کاربر و هدایت خودکار به برنامهی MVC Client انجام میشود.
در ادامه از برنامهی کلاینت logout کنید. اکنون در صفحهی login مجددا بر روی دکمهی Windows کلیک نمائید. اینبار بدون پرسیدن سؤالی، لاگین شده و وارد برنامهی کلاینت خواهید شد؛ چون پیشتر کار اتصال اکانت ویندوزی به اکانتی در سمت IDP انجام شدهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.
یک نکته: برای آزمایش برنامه جهت فعالسازی Windows Authentication بهتر است برنامهی IDP را توسط IIS Express اجرا کنید و یا اگر از IIS Express استفاده نمیکنید، نیاز است UseHttpSys فایل program.cs را مطابق توضیحات «یک نکتهی تکمیلی: UseHttpSys و استفادهی از HTTPS» فعال کنید.
در این مطلب نحوهی یکپارچه سازی Windows Authentication دومینهای ویندوزی را با IdentityServer بررسی میکنیم.
کار با تامین کنندههای هویت خارجی
اغلب کاربران، دارای اکانت ثبت شدهای در جای دیگری نیز هستند و شاید آنچنان نسبت به ایجاد اکانت جدیدی در IDP ما رضایت نداشته باشند. برای چنین حالتی، امکان یکپارچه سازی IdentityServer با انواع و اقسام IDPهای دیگر نیز پیش بینی شدهاست. در اینجا تمام اینها، روشهای مختلفی برای ورود به سیستم، توسط یک کاربر هستند. کاربر ممکن است توسط اکانت خود در شبکهی ویندوزی به سیستم وارد شود و یا توسط اکانت خود در گوگل، اما در نهایت از دیدگاه سیستم ما، یک کاربر مشخص بیشتر نیست.
نگاهی به شیوهی پشتیبانی از تامین کنندههای هویت خارجی توسط Quick Start UI
Quick Start UI ای را که در «قسمت چهارم - نصب و راه اندازی IdentityServer» به IDP اضافه کردیم، دارای کدهای کار با تامین کنندههای هویت خارجی نیز میباشد. برای بررسی آن، کنترلر DNT.IDP\Controllers\Account\ExternalController.cs را باز کنید:
[HttpGet] public async Task<IActionResult> Challenge(string provider, string returnUrl) [HttpGet] public async Task<IActionResult> Callback()
در اکشن متد Callback، اطلاعات کاربر از کوکی رمزنگاری شدهی متد Challenge استخراج میشود و بر اساس آن هویت کاربر در سطح IDP شکل میگیرد.
فعالسازی Windows Authentication برای ورود به IDP
در ادامه میخواهیم برنامه را جهت استفادهی از اکانت ویندوزی کاربران جهت ورود به IDP تنظیم کنیم. برای این منظور باید نکات مطلب «فعالسازی Windows Authentication در برنامههای ASP.NET Core 2.0» را پیشتر مطالعه کرده باشید.
پس از فعالسازی Windows Authentication در برنامه، اگر برنامهی IDP را توسط IIS و یا IIS Express و یا HttpSys اجرا کنید، دکمهی جدید Windows را در قسمت External Login مشاهده خواهید کرد:
یک نکته: برچسب این دکمه را در حالت استفادهی از مشتقات IIS، به صورت زیر میتوان تغییر داد:
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.Configure<IISOptions>(iis => { iis.AuthenticationDisplayName = "Windows Account"; iis.AutomaticAuthentication = false; });
اتصال کاربر وارد شدهی از یک تامین کنندهی هویت خارجی به کاربران بانک اطلاعاتی برنامه
سازندهی کنترلر DNT.IDP\Controllers\Account\ExternalController.cs نیز همانند کنترلر Account که آنرا در قسمت قبل تغییر دادیم، از TestUserStore استفاده میکند:
public ExternalController( IIdentityServerInteractionService interaction, IClientStore clientStore, IEventService events, TestUserStore users = null) { _users = users ?? new TestUserStore(TestUsers.Users); _interaction = interaction; _clientStore = clientStore; _events = events; }
private readonly IUsersService _usersService; public ExternalController( // ... IUsersService usersService) { // ... _usersService = usersService; }
الف) در متد FindUserFromExternalProvider
سطر قدیمی
var user = _users.FindByExternalProvider(provider, providerUserId);
var user = await _usersService.GetUserByProviderAsync(provider, providerUserId);
private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims)> FindUserFromExternalProvider(AuthenticateResult result)
private TestUser AutoProvisionUser(string provider, string providerUserId, IEnumerable<Claim> claims) { var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList()); return user; }
مفهوم «Provisioning a user» در اینجا به معنای درخواست از کاربر، جهت ورود اطلاعاتی مانند نام و نام خانوادگی او است که پیشتر صفحهی ثبت کاربر جدید را برای این منظور در قسمت قبل ایجاد کردهایم و از آن میشود در اینجا استفادهی مجدد کرد. بنابراین در ادامه، گردش کاری ورود کاربر از طریق تامین کنندهی هویت خارجی را به نحوی اصلاح میکنیم که کاربر جدید، ابتدا به صفحهی ثبت نام وارد شود و اطلاعات تکمیلی خود را وارد کند؛ سپس به صورت خودکار به متد Callback بازگشته و ادامهی مراحل را طی نماید:
در اکشن متد نمایش صفحهی ثبت نام کاربر جدید، متد RegisterUser تنها آدرس بازگشت به صفحهی قبلی را دریافت میکند:
[HttpGet] public IActionResult RegisterUser(string returnUrl)
namespace DNT.IDP.Controllers.UserRegistration { public class RegistrationInputModel { public string ReturnUrl { get; set; } public string Provider { get; set; } public string ProviderUserId { get; set; } public bool IsProvisioningFromExternal => !string.IsNullOrWhiteSpace(Provider); } }
namespace DNT.IDP.Controllers.Account { [SecurityHeaders] [AllowAnonymous] public class ExternalController : Controller { public async Task<IActionResult> Callback() { var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme); var returnUrl = result.Properties.Items["returnUrl"] ?? "~/"; var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result); if (user == null) { // user = AutoProvisionUser(provider, providerUserId, claims); var returnUrlAfterRegistration = Url.Action("Callback", new { returnUrl = returnUrl }); var continueWithUrl = Url.Action("RegisterUser", "UserRegistration" , new { returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId }); return Redirect(continueWithUrl); }
returnUrl ارسالی به اکشن متد RegisterUser، به همین اکشن متد جاری اشاره میکند. یعنی کاربر پس از تکمیل اطلاعات و اینبار نال نبودن user او، گردش کاری جاری را ادامه خواهد داد.
در ادامه نیاز است امضای متد نمایش صفحهی ثبت نام را نیز بر این اساس اصلاح کنیم:
namespace DNT.IDP.Controllers.UserRegistration { public class UserRegistrationController : Controller { [HttpGet] public IActionResult RegisterUser(RegistrationInputModel registrationInputModel) { var vm = new RegisterUserViewModel { ReturnUrl = registrationInputModel.ReturnUrl, Provider = registrationInputModel.Provider, ProviderUserId = registrationInputModel.ProviderUserId }; return View(vm); }
namespace DNT.IDP.Controllers.UserRegistration { public class RegisterUserViewModel : RegistrationInputModel {
اکنون نیاز است RegisterUser.cshtml را اصلاح کنیم:
- ابتدا دو فیلد مخفی دیگر Provider و ProviderUserId را نیز به این فرم اضافه میکنیم؛ از این جهت که در حین postback به سمت سرور به مقادیر آنها نیاز داریم:
<inputtype="hidden"asp-for="ReturnUrl"/> <inputtype="hidden"asp-for="Provider"/> <inputtype="hidden"asp-for="ProviderUserId"/>
@if (!Model.IsProvisioningFromExternal) { <div> <label asp-for="Password"></label> <input type="password" placeholder="Password" asp-for="Password" autocomplete="off"> </div> }
پس از آن نیاز است اطلاعات اکانت خارجی این کاربر را در حین postback و ارسال اطلاعات به اکشن متد RegisterUser، ثبت کنیم:
namespace DNT.IDP.Controllers.UserRegistration { public class UserRegistrationController : Controller { [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> RegisterUser(RegisterUserViewModel model) { // ... if (model.IsProvisioningFromExternal) { userToCreate.UserLogins.Add(new UserLogin { LoginProvider = model.Provider, ProviderKey = model.ProviderUserId }); } // add it through the repository await _usersService.AddUserAsync(userToCreate); // ... } }
همچنین در ادامهی این اکشن متد، کار لاگین خودکار کاربر نیز انجام میشود. با توجه به اینکه پس از ثبت اطلاعات کاربر نیاز است مجددا گردش کاری اکشن متد Callback طی شود، این لاگین خودکار را نیز برای حالت ورود از طریق تامین کنندهی خارجی، غیرفعال میکنیم:
if (!model.IsProvisioningFromExternal) { // log the user in // issue authentication cookie with subject ID and username var props = new AuthenticationProperties { IsPersistent = false, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; await HttpContext.SignInAsync(userToCreate.SubjectId, userToCreate.Username, props); }
بررسی ورود به سیستم توسط دکمهی External Login -> Windows
پس از این تغییرات، اکنون در حین ورود به سیستم (تصویر ابتدای بحث در قسمت فعالسازی اعتبارسنجی ویندوزی)، گزینهی External Login -> Windows را انتخاب میکنیم. بلافاصله به صفحهی ثبتنام کاربر هدایت خواهیم شد:
همانطور که مشاهده میکنید، IDP اکانت ویندوزی جاری را تشخیص داده و فعال کردهاست. همچنین در اینجا خبری از ورود کلمهی عبور هم نیست.
پس از تکمیل این فرم، بلافاصله کار ثبت اطلاعات کاربر و هدایت خودکار به برنامهی MVC Client انجام میشود.
در ادامه از برنامهی کلاینت logout کنید. اکنون در صفحهی login مجددا بر روی دکمهی Windows کلیک نمائید. اینبار بدون پرسیدن سؤالی، لاگین شده و وارد برنامهی کلاینت خواهید شد؛ چون پیشتر کار اتصال اکانت ویندوزی به اکانتی در سمت IDP انجام شدهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.
یک نکته: برای آزمایش برنامه جهت فعالسازی Windows Authentication بهتر است برنامهی IDP را توسط IIS Express اجرا کنید و یا اگر از IIS Express استفاده نمیکنید، نیاز است UseHttpSys فایل program.cs را مطابق توضیحات «یک نکتهی تکمیلی: UseHttpSys و استفادهی از HTTPS» فعال کنید.
فرض کنید میخواهید برای یک پروژه، امکانی را درنظر بگیرید که بتوان برای تمامی رکوردهای موجودیتهای (Entity) آن پروژه، زمان ساخته شدن و به روزرسانی، به صورت خودکار ثبت شود.
تابع AddCreatedUpdatedDate نیز به شکل زیر تعریف خواهد شد:
همانطور که ملاحظه مینمایید از ChangeTracker استفاده شدهاست که پیشتر مطلب کاملی در سایت در رابطه با آن منتشر شدهاست. در حقیقت لیستی از رکوردهای موجودیتهایی را که از BaseEntity ارث بری کرده باشند و در حال اضافه شدن یا ویرایش شدن هستند، در entries قرار میدهیم و سپس بررسی میکنیم که اگر این رکورد در حال اضافه شدن برای اولین بار است، آنگاه مقدار برابری را برای CreatedDate و UpdatedDate آن درنظر میگیریم؛ اما اگر این رکورد در حال ویرایش شدن باشد، آنگاه فقط مقدار UpdatedDate را بهروزرسانی میکنیم.
حال برای اینکه موجودیتی دارای این قابلیت شود که برای هر رکورد آن، تاریخ ساخت و به روز رسانی به صورت خودکار ثبت شود، باید از کلاس پایه BaseEntity ارث بری نماید. برای مثال:
کار با تعریف یک کلاس پایه به شکل زیر شروع میشود:
public class BaseEntity { public DateTimeOffset CreatedDate { get; set; } public DateTimeOffset UpdatedDate { get; set; } }
سپس برای اینکه کار مقداردهی، به صورت خودکار انجام گیرد، باید متدهای SaveChanges و SaveChangesAsync به شکل زیر در ApplicationDbContext پروژه override شوند:
//override because we need add created and updated date to some entities public override async Task<int> SaveChangesAsync( CancellationToken cancellationToken = default(CancellationToken)) { AddCreatedUpdatedDate(); return (await base.SaveChangesAsync(true, cancellationToken)); } //override because we need add created and updated date to some entities public override int SaveChanges() { AddCreatedUpdatedDate(); return base.SaveChanges(); }
/// <summary> /// Add created and updated date to any entities that /// inherit from BaseEntity class /// </summary> public void AddCreatedUpdatedDate() { var entries = ChangeTracker .Entries() .Where(e => e.Entity is BaseEntity && ( e.State == EntityState.Added || e.State == EntityState.Modified)); foreach (var entityEntry in entries) { ((BaseEntity)entityEntry.Entity).UpdatedDate = DateTimeOffset.UtcNow; if (entityEntry.State == EntityState.Added) { ((BaseEntity)entityEntry.Entity).CreatedDate = DateTimeOffset.UtcNow; } } }
public class Student: BaseEntity { public int StudentID { get; set; } public string StudentName { get; set; } public DateTimeOffset? DateOfBirth { get; set; } public decimal Height { get; set; } public float Weight { get; set; } }