اشتراکها
بررسی مفهوم Authorization
رفتار Blazorهای پیش از داتنت 8 در مورد مدیریت حالت
پیش از دات نت 8، دو حالت عمده برای توسعهی برنامههای Blazor وجود داشت: Blazor Server و Blazor WASM. در هر دو حالت، طول عمر سیستم تزریق وابستگیهای ایجاد و مدیریت شدهی توسط Blazor، معادل طول عمر برنامهاست.
در برنامههای Blazor Server، طول عمر سیستم تزریق وابستگیها، توسط ASP.NET Core قرار گرفتهی بر روی سرور مدیریت شده و نمونههای ایجاد شدهی سرویسهای توسط آن، به ازای هر کاربر متفاوت است. بنابراین اگر طول عمر سرویسی در اینجا به صورت Scoped تعریف شود، این سرویس فقط یکبار در طول عمر برنامه، به ازای یک کاربر جاری برنامه، تولید و نمونه سازی میشود. در این مدل برنامهها، سرویسهایی با طول عمر Singleton، بین تمام کاربران به اشتراک گذاشته میشوند. به همین جهت است که در این نوع برنامهها، مدیریت سرویس Context مخصوص EF-Core نکات خاصی را به همراه دارد. چون اگر بر اساس سیستم پیشفرض تزریق وابستگیها و طول عمر Scoped این سرویس عمل شود، یک Context فقط یکبار بهازای یک کاربر، یکبار نمونه سازی شده و تا پایان طول عمر برنامه، بدون تغییر زنده نگه داشته میشود؛ در حالیکه عموم توسعه دهندگان EF-Core تصور میکنند سرویسهای Scoped، پس از پایان یک درخواست، پایان یافته و Dispose میشوند، اما در اینجا پایان درخواستی نداریم. یک اتصال دائم SignalR را داریم و تا زمانیکه برقرار است، یعنی برنامه زندهاست. بنابراین در برنامههای Blazor Server، سرویسهای Scoped، به ازای هر کاربر، همانند Singleton رفتار میکنند (در سراسر برنامه به ازای یک کاربر در دسترس هستند) و سرویسهایی از اساس Singleton، بین تمام کاربران به اشتراک گذاشته میشوند.
در برنامههای Blazor WASM، طول عمر سیستم تزریق وابستگیها، توسط برنامهی وباسمبلی در حال اجرای بر روی مرورگر مدیریت میشود. یعنی مختص به یک کاربر بوده و طول عمر آن وابستهاست به طول عمر برگهی جاری مرورگر. بنابراین دراینجا بین سرویسهای Scoped و Singleton، تفاوتی وجود ندارد و همانند هم رفتار میکنند (هر دو مختص به یک کاربر و وابسته به طول عمر برگهی جاری هستند).
در هیچکدام از این حالتها، امکان دسترسی به HttpContext وجود ندارد (نه داخل اتصال دائم SignalR برنامههای Blazor Server و نه داخل برنامهی وباسمبلی در حال اجرای در مرورگر). اطلاعات بیشتر
بنابراین در این برنامهها برای نگهداری اطلاعات کاربر لاگین شدهی به سیستم و یا سایر اطلاعات سراسری برنامه، عموما از سرویسهایی با طول عمر Scoped استفاده میشود که در تمام قسمتهای برنامه به ازای هر کاربر، قابل دسترسی هستند.
رفتار Blazor 8x در مورد مدیریت حالت
هرچند دات نت 8 به همراه حالتهای رندر جدیدی است، اما هنوز هم میتوان برنامههایی کاملا توسعه یافته بر اساس مدلهای قبلی Blazor Server و یا Blazor WASM را همانند داتنتهای پیش از 8 داشت. بنابراین اگر تصمیم گرفتید که بجای استفاده از جزیرههای تعاملی، کل برنامه را به صورت سراسری تعاملی کنید، همان نکات قبلی، در اینجا هم صادق هستند و از لحاظ مدیریت حالت، تفاوتی نمیکنند.
اما ... اگر تصمیم گرفتید که از حالتهای رندر جدید استفاده کنید، مدیریت حالت آن متفاوت است؛ برای مثال دیگر با یک سیستم مدیریت تزریق وابستگیها که طول عمر آن با طول عمر برنامهی Blazor یکی است، مواجه نیستیم و حالتهای زیر برای آنها متصور است:
حالت رندر: صفحات رندر شدهی در سمت سرور یا Server-rendered pages
مفهوم: یک صفحهی Blazor که در سمت سرور رندر شده و HTML نهایی آن به سمت مرورگر کاربر ارسال میشود. در این حالت هیچ اتصال SignalR و یا برنامهی وباسمبلی اجرا نخواهد شد.
عواقب: طول عمر سرویسهای Scoped، بهمحض پایان رندر صفحه در سمت سرور، پایان خواهند یافت.
بنابراین در این حالت طول عمر یک سرویس Scoped، بسیار کوتاه است (در حد ابتدا و انتهای رندر صفحه). همچنین چون برنامه در سمت سرور اجرا میشود، دسترسی کامل و بدون مشکلی را به HttpContext دارد.
صفحات SSR، بدون حالت (stateless) هستند؛ به این معنا که حالت کاربر در بین هدایت به صفحات مختلف برنامه ذخیره نمیشود. به آنها میتوان از این لحاظ بهمانند برنامههای MVC/Razor pages نگاه کرد. در این حالت اگر میخواهید حالت کاربران را ذخیره کنید، استفاده از کوکیها و یا سشنها، راهحل متداول اینکار هستند.
حالت رندر: صفحات استریمی (Streamed pages)
مفهوم: یک صفحهی Blazor که در سمت سرور رندر شده و قطعات آماده شدهی HTML آن به صورت استریمی از دادهها، به سمت مرورگر کاربر ارسال میشوند. در این حالت هیچ اتصال SignalR و یا برنامهی وباسمبلی اجرا نخواهد شد.
عواقب: طول عمر سرویسهای Scoped، بهمحض پایان رندر صفحه در سمت سرور، پایان خواهند یافت.
بنابراین در این حالت طول عمر یک سرویس Scoped، بسیار کوتاه است (در حد ابتدا و انتهای رندر صفحه). همچنین چون برنامه در سمت سرور اجرا میشود، دسترسی کامل و بدون مشکلی را به HttpContext دارد.
حالت رندر: Blazor server page
مفهوم: یک صفحهی Blazor Server که یک اتصال دائم SignalR را با سرور دارد.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر اتصال SignalR است و با قطع این اتصال، پایان خواهند یافت. این نوع برنامهها اصطلاحا stateful هستند و از لحاظ دسترسی به حالت کاربر، تجربهی کاربری همانند یک برنامهی دسکتاپ را ارائه میدهند.
در این نوع برنامهها و درون اتصال SignalR، دسترسی به HttpContext وجود ندارد.
حالت رندر: Blazor wasm page
مفهوم: صفحهای که به کمک فناوری وباسمبلی، درون مرورگر کاربر اجرا میشود.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر برگه و صفحهی جاری است و با بسته شدن آن، پایان میپذیرد. این نوع برنامهها نیز اصطلاحا stateful هستند و از لحاظ دسترسی به حالت کاربر، تجربهی کاربری همانند یک برنامهی دسکتاپ را ارائه میدهند (البته فقط درون مروگر کاربر).
در این نوع برنامهها، دسترسی به HttpContext وجود ندارد.
حالت رندر: جزیرهی تعاملی Blazor Server و یا Blazor server island
مفهوم: یک کامپوننت Blazor Server که درون یک صفحهی دیگر (که عموما از نوع SSR است) قرار گرفته و یک اتصال SignalR را با سرور برقرار میکند.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر اتصال SignalR است و با قطع این اتصال، پایان خواهند یافت؛ برای مثال کاربر به صفحهای دیگر در این برنامه مراجعه کند. بنابراین این نوع کامپوننتها هم تا زمانیکه کاربر در صفحهی جاری قرار دارد، stateful هستند.
در این نوع برنامهها و درون اتصال SignalR، دسترسی به HttpContext وجود ندارد.
حالت رندر: جزیرهی تعاملی Blazor WASM و یا Blazor wasm island
مفهوم: یک کامپوننت Blazor WASM که درون یک صفحهی دیگر (که عموما از نوع SSR است) توسط فناوری وباسمبلی، درون مرورگر کاربر اجرا میشود.
عواقب: طول عمر سرویسهای Scoped، معادل مدت زمان فعال بودن صفحهی جاری است. به محض اینکه کاربر به صفحهای دیگر مراجعه و این کامپوننت دیگر فعال نباشد، طول عمر آن خاتمه خواهد یافت. بنابراین این نوع کامپوننتها هم تا زمانیکه کاربر در صفحهی جاری قرار دارد، stateful هستند (البته این حالت درون مرورگر کاربر مدیریت میشود و نه در سمت سرور).
در این نوع برنامهها، دسترسی به HttpContext وجود ندارد.
نتیجهگیری
همانطور که مشاهده میکنید، در صفحات SSR، دسترسی کاملی به HttpContext سمت سرور وجود دارد (که البته کوتاه مدت بوده و با پایان رندر صفحه، خاتمه خواهد یافت؛ حالتی مانند صفحات MVC و Razor pages)، اما در جزایر تعاملی واقع در آنها، خیر.
مسالهی مهم در اینجا، مدیریت اختلاط حالت صفحات SSR و جزایر تعاملی واقع در آنها است. مایکروسافت جهت پیاده سازی اعتبارسنجی و احراز هویت کاربران در Blazor 8x و برای انتقال حالت به این جزایر، از دو روش Root-level cascading values و سرویس PersistentComponentState استفاده کردهاست که آنها را در دو قسمت بعدی، با توضیحات بیشتری بررسی میکنیم.
پیش از دات نت 8، دو حالت عمده برای توسعهی برنامههای Blazor وجود داشت: Blazor Server و Blazor WASM. در هر دو حالت، طول عمر سیستم تزریق وابستگیهای ایجاد و مدیریت شدهی توسط Blazor، معادل طول عمر برنامهاست.
در برنامههای Blazor Server، طول عمر سیستم تزریق وابستگیها، توسط ASP.NET Core قرار گرفتهی بر روی سرور مدیریت شده و نمونههای ایجاد شدهی سرویسهای توسط آن، به ازای هر کاربر متفاوت است. بنابراین اگر طول عمر سرویسی در اینجا به صورت Scoped تعریف شود، این سرویس فقط یکبار در طول عمر برنامه، به ازای یک کاربر جاری برنامه، تولید و نمونه سازی میشود. در این مدل برنامهها، سرویسهایی با طول عمر Singleton، بین تمام کاربران به اشتراک گذاشته میشوند. به همین جهت است که در این نوع برنامهها، مدیریت سرویس Context مخصوص EF-Core نکات خاصی را به همراه دارد. چون اگر بر اساس سیستم پیشفرض تزریق وابستگیها و طول عمر Scoped این سرویس عمل شود، یک Context فقط یکبار بهازای یک کاربر، یکبار نمونه سازی شده و تا پایان طول عمر برنامه، بدون تغییر زنده نگه داشته میشود؛ در حالیکه عموم توسعه دهندگان EF-Core تصور میکنند سرویسهای Scoped، پس از پایان یک درخواست، پایان یافته و Dispose میشوند، اما در اینجا پایان درخواستی نداریم. یک اتصال دائم SignalR را داریم و تا زمانیکه برقرار است، یعنی برنامه زندهاست. بنابراین در برنامههای Blazor Server، سرویسهای Scoped، به ازای هر کاربر، همانند Singleton رفتار میکنند (در سراسر برنامه به ازای یک کاربر در دسترس هستند) و سرویسهایی از اساس Singleton، بین تمام کاربران به اشتراک گذاشته میشوند.
در برنامههای Blazor WASM، طول عمر سیستم تزریق وابستگیها، توسط برنامهی وباسمبلی در حال اجرای بر روی مرورگر مدیریت میشود. یعنی مختص به یک کاربر بوده و طول عمر آن وابستهاست به طول عمر برگهی جاری مرورگر. بنابراین دراینجا بین سرویسهای Scoped و Singleton، تفاوتی وجود ندارد و همانند هم رفتار میکنند (هر دو مختص به یک کاربر و وابسته به طول عمر برگهی جاری هستند).
در هیچکدام از این حالتها، امکان دسترسی به HttpContext وجود ندارد (نه داخل اتصال دائم SignalR برنامههای Blazor Server و نه داخل برنامهی وباسمبلی در حال اجرای در مرورگر). اطلاعات بیشتر
بنابراین در این برنامهها برای نگهداری اطلاعات کاربر لاگین شدهی به سیستم و یا سایر اطلاعات سراسری برنامه، عموما از سرویسهایی با طول عمر Scoped استفاده میشود که در تمام قسمتهای برنامه به ازای هر کاربر، قابل دسترسی هستند.
رفتار Blazor 8x در مورد مدیریت حالت
هرچند دات نت 8 به همراه حالتهای رندر جدیدی است، اما هنوز هم میتوان برنامههایی کاملا توسعه یافته بر اساس مدلهای قبلی Blazor Server و یا Blazor WASM را همانند داتنتهای پیش از 8 داشت. بنابراین اگر تصمیم گرفتید که بجای استفاده از جزیرههای تعاملی، کل برنامه را به صورت سراسری تعاملی کنید، همان نکات قبلی، در اینجا هم صادق هستند و از لحاظ مدیریت حالت، تفاوتی نمیکنند.
اما ... اگر تصمیم گرفتید که از حالتهای رندر جدید استفاده کنید، مدیریت حالت آن متفاوت است؛ برای مثال دیگر با یک سیستم مدیریت تزریق وابستگیها که طول عمر آن با طول عمر برنامهی Blazor یکی است، مواجه نیستیم و حالتهای زیر برای آنها متصور است:
حالت رندر: صفحات رندر شدهی در سمت سرور یا Server-rendered pages
مفهوم: یک صفحهی Blazor که در سمت سرور رندر شده و HTML نهایی آن به سمت مرورگر کاربر ارسال میشود. در این حالت هیچ اتصال SignalR و یا برنامهی وباسمبلی اجرا نخواهد شد.
عواقب: طول عمر سرویسهای Scoped، بهمحض پایان رندر صفحه در سمت سرور، پایان خواهند یافت.
بنابراین در این حالت طول عمر یک سرویس Scoped، بسیار کوتاه است (در حد ابتدا و انتهای رندر صفحه). همچنین چون برنامه در سمت سرور اجرا میشود، دسترسی کامل و بدون مشکلی را به HttpContext دارد.
صفحات SSR، بدون حالت (stateless) هستند؛ به این معنا که حالت کاربر در بین هدایت به صفحات مختلف برنامه ذخیره نمیشود. به آنها میتوان از این لحاظ بهمانند برنامههای MVC/Razor pages نگاه کرد. در این حالت اگر میخواهید حالت کاربران را ذخیره کنید، استفاده از کوکیها و یا سشنها، راهحل متداول اینکار هستند.
حالت رندر: صفحات استریمی (Streamed pages)
مفهوم: یک صفحهی Blazor که در سمت سرور رندر شده و قطعات آماده شدهی HTML آن به صورت استریمی از دادهها، به سمت مرورگر کاربر ارسال میشوند. در این حالت هیچ اتصال SignalR و یا برنامهی وباسمبلی اجرا نخواهد شد.
عواقب: طول عمر سرویسهای Scoped، بهمحض پایان رندر صفحه در سمت سرور، پایان خواهند یافت.
بنابراین در این حالت طول عمر یک سرویس Scoped، بسیار کوتاه است (در حد ابتدا و انتهای رندر صفحه). همچنین چون برنامه در سمت سرور اجرا میشود، دسترسی کامل و بدون مشکلی را به HttpContext دارد.
حالت رندر: Blazor server page
مفهوم: یک صفحهی Blazor Server که یک اتصال دائم SignalR را با سرور دارد.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر اتصال SignalR است و با قطع این اتصال، پایان خواهند یافت. این نوع برنامهها اصطلاحا stateful هستند و از لحاظ دسترسی به حالت کاربر، تجربهی کاربری همانند یک برنامهی دسکتاپ را ارائه میدهند.
در این نوع برنامهها و درون اتصال SignalR، دسترسی به HttpContext وجود ندارد.
حالت رندر: Blazor wasm page
مفهوم: صفحهای که به کمک فناوری وباسمبلی، درون مرورگر کاربر اجرا میشود.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر برگه و صفحهی جاری است و با بسته شدن آن، پایان میپذیرد. این نوع برنامهها نیز اصطلاحا stateful هستند و از لحاظ دسترسی به حالت کاربر، تجربهی کاربری همانند یک برنامهی دسکتاپ را ارائه میدهند (البته فقط درون مروگر کاربر).
در این نوع برنامهها، دسترسی به HttpContext وجود ندارد.
حالت رندر: جزیرهی تعاملی Blazor Server و یا Blazor server island
مفهوم: یک کامپوننت Blazor Server که درون یک صفحهی دیگر (که عموما از نوع SSR است) قرار گرفته و یک اتصال SignalR را با سرور برقرار میکند.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر اتصال SignalR است و با قطع این اتصال، پایان خواهند یافت؛ برای مثال کاربر به صفحهای دیگر در این برنامه مراجعه کند. بنابراین این نوع کامپوننتها هم تا زمانیکه کاربر در صفحهی جاری قرار دارد، stateful هستند.
در این نوع برنامهها و درون اتصال SignalR، دسترسی به HttpContext وجود ندارد.
حالت رندر: جزیرهی تعاملی Blazor WASM و یا Blazor wasm island
مفهوم: یک کامپوننت Blazor WASM که درون یک صفحهی دیگر (که عموما از نوع SSR است) توسط فناوری وباسمبلی، درون مرورگر کاربر اجرا میشود.
عواقب: طول عمر سرویسهای Scoped، معادل مدت زمان فعال بودن صفحهی جاری است. به محض اینکه کاربر به صفحهای دیگر مراجعه و این کامپوننت دیگر فعال نباشد، طول عمر آن خاتمه خواهد یافت. بنابراین این نوع کامپوننتها هم تا زمانیکه کاربر در صفحهی جاری قرار دارد، stateful هستند (البته این حالت درون مرورگر کاربر مدیریت میشود و نه در سمت سرور).
در این نوع برنامهها، دسترسی به HttpContext وجود ندارد.
نتیجهگیری
همانطور که مشاهده میکنید، در صفحات SSR، دسترسی کاملی به HttpContext سمت سرور وجود دارد (که البته کوتاه مدت بوده و با پایان رندر صفحه، خاتمه خواهد یافت؛ حالتی مانند صفحات MVC و Razor pages)، اما در جزایر تعاملی واقع در آنها، خیر.
مسالهی مهم در اینجا، مدیریت اختلاط حالت صفحات SSR و جزایر تعاملی واقع در آنها است. مایکروسافت جهت پیاده سازی اعتبارسنجی و احراز هویت کاربران در Blazor 8x و برای انتقال حالت به این جزایر، از دو روش Root-level cascading values و سرویس PersistentComponentState استفاده کردهاست که آنها را در دو قسمت بعدی، با توضیحات بیشتری بررسی میکنیم.
همیشه نمیتوان کاربران را وادار به استفادهی از صفحهی لاگین برنامهی 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» فعال کنید.
قسمت «نحوهی ارسال درخواستهای Ajax ایی به همراه توکن صادر شده» را مطالعه کنید. اگر توکنی به سمت سرور ارسال نشود و هدر نهایی حاوی key/value مربوط به authorization ذکر شده نباشد:
مقدار access token در سمت سرور قابل بازیابی نخواهد بود.
با استفاده از developer tools مرورگرها بررسی کنید که آیا هدر authorization را به شکل زیر به سمت سرور ارسال میکنید یا خیر؟ (این هدر ویژه هست که actionContext.Request.Headers.Authorization را وهله سازی میکند)
$.ajax({ headers: { 'Authorization': 'Bearer ' + jwtToken },
با استفاده از developer tools مرورگرها بررسی کنید که آیا هدر authorization را به شکل زیر به سمت سرور ارسال میکنید یا خیر؟ (این هدر ویژه هست که actionContext.Request.Headers.Authorization را وهله سازی میکند)
- در مقدمه مطلب «ذخیره سازی اطلاعات در مرورگر توسط برنامههای Angular» در مورد محدودیت حجمهای حالتهای مختلف ذخیره سازی اطلاعات در سمت کلاینت، بیشتر توضیح داده شدهاست.
- روش پیاده سازی dynamic permission شما و قرار دادن اطلاعات آن در توکن، در این حالت بیمورد است. از این جهت که به نظر قصد ندارید از اطلاعات آن در سمت کلاینت استفاده کنید (محدود کردن دسترسی به صفحات یک برنامهی SPA و نه یک برنامهی MVC). توکن و هرچیزی که در آن است جهت کاربردهای سمت کلاینت بیشتر باید مورد استفاده قرارگیرند تا سمت سرور. این بحث JWT برای برنامههای Angular و کلا SPA (تک صفحهای وب) بیشتر استفاده میشود (سمت سرور Web API خالص، سمت کاربر SPA خالص). اگر برنامهی شما چنین چیزی نیست، از آن استفاده نکنید.
چون اطلاعات دسترسی به صفحات به نظر سایت MVC شما مطلقا کاربردی در سمت کلاینت ندارند، آنرا به توکن اضافه نکنید. در عوض در متد CanUserAccess، قسمت user.HasClaim را با کوئری گرفتن از بانک اطلاعاتی جایگزین کنید.
- مثال سمت کلاینت بحث جاری در سری «احراز هویت و اعتبارسنجی کاربران در برنامههای Angular» عمیقتر بررسی شدهاست و هدف از قسمتهای مختلف توکن آنرا جهت استفادهی در سمت کلاینت (استفاده از نقشها جهت دسترسی به صفحات برنامهی سمت کلاینت Angular، استفاده از تاریخ انقضای توکن جهت بررسی اعتبار آن، استفاده از نام نمایشی قرار گرفتهی در توکن برای نمایش آن در سمت کلاینت و غیره)، بهتر درک خواهید کرد. در سمت سرور با داشتن Id شخص، مابقی را میتوان از بانک اطلاعاتی کوئری گرفت و نیازی به سنگین کردن توکن نیست.
اوراکل هم سیستمی مشابه in memory oltp را راه اندازی کرده است که در مقایسه با sql server
Sql server سه برابر سریعتر بوده است.
امروزه اکثر شرکتها چه کوچک و چه بزرگ به سمت sql server حرکت میکنند. این حرکت به دلایل زیر صورت میگیرد
برتری sql server بر اوراکل از لحاظ کارایی
ارزانتر بودن sql server: اوراکل در حال حاضر قیمتی حدود 250 هزار دلار دارد در صورتی که بهترین و گرانترین نسخه sql server قیمتی به مراتب پایینتر داره
Sql server سه برابر سریعتر بوده است.
امروزه اکثر شرکتها چه کوچک و چه بزرگ به سمت sql server حرکت میکنند. این حرکت به دلایل زیر صورت میگیرد
برتری sql server بر اوراکل از لحاظ کارایی
ارزانتر بودن sql server: اوراکل در حال حاضر قیمتی حدود 250 هزار دلار دارد در صورتی که بهترین و گرانترین نسخه sql server قیمتی به مراتب پایینتر داره
اشتراکها