مدتی است بدنبال برنامه نویسی اندروید هستم و بین انتخاب زبان سی شارپ و جاوا مردد هستم . کدام گزینهی بهتری برای کار هست استفاده از جاوا یا سی شارپ ؟ آیا ممکنه بلایی که سر سیلورلایت اومد سر برنامه نویسان اندروید (سی شارپ) هم بیاد ؟ اگر بخواهیم آینده نگری کنیم کار رو با کدوم پیش ببریم و ابزارها و امکانات کدوم بهتر هست ؟
- اصلا راجع به تشخیص بحث نشد. نه SP و نه EF و نه L2SQL و نه NHibernate هیچکدام روش تشخیصی برای درک اینکه آیا ورودی فعلی خطرناک است یا خیر ندارند. کوئری پارامتری است که اینها را حفظ میکند: +
- در مورد توزیع از چه نوع؟ برنامه ویندوزی یا برنامه وب؟ برنامههای ویندوزی از نظر من اصلا برای یک سازمان مناسب نیستند : +
- مثال Round Trip اصلا واضح نبود ...
- مطلبی که لینک دادید بیشتر مربوط به نگارش اول EF بود و الان خیلی تغییر کرده و حالا اگر با EF مشکل دارید از NHibernate استفاده کنید یا موارد دیگر.
- مورد آخر بیشتر یک نظر شخصی است (و محترم) ولی دلایل آن قانع کننده نبودند.
- در مورد توزیع از چه نوع؟ برنامه ویندوزی یا برنامه وب؟ برنامههای ویندوزی از نظر من اصلا برای یک سازمان مناسب نیستند : +
- مثال Round Trip اصلا واضح نبود ...
- مطلبی که لینک دادید بیشتر مربوط به نگارش اول EF بود و الان خیلی تغییر کرده و حالا اگر با EF مشکل دارید از NHibernate استفاده کنید یا موارد دیگر.
- مورد آخر بیشتر یک نظر شخصی است (و محترم) ولی دلایل آن قانع کننده نبودند.
ویندوز 8.1 دارای امکانات و API توکاری جهت نمایش و خواندن فایلهای PDF در برنامههای مترو است. در ادامه قصد داریم از این امکانات در یک برنامهی متداول دات نت، برای مثال یک برنامهی کنسول غیر مترو استفاده کنیم.
آماده سازی برنامههای دات نت برای دسترسی به API مترو ویندوز 8.1
ابتدا یک برنامهی کنسول دات نت 4.5.1 را آغاز کنید. برای دسترسی به API ویندوز 8.1 حتما نیاز است که حداقل از دات نت 4.5.1 شروع کرد. سپس برنامه را در VS.NET بسته و فایل پروژه آنرا در یک ادیتور متنی باز کنید.
در ابتدای فایل csproj، نیاز است سطر TargetPlatformVersion ذیل اضافه شود.
سپس در همین فایل، ارجاعات زیر را نیز اضافه نمائید:
مواردی مانند System.Runtime، System.Runtime.WindowsRuntime امکان دسترسی به API ویندوز 8 را در برنامههای دات نت میسر میکنند.
یک نکته
اگر میخواهید این فرآیند را ساده و خودکار کنید، از قالبهای پروژهی مخصوص DesktopWinRT.Templates.vsix استفاده نمائید.
DesktopWinRT.Templates.vsix
افزودن ارجاعی به Nito.AsyncEx
چون برنامهی مورد استفاده کنسول است و API ویندوز 8 کاملا async طراحی شدهاست، نیاز است با کمک AsyncContext موجود در کتابخانهی Nito.AsyncEx بتوان از امکانات async و await در متد Main برنامه استفاده کرد. البته اگر از سایر برنامههای دسکتاپ استفاده میکنید، فقط کافی است امضای متد رخدادن گردان را به async تغییر دهید.
تبدیل استریمهای دات نت به استریمهای WinRT
اکثر متدهای WinRT با استریمهایی از نوع IRandomAccessStream کار میکنند. برای اینکه بتوان استریم استاندارد دات نت را به این نوع تبدیل کرد، میتوان از کلاسهای ذیل کمک گرفت:
تا اینجا به یک متد الحاقی جدیدی به نام AsRandomAccessStream میرسیم که امکان تبدیل استریم استاندارد دات نت را به IRandomAccessStream مخصوص WinRT دارد. از آن میتوان برای باز کردن یک فایل و ارسال استریم آن به توابع WinRT و یا ثبت استریم WinRT در یک فایل استفاده کرد.
خواندن فایلهای PDF و تبدیل صفحات آنها به تصویر
در ادامه کد کامل استفاده از API جدید ویندوز 8.1 را جهت خواندن فایلهای PDF ملاحظه میکنید. این امکانات جدید در فضای نام Windows.Data.Pdf قرار دارند و صرفا امکان خواندن فایلهای PDF را تدارک دیدهاند.
توضیحات:
- متد AsyncContext.Run جزو امکانات Nito.AsyncEx است و امکان نوشتن کدهای await دار را در متد Main یک برنامهی کنسول فراهم میکند.
- متد File.Openدات نت، خروجی از نوع استریم دارد. برای تبدیل آن به نوع IRandomAccessStream، از متد الحاقی AsRandomAccessStream که پیشتر تهیه کردیم، میتوان استفاده کرد.
- در ادامه متد PdfDocument.LoadFromStreamAsync این استریم خاص را دریافت کرده و امکان دسترسی به API ویندوز 8.1 را میسر میکند.
- توسط متد pdfDocument.GetPage میتوان به صفحات مختلف فایل PDF باز شده دسترسی یافت. در اینجا متد page.RenderToStreamAsync، سبب رندر شدن صفحه با فرمت PNG میشود. این خروجی نهایتا باید در یک استریم از نوع IRandomAccessStream ثبت شود. در اینجا نیز میتوان از متد File.Open در حالت FileMode.OpenOrCreate استفاده کرد.
- اگر میخواهید ابعاد تصویر نهایی و ویژگیهای آنرا تغییر دهید، میتوان از پارامتر دوم متد page.RenderToStreamAsync استفاده کرد که شیءایی از نوع PdfPageRenderOptions را میپذیرد.
کدهای کامل این پروژه را از اینجا میتوانید دریافت کنید
MicrosoftStreamExtensions.zip
برای مطالعه بیشتر
How to use specific WinRT API from Desktop apps
How to call WinRT APIs from .NET desktop apps
آماده سازی برنامههای دات نت برای دسترسی به API مترو ویندوز 8.1
ابتدا یک برنامهی کنسول دات نت 4.5.1 را آغاز کنید. برای دسترسی به API ویندوز 8.1 حتما نیاز است که حداقل از دات نت 4.5.1 شروع کرد. سپس برنامه را در VS.NET بسته و فایل پروژه آنرا در یک ادیتور متنی باز کنید.
در ابتدای فایل csproj، نیاز است سطر TargetPlatformVersion ذیل اضافه شود.
<PropertyGroup> <TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion> <TargetPlatformVersion>8.1</TargetPlatformVersion> </PropertyGroup>
<ItemGroup> <Reference Include="System" /> <Reference Include="System.ComponentModel.DataAnnotations" /> <Reference Include="System.Core" /> <Reference Include="System.ObjectModel" /> <Reference Include="System.Xml.Linq" /> <Reference Include="System.Data.DataSetExtensions" /> <Reference Include="Microsoft.CSharp" /> <Reference Include="System.Data" /> <Reference Include="System.Xml" /> <Reference Include="System.Threading" /> <Reference Include="System.Threading.Tasks" /> </ItemGroup> <ItemGroup> <Reference Include="Windows" /> <Reference Include="System.Runtime" /> <Reference Include="System.Runtime.WindowsRuntime" /> </ItemGroup>
یک نکته
اگر میخواهید این فرآیند را ساده و خودکار کنید، از قالبهای پروژهی مخصوص DesktopWinRT.Templates.vsix استفاده نمائید.
DesktopWinRT.Templates.vsix
افزودن ارجاعی به Nito.AsyncEx
چون برنامهی مورد استفاده کنسول است و API ویندوز 8 کاملا async طراحی شدهاست، نیاز است با کمک AsyncContext موجود در کتابخانهی Nito.AsyncEx بتوان از امکانات async و await در متد Main برنامه استفاده کرد. البته اگر از سایر برنامههای دسکتاپ استفاده میکنید، فقط کافی است امضای متد رخدادن گردان را به async تغییر دهید.
install-package Nito.AsyncEx
تبدیل استریمهای دات نت به استریمهای WinRT
اکثر متدهای WinRT با استریمهایی از نوع IRandomAccessStream کار میکنند. برای اینکه بتوان استریم استاندارد دات نت را به این نوع تبدیل کرد، میتوان از کلاسهای ذیل کمک گرفت:
using System; using System.IO; using Windows.Storage.Streams; namespace ConsoleWin81PdfApiTest { public static class MicrosoftStreamExtensions { public static IRandomAccessStream AsRandomAccessStream(this Stream stream) { return new RandomStream(stream); } } class RandomStream : IRandomAccessStream { readonly Stream _internstream; public RandomStream(Stream underlyingstream) { _internstream = underlyingstream; } public IInputStream GetInputStreamAt(ulong position) { _internstream.Position = (long)position; return _internstream.AsInputStream(); } public IOutputStream GetOutputStreamAt(ulong position) { _internstream.Position = (long)position; return _internstream.AsOutputStream(); } public ulong Size { get { return (ulong)_internstream.Length; } set { _internstream.SetLength((long)value); } } public bool CanRead { get { return _internstream.CanRead; } } public bool CanWrite { get { return _internstream.CanWrite; } } public IRandomAccessStream CloneStream() { throw new NotSupportedException(); } public ulong Position { get { return (ulong)_internstream.Position; } } public void Seek(ulong position) { _internstream.Seek((long)position, SeekOrigin.Begin); } public void Dispose() { _internstream.Dispose(); } public Windows.Foundation.IAsyncOperationWithProgress<IBuffer, uint> ReadAsync(IBuffer buffer, uint count, InputStreamOptions options) { return GetInputStreamAt(Position).ReadAsync(buffer, count, options); } public Windows.Foundation.IAsyncOperation<bool> FlushAsync() { return GetOutputStreamAt(Position).FlushAsync(); } public Windows.Foundation.IAsyncOperationWithProgress<uint, uint> WriteAsync(IBuffer buffer) { return GetOutputStreamAt(Position).WriteAsync(buffer); } } }
خواندن فایلهای PDF و تبدیل صفحات آنها به تصویر
در ادامه کد کامل استفاده از API جدید ویندوز 8.1 را جهت خواندن فایلهای PDF ملاحظه میکنید. این امکانات جدید در فضای نام Windows.Data.Pdf قرار دارند و صرفا امکان خواندن فایلهای PDF را تدارک دیدهاند.
using System; using System.IO; using System.Threading.Tasks; using Windows.Data.Pdf; using Nito.AsyncEx; namespace ConsoleWin81PdfApiTest { class Program { static void Main(string[] args) { AsyncContext.Run(async () => { await test(); }); } private static async Task test() { using (var randomAccessStream = File.Open("PieChartPdfReport.pdf", FileMode.Open).AsRandomAccessStream()) { var pdfDocument = await PdfDocument.LoadFromStreamAsync(randomAccessStream); for (uint i = 0; i < pdfDocument.PageCount; i++) { using (var page = pdfDocument.GetPage(i)) { /*var renderOptions = new PdfPageRenderOptions { BackgroundColor = Colors.LightGray, DestinationHeight = (uint) (page.Size.Height*10) };*/ using (var stream = File.Open(string.Format("page-{0}.png", i + 1), FileMode.OpenOrCreate).AsRandomAccessStream()) { await page.RenderToStreamAsync(stream/*, renderOptions*/); await stream.FlushAsync(); } } } } } } }
توضیحات:
- متد AsyncContext.Run جزو امکانات Nito.AsyncEx است و امکان نوشتن کدهای await دار را در متد Main یک برنامهی کنسول فراهم میکند.
- متد File.Openدات نت، خروجی از نوع استریم دارد. برای تبدیل آن به نوع IRandomAccessStream، از متد الحاقی AsRandomAccessStream که پیشتر تهیه کردیم، میتوان استفاده کرد.
- در ادامه متد PdfDocument.LoadFromStreamAsync این استریم خاص را دریافت کرده و امکان دسترسی به API ویندوز 8.1 را میسر میکند.
- توسط متد pdfDocument.GetPage میتوان به صفحات مختلف فایل PDF باز شده دسترسی یافت. در اینجا متد page.RenderToStreamAsync، سبب رندر شدن صفحه با فرمت PNG میشود. این خروجی نهایتا باید در یک استریم از نوع IRandomAccessStream ثبت شود. در اینجا نیز میتوان از متد File.Open در حالت FileMode.OpenOrCreate استفاده کرد.
- اگر میخواهید ابعاد تصویر نهایی و ویژگیهای آنرا تغییر دهید، میتوان از پارامتر دوم متد page.RenderToStreamAsync استفاده کرد که شیءایی از نوع PdfPageRenderOptions را میپذیرد.
کدهای کامل این پروژه را از اینجا میتوانید دریافت کنید
MicrosoftStreamExtensions.zip
برای مطالعه بیشتر
How to use specific WinRT API from Desktop apps
How to call WinRT APIs from .NET desktop apps
در قسمت سوم، کار ورود به سیستم و امکان مشاهدهی صفحهی محافظت شدهی پس از لاگین را پیاده سازی کردیم. در این قسمت میخواهیم امکان دسترسی به مسیر http://localhost:4200/protectedPage را کنترل کنیم. تا اینجا اگر کاربر بدون لاگین کردن نیز این مسیر را درخواست کند، میتواند حداقل ساختار ابتدایی آنرا مشاهده کند که باید مدیریت شود و این مدیریت دسترسی میتواند بر اساس وضعیت لاگین کاربر و همچنین نقشهای او در سیستم باشد:
طراحی بخشهایی از این قسمت، از پروژهی «کنترل دسترسیها در Angular با استفاده از Ng2Permission» ایده گرفته شدهاند.
استخراج اطلاعات کاربر وارد شدهی به سیستم از توکن دسترسی او
یکی از روشهای دسترسی به اطلاعات کاربر در سمت کلاینت، مانند نقشهای او، تدارک متدی در سمت سرور و بازگشت Claims او به سمت کلاینت است:
اما با توجه به اینکه در زمان لاگین، نقشهای او (و سایر Claims دلخواه) نیز به توکن دسترسی نهایی اضافه میشوند، میتوان این کوئری گرفتن مدام از سرور را تبدیل به روش بسیار سریعتر استخراج آنها از توکنی که هم اکنون در کش مرورگر ذخیره شدهاست، کرد.
همچنین باید دقت داشت چون این توکن دارای امضای دیجیتال است، کوچکترین تغییری در آن در سمت کاربر، سبب برگشت خوردن خودکار درخواست ارسالی به سمت سرور میشود (یکی از مراحل اعتبارسنجی کاربر در سمت سرور، اعتبارسنجی توکن دریافتی (قسمت cfg.TokenValidationParameters) و همچنین بررسی خودکار امضای دیجیتال آن است). بنابراین نگرانی از این بابت وجود ندارد.
اگر اطلاعات کاربر در سمت سرور تغییر کنند، با اولین درخواست ارسالی به سمت سرور، رخداد OnTokenValidated وارد عمل شده و درخواست ارسالی را برگشت میزند (در مورد پیاده سازی سمت سرور این مورد، در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» بیشتر بحث شدهاست). در این حالت کاربر مجبور به لاگین مجدد خواهد شد که این مورد سبب به روز رسانی خودکار اطلاعات توکنهای ذخیره شدهی او در مرورگر نیز میشود.
اگر از قسمت دوم این سری بهخاطر داشته باشید، توکن decode شدهی برنامه، چنین شکلی را دارد:
به همین جهت متدی را برای تبدیل این اطلاعات به شیء کاربر، ایجاد خواهیم کرد و در سراسر برنامه از این اطلاعات آماده، برای تعیین دسترسی او به قسمتهای مختلف برنامهی سمت کلاینت، استفاده میکنیم.
برای این منظور اینترفیس src\app\core\models\auth-user.ts را به صورت ذیل ایجاد میکنیم:
پس از آن متد getAuthUser را جهت استخراج خواص ویژهی توکن دسترسی decode شدهی فوق، به صورت ذیل به سرویس Auth اضافه میکنیم:
کار با این متد بسیار سریع است و نیازی به رفت و برگشتی به سمت سرور ندارد؛ چون تمام اطلاعات استخراجی توسط آن هم اکنون در کش سریع مرورگر کلاینت موجود هستند. استفادهی از متد Object.freeze هم سبب read-only شدن این خروجی میشود.
همچنین در اینجا تمام نقشهای دریافتی، تبدیل به LowerCase شدهاند. با اینکار مقایسهی بعدی آنها با نقشهای درخواستی، حساس به بزرگی و کوچکی حروف نخواهد بود.
تعریف نقشهای دسترسی به مسیرهای مختلف سمت کلاینت
مرسوم است اطلاعات اضافی مرتبط با هر مسیر را به خاصیت data آن route انتساب میدهند. به همین جهت به فایل dashboard-routing.module.ts مراجعه کرده و نقشهای مجاز به دسترسی به مسیر protectedPage را به خاصیت data آن به صورت ذیل اضافه میکنیم:
که در اینجا ساختار اینترفیس AuthGuardPermission، در فایل جدید app\core\models\auth-guard-permission.ts به صورت ذیل تعریف شدهاست:
به این ترتیب هر قسمتی از برنامه که نیاز به اطلاعات سطوح دسترسی مسیری را داشت، ابتدا به دنبال route.data["permission"] خواهد گشت (کلیدی به نام permission در خاصیت data یک مسیر) و سپس اطلاعات آنرا بر اساس ساختار اینترفیس AuthGuardPermission، تحلیل میکند.
در اینجا تنها باید یکی از خواص permittedRoles (نقشهای مجاز به دسترسی/صدور دسترسی فقط برای این نقشهای مشخص، منهای مابقی) و یا deniedRoles (نقشهای غیرمجاز به دسترسی/دسترسی همهی نقشهای ممکن، منهای این نقشهای تعیین شده)، مقدار دهی شوند.
افزودن کامپوننت «دسترسی ندارید» به ماژول Authentication
در ادامه میخواهیم اگر کاربری به مسیری دسترسی نداشت، به صورت خودکار به صفحهی «دسترسی ندارید» هدایت شود. به همین جهت این کامپوننت را به صورت ذیل به ماژول authentication اضافه میکنیم:
با این خروجی که سبب درج خودکار آن در قسمت declaration فایل authentication.module نیز میشود:
سپس به فایل authentication-routing.module.ts مراجعه کرده و مسیریابی آنرا نیز اضافه میکنیم:
قالب access-denied.component.html را نیز به صورت ذیل تکمیل میکنیم:
که دکمهی Back آن به کمک سرویس Location، صورت ذیل پیاده سازی شدهاست:
در اینجا اگر کاربر به سیستم وارد نشده باشد، دکمهی لاگین نیز به او نمایش داده میشود. همچنین وجود "queryParamsHandling="merge در لینک مراجعهی به صفحهی لاگین، سبب خواهد شد تا query string موجود در صفحه نیز حفظ شود و به صفحهی لاگین انتقال پیدا کند. در صفحهی لاگین نیز جهت پردازش این نوع کوئری استرینگها، تمهیدات لازم درنظر گرفته شدهاند.
دکمهی back آن نیز توسط سرویس توکار Location واقع در مسیر angular/common@ پیاده سازی شدهاست.
ایجاد یک محافظ مسیر سمت کلاینت برای بررسی وضعیت کاربر جاری و همچنین نقشهای او
پس از تعریف متد getAuthUser و استخراج اطلاعات کاربر از توکن دسترسی دریافتی که شامل نقشهای او نیز میشود، اکنون میتوان متد بررسی این نقشها را نیز به سرویس Auth اضافه کرد:
متد some در جاوا اسکریپت شبیه به متد Any در C# LINQ عمل میکند. همچنین در مقایسهی صورت گرفته، با توجه به اینکه user.roles را پیشتر به LowerCase تبدیل کردیم، حساسیتی بین نقش Admin و admin و کلا کوچکی و بزرگی نام نقشها وجود ندارد.
اکنون در هر قسمتی از برنامه که نیاز به بررسی امکان دسترسی یک کاربر به نقش یا نقشهایی خاص وجود داشته باشد، میتوان AuthService را به سازندهی آن تزریق و سپس از متد فوق جهت بررسی نهایی، استفاده کرد.
در ادامه یک Route Guard جدید را در مسیر app\core\services\auth.guard.ts ایجاد میکنیم. کار آن بررسی خودکار امکان دسترسی به یک مسیر درخواستی است:
در اینجا در ابتدا وضعیت لاگین کاربر بررسی میگردد. این وضعیت نیز از طریق سرویس Auth که به سازندهی کلاس تزریق شدهاست، تامین میشود. اگر کاربر هنوز لاگین نکرده باشد، به صفحهی عدم دسترسی هدایت خواهد شد.
سپس خاصیت permission اطلاعات مسیر استخراج میشود. اگر چنین مقداری وجود نداشت، همینجا کار با موفقیت خاتمه پیدا میکند.
در آخر وضعیت دسترسی به نقشهای استخراجی deniedRoles و permittedRoles که از اطلاعات مسیر دریافت شدند، توسط متد isAuthUserInRoles سرویس Auth بررسی میشوند.
در متد showAccessDenied کار ارسال آدرس درخواستی (state.url) به صورت یک کوئری استرینگ (queryParams) با کلید returnUrl به صفحهی accessDenied صورت میگیرد. در این صفحه نیز دکمهی لاگین به همراه "queryParamsHandling="merge است. یعنی کامپوننت لاگین برنامه، کوئری استرینگ returnUrl را دریافت میکند:
و پس از لاگین موفق، در صورت وجود این کوئری استرینگ، هدایت خودکار کاربر، به مسیر returnUrl پیشین صورت خواهد گرفت:
محل معرفی این AuthGuard جدید که در حقیقت یک سرویس است، در ماژول Core، در قسمت providers آن، به صورت ذیل میباشد:
در آخر برای اعمال این Guard جدید، به فایل dashboard-routing.module.ts مراجعه کرده و خاصیت canActivate را مقدار دهی میکنیم:
به این ترتیب با درخواست این مسیر، پیش از فعالسازی و نمایش آن، توسط AuthGuard معرفی شدهی به آن، کار بررسی وضعیت کاربر و نقشهای او که از خاصیت permission خاصیت data دریافت میشوند، صورت گرفته و اگر عملیات تعیین اعتبار اطلاعات با موفقیت به پایان رسید، آنگاه کاربر مجوز دسترسی به این قسمت از برنامه را خواهد یافت.
اگر قصد آزمایش آنرا داشتید، فقط کافی است بجای نقش Admin، مثلا Admin1 را در permittedRoles مقدار دهی کنید، تا صفحهی access denied را در صورت درخواست مسیر protectedPage، بتوان مشاهده کرد.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
طراحی بخشهایی از این قسمت، از پروژهی «کنترل دسترسیها در Angular با استفاده از Ng2Permission» ایده گرفته شدهاند.
استخراج اطلاعات کاربر وارد شدهی به سیستم از توکن دسترسی او
یکی از روشهای دسترسی به اطلاعات کاربر در سمت کلاینت، مانند نقشهای او، تدارک متدی در سمت سرور و بازگشت Claims او به سمت کلاینت است:
public IActionResult Get() { var user = this.User.Identity as ClaimsIdentity; var config = new { userName = user.Name, roles = user.Claims.Where(x => x.Type == ClaimTypes.Role).Select(x => x.Value).ToList() }; return Ok(config); }
همچنین باید دقت داشت چون این توکن دارای امضای دیجیتال است، کوچکترین تغییری در آن در سمت کاربر، سبب برگشت خوردن خودکار درخواست ارسالی به سمت سرور میشود (یکی از مراحل اعتبارسنجی کاربر در سمت سرور، اعتبارسنجی توکن دریافتی (قسمت cfg.TokenValidationParameters) و همچنین بررسی خودکار امضای دیجیتال آن است). بنابراین نگرانی از این بابت وجود ندارد.
اگر اطلاعات کاربر در سمت سرور تغییر کنند، با اولین درخواست ارسالی به سمت سرور، رخداد OnTokenValidated وارد عمل شده و درخواست ارسالی را برگشت میزند (در مورد پیاده سازی سمت سرور این مورد، در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» بیشتر بحث شدهاست). در این حالت کاربر مجبور به لاگین مجدد خواهد شد که این مورد سبب به روز رسانی خودکار اطلاعات توکنهای ذخیره شدهی او در مرورگر نیز میشود.
اگر از قسمت دوم این سری بهخاطر داشته باشید، توکن decode شدهی برنامه، چنین شکلی را دارد:
{ "jti": "d1272eb5-1061-45bd-9209-3ccbc6ddcf0a", "iss": "http://localhost/", "iat": 1513070340, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid", "DisplayName": "وحید", "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber": "709b64868a1d4d108ee58369f5c3c1f3", "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [ "Admin", "User" ], "nbf": 1513070340, "exp": 1513070460, "aud": "Any" }
برای این منظور اینترفیس src\app\core\models\auth-user.ts را به صورت ذیل ایجاد میکنیم:
export interface AuthUser { userId: string; userName: string; displayName: string; roles: string[]; }
getAuthUser(): AuthUser { if (!this.isLoggedIn()) { return null; } const decodedToken = this.getDecodedAccessToken(); let roles = decodedToken["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"]; if (roles) { roles = roles.map(role => role.toLowerCase()); } return Object.freeze({ userId: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"], userName: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"], displayName: decodedToken["DisplayName"], roles: roles }); }
همچنین در اینجا تمام نقشهای دریافتی، تبدیل به LowerCase شدهاند. با اینکار مقایسهی بعدی آنها با نقشهای درخواستی، حساس به بزرگی و کوچکی حروف نخواهد بود.
تعریف نقشهای دسترسی به مسیرهای مختلف سمت کلاینت
مرسوم است اطلاعات اضافی مرتبط با هر مسیر را به خاصیت data آن route انتساب میدهند. به همین جهت به فایل dashboard-routing.module.ts مراجعه کرده و نقشهای مجاز به دسترسی به مسیر protectedPage را به خاصیت data آن به صورت ذیل اضافه میکنیم:
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; import { AuthGuardPermission } from "../core/models/auth-guard-permission"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent, data: { permission: { permittedRoles: ["Admin"], deniedRoles: null } as AuthGuardPermission } } ];
export interface AuthGuardPermission { permittedRoles?: string[]; deniedRoles?: string[]; }
در اینجا تنها باید یکی از خواص permittedRoles (نقشهای مجاز به دسترسی/صدور دسترسی فقط برای این نقشهای مشخص، منهای مابقی) و یا deniedRoles (نقشهای غیرمجاز به دسترسی/دسترسی همهی نقشهای ممکن، منهای این نقشهای تعیین شده)، مقدار دهی شوند.
افزودن کامپوننت «دسترسی ندارید» به ماژول Authentication
در ادامه میخواهیم اگر کاربری به مسیری دسترسی نداشت، به صورت خودکار به صفحهی «دسترسی ندارید» هدایت شود. به همین جهت این کامپوننت را به صورت ذیل به ماژول authentication اضافه میکنیم:
>ng g c Authentication/AccessDenied
AccessDenied create src/app/Authentication/access-denied/access-denied.component.html (32 bytes) create src/app/Authentication/access-denied/access-denied.component.ts (296 bytes) create src/app/Authentication/access-denied/access-denied.component.css (0 bytes) update src/app/Authentication/authentication.module.ts (550 bytes)
import { LoginComponent } from "./login/login.component"; import { AccessDeniedComponent } from "./access-denied/access-denied.component"; const routes: Routes = [ { path: "login", component: LoginComponent }, { path: "accessDenied", component: AccessDeniedComponent } ];
<h1 class="text-danger"> <span class="glyphicon glyphicon-ban-circle"></span> Access Denied </h1> <p>Sorry! You don't have access to this page.</p> <button class="btn btn-default" (click)="goBack()"> <span class="glyphicon glyphicon-arrow-left"></span> Back </button> <button *ngIf="!isAuthenticated" class="btn btn-success" [routerLink]="['/login']" queryParamsHandling="merge"> Login </button>
import { Component, OnInit } from "@angular/core"; import { Location } from "@angular/common"; import { AuthService } from "../../core/services/auth.service"; @Component({ selector: "app-access-denied", templateUrl: "./access-denied.component.html", styleUrls: ["./access-denied.component.css"] }) export class AccessDeniedComponent implements OnInit { isAuthenticated = false; constructor( private location: Location, private authService: AuthService ) { } ngOnInit() { this.isAuthenticated = this.authService.isLoggedIn(); } goBack() { this.location.back(); // <-- go back to previous location on cancel } }
در اینجا اگر کاربر به سیستم وارد نشده باشد، دکمهی لاگین نیز به او نمایش داده میشود. همچنین وجود "queryParamsHandling="merge در لینک مراجعهی به صفحهی لاگین، سبب خواهد شد تا query string موجود در صفحه نیز حفظ شود و به صفحهی لاگین انتقال پیدا کند. در صفحهی لاگین نیز جهت پردازش این نوع کوئری استرینگها، تمهیدات لازم درنظر گرفته شدهاند.
دکمهی back آن نیز توسط سرویس توکار Location واقع در مسیر angular/common@ پیاده سازی شدهاست.
ایجاد یک محافظ مسیر سمت کلاینت برای بررسی وضعیت کاربر جاری و همچنین نقشهای او
پس از تعریف متد getAuthUser و استخراج اطلاعات کاربر از توکن دسترسی دریافتی که شامل نقشهای او نیز میشود، اکنون میتوان متد بررسی این نقشها را نیز به سرویس Auth اضافه کرد:
isAuthUserInRoles(requiredRoles: string[]): boolean { const user = this.getAuthUser(); if (!user || !user.roles) { return false; } return requiredRoles.some(requiredRole => user.roles.indexOf(requiredRole.toLowerCase()) >= 0); } isAuthUserInRole(requiredRole: string): boolean { return this.isAuthUserInRoles([requiredRole]); }
اکنون در هر قسمتی از برنامه که نیاز به بررسی امکان دسترسی یک کاربر به نقش یا نقشهایی خاص وجود داشته باشد، میتوان AuthService را به سازندهی آن تزریق و سپس از متد فوق جهت بررسی نهایی، استفاده کرد.
در ادامه یک Route Guard جدید را در مسیر app\core\services\auth.guard.ts ایجاد میکنیم. کار آن بررسی خودکار امکان دسترسی به یک مسیر درخواستی است:
import { Injectable } from "@angular/core"; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router"; import { AuthService } from "./auth.service"; import { AuthGuardPermission } from "../models/auth-guard-permission"; @Injectable() export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { if (!this.authService.isLoggedIn()) { this.showAccessDenied(state); return false; } const permissionData = route.data["permission"] as AuthGuardPermission; if (!permissionData) { return true; } if (Array.isArray(permissionData.deniedRoles) && Array.isArray(permissionData.permittedRoles)) { throw new Error("Don't set both 'deniedRoles' and 'permittedRoles' in route data."); } if (Array.isArray(permissionData.permittedRoles)) { const isInRole = this.authService.isAuthUserInRoles(permissionData.permittedRoles); if (isInRole) { return true; } this.showAccessDenied(state); return false; } if (Array.isArray(permissionData.deniedRoles)) { const isInRole = this.authService.isAuthUserInRoles(permissionData.deniedRoles); if (!isInRole) { return true; } this.showAccessDenied(state); return false; } } private showAccessDenied(state: RouterStateSnapshot) { this.router.navigate(["/accessDenied"], { queryParams: { returnUrl: state.url } }); } }
سپس خاصیت permission اطلاعات مسیر استخراج میشود. اگر چنین مقداری وجود نداشت، همینجا کار با موفقیت خاتمه پیدا میکند.
در آخر وضعیت دسترسی به نقشهای استخراجی deniedRoles و permittedRoles که از اطلاعات مسیر دریافت شدند، توسط متد isAuthUserInRoles سرویس Auth بررسی میشوند.
در متد showAccessDenied کار ارسال آدرس درخواستی (state.url) به صورت یک کوئری استرینگ (queryParams) با کلید returnUrl به صفحهی accessDenied صورت میگیرد. در این صفحه نیز دکمهی لاگین به همراه "queryParamsHandling="merge است. یعنی کامپوننت لاگین برنامه، کوئری استرینگ returnUrl را دریافت میکند:
this.returnUrl = this.route.snapshot.queryParams["returnUrl"];
if (this.returnUrl) { this.router.navigate([this.returnUrl]); } else { this.router.navigate(["/protectedPage"]); }
محل معرفی این AuthGuard جدید که در حقیقت یک سرویس است، در ماژول Core، در قسمت providers آن، به صورت ذیل میباشد:
import { AuthGuard } from "./services/auth.guard"; @NgModule({ providers: [ AuthGuard ] }) export class CoreModule {}
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; import { AuthGuardPermission } from "../core/models/auth-guard-permission"; import { AuthGuard } from "../core/services/auth.guard"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent, data: { permission: { permittedRoles: ["Admin"], deniedRoles: null } as AuthGuardPermission }, canActivate: [AuthGuard] } ];
اگر قصد آزمایش آنرا داشتید، فقط کافی است بجای نقش Admin، مثلا Admin1 را در permittedRoles مقدار دهی کنید، تا صفحهی access denied را در صورت درخواست مسیر protectedPage، بتوان مشاهده کرد.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
قدرت الکترون برگرفته از فناوری وب است و هر آنچه که در آنجا امکان پذیر باشد، در اینجا نیز امکان پذیر است و خصوصیت برنامههای دسکتاپ را نیز داراست. الکترون به دلیل بارگذاری فایلهای html، به شما اجازه میدهد تا از ابزارهایی چون بوت استرپ و فریمورکها و کیتهای مشابهی چون جیکوئری و انگیولار، امبر Ember و ... در آن استفاده کنید. ولی با این حال، الکترون نوپا سعی دارد کیتهای اختصاصی خودش را هم داشته باشد، که در این مقاله به آنها اشاره میکنیم.
یکی از این کیتها، فوتون نام دارد. فوتون شامل یک سری css ,sass، فونت و قالبهای html است که به شما اجازه میدهد تا یک برنامه با ظاهری شبیه به برنامههای مک را داشته باشید. کامپوننتهای فوتون شامل tab ها، لیستها، منوی کناری، دکمههای معمولی یا جعبه ابزاری و کنترلهای فرمها میشود. با این حال اگر هم دوست ندارید که از این کامپوننتها استفاده کنید، میتوانید از layout های آن استفاده کند که پنجرهی شما را تقسیم بندی میکنند.
برای استفاده از فوتون لازم است آن را دانلود کرده و فایلهای آن را در پروژهی خود کپی کنید. دایرکتوری dist آن شامل یک مثال میشود که میتوانید آن را به داخل دایرکتوری پروژه خود کپی کنید؛ یا خود این دایرکتوری را به عنوان دایرکتوری پروژه تعیین کنید. بعد از آن، فایلهای موجود در دایرکتوری template را به داخل دایرکتوری والد، یعنی dist انتقال دهید و داخل فایل html، مسیر فایل css را تصحیح نمایید. فقط میماند که الکترون را بر روی این محل نصب کنید، یا اینکه الکترون نصب شدهی به صورت عمومی (Global) را در اختیار آن قرار دهید.
بعد از آن ممکن است با خطا مواجه شوید و وقتی فایل اصلی را که در اینجا نام آن app.js است، باز کنید، خطوط زیر را میبینید:
این نوع استفاده از ماژولهای داخلی، متعلق به نسخههای اولیه است و در نسخههای اخیر پشتیبانی نمیشود. پس بهتر است این خطوط را به صورتهایی که قبلا گفتهایم تغییر دهید.
سپس برنامه را اجرا کنید تا رابط جدید کاربری را ببینید.
فقط یک مشکلی هست و آن هم این است که باید فریم یا پنجرهای را که خود الکترون تولید میکند، حذف کنیم برای حذف آن میتوانید از خصوصیت frame در شیء Browser Window استفاده کنید:
در نسخههای 10 به بعد مک، از آنجاکه این خصوصیت، نه تنها فریم کرومیوم را حذف میکند، بلکه قابلیتهایی چون تغییر اندازه و ... را از آن نیز میگیرد، برای همین خصوصیت titleBarStyle را که به دو شکل هم میتواند نوشته شود، مورد استفاده قرار میدهیم.
حالا اگر برنامه را مجددا اجرا کنید، میبینید که قابهای دور آن حذف شدهاند، ولی با چند ثانیه کار کردن متوجه این ایراد میشوید که پنجره قابل درگ کردن و جابجایی نمیباشد. برای حل آن باید از css کمک بگیریم:
دستور بالا را به هر المانی انتساب دهید، آن المان و فرزندانش قابل درگ خواهند بود، ولی اگر المانی را با این خصوصیت تنظیم کردید، ولی قصد دارید که یکی یا چند عدد از المانهای فرزند این خاصیت را نداشته باشند، این دستور را به آنان انتساب دهید:
از آنجاکه در این رابط کاربری، نوار عنوان تگ مشخصی دارد:
با اضافه کردن این دستور css میتوانید به آن قابلیت درگ را بدهید:
حالا مجددا برنامه را تست کنید تا نتیجه کار را ببینید.
همانطور که میبینید با کمترین زحمت، به چنین رابط کاربری رسیدید. تصویر زیر متعلق به برنامهای است که در دو قسمت قبلی (+ + ) ساختیم و حالا با استفاده از این پکیج، ظاهر آن را تغییر دادهایم:
Electron UI Kit
دومین رابط کاربری که معرفی میکنیم در واقع یک کیت از یک سری کامپوننت است که بسیار شبیه به برنامههای دسکتاپ طراحی شده و شامل لیستها، گریدها، کنترلها و ... است که در دو فایل استایل، برای ویندوز و مک، مجزا شدهاند.
Maverix
یک استایل تحت وب به نام Maverix است که البته در مورد برنامههای دسکتاپ و الکترون حرفی نزده و خود را فریمورکی برای استفاده در برنامههای تحت وب معرفی کرده است. ولی از آنجا که کنترلهای موجود آن بر اساس سیستم عامل مک ایجاد شدهاند، به راحتی میتوانند خود را بجای برنامههای دسکتاپ جا بزنند. میتوانید دموی آن را نیز ببینید.
یکی از این کیتها، فوتون نام دارد. فوتون شامل یک سری css ,sass، فونت و قالبهای html است که به شما اجازه میدهد تا یک برنامه با ظاهری شبیه به برنامههای مک را داشته باشید. کامپوننتهای فوتون شامل tab ها، لیستها، منوی کناری، دکمههای معمولی یا جعبه ابزاری و کنترلهای فرمها میشود. با این حال اگر هم دوست ندارید که از این کامپوننتها استفاده کنید، میتوانید از layout های آن استفاده کند که پنجرهی شما را تقسیم بندی میکنند.
برای استفاده از فوتون لازم است آن را دانلود کرده و فایلهای آن را در پروژهی خود کپی کنید. دایرکتوری dist آن شامل یک مثال میشود که میتوانید آن را به داخل دایرکتوری پروژه خود کپی کنید؛ یا خود این دایرکتوری را به عنوان دایرکتوری پروژه تعیین کنید. بعد از آن، فایلهای موجود در دایرکتوری template را به داخل دایرکتوری والد، یعنی dist انتقال دهید و داخل فایل html، مسیر فایل css را تصحیح نمایید. فقط میماند که الکترون را بر روی این محل نصب کنید، یا اینکه الکترون نصب شدهی به صورت عمومی (Global) را در اختیار آن قرار دهید.
بعد از آن ممکن است با خطا مواجه شوید و وقتی فایل اصلی را که در اینجا نام آن app.js است، باز کنید، خطوط زیر را میبینید:
var app=require('app'); var BrowserWindow=require('browser-window');
سپس برنامه را اجرا کنید تا رابط جدید کاربری را ببینید.
فقط یک مشکلی هست و آن هم این است که باید فریم یا پنجرهای را که خود الکترون تولید میکند، حذف کنیم برای حذف آن میتوانید از خصوصیت frame در شیء Browser Window استفاده کنید:
if(process.platform=='darwin') { mainWindow= new BrowserWindow({ width: 1000, height: 500, 'min-width': 1000, 'min-height': 500, 'accept-first-mouse': true, 'title-bar-style': 'hidden', titleBarStyle:'hidden' }); } else { new BrowserWindow({ width: 1000, height: 500, 'min-width': 1000, 'min-height': 500, frame:false }); }
حالا اگر برنامه را مجددا اجرا کنید، میبینید که قابهای دور آن حذف شدهاند، ولی با چند ثانیه کار کردن متوجه این ایراد میشوید که پنجره قابل درگ کردن و جابجایی نمیباشد. برای حل آن باید از css کمک بگیریم:
-webkit-app-region: drag
-webkit-app-region: no-drag;
<header class="toolbar toolbar-header" >
<header class="toolbar toolbar-header" style="-webkit-app-region: drag">
همانطور که میبینید با کمترین زحمت، به چنین رابط کاربری رسیدید. تصویر زیر متعلق به برنامهای است که در دو قسمت قبلی (+ + ) ساختیم و حالا با استفاده از این پکیج، ظاهر آن را تغییر دادهایم:
Electron UI Kit
دومین رابط کاربری که معرفی میکنیم در واقع یک کیت از یک سری کامپوننت است که بسیار شبیه به برنامههای دسکتاپ طراحی شده و شامل لیستها، گریدها، کنترلها و ... است که در دو فایل استایل، برای ویندوز و مک، مجزا شدهاند.
Maverix
یک استایل تحت وب به نام Maverix است که البته در مورد برنامههای دسکتاپ و الکترون حرفی نزده و خود را فریمورکی برای استفاده در برنامههای تحت وب معرفی کرده است. ولی از آنجا که کنترلهای موجود آن بر اساس سیستم عامل مک ایجاد شدهاند، به راحتی میتوانند خود را بجای برنامههای دسکتاپ جا بزنند. میتوانید دموی آن را نیز ببینید.
تا اینجا تمام قسمتهای این سری، برای اساس اطلاعات یک کلاس Config استاتیک تشکیل شدهی در حافظه ارائه شدند. این روش برای دمو و توضیح مفاهیم پایهی IdentityServer بسیار مفید است؛ اما برای دنیای واقعی خیر. بنابراین در ادامه میخواهیم این قسمت را با اطلاعات ذخیره شدهی در بانک اطلاعاتی تعویض کنیم. یک روش مدیریت آن، نصب ASP.NET Core Identity دقیقا داخل همان پروژهی IDP است. در این حالت کدهای ASP.NET Core Identity مایکروسافت، کار مدیریت کاربران IDP را انجام میدهند. روش دیگر اینکار را که در اینجا بررسی خواهیم کرد، تغییر کدهای Quick Start UI اضافه شدهی در «قسمت چهارم - نصب و راه اندازی IdentityServer»، جهت پذیرفتن مدیریت کاربران مبتنی بر بانک اطلاعاتی تهیه شدهی توسط خودمان است. مزیت آن آشنا شدن بیشتر با کدهای Quick Start UI و درک زیرساخت آن است.
تکمیل ساختار پروژهی IDP
تا اینجا برای IDP، یک پروژهی خالی وب را ایجاد و به مرور، آنرا تکمیل کردیم. اما اکنون نیاز است پشتیبانی از بانک اطلاعاتی را نیز به آن اضافه کنیم. برای این منظور چهار پروژهی Class library کمکی را نیز به Solution آن اضافه میکنیم:
- DNT.IDP.DomainClasses
در این پروژه، کلاسهای متناظر با موجودیتهای جداول مرتبط با اطلاعات کاربران قرار میگیرند.
- DNT.IDP.DataLayer
این پروژه Context برنامه و Migrations آنرا تشکیل میدهد. همچنین به همراه تنظیمات و Seed اولیهی اطلاعات بانک اطلاعاتی نیز میباشد.
رشتهی اتصالی آن نیز در فایل DNT.IDP\appsettings.json ذخیره شدهاست.
- DNT.IDP.Common
الگوریتم هش کردن اطلاعات، در این پروژهی مشترک بین چند پروژهی دیگر قرار گرفتهاست. از آن جهت هش کردن کلمات عبور، در دو پروژهی DataLayer و همچنین Services استفاده میکنیم.
- DNT.IDP.Services
کلاس سرویس کاربران که با استفاده از DataLayer با بانک اطلاعاتی ارتباط برقرار میکند، در این پروژه قرار گرفتهاست.
ساختار بانک اطلاعاتی کاربران IdentityServer
در اینجا ساختار بانک اطلاعاتی کاربران IdentityServer، بر اساس جداول کاربران و Claims آنها تشکیل میشود:
در اینجا SubjectId همان Id کاربر، در سطح IDP است. این خاصیت به صورت یک کلید خارجی در جداول UserClaims و UserLogins نیز بکار میرود.
ساختار Claims او نیز به صورت زیر تعریف میشود که با تعریف یک Claim استاندارد، سازگاری دارد:
همچنین کاربر میتوان تعدادی لاگین نیز داشته باشد:
هدف از آن، یکپارچه سازی سیستم، با IDPهای ثالث مانند گوگل، توئیتر و امثال آنها است.
در پروژهی DNT.IDP.DataLayer در پوشهی Configurations آن، کلاسهای UserConfiguration و UserClaimConfiguration را مشاهده میکنید که حاوی اطلاعات اولیهای برای تشکیل User 1 و User 2 به همراه Claims آنها هستند. این اطلاعات را دقیقا از فایل استاتیک Config که در قسمتهای قبل تکمیل کردیم، به این دو کلاس جدید IEntityTypeConfiguration منتقل کردهایم تا به این ترتیب متد GetUsers فایل استاتیک Config را با نمونهی دیتابیسی آن جایگزین کنیم.
سرویسی که از طریق Context برنامه با بانک اطلاعاتی ارتباط برقرار میکند، چنین ساختاری را دارد:
که توسط آن امکان دسترسی به یک کاربر، اطلاعات Claims او و افزودن رکوردهایی جدید وجود دارد.
تنظیمات نهایی این سرویسها و Context برنامه نیز در فایل DNT.IDP\Startup.cs جهت معرفی به سیستم تزریق وابستگیها، صورت گرفتهاند. همچنین در اینجا متد initializeDb را نیز مشاهده میکنید که با فراخوانی متد context.Database.Migrate، تمام کلاسهای Migrations پروژهی DataLayer را به صورت خودکار به بانک اطلاعاتی اعمال میکند.
غیرفعال کردن صفحهی Consent در Quick Start UI
در «قسمت چهارم - نصب و راه اندازی IdentityServer» فایلهای Quick Start UI را به پروژهی IDP اضافه کردیم. در ادامه میخواهیم قدم به قدم این پروژه را تغییر دهیم.
در صفحهی Consent در Quick Start UI، لیست scopes درخواستی برنامهی کلاینت ذکر شده و سپس کاربر انتخاب میکند که کدامیک از آنها، باید به برنامهی کلاینت ارائه شوند. این صفحه، برای سناریوی ما که تمام برنامههای کلاینت توسط ما توسعه یافتهاند، بیمعنا است و صرفا برای کلاینتهای ثالثی که قرار است از IDP ما استفاده کنند، معنا پیدا میکند. برای غیرفعال کردن آن کافی است به فایل استاتیک Config مراجعه کرده و خاصیت RequireConsent کلاینت مدنظر را به false تنظیم کرد.
تغییر نام پوشهی Quickstart و سپس اصلاح فضای نام پیشفرض کنترلرهای آن
در حال حاضر کدهای کنترلرهای Quick Start UI داخل پوشهی Quickstart برنامهی IDP قرار گرفتهاند. با توجه به اینکه قصد داریم این کدها را تغییر دهیم و همچنین این پوشه در اساس، همان پوشهی استاندارد Controllers است، ابتدا نام این پوشه را به Controllers تغییر داده و سپس در تمام کنترلرهای ذیل آن، فضای نام پیشفرض IdentityServer4.Quickstart.UI را نیز به فضای نام متناسبی با پوشه بندی پروژهی جاری تغییر میدهیم. برای مثال کنترلر Account واقع در پوشهی Account، اینبار دارای فضای نام DNT.IDP.Controllers.Account خواهد شد و به همین ترتیب برای مابقی کنترلها عمل میکنیم.
پس از این تغییرات، عبارات using موجود در Viewها را نیز باید تغییر دهید تا برنامه در زمان اجرا به مشکلی برنخورد. البته ASP.NET Core 2.1 در زمان کامپایل برنامه، تمام Viewهای آنرا نیز کامپایل میکند و اگر خطایی در آنها وجود داشته باشد، امکان بررسی و رفع آنها پیش از اجرای برنامه، میسر است.
و یا میتوان جهت سهولت کار، فایل DNT.IDP\Views\_ViewImports.cshtml را جهت معرفی این فضاهای نام جدید ویرایش کرد تا نیازی به تغییر Viewها نباشد:
تعامل با IdentityServer از طریق کدهای سفارشی
پس از تشکیل «ساختار بانک اطلاعاتی کاربران IdentityServer» و همچنین تهیه سرویسهای متناظری جهت کار با آن، اکنون نیاز است مطمئن شویم IdentityServer از این بانک اطلاعاتی برای دریافت اطلاعات کاربران خود استفاده میکند.
در حال حاضر، با استفاده از متد الحاقی AddTestUsers معرفی شدهی در فایل DNT.IDP\Startup.cs، اطلاعات کاربران درون حافظهای برنامه را از متد ()Config.GetUsers دریافت میکنیم.
بنابراین اولین قدم، بررسی ساختار متد AddTestUsers است. برای این منظور به مخزن کد IdentityServer4 مراجعه کرده و کدهای متد الحاقی AddTestUsers را بررسی میکنیم:
- ابتدا یک TestUserStore را به صورت Singleton ثبت کردهاست.
- سپس سرویس پروفایل کاربران را اضافه کردهاست. این سرویس با پیاده سازی اینترفیس IProfileService تهیه میشود. کار آن اتصال یک User Store سفارشی به سرویس کاربران و دریافت اطلاعات پروفایل آنها مانند Claims است.
- در آخر TestUserResourceOwnerPasswordValidator، کار اعتبارسنجی کلمهی عبور و نام کاربری را در صورت استفادهی از Flow ویژهای به نام ResourceOwner که استفادهی از آن توصیه نمیشود (ROBC Flow)، انجام میدهد.
برای جایگزین کردن AddTestUsers، کلاس جدید IdentityServerBuilderExtensions را در ریشهی پروژهی IDP با محتوای ذیل اضافه میکنیم:
در اینجا ابتدا IUsersService سفارشی برنامه معرفی شدهاست که User Store سفارشی برنامه است. البته چون UsersService ما با بانک اطلاعاتی کار میکند، نباید به صورت Singleton ثبت شود و باید در پایان هر درخواست به صورت خودکار Dispose گردد. به همین جهت طول عمر آن Scoped تعریف شدهاست. در کل ضرورتی به ذکر این سطر نیست؛ چون پیشتر کار ثبت IUsersService در کلاس Startup برنامه انجام شدهاست.
سپس یک ProfileService سفارشی را ثبت کردهایم. این سرویس، با پیاده سازی IProfileService به صورت زیر پیاده سازی میشود:
سرویس پروفایل، توسط سرویس کاربران برنامه که در ابتدای مطلب آنرا تهیه کردیم، امکان دسترسی به اطلاعات پروفایل کاربران را مانند Claims او، پیدا میکند.
در متدهای آن، ابتدا subjectId و یا همان Id منحصربفرد کاربر جاری سیستم، دریافت شده و سپس بر اساس آن میتوان از usersService، جهت دریافت اطلاعات مختلف کاربر، کوئری گرفت و نتیجه را در خواص context جاری، برای استفادههای بعدی، ذخیره کرد.
اکنون به کلاس src\IDP\DNT.IDP\Startup.cs مراجعه کرده و متد AddTestUsers را با AddCustomUserStore جایگزین میکنیم:
تا اینجا فقط این سرویسهای جدید را ثبت کردهایم، اما هنوز کار خاصی را انجام نمیدهند و باید از آنها در برنامه استفاده کرد.
اتصال IdentityServer به User Store سفارشی
در ادامه، سازندهی کنترلر DNT.IDP\Quickstart\Account\AccountController.cs را بررسی میکنیم:
- سرویس توکار IIdentityServerInteractionService، کار تعامل برنامه با IdentityServer4 را انجام میدهد.
- IClientStore پیاده سازی محل ذخیره سازی اطلاعات کلاینتها را ارائه میدهد که در حال حاضر توسط متد استاتیک Config در اختیار آن قرار میگیرد.
- IEventService رخدادهایی مانند لاگین موفقیت آمیز یک کاربر را گزارش میدهد.
- در آخر، TestUserStore تزریق شدهاست که میخواهیم آنرا با User Store سفارشی خودمان جایگزین کنیم. بنابراین در ابتدا TestUserStore را با UserStore سفارشی خودمان جایگزین میکنیم:
فعلا فیلد TestUserStore را نیز سطح کلاس جاری باقی نگه میداریم. از این جهت که قسمتهای لاگین خارجی سیستم (استفاده از گوگل، توئیتر و ...) هنوز از آن استفاده میکنند و آنرا در قسمتی دیگر تغییر خواهیم داد.
پس از معرفی فیلد usersService_، اکنون در قسمت زیر از آن استفاده میکنیم:
در اکشن متد لاگین، جهت بررسی صحت نام کاربری و کلمهی عبور و همچنین یافتن کاربر متناظر با آن:
تا همینجا برنامه را کامپایل کرده و اجرا کنید. پس از لاگین در آدرس https://localhost:5001/Gallery/IdentityInformation، هنوز اطلاعات User Claims کاربر وارد شدهی به سیستم نمایش داده میشوند که بیانگر صحت عملکرد CustomUserProfileService است.
افزودن امکان ثبت کاربران جدید به برنامهی IDP
پس از اتصال قسمت login برنامهی IDP به بانک اطلاعاتی، اکنون میخواهیم امکان ثبت کاربران را نیز به آن اضافه کنیم.
این قسمت شامل تغییرات ذیل است:
الف) اضافه شدن RegisterUserViewModel
این ViewModel که فیلدهای فرم ثبتنام را تشکیل میدهد، ابتدا با نام کاربری و کلمهی عبور شروع میشود:
سپس سایر خواصی که در اینجا اضافه میشوند:
در کنترلر UserRegistrationController، تبدیل به UserClaims شده و در جدول مخصوص آن ذخیره خواهند شد.
ب) افزودن UserRegistrationController
این کنترلر، RegisterUserViewModel را دریافت کرده و سپس بر اساس آن، شیء User ابتدای بحث را تشکیل میدهد. ابتدا نام کاربری و کلمهی عبور را در جدول کاربران ثبت میکند و سپس سایر خواص این ViewModel را در جدول UserClaims:
ج) افزودن RegisterUser.cshtml
این فایل، view متناظر با ViewModel فوق را ارائه میدهد که توسط آن، کاربری میتواند اطلاعات خود را ثبت کرده و وارد سیستم شود.
د) اصلاح فایل ViewImports.cshtml_ جهت تعریف فضای نام UserRegistration
در RegisterUser.cshtml از RegisterUserViewModel استفاده میشود. به همین جهت بهتر است فضای نام آنرا به ViewImports اضافه کرد.
ه) افزودن لینک ثبت نام به صفحهی لاگین در Login.cshtml
این لینک دقیقا در ذیل چکباکس Remember My Login اضافه شدهاست.
اکنون اگر برنامه را اجرا کنیم، ابتدا مشاهده میکنیم که صفحهی لاگین به همراه لینک ثبت نام ظاهر میشود:
و پس از کلیک بر روی آن، صفحهی ثبت کاربر جدید به صورت زیر نمایش داده خواهد شد:
برای آزمایش، کاربری را ثبت کنید. پس از ثبت اطلاعات، بلافاصله وارد سیستم خواهید شد. البته چون در اینجا subscriptionlevel به FreeUser تنظیم شدهاست، این کاربر یکسری از لینکهای برنامهی MVC Client را به علت نداشتن دسترسی، مشاهده نخواهد کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.
تکمیل ساختار پروژهی IDP
تا اینجا برای IDP، یک پروژهی خالی وب را ایجاد و به مرور، آنرا تکمیل کردیم. اما اکنون نیاز است پشتیبانی از بانک اطلاعاتی را نیز به آن اضافه کنیم. برای این منظور چهار پروژهی Class library کمکی را نیز به Solution آن اضافه میکنیم:
- DNT.IDP.DomainClasses
در این پروژه، کلاسهای متناظر با موجودیتهای جداول مرتبط با اطلاعات کاربران قرار میگیرند.
- DNT.IDP.DataLayer
این پروژه Context برنامه و Migrations آنرا تشکیل میدهد. همچنین به همراه تنظیمات و Seed اولیهی اطلاعات بانک اطلاعاتی نیز میباشد.
رشتهی اتصالی آن نیز در فایل DNT.IDP\appsettings.json ذخیره شدهاست.
- DNT.IDP.Common
الگوریتم هش کردن اطلاعات، در این پروژهی مشترک بین چند پروژهی دیگر قرار گرفتهاست. از آن جهت هش کردن کلمات عبور، در دو پروژهی DataLayer و همچنین Services استفاده میکنیم.
- DNT.IDP.Services
کلاس سرویس کاربران که با استفاده از DataLayer با بانک اطلاعاتی ارتباط برقرار میکند، در این پروژه قرار گرفتهاست.
ساختار بانک اطلاعاتی کاربران IdentityServer
در اینجا ساختار بانک اطلاعاتی کاربران IdentityServer، بر اساس جداول کاربران و Claims آنها تشکیل میشود:
namespace DNT.IDP.DomainClasses { public class User { [Key] [MaxLength(50)] public string SubjectId { get; set; } [MaxLength(100)] [Required] public string Username { get; set; } [MaxLength(100)] public string Password { get; set; } [Required] public bool IsActive { get; set; } public ICollection<UserClaim> UserClaims { get; set; } public ICollection<UserLogin> UserLogins { get; set; } } }
ساختار Claims او نیز به صورت زیر تعریف میشود که با تعریف یک Claim استاندارد، سازگاری دارد:
namespace DNT.IDP.DomainClasses { public class UserClaim { public int Id { get; set; } [MaxLength(50)] [Required] public string SubjectId { get; set; } public User User { get; set; } [Required] [MaxLength(250)] public string ClaimType { get; set; } [Required] [MaxLength(250)] public string ClaimValue { get; set; } } }
namespace DNT.IDP.DomainClasses { public class UserLogin { public int Id { get; set; } [MaxLength(50)] [Required] public string SubjectId { get; set; } public User User { get; set; } [Required] [MaxLength(250)] public string LoginProvider { get; set; } [Required] [MaxLength(250)] public string ProviderKey { get; set; } } }
در پروژهی DNT.IDP.DataLayer در پوشهی Configurations آن، کلاسهای UserConfiguration و UserClaimConfiguration را مشاهده میکنید که حاوی اطلاعات اولیهای برای تشکیل User 1 و User 2 به همراه Claims آنها هستند. این اطلاعات را دقیقا از فایل استاتیک Config که در قسمتهای قبل تکمیل کردیم، به این دو کلاس جدید IEntityTypeConfiguration منتقل کردهایم تا به این ترتیب متد GetUsers فایل استاتیک Config را با نمونهی دیتابیسی آن جایگزین کنیم.
سرویسی که از طریق Context برنامه با بانک اطلاعاتی ارتباط برقرار میکند، چنین ساختاری را دارد:
public interface IUsersService { Task<bool> AreUserCredentialsValidAsync(string username, string password); Task<User> GetUserByEmailAsync(string email); Task<User> GetUserByProviderAsync(string loginProvider, string providerKey); Task<User> GetUserBySubjectIdAsync(string subjectId); Task<User> GetUserByUsernameAsync(string username); Task<IEnumerable<UserClaim>> GetUserClaimsBySubjectIdAsync(string subjectId); Task<IEnumerable<UserLogin>> GetUserLoginsBySubjectIdAsync(string subjectId); Task<bool> IsUserActiveAsync(string subjectId); Task AddUserAsync(User user); Task AddUserLoginAsync(string subjectId, string loginProvider, string providerKey); Task AddUserClaimAsync(string subjectId, string claimType, string claimValue); }
تنظیمات نهایی این سرویسها و Context برنامه نیز در فایل DNT.IDP\Startup.cs جهت معرفی به سیستم تزریق وابستگیها، صورت گرفتهاند. همچنین در اینجا متد initializeDb را نیز مشاهده میکنید که با فراخوانی متد context.Database.Migrate، تمام کلاسهای Migrations پروژهی DataLayer را به صورت خودکار به بانک اطلاعاتی اعمال میکند.
غیرفعال کردن صفحهی Consent در Quick Start UI
در «قسمت چهارم - نصب و راه اندازی IdentityServer» فایلهای Quick Start UI را به پروژهی IDP اضافه کردیم. در ادامه میخواهیم قدم به قدم این پروژه را تغییر دهیم.
در صفحهی Consent در Quick Start UI، لیست scopes درخواستی برنامهی کلاینت ذکر شده و سپس کاربر انتخاب میکند که کدامیک از آنها، باید به برنامهی کلاینت ارائه شوند. این صفحه، برای سناریوی ما که تمام برنامههای کلاینت توسط ما توسعه یافتهاند، بیمعنا است و صرفا برای کلاینتهای ثالثی که قرار است از IDP ما استفاده کنند، معنا پیدا میکند. برای غیرفعال کردن آن کافی است به فایل استاتیک Config مراجعه کرده و خاصیت RequireConsent کلاینت مدنظر را به false تنظیم کرد.
تغییر نام پوشهی Quickstart و سپس اصلاح فضای نام پیشفرض کنترلرهای آن
در حال حاضر کدهای کنترلرهای Quick Start UI داخل پوشهی Quickstart برنامهی IDP قرار گرفتهاند. با توجه به اینکه قصد داریم این کدها را تغییر دهیم و همچنین این پوشه در اساس، همان پوشهی استاندارد Controllers است، ابتدا نام این پوشه را به Controllers تغییر داده و سپس در تمام کنترلرهای ذیل آن، فضای نام پیشفرض IdentityServer4.Quickstart.UI را نیز به فضای نام متناسبی با پوشه بندی پروژهی جاری تغییر میدهیم. برای مثال کنترلر Account واقع در پوشهی Account، اینبار دارای فضای نام DNT.IDP.Controllers.Account خواهد شد و به همین ترتیب برای مابقی کنترلها عمل میکنیم.
پس از این تغییرات، عبارات using موجود در Viewها را نیز باید تغییر دهید تا برنامه در زمان اجرا به مشکلی برنخورد. البته ASP.NET Core 2.1 در زمان کامپایل برنامه، تمام Viewهای آنرا نیز کامپایل میکند و اگر خطایی در آنها وجود داشته باشد، امکان بررسی و رفع آنها پیش از اجرای برنامه، میسر است.
و یا میتوان جهت سهولت کار، فایل DNT.IDP\Views\_ViewImports.cshtml را جهت معرفی این فضاهای نام جدید ویرایش کرد تا نیازی به تغییر Viewها نباشد:
@using DNT.IDP.Controllers.Account; @using DNT.IDP.Controllers.Consent; @using DNT.IDP.Controllers.Grants; @using DNT.IDP.Controllers.Home; @using DNT.IDP.Controllers.Diagnostics; @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
تعامل با IdentityServer از طریق کدهای سفارشی
پس از تشکیل «ساختار بانک اطلاعاتی کاربران IdentityServer» و همچنین تهیه سرویسهای متناظری جهت کار با آن، اکنون نیاز است مطمئن شویم IdentityServer از این بانک اطلاعاتی برای دریافت اطلاعات کاربران خود استفاده میکند.
در حال حاضر، با استفاده از متد الحاقی AddTestUsers معرفی شدهی در فایل DNT.IDP\Startup.cs، اطلاعات کاربران درون حافظهای برنامه را از متد ()Config.GetUsers دریافت میکنیم.
بنابراین اولین قدم، بررسی ساختار متد AddTestUsers است. برای این منظور به مخزن کد IdentityServer4 مراجعه کرده و کدهای متد الحاقی AddTestUsers را بررسی میکنیم:
public static class IdentityServerBuilderExtensions { public static IIdentityServerBuilder AddTestUsers(this IIdentityServerBuilder builder, List<TestUser> users) { builder.Services.AddSingleton(new TestUserStore(users)); builder.AddProfileService<TestUserProfileService>(); builder.AddResourceOwnerValidator<TestUserResourceOwnerPasswordValidator>(); return builder; } }
- سپس سرویس پروفایل کاربران را اضافه کردهاست. این سرویس با پیاده سازی اینترفیس IProfileService تهیه میشود. کار آن اتصال یک User Store سفارشی به سرویس کاربران و دریافت اطلاعات پروفایل آنها مانند Claims است.
- در آخر TestUserResourceOwnerPasswordValidator، کار اعتبارسنجی کلمهی عبور و نام کاربری را در صورت استفادهی از Flow ویژهای به نام ResourceOwner که استفادهی از آن توصیه نمیشود (ROBC Flow)، انجام میدهد.
برای جایگزین کردن AddTestUsers، کلاس جدید IdentityServerBuilderExtensions را در ریشهی پروژهی IDP با محتوای ذیل اضافه میکنیم:
using DNT.IDP.Services; using Microsoft.Extensions.DependencyInjection; namespace DNT.IDP { public static class IdentityServerBuilderExtensions { public static IIdentityServerBuilder AddCustomUserStore(this IIdentityServerBuilder builder) { // builder.Services.AddScoped<IUsersService, UsersService>(); builder.AddProfileService<CustomUserProfileService>(); return builder; } } }
سپس یک ProfileService سفارشی را ثبت کردهایم. این سرویس، با پیاده سازی IProfileService به صورت زیر پیاده سازی میشود:
namespace DNT.IDP.Services { public class CustomUserProfileService : IProfileService { private readonly IUsersService _usersService; public CustomUserProfileService(IUsersService usersService) { _usersService = usersService; } public async Task GetProfileDataAsync(ProfileDataRequestContext context) { var subjectId = context.Subject.GetSubjectId(); var claimsForUser = await _usersService.GetUserClaimsBySubjectIdAsync(subjectId); context.IssuedClaims = claimsForUser.Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToList(); } public async Task IsActiveAsync(IsActiveContext context) { var subjectId = context.Subject.GetSubjectId(); context.IsActive = await _usersService.IsUserActiveAsync(subjectId); } } }
در متدهای آن، ابتدا subjectId و یا همان Id منحصربفرد کاربر جاری سیستم، دریافت شده و سپس بر اساس آن میتوان از usersService، جهت دریافت اطلاعات مختلف کاربر، کوئری گرفت و نتیجه را در خواص context جاری، برای استفادههای بعدی، ذخیره کرد.
اکنون به کلاس src\IDP\DNT.IDP\Startup.cs مراجعه کرده و متد AddTestUsers را با AddCustomUserStore جایگزین میکنیم:
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddDeveloperSigningCredential() .AddCustomUserStore() .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients());
اتصال IdentityServer به User Store سفارشی
در ادامه، سازندهی کنترلر DNT.IDP\Quickstart\Account\AccountController.cs را بررسی میکنیم:
public AccountController( IIdentityServerInteractionService interaction, IClientStore clientStore, IAuthenticationSchemeProvider schemeProvider, IEventService events, TestUserStore users = null) { _users = users ?? new TestUserStore(TestUsers.Users); _interaction = interaction; _clientStore = clientStore; _schemeProvider = schemeProvider; _events = events; }
- IClientStore پیاده سازی محل ذخیره سازی اطلاعات کلاینتها را ارائه میدهد که در حال حاضر توسط متد استاتیک Config در اختیار آن قرار میگیرد.
- IEventService رخدادهایی مانند لاگین موفقیت آمیز یک کاربر را گزارش میدهد.
- در آخر، TestUserStore تزریق شدهاست که میخواهیم آنرا با User Store سفارشی خودمان جایگزین کنیم. بنابراین در ابتدا TestUserStore را با UserStore سفارشی خودمان جایگزین میکنیم:
private readonly TestUserStore _users; private readonly IUsersService _usersService; public AccountController( // ... IUsersService usersService) { _usersService = usersService; // ... }
پس از معرفی فیلد usersService_، اکنون در قسمت زیر از آن استفاده میکنیم:
در اکشن متد لاگین، جهت بررسی صحت نام کاربری و کلمهی عبور و همچنین یافتن کاربر متناظر با آن:
public async Task<IActionResult> Login(LoginInputModel model, string button) { //... if (ModelState.IsValid) { if (await _usersService.AreUserCredentialsValidAsync(model.Username, model.Password)) { var user = await _usersService.GetUserByUsernameAsync(model.Username);
افزودن امکان ثبت کاربران جدید به برنامهی IDP
پس از اتصال قسمت login برنامهی IDP به بانک اطلاعاتی، اکنون میخواهیم امکان ثبت کاربران را نیز به آن اضافه کنیم.
این قسمت شامل تغییرات ذیل است:
الف) اضافه شدن RegisterUserViewModel
این ViewModel که فیلدهای فرم ثبتنام را تشکیل میدهد، ابتدا با نام کاربری و کلمهی عبور شروع میشود:
public class RegisterUserViewModel { // credentials [MaxLength(100)] public string Username { get; set; } [MaxLength(100)] public string Password { get; set; }
public class RegisterUserViewModel { // ... // claims [Required] [MaxLength(100)] public string Firstname { get; set; } [Required] [MaxLength(100)] public string Lastname { get; set; } [Required] [MaxLength(150)] public string Email { get; set; } [Required] [MaxLength(200)] public string Address { get; set; } [Required] [MaxLength(2)] public string Country { get; set; }
ب) افزودن UserRegistrationController
این کنترلر، RegisterUserViewModel را دریافت کرده و سپس بر اساس آن، شیء User ابتدای بحث را تشکیل میدهد. ابتدا نام کاربری و کلمهی عبور را در جدول کاربران ثبت میکند و سپس سایر خواص این ViewModel را در جدول UserClaims:
varuserToCreate=newUser { Password=model.Password.GetSha256Hash(), Username=model.Username, IsActive=true }; userToCreate.UserClaims.Add(newUserClaim("country",model.Country)); userToCreate.UserClaims.Add(newUserClaim("address",model.Address)); userToCreate.UserClaims.Add(newUserClaim("given_name",model.Firstname)); userToCreate.UserClaims.Add(newUserClaim("family_name",model.Lastname)); userToCreate.UserClaims.Add(newUserClaim("email",model.Email)); userToCreate.UserClaims.Add(newUserClaim("subscriptionlevel","FreeUser"));
این فایل، view متناظر با ViewModel فوق را ارائه میدهد که توسط آن، کاربری میتواند اطلاعات خود را ثبت کرده و وارد سیستم شود.
د) اصلاح فایل ViewImports.cshtml_ جهت تعریف فضای نام UserRegistration
در RegisterUser.cshtml از RegisterUserViewModel استفاده میشود. به همین جهت بهتر است فضای نام آنرا به ViewImports اضافه کرد.
ه) افزودن لینک ثبت نام به صفحهی لاگین در Login.cshtml
این لینک دقیقا در ذیل چکباکس Remember My Login اضافه شدهاست.
اکنون اگر برنامه را اجرا کنیم، ابتدا مشاهده میکنیم که صفحهی لاگین به همراه لینک ثبت نام ظاهر میشود:
و پس از کلیک بر روی آن، صفحهی ثبت کاربر جدید به صورت زیر نمایش داده خواهد شد:
برای آزمایش، کاربری را ثبت کنید. پس از ثبت اطلاعات، بلافاصله وارد سیستم خواهید شد. البته چون در اینجا subscriptionlevel به FreeUser تنظیم شدهاست، این کاربر یکسری از لینکهای برنامهی MVC Client را به علت نداشتن دسترسی، مشاهده نخواهد کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.
یک نکتهی تکمیلی
کتابخانهی « DNTScanner.Core » امکان کار با اسکنر را در برنامههای NET 4x. و همچنین NET Core. ویندوزی میسر میکند. روشی که در آن مورد استفاده قرار گرفته، مشکلات مطرح شدهی در نظرات مطلب جاری را مانند عدم امکان استفادهی از آن، در سرویسهای پسزمینه را ندارد؛ از این جهت که برای دسترسی به اسکنر، هیچ نوع UI ای را نمایش نمیدهد و تمام تنظیمات آن با کدنویسی است.
مثالها
- روش استفاده از آن در برنامههای کنسول
- روش استفادهی از آن در یک برنامهی وب ASP.NET Core که قسمت اسکنر آن به صورت یک کلاینت کنسول تهیه شدهاست و ارتباط بین این دو از طریق SignalR.Core برقرار میشود.
کتابخانهی « DNTScanner.Core » امکان کار با اسکنر را در برنامههای NET 4x. و همچنین NET Core. ویندوزی میسر میکند. روشی که در آن مورد استفاده قرار گرفته، مشکلات مطرح شدهی در نظرات مطلب جاری را مانند عدم امکان استفادهی از آن، در سرویسهای پسزمینه را ندارد؛ از این جهت که برای دسترسی به اسکنر، هیچ نوع UI ای را نمایش نمیدهد و تمام تنظیمات آن با کدنویسی است.
مثالها
- روش استفاده از آن در برنامههای کنسول
- روش استفادهی از آن در یک برنامهی وب ASP.NET Core که قسمت اسکنر آن به صورت یک کلاینت کنسول تهیه شدهاست و ارتباط بین این دو از طریق SignalR.Core برقرار میشود.
نظرات مطالب
مدیریت سراسری خطاها در یک برنامهی Angular
- در مورد اینکه چه استثناهایی باید مدیریت شوند یا خیر، مطلب «نکات کار با استثناءها در دات نت» را مطالعه کنید.
- علت عمل نکردن فیلتری که به آن لینک دادید (که من با آن موافق نیستم)، این است که دیگر نباید از میانافزار مدیریت استثناهای مخصوص توسعه دهندههای ASP.NET Core در این حالت استفاده کنید، چون با آن تداخل میکند و پیش از آن وارد عمل میشود. علت دریافت صفحهی HTML ایی که مشاهده میکنید، همین مورد است. این صفحه برای برنامههای ASP.NET Core دارای Viewهای Razor طراحی شدهاست و نه مخصوص حالت کار صرفا Web API آن.
- یکی از مشکلات آن فیلتر هم این است که به هیچ عنوان نباید اصل خطای رخدادهی در سمت سرور را به سمت کلاینت ارسال کرد و به کاربر نمایش داد. این مورد امکان دیباگ از راه دور برنامهی شما را توسط یک مهاجم سهولت میبخشد و از دیدگاه امنیتی اشتباه است. این موارد را فقط باید توسط امکانات Logging توکار ASP.NET Core ثبت و در سمت سرور با «دسترسی ادمین» بررسی کنید. کاربر هم فقط باید جملهی کلی «خطایی رخ دادهاست» را مشاهده کند و نه جزئیات آنرا.
- علت عمل نکردن فیلتری که به آن لینک دادید (که من با آن موافق نیستم)، این است که دیگر نباید از میانافزار مدیریت استثناهای مخصوص توسعه دهندههای ASP.NET Core در این حالت استفاده کنید، چون با آن تداخل میکند و پیش از آن وارد عمل میشود. علت دریافت صفحهی HTML ایی که مشاهده میکنید، همین مورد است. این صفحه برای برنامههای ASP.NET Core دارای Viewهای Razor طراحی شدهاست و نه مخصوص حالت کار صرفا Web API آن.
- یکی از مشکلات آن فیلتر هم این است که به هیچ عنوان نباید اصل خطای رخدادهی در سمت سرور را به سمت کلاینت ارسال کرد و به کاربر نمایش داد. این مورد امکان دیباگ از راه دور برنامهی شما را توسط یک مهاجم سهولت میبخشد و از دیدگاه امنیتی اشتباه است. این موارد را فقط باید توسط امکانات Logging توکار ASP.NET Core ثبت و در سمت سرور با «دسترسی ادمین» بررسی کنید. کاربر هم فقط باید جملهی کلی «خطایی رخ دادهاست» را مشاهده کند و نه جزئیات آنرا.
نظرات مطالب
بررسی امنیتی، حین استفاده از jQuery Ajax
با تشکر از مطالب پر بارتون
مهندس جهت تکمیل این بحث و کامل شدن مسئله امنیتی میتوان این مطلب شما را نیز به عنوان تکمیل کننده این بحث در نظر گرفت؟
حذف هدرهای مربوط به وب سرور از یک برنامهی ASP.Net
مهندس جهت تکمیل این بحث و کامل شدن مسئله امنیتی میتوان این مطلب شما را نیز به عنوان تکمیل کننده این بحث در نظر گرفت؟
حذف هدرهای مربوط به وب سرور از یک برنامهی ASP.Net