مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت اول - نیاز به تامین کننده‌ی هویت مرکزی
عصر Thick Clients

امن سازی برنامه‌های وب همواره چالش برانگیز بوده‌است؛ خصوصا این روزها که نیاز است برنامه‌ها، خارج از دیوارهای یک شرکت نیز در دسترس باشند و توسط انواع و اقسام وسایل ارتباطی مورد استفاده قرار گیرند. در سال‌های قبل، عموما برنامه‌های thick clients مانند WPF و WinForms برای شرکت‌ها توسعه داده می‌شدند و یا برنامه‌های وب مانند ASP.NET Web Forms که مبتنی بر سرویس‌ها نبودند. در برنامه‌های ویندوزی، پس از لاگین شخص به شبکه و دومین داخلی شرکت، عموما از روش Windows Authentication برای مشخص سازی سطوح دسترسی کاربران استفاده می‌شود. در برنامه‌های Web Forms نیز بیشتر روش Forms Authentication برای اعتبارسنجی کاربران مرسوم است. امن سازی این نوع برنامه‌ها ساده‌است. عموما بر روی یک دومین ارائه می‌شوند و کوکی‌های اعتبارسنجی کاربران برای ارائه‌ی مباحثی مانند single sign-on (داشتن تنها یک صفحه‌ی لاگین برای دسترسی به تمام برنامه‌های شرکت)، میسر است.


عصر شروع به‌کارگیری سرویس‌های وب

در سال‌های اخیر این شیوه‌ی کاری تغییر کرده و بیشتر بر اساس بکارگیری برنامه‌های مبتنی بر سرویس‌ها شده‌است. برای مثال برای این مورد استاندارد WS-Security مربوط به WCF ارائه شده‌است که باز هم مرتبط است به برنامه‌های یک دومین و یا یک Application pool. اگر تعداد دومین‌ها بیشتر شوند و نیاز به ارتباط امن بین آن‌ها باشد، استاندارد SAML 2.0 مورد استفاده قرار می‌گرفت که هدف از آن، انتقال امن اعتبارسنجی و سطوح دسترسی کاربران بین دومین‌های مختلف است. همانطور که ملاحظه می‌کنید تمام این برنامه‌ها و استانداردها، داخل دیوارهای یک شرکت و یک دومین زندگی می‌کنند.


عصر شروع به‌کارگیری Restful-API's

پس از آن باز هم شیوه‌ی طراحی برنامه‌های تغییر کرد و شروع به ایجاد Restful-API's و HTTP API's کردیم. این‌ها دیگر الزاما داخل یک دومین ارائه نمی‌شوند و گاهی از اوقات حتی تحت کنترل ما هم نیستند. همچنین برنامه‌های ارائه شده نیز دیگر thick clients نیستند و ممکن است برنامه‌های سمت کلاینت Angular و یا حتی موبایل باشند که مستقیما با سرویس‌های API برنامه‌ها کار می‌کنند. حتی ممکن است یک API را طراحی کنیم که با یک API دیگر کار می‌کند.


در این حالت دیگر نمی‌توان این APIها را با نگهداری آن‌ها داخل دیوارهای یک شرکت محافظت کرد. اگر قرار است با یک HTTP API کار کنیم، این API باید عمومی باشد و در اینجا دیگر نمی‌توان از روش Forms Authentication استفاده کرد. زیرا این روش اعتبارسنجی مختص برنامه‌های سمت سرور قرار گرفته‌ی در یک دومین طراحی شده‌است و آنچنان سازگاری با برنامه‌های سمت کلاینت و موبایل خارج از دیوارهای آن ندارد. همچنین ارسال نام کاربری و کلمه‌ی عبور به ازای هر درخواست نیز روشی بسیار بدوی و نا امن است. اینجا است که عصر امن سازی برنامه‌ها به کمک توکن‌ها شروع می‌شود. با استفاده‌ی از توکن‌ها، بجای هر بار ارسال نام کاربری و کلمه‌ی عبور به ازای هر درخواست از API، صرفا لیست سطوح دسترسی امضا شده‌ی به امکاناتی خاص، ارسال می‌شوند.


عصر شروع به‌کارگیری Security Tokens

بنابراین در اینجا نیاز به برنامه‌ای برای تولید توکن‌ها و ارسال آن‌ها به کلاینت‌ها داریم. روش متداول پیاده سازی آن، ساخت یک برنامه‌ی ابتدایی، برای دریافت نام کاربری و کلمه‌ی عبور از کاربران و سپس بازگشت یک JSON Web Token به آن‌ها است که بیانگر سطوح دسترسی آن‌ها به قسمت‌های مختلف برنامه است و کاربران باید این توکن را به ازای هر درخواستی به سمت سرور (بجای نام کاربری و کلمه‌ی عبور و خود) ارسال کنند.
مشکل این روش در اینجا است که آن برنامه‌ی خاص، باید از نام کاربری و کلمه‌ی عبور کاربران مطلع باشد تا بتواند توکن مناسبی را برای آن کاربر خاص تولید کند. هر چند این روش برای یک تک برنامه‌ی خاص بسیار مناسب به نظر می‌رسد، اما در یک شرکت، ده‌ها برنامه مشغول به کارند و به اشتراک گذاری نام کاربری و کلمه‌ی عبور کاربران، با تک تک آن‌ها ایده‌ی مناسبی نیست و پس از مدتی از کنترل خارج خواهد شد. برای مثال کاربری در یک برنامه، کلمه‌ی عبور خود را تغییر می‌دهد اما در برنامه‌ای دیگر خیر و همین مساله‌ی عدم هماهنگی بین برنامه‌ها و همچنین بخش‌های مختلف یک شرکت، مدیریت یک دست برنامه‌ها را تقریبا غیر ممکن می‌کند. همچنین در اینجا برنامه‌های ثالث را نیز باید در نظر داشت که آیا ضروری است آن‌ها به ریز اطلاعات کاربران شرکت دسترسی پیدا کنند؟
به علاوه مشکل دیگر توسعه‌ی این نوع برنامه‌های صدور توکن خانگی، اختراع مجدد چرخ است. در اینجا برای بهبود امنیت برنامه باید منقضی شدن، تمدید، امضای دیجیتال و اعتبارسنجی توکن‌ها را خودمان پیاده سازی کنیم. توسعه‌ی یک چنین سیستمی اگر غیرممکن نباشد، بسیار سخت و پیچیده است و همچنین باید باگ‌های امنیتی ممکن را نیز مدنظر داشت.


بنابراین تا اینجا به این نتیجه رسیده‌ایم که دیگر نمی‌خواهیم مدیریت نام کاربری و کلمه‌ی عبور کاربران را در سطح هیچکدام از برنامه‌های خود انجام دهیم و هیچکدام از آن‌ها قرار نیست دریافت کننده‌ی این اطلاعات باشند. قرار است این کار، به یک تک برنامه‌ی مرکزی مخصوص اینکار منتقل شود و برای اینکار نیاز به پروتکلی امن است که بتوان این توکن‌های تولیدی را ارسال و پردازش کرد.


حرکت به سمت «تامین کننده‌ی هویت مرکزی»


در گذشته، هر تک برنامه‌ای دارای صفحه‌ی لاگین و امکانات مدیریت کاربران آن، تغییر کلمه‌ی عبور، تنظیم مجدد آن و این‌گونه عملیات بود. این‌روزها دیگر چنین کاری مرسوم نیست. این وظیفه‌ی برنامه‌ی شما نیست که بررسی کند کاربر وارد شده‌ی به سیستم کیست و آیا ادعای او صحیح است یا خیر؟ این نوع عملیات وظیفه‌ی یک Identity provider و یا به اختصار IDP است. کار IDP اعتبارسنجی کاربر و در صورت نیاز، ارائه‌ی اثبات هویت کاربر، به برنامه‌ی درخواست کننده‌است.
در یک IDP عملیاتی مانند ثبت کاربران و مدیریت آن‌ها انجام می‌شود. اینجا است که مفاهیمی مانند قفل کردن اکانت و یا تغییر کلمه‌ی عبور و امثال آن انجام می‌شود و نه اینکه به ازای هر برنامه‌ی تهیه شده‌ی برای یک شرکت، آن برنامه راسا اقدام به انجام چنین عملیاتی کند. به این ترتیب می‌توان به امکان استفاده‌ی مجدد از اطلاعات هویت کاربران و سطوح دسترسی آن‌ها در بین تمام برنامه‌های موجود رسید.
همچنین با داشتن یک برنامه‌ی IDP خوب پیاده سازی شده، از توزیع باگ‌های امنیتی مختلف در بین برنامه‌های گوناگون تهیه شده که هر کدام سیستم امنیتی خاص خودشان را دارند، جلوگیری خواهد شد. برای مثال فرض کنید می‌خواهید الگوریتم هش کردن پسوردهای سیستم را که امروز نا امن اعلام شده‌است، تغییر دهید. با داشتن یک IDP، دیگر نیازی نیست تا تمام برنامه‌های خود را برای رفع یک چنین باگ‌هایی، تک تک تغییر دهید.


به علاوه این روزها روش استفاده‌ی از نام کاربری و کلمه‌ی عبور، تنها راه ورود به یک سیستم نیست و استفاده از کلیدهای دیجیتال و یا روش‌های ویژه‌ی ابزارهای موبایل نیز به این لیست اضافه شده‌اند.


حرکت به سمت استاندارد OAuth 2

OAuth 2.0 پروتکلی است استاندارد، برای Authorization امن کاربران، توسط برنامه‌های وب، موبایل و دسکتاپ. به این ترتیب می‌توان امکان دسترسی یک برنامه را به یک API، به نحوی استاندارد و امن میسر ساخت. OAuth 2.0 یک توکن دسترسی (Access token) را تولید می‌کند و در اختیار کلاینت قرار می‌دهد. سپس آن کلاینت با ارسال این توکن به API مدنظر، امکان دسترسی به امکانات مختلف آن‌را خواهد یافت. به علاوه چون ماهیت برنامه‌های کلاینت وب و غیر وب متفاوت است، این استاندارد نحوه‌ی دریافت چنین توکنی را برای برنامه‌های مختلف نیز تعریف می‌کند. به این ترتیب موارد مشترکی مانند تولید و نحوه‌ی انتقال توکن‌ها به کلاینت‌های مختلف توسط این پروتکل استاندارد بیان می‌شود. در این حالت راه‌حل‌های خانگی ما تبدیل به راه‌حل‌هایی می‌شوند که استاندارد OAuth 2.0 را پیاده سازی کرده باشند. بنابراین IDP ما باید بر مبنای این استاندارد تهیه شده باشد. برای مثال IdentityServer که در این سری بررسی خواهد شد و یا Azure Active Directory، نمونه‌ای از IDPهایی هستند که این استاندارد را کاملا پیاده سازی کرده‌اند.
البته باید دقت داشت که این توکن‌های دسترسی، تنها سطوح دسترسی به منابع API را مشخص می‌کنند و یا به عبارتی عملیات Authorization توسط آن‌ها میسر می‌شود. عملیات ورود به سیستم اصطلاحا Authentication نام دارد و توسط استاندارد دیگری به نام OpenID Connect مدیریت می‌شود.


حرکت به سمت استاندارد OpenID Connect


OpenID Connect یک لایه‌ی امنیتی بر فراز پروتکل OAuth 2.0 است که به اختصار به آن OIDC نیز گفته می‌شود. توسط آن یک کلاینت می‌تواند یک Identity token را علاوه بر Access token درخواست کند. از این Identity token برای ورود به برنامه‌ی کلاینت استفاده می‌شود (Authentication) و پس از آن، برنامه‌ی کلاینت بر اساس سطوح دسترسی تعریف شده‌ی در Access token، امکان دسترسی به امکانات مختلف یک API را خواهد یافت (Authorization). همچنین OpenID Connect امکان دسترسی به اطلاعات بیشتری از یک کاربر را نیز میسر می‌کند.
بنابراین OpenID Connect پروتکلی است که در عمل استفاده می‌شود و توسعه دهنده و جایگزین کننده‌ی پروتکل OAuth 2.0 می‌باشد. هرچند ممکن است در بسیاری از منابع صرفا به OAuth 2.0 بپردازند، اما در پشت صحنه با همان OpenID Connect کار می‌کنند.
مزیت دیگر کار با OpenID Connect، عدم الزام به استفاده‌ی از API، در برنامه‌ای خاص و یا قدیمی است. اگر برنامه‌ی وب شما با هیچ نوع API ایی کار نمی‌کند، باز هم می‌توانید از امکانات OpenID Connect بهره‌مند شوید.
مطالب
اتصال و کار با SQL Server توسط VSCode
نگارش‌های بعدی SQL Server چندسکویی بوده و هم اکنون نگارش‌های آزمایشی آن برای لینوکس در دسترس هستند. به همین جهت مایکروسافت افزونه‌ی چندسکویی را برای VSCode به منظور اتصال و کار با SQL Server تدارک دیده‌است که آن‌را می‌توان یک نمونه‌ی سبک وزن Management Studio آن دانست.




دریافت و نصب افزونه‌ی SQL Server مخصوص VSCode

برای افزودن این افزونه، ابتدا در برگه‌ی Extensions، عبارت mssql را جستجو کرده و سپس آن‌را نصب کنید:


پس از نصب آن، مرحله‌ی بعد، ایجاد یک فایل خالی با پسوند sql است.


انجام اینکار ضروری است و شبیه به حالت نصب افزونه‌ی #C می‌باشد. به این ترتیب وابستگی‌های اصلی آن دریافت، نصب و فعال خواهند شد. این ابزارها نیز سورس باز بوده و موتور SQL Formatter، اجرای SQL و Intellisense آن‌را فراهم می‌کند و چون مبتنی بر NET Core. تهیه شده‌است، چندسکویی است.

تا اینجا مزیتی را که به دست خواهیم آورد Syntax highlighting و Intellisense جهت درج واژه‌های کلیدی عبارات SQL است:

و یا اگر بر روی فایل sql جاری کلیک راست کنیم، گزینه‌ی Format Document آن سبب می‌شود تا کدهای SQL نوشته شده، با فرمتی استاندارد، مرتب و یک‌دست شوند:


بنابراین اگر علاقمندید تا فایل‌ها و عبارات SQL خود را فرمت کنید، این افزونه‌ی سبک وزن چندسکویی، یک چنین قابلیت توکاری را به همراه دارد.
همچنین اگر علاقمندید به یک کتابخانه‌ی سورس باز چندسکویی SQL Formatter و SQL Parser دات نتی دسترسی داشته باشید، کدهای Microsoft/sqltoolsservice در دسترس هستند.


اتصال به SQL Server و کار با آن

پس از نصب مقدماتی افزونه‌ی mssql، دکمه‌های ctrl+shift+p (و یا F1) را فشرده و عبارت sql را جستجو کنید:


در اینجا سایر قابلیت‌های این افزونه‌ی نصب شده را می‌توان مشاهده کرد. در لیست ظاهر شده، گزینه‌ی Connect را انتخاب کنید. بلافاصله گزینه‌ی انتخاب پروفایل ظاهر می‌شود. چون هنوز پروفایلی را تعریف نکرده‌ایم، گزینه‌ی Create connection profile را انتخاب خواهیم کرد:


در ادامه باید نام سرور را وارد کرد. یا می‌توانید نام سرور کامل SQL خود را وارد کنید و یا اگر با LocalDB کار می‌کنید نیز امکان اتصال به آن با تایپlocaldb\MSSQLLocalDB  وجود دارد:


سپس نام بانک اطلاعاتی را که می‌خواهیم به آن متصل شویم ذکر می‌کنیم:


در مرحله‌ی بعد، باید نوع اعتبارسنجی اتصال مشخص شود:


چون در ویندوز هستیم، می‌توان گزینه‌ی Integrated را نیز انتخاب کرد (یا همان Windows Authentication).

در آخر، جهت تکمیل کار و دخیره‌ی این اطلاعات وارد شده، می‌توان نام پروفایل دلخواهی را وارد کرد:


اکنون کار اتصال به این بانک اطلاعاتی انجام شده و اگر به status bar دقت کنید، نمایش می‌دهد که در حال به روز رسانی اطلاعات intellisense است.


برای نمونه اینبار دیگر intellisense ظاهر شده منحصر به درج واژه‌های کلیدی SQL نیست. بلکه شامل تمام اشیاء بانک اطلاعاتی که به آن متصل شده‌ایم نیز می‌باشد:


در ادامه برای اجرا این کوئری می‌توان دکمه‌های Ctrl+Shift+E را فشرد و یا ctrl+shift+p (و یا F1) را فشرده و در منوی ظاهر شده، گزینه‌ی execute query را انتخاب کنید (این گزینه بر روی منوی کلیک راست ظاهر شده‌ی بر روی فایل sql جاری نیز قرار دارد):





نگاهی به محل ذخیره سازی اطلاعات اتصال به بانک اطلاعاتی

پروفایلی را که در قسمت قبل ایجاد کردیم، در منوی File->Preferences->Settings قابل مشاهده است:
// Place your settings in this file to overwrite the default settings
{
    "workbench.colorTheme": "Default Light+",
    "files.autoSave": "afterDelay",
    "typescript.check.tscVersion": false,
    "terminal.integrated.shell.windows": "cmd.exe",
    "workbench.iconTheme": "material-icon-theme",
    "vsicons.dontShowNewVersionMessage": true,
    "mssql.connections": [
        {
            "server": "(localdb)\\MSSQLLocalDB",
            "database": "TestASPNETCoreIdentityDb",
            "authenticationType": "Integrated",
            "profileName": "testLocalDB",
            "password": ""
        }
    ]
}
همانطور که مشخص است، کلید mssql.connections یک آرایه است و در اینجا می‌توان چندین پروفایل مختلف را تعریف و استفاده کرد.
برای مثال پروفایلی را که تعریف کردیم، در دفعات بعدی انتخاب گزینه‌ی Connect، به صورت ذیل ظاهر می‌شود:



تهیه‌ی خروجی از کوئری اجرا شده

اگر به نوار ابزار سمت راست نتیجه‌ی کوئری اجرا شده دقت کنید، سه دکمه‌ی تهیه‌ی خروجی با فرمت‌های csv، json و اکسل نیز در اینجا قرار داده شده‌است:


برای مثال اگر گزینه‌ی json آن‌را انتخاب کنید، بلافاصله نام فایلی را پرسیده و سپس این نتیجه را با فرمت JSON نمایش می‌دهد:


ضمن اینکه حتی می‌توان سطرها و سلول‌های خاصی را نیز از این خروجی انتخاب کرد و سپس با کلیک بر روی آن‌ها، تنها از این انتخاب، یک خروجی ویژه را تهیه نمود:



مشاهده‌ی ساختار اشیاء

اگر بر روی هر کدام از اجزای یک کوئری SQL متصل به بانک اطلاعاتی، کلیک راست کنیم، گزینه‌ی Go to definition نیز ظاهر می‌شود:


با انتخاب آن، بلافاصله عبارت کامل CREATE TABLE [dbo].[AppRoles] ظاهر می‌شود که در اینجا می‌توان ساختار این جدول را به صورت یک عبارت SQL مشاهده کرد.



تغییر تنظیمات افزونه‌ی MSSql

در منوی File->Preferences->Settings با جستجوی mssql می‌توان تنظیمات پیش فرض این افزونه را یافت. برای مثال اگر می‌خواهید تا SQL Formatter آن به صورت خودکار تمام واژه‌های کلیدی را با حروف بزرگ نمایش دهد، گزینه‌ی mssql.format.keywordCasing را انتخاب کنید. در کنار آن آیکن قلم ویرایش ظاهر می‌شود. با کلیک بر روی آن، منوی انتخاب uppercase را خواهیم داشت:


پس از این تغییر، اکنون بر روی صفحه کلیک راست کرده و گزینه‌ی Format Document را انتخاب کنید. در این حالت علاوه بر تغییر فرمت سند SQL جاری، تمام واژه‌های کلیدی آن نیز uppercase خواهند شد.
نظرات اشتراک‌ها
نگارش بعدی ASP.NET Core از Full .NET Framework پشتیبانی نمی‌کند
حرف شما درسته، ولی با توجه به امکان استفاده از DLL‌های Full .NET Framework در NET Core 2 می‌توان به راهکارهایی دیگری نیز دست یافت.

Support for referencing .NET Framework libraries and NuGet packages
برای مثال یک برنامه با Web API & Owin (نه Web API Core) نوشتم و با dotnet (نسخه دو) اون رو در لینوکس اجرا کردم، بدون نصب Mono
نظرات مطالب
نکات کار با استثناءها در دات نت
یک نکته‌ی تکمیلی: بهبود خروجی stack trace استثناءهای رخ داده
خروجی رشته‌ای stack trace استثناءهای رخ داده، سال‌ها است که به روز رسانی نشده و با افزوده شدن امکانات بیشتری به زبان #C، یک چنین خروجی‌های نامفهومی را تولید می‌کند:
System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
.
.
.
at Program.<>c__DisplayClass8_0.<Method>b__0()
کتابخانه‌ی «Ben.Demystifier » با تغییر ساده‌ی ()exception.Demystify، یک شیء استثنای جدید را تولید می‌کند که خوانایی بیشتری را داشته و با مفاهیم جدید زبان #C سازگاری دارد. این کتابخانه اکنون در بسیاری از محصولات خود مایکروسافت هم مورد استفاده قرار می‌گیرد و تبدیل به یک استاندارد شده‌است. یک نمونه‌ی ساده‌ی استفاده‌ی از آن، لاگ کردن خطاها با استفاده از این شیء بهبود یافته‌است:
_logger.LogError(ex.Demystify(), "Invalid operation.");
مطالب
استفاده از کتابخانه‌های ثالث جاوا اسکریپتی در برنامه‌های AngularJS 2.0
هزاران کتابخانه‌ی جاوا اسکریپتی مستقل از AngularJS 2.0 وجود دارند که نیاز است بتوانیم از آن‌ها در برنامه‌های مختلفی استفاده کنیم. در این مطلب، دو روش بارگذاری آن‌ها را بررسی خواهیم کرد.


هدف: استفاده از کتابخانه‌ی jsSHA

می‌خواهیم در یک برنامه‌ی AngularJS 2.0، از کتابخانه‌ی jsSHA استفاده کرده و هش SHA512 یک رشته را محاسبه کنیم.


تامین پیشنیازهای اولیه

می‌توان فایل‌های این کتابخانه را مستقیما از GitHub دریافت و به پروژه اضافه کرد. اما بهتر است این‌کار را توسط npm مدیریت کنیم. به همین جهت فایل package.json آن‌را گشوده و سپس مدخل متناظری را به آن اضافه کنید:
"dependencies": {
    // ...
    "jssha": "^2.1.0",
    // ...
  },
به این ترتیب با اجرای دستور npm install بر روی پوشه‌ی جاری و یا ذخیره‌ی فایل در ویژوال استودیو، کار دریافت خودکار این کتابخانه صورت گرفته و در مسیر node_modules\jssha\src ذخیره می‌شود.


بارگذاری فایل‌های کتابخانه به صورت پویا

یک روش استفاده از این کتابخانه یا هر کتابخانه‌ی جاوا اسکریپتی، افزودن مدخل تعریف آن به صفحه‌ی index.html است:
 <script src="node_modules/jssha/src/sha512.js"></script>
این روش هر چند کار می‌کند، اما با توجه به اینکه AngularJS 2.0 از System.JS برای مدیریت ماژول‌های خود کمک می‌گیرد، می‌تواند با روش پویای ذیل جایگزین شود. برای این منظور ابتدا فایل systemjs.config.js را باز کنید و سپس دو تغییر ذیل را به آن اعمال نمائید:
// map tells the System loader where to look for things
var map = {
    // ...
    'jssha': 'node_modules/jssha/src'
};
 
// packages tells the System loader how to load when no filename and/or no extension
var packages = {
    // ...
    'jssha': { main: 'sha512.js', defaultExtension: 'js' }
};
در اینجا به اشیاء map و packages آن فایل که کار بارگذاری ماژول‌ها را به صورت خودکار انجام می‌دهد، تعاریف جدید jssha را اضافه کرده‌ایم. در قسمت map، مسیر پوشه‌ی فایل‌های js این کتابخانه مشخص شده‌اند و در قسمت packages، نام فایل اصلی مدنظر و پسوندهای آن‌ها ذکر گردید‌ه‌اند.
به این ترتیب هر زمانیکه کار import این کتابخانه صورت گیرد، بارگذاری پویای آن انجام خواهد شد. به علاوه ابزارهای بسته بندی و deploy پروژه هم این فایل را پردازش کرده و به صورت خودکار، کار bundling، فشرده سازی و یکی سازی اسکریپت‌ها را انجام می‌دهند.


استفاده از jsSHA به صورت untyped

پس از دریافت بسته‌های این کتابخانه و مشخص سازی نحوه‌ی بارگذاری پویای آن، اکنون نوبت به استفاده‌ی از آن است. در اینجا منظور از untyped این است که فرض کنیم برای این کتابخانه، فایل‌های typings مخصوص TypeScript وجود ندارند و پس از جستجوی در مخزن کد https://github.com/DefinitelyTyped/DefinitelyTyped نتوانسته‌ایم معادلی را برای آن پیدا کنیم. بنابراین فایل جدید untyped-sha.component.ts را با محتوای ذیل به پروژه اضافه کنید:
import { Component, OnInit } from '@angular/core';
 
var jsSHA = require("jssha"); // ==> loads `sha512.js` file dynamically using `systemjs.config.js` file definitions
//declare var jsSHA: any; // ==> this requires adding <script src="node_modules/jssha/src/sha512.js"></script> to the first page manually.
 
@Component({
    templateUrl: 'app/using-third-party-libraries/untyped-sha.component.html'
})
export class UnTypedShaComponent implements OnInit {
    hash: String;
 
    ngOnInit(): void {
        let shaObj = new jsSHA("SHA-512", "TEXT");
        shaObj.update("This is a test");
        this.hash = shaObj.getHash("HEX");
    }
}
با این قالب untyped-sha.component.html
<h1>SHA-512 Hash / UnTyped</h1>
 
<p>String: This is a test</p>
<p>HEX: {{hash}}</p>
توضیحات
هر زمانیکه فایل‌های typing یک کتابخانه‌ی جاوا اسکریپتی در دسترس نبودند، فقط کافی است از روش declare var jsSHA: any استفاده کنید. در اینجا any به همان حالت استاندارد و بی‌نوع جاوا اسکریپت اشاره می‌کند. در این حالت برنامه بدون مشکل کامپایل خواهد شد؛ اما از تمام مزایای TypeScript مانند بررسی نوع‌ها و همچنین intellisense محروم می‌شویم.
در این مثال در hook ویژه‌ای به نام OnInit، کار ساخت شیء SHA را انجام داده و سپس هش عبارت This is a test محاسبه شده و به خاصیت عمومی hash انتساب داده می‌شود. سپس این خاصیت عمومی، در قالب این کامپوننت از طریق روش interpolation نمایش داده شده‌است.

دو نکته‌ی مهم
الف) اگر از روش declare var jsSHA: any استفاده کردید، کار بارگذاری فایل sha512.js به صورت خودکار رخ نخواهد داد؛ چون ماژولی را import نمی‌کند. بنابراین تعاریف systemjs.config.js ندید گرفته خواهد شد. در این حالت باید از همان روش متداول افزودن تگ script این کتابخانه به فایل index.html استفاده کرد.
ب) برای بارگذاری پویای کتابخانه‌ی jsSHA بر اساس تعاریف فایل systemjs.config.js از متد require کمک بگیرید:
 var jsSHA = require("jssha");
در این حالت باز هم متغیر jsSHA تعریف شده از نوع any است؛ اما اینبار متد require کار بارگذاری خودکار ماژولی را به نام jssha، انجام می‌دهد. این بارگذاری هم بر اساس تعاریف قسمت «بارگذاری فایل‌های کتابخانه به صورت پویا» ابتدای بحث کار می‌کند.


استفاده از jsSHA به صورت typed

کتابخانه‌ی jsSHA در مخزن کد https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/jssha دارای فایل d.ts. مخصوص خود است. برای نصب آن از یکی از دو روش ذیل استفاده کنید:
الف) نصب دستی فایل‌های typings
 npm install -g typings
typings install jssha --save --ambient
توسط خط فرمان، به پوشه‌ی ریشه‌ی پروژه وارد شده و دو دستور فوق را صادر کنید. به این ترتیب فایل‌های d.ts. لازم، به پوشه‌ی typings پروژه اضافه می‌شوند.
ب) تکمیل فایل typings.ts
{
    "ambientDependencies": {
         // ...
        "jssha": "registry:dt/jssha#2.1.0+20160317120654"
    }
}
برای این منظور فایل typings.json را گشوده و سپس سطر جدید فوق را به آن اضافه کنید. اکنون اگر فایل package.json را یکبار دیگر ذخیره کنید و یا دستور npm install را صادر کنید، همان مراحل قسمت الف تکرار خواهند شد.

پس از نصب فایل‌های typings این پروژه، به فایل main.ts مراجعه کرده و مدخل ذیل را به ابتدای آن اضافه کنید:
/// <reference path="../typings/browser/ambient/jssha/index.d.ts" />
اینکار سبب خواهد شد تا intellisense درون ویژوال استودیو بتواند مداخل متناظر را یافته و راهنمای مناسبی را ارائه دهد.

در ادامه فایل جدید typed-sha.component.ts را با محتوای ذیل به پروژه اضافه کنید:
import { Component, OnInit } from '@angular/core';
//import { jsSHA } from "jssha";
import * as jsSHA from "jssha"; // ===> var jsSHA = require("jssha"); // ===> loads `sha512.js` file dynamically using `systemjs.config.js` file definitions
 
@Component({
    templateUrl: 'app/using-third-party-libraries/typed-sha.component.html'
})
export class TypedShaComponent implements OnInit{
    hash: String;
 
    ngOnInit(): void {
        let shaObj = new jsSHA("SHA-512", "TEXT");
        shaObj.update("This is a test");
        this.hash = shaObj.getHash("HEX");
    }
}
محتویات فایل typed-sha.component.html با محتویات فایل untyped-sha.component.html که پیشتر عنوان شد، یکی است.
در اینجا تنها نکته‌ی مهم و جدید نسبت به روش قبل (استفاده از jsSHA به صورت untyped)، روش import این کتابخانه است. روش "import * as jsSHA from "jssha به عبارت var jsSHA = require("jssha") ترجمه می‌شود که در نهایت سبب بارگذاری خودکار فایل‌های jssha بر اساس تعاریف مداخل آن در فایل systemjs.config.js می‌گردد.


کدهای کامل این پروژه را از اینجا می‌توانید دریافت کنید.
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت دهم- ذخیره سازی اطلاعات کاربران IDP در بانک اطلاعاتی
تا اینجا تمام قسمت‌های این سری، برای اساس اطلاعات یک کلاس 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 آن‌ها تشکیل می‌شود:
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; }
    }
}
در اینجا SubjectId همان Id کاربر، در سطح IDP است. این خاصیت به صورت یک کلید خارجی در جداول UserClaims و UserLogins نیز بکار می‌رود.
ساختار 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; }
    }
}
هدف از آن، یکپارچه سازی سیستم، با IDPهای ثالث مانند گوگل، توئیتر و امثال آن‌ها است.

در پروژه‌ی 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);
    }
که توسط آن امکان دسترسی به یک کاربر، اطلاعات 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ها نباشد:
@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;
        }
}
- ابتدا یک TestUserStore را به صورت Singleton ثبت کرده‌است.
- سپس سرویس پروفایل کاربران را اضافه کرده‌است. این سرویس با پیاده سازی اینترفیس 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;
        }
    }
}
در اینجا ابتدا IUsersService سفارشی برنامه معرفی شده‌است که User Store سفارشی برنامه است. البته چون UsersService ما با بانک اطلاعاتی کار می‌کند، نباید به صورت Singleton ثبت شود و باید در پایان هر درخواست به صورت خودکار Dispose گردد. به همین جهت طول عمر آن Scoped تعریف شده‌است. در کل ضرورتی به ذکر این سطر نیست؛ چون پیشتر کار ثبت IUsersService در کلاس Startup برنامه انجام شده‌است.
سپس یک 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);
        }
    }
}
سرویس پروفایل، توسط سرویس کاربران برنامه که در ابتدای مطلب آن‌را تهیه کردیم، امکان دسترسی به اطلاعات پروفایل کاربران را مانند Claims او، پیدا می‌کند.
در متدهای آن، ابتدا 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;
        }
- سرویس توکار IIdentityServerInteractionService، کار تعامل برنامه با IdentityServer4‌  را انجام می‌دهد.
- IClientStore پیاده سازی محل ذخیره سازی اطلاعات کلاینت‌ها را ارائه می‌دهد که در حال حاضر توسط متد استاتیک Config در اختیار آن قرار می‌گیرد.
- IEventService رخ‌دادهایی مانند لاگین موفقیت آمیز یک کاربر را گزارش می‌دهد.
- در آخر، TestUserStore تزریق شده‌است که می‌خواهیم آن‌را با User Store سفارشی خودمان جایگزین کنیم.  بنابراین در ابتدا TestUserStore را با UserStore سفارشی خودمان جایگزین می‌کنیم:
        private readonly TestUserStore _users;
        private readonly IUsersService _usersService;
        public AccountController(
    // ...
            IUsersService usersService)
        {
            _usersService = usersService;
    // ...
        }
فعلا فیلد TestUserStore را نیز سطح کلاس جاری باقی نگه می‌داریم. از این جهت که قسمت‌های لاگین خارجی سیستم (استفاده از گوگل، توئیتر و ...) هنوز از آن استفاده می‌کنند و آن‌را در قسمتی دیگر تغییر خواهیم داد.
پس از معرفی فیلد 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);
تا همینجا برنامه را کامپایل کرده و اجرا کنید. پس از لاگین در آدرس https://localhost:5001/Gallery/IdentityInformation، هنوز اطلاعات User Claims کاربر وارد شده‌ی به سیستم نمایش داده می‌شوند که بیانگر صحت عملکرد CustomUserProfileService است.


افزودن امکان ثبت کاربران جدید به برنامه‌ی 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، تبدیل به UserClaims شده و در جدول مخصوص آن ذخیره خواهند شد.
ب) افزودن 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"));
ج) افزودن 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 وارد کنید.
مطالب
جایگزین کردن jQuery با JavaScript خالص - قسمت سوم - تغییر شیوه‌نامه‌ی المان‌ها
در این قسمت روش جایگزین کردن متد css جی‌کوئری را با کدهای خالص جاوا اسکریپتی بررسی می‌کنیم.

کار با Inline Styles
  <h1>News</h1>
  <div>Welcome to our site!</div>

  <h2>World</h2>

  <h3>Title 1</h3>
  <div>description 1.</div>

  <h2>Science</h2>

  <h3>Title 2</h3>
  <div>description 2.</div>
در این مثال می‌خواهیم با استفاده از جاوا اسکریپت، المان‌های h2 و h3 را یافته و سپس h2ها را آبی و h3ها را سبز کنیم:
  var headings = document.querySelectorAll('h2, h3');
  for (var i = 0; i < headings.length; i++) {
    if (headings[i].tagName === 'H2') {
      headings[i].style.color = 'blue';
    }
    else {
      headings[i].style.color = 'green';
    }
  }
برای تغییر inline style المان‌ها، از خاصیت style آن‌ها استفاده می‌شود که در نهایت این شیوه‌نامه‌های جدید توسط ویژگی style به همان المان اضافه می‌شوند:
<h2 style="color: blue">….</h2>
<h3 style="color: green">….</h3>
خاصیت style جزو استاندارد DOM Level 2 است که در سال 2000 تصویب شده‌است (از زمان IE 8.0 به بعد در دسترس است). باید دقت داشت که از این روش بیشتر در کتابخانه‌های جاوا اسکریپتی برای شرایطی خاص، جهت تغییر پویای رابط کاربری استفاده می‌شود و هر تغییری که در اینجا اعمال شود، مقادیر قبلی موجود را بازنویسی می‌کند.
همچنین اگر بخواهیم به یک المان چندین شیوه‌نامه را انتساب دهیم، روش کار به صورت زیر است:
  <h2>World</h2>

  ...

  <h2>Science</h2>

 <script>
  var headings = document.querySelectorAll('h2');
  for (var i = 0; i < headings.length; i++) {
    headings[i].style.color = 'blue';
    headings[i].style.fontWeight = 'bold';
  }
 </script>
پس از یافتن المان‌های مدنظر، تنها کافی است نام شیوه‌نامه‌ی مدنظر را به خاصیت style اضافه و مقدار دهی کنیم. در اینجا نام شیوه‌نامه‌ای که «کبابی» باشد، مانند font-weight، به صورت camel case مانند fontWeight درج خواهد شد؛ هرچند از همان نام اصلی نیز می‌توان به صورت زیر استفاده کرد:
 headings[i].style['font-weight'] = 'bold';
روش دیگری نیز برای انجام این تغییرات چندتایی وجود دارد:
  var headings = document.querySelectorAll('h2');

  for (var i = 0; i < headings.length; i++) {
    headings[i].style.cssText = 'color: blue; font-weight: bold';
  }
خاصیت style یک المان، از نوع اینترفیس CSSStyleDeclaration است که دارای خاصیت استاندارد cssText نیز می‌باشد. توسط این خاصیت می‌توان چندین شیوه‌نامه را به صورت یکجا به عنصری انتساب داد و یا تمام آن‌ها را خواند.


کار با Style Sheets

Inline styles تنها روش کار با شیوه‌نامه‌ها نیست. روش صحیح و قابل مدیریت کار با شیوه‌نامه‌ها استفاده از فایل‌های style sheets است. برای مثال تغییرات قبل را می‌توان در فایلی به نام styles.css و با محتوای زیر ایجاد کرد:
h2 { color: blue; }
h3 { color: green; }
و سپس آن‌را به صفحه متصل نمود:
 <link href="styles.css" rel="style sheet">
و یا حتی می‌توان این شیوه نامه را به صورت inline نیز به ابتدای صفحه اضافه نمود:
<style>
  h2 { color: blue; }
  h3 { color: green; }
</style>
اما ممکن است در برنامه بخواهیم امکان تغییر پویای قالب را به کاربران بدهیم. در یک چنین حالتی اعمال این نوع شیوه‌نامه‌ها توسط جاوا اسکریپت مفهوم پیدا می‌کند:
var sheet = document.styleSheets[0];
sheet.insertRule('h2 { font-style: italic; }', sheet.cssRules.length - 1);
اولین سطر، اولین تگ style اضافه شده به صفحه را یافته (این style می‌تواند inline و یا لینک شده‌ی توسط یک فایل باشد) و سپس شیوه نامه‌ی جدیدی را توسط متد insertRule، در انتهای آن به صورت پویا درج می‌کند.


مخفی کردن و نمایش دادن المان‌ها در صفحه

جی‌کوئری به همراه متدهای hide و show است که کار مخفی کردن و یا نمایش دادن مجدد یک المان‌را انجام می‌دهند:
// hide an element
$element.hide();

// show it again
$element.show();
در کل روش‌های زیادی برای مخفی کردن یک المان وجود دارند. برای مثال می‌توان opacity آن‌را به صفر تنظیم کرد و یا position آن‌را به absolute و سپس آن‌را در مختصاتی خارج از صفحه قرار داد. اما عموما خاصیت display را به none تنظیم می‌کنند. همچنین در استاندارد W3C HTML5، خاصیت جدید hidden از نوع boolean نیز به المان‌ها اضافه شده‌اند که دقیقا برای همین‌منظور بکار می‌رود. مزیت مهم این خاصیت نه فقط استاندارد بودن آن، بلکه بالابردن دسترسی پذیری المان‌های صفحه توسط برنامه‌های «screen reader» مخصوص معلولین است. بنابراین با استفاده از جاوا اسکریپت خالص برای مخفی کردن یک المان می‌توان نوشت:
element.setAttribute('hidden', '');
این روش 25 بار سریعتر از متد hide جی‌کوئری است! از این جهت که jQuery در پشت صحنه مدام متد window.getComputedStyle را برای موارد خاص و بحرانی کار با شیوه‌نامه‌ها فراخوانی می‌کند (در تمام متدهایی که با CSS کار می‌کنند) و این متد تاثیر منفی بر روی کارآیی برنامه دارد.
و چون خاصیت hidden از نوع Boolean است، ذکر آن در یک المان یعنی تنظیم آن به true و حذف آن، یعنی تنظیم آن به false یا نمایش مجدد المان در اینجا:
element.removeAttribute('hidden');


اندازه‌گیری تاثیر شیوه‌نامه‌ها بر روی طول و عرض المان‌ها

CSS Box Model یک چنین تعریفی را دارد:


زمانیکه از متدهای ()width و ()height جی‌کوئری بر روی المانی استفاده می‌شود، صرفا طول و عرض قسمت «content» را دریافت خواهید کرد.
برای این منظور در جاوا اسکریپت خالص این خواص در اختیار ما است:
<style>
  .box {
    padding: 10px;
    margin: 5px;
    border: 3px solid;
    display: inline-block;
  }
</style>
<span class="box">a box</span>
روش اندازه گیری Content + Padding توسط جاوا اسکریپت خالص:
 // returns 38
var clientHeight = document.querySelector('.box').clientHeight;

// returns 55
var clientWidth = document.querySelector('.box').clientWidth;
این خواص هرچند اخیرا به استانداردهای CSS به صورت رسمی اضافه شده‌اند، اما از زمان IE 6.0 پشتیبانی می‌شده‌اند و با متدهای ()innerWidth و ()innerHeight جی‌کوئری قابل مقایسه هستند.

روش اندازه گیری Content + Padding + Border توسط جاوا اسکریپت خالص:
 // returns 44
var offsetHeight = document.querySelector('.box').offsetHeight;

// returns 61
var offsetWidth = document.querySelector('.box').offsetWidth;
این خواص از زمان IE 8.0 پشتیبانی می‌شده‌اند.
بازخوردهای دوره
آشنایی با AOP Interceptors
- یکی از اهداف مهم AOP این است که به صورت لایه‌ای نامریی عمل کند و هر زمان که نیاز باشد، بتوان بدون کوچکترین تغییری در کدهای اصلی برنامه، کل منطق آن‌را حذف، یا با نمونه‌‌ای دیگر جایگزین کرد. بنابراین دریافت یک مقدار از Interceptor داخل متدی در برنامه، نقض کننده‌ی فلسفه‌ی وجودی این عملیات است.
- اما توسط پارامتر IInvocation و مقداری Reflection، دسترسی کاملی به اطلاعات متد فراخوان هست و در اینجا می‌توان در صورت نیاز، پارامتر و مقداری را نیز به آن ارسال کرد.
- در ASP.NET MVC، مفهوم فیلترها دقیقا پیاده سازی کننده‌ی Interceptor‌های AOP هستند. در اینجا نیز مستقیما اطلاعاتی به فراخوان، در صورت نیاز بازگشت داده نمی‌شود. اما Context جاری در اختیار Interceptor و فیلتر هست. به این ترتیب Interceptor فرصت خواهد داشت به این Context مشترک، اطلاعاتی را اضافه کند یا تغییر دهد. مثلا به لیست خطاهای آن یک خطای اعتبارسنجی جدید را اضافه کند.
اشتراک‌ها
به کارگیری مفهوم Double-checked locking

ضرورت استفاده به هنگام Caching، خصوصا اگر تعداد خوانش بالا و همزمان روی دیتای کش شده باشد و از طرفی Reload دیتا زمانبر.

if (exists in cache)
{
   return from cache
}
lock(staticObject)
{
   if (exists in cache)
   {
       return from cache
   }
   read from db & cache & return
}


به کارگیری مفهوم Double-checked locking
مطالب دوره‌ها
افزونه‌ای برای کپسوله سازی نکات ارسال یک فرم ASP.NET MVC به سرور توسط jQuery Ajax
اگر مطالب سایت جاری را مطالعه و دنبال کرده باشید، تاکنون به صورت پراکنده نکات زیادی را در مورد استفاده از jQuery Ajax تهیه و ارائه کرده‌ایم. در این مطلب قصد داریم تا این نکات را نظم بخشیده و جهت استفاده مجدد، به صورت یک افزونه کپسوله سازی کنیم.

در کدها و افزونه‌ای که در ادامه ارائه خواهند شد، این مسایل درنظر گرفته شده است:

- چگونه اعتبار سنجی سمت کاربر را در حین استفاده از Ajax فعال کنیم.
- چگونه از چندبار کلیک کاربر در حین ارسال فرم به سرور جلوگیری نمائیم.
- چگونه Complex Types قابل تعریف در EF Code first را نیز در اینجا مدیریت کنیم.
- نحوه تعریف صحیح آدرس‌های کنترلرها چگونه باید باشد.
- نحوه اعلام وضعیت لاگین شخص به او، در صورت بروز مشکل.
- ارسال صحیح anti forgery token در حین اعمال Ajax ایی.
- بررسی Ajax بودن درخواست رسیده و تهیه یک فیلتر سفارشی مخصوص آن.
- از کش شدن اطلاعات Ajax ایی جلوگیری شود.


ابتدا معرفی مدل برنامه
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace jQueryMvcSample01.Models
{
    public class User
    {
        [Required(ErrorMessage = "(*)"), DisplayName("نام")]
        public string Name { set; get; }

        public PhoneInfo PhoneInfo { set; get; }
    }

    public class PhoneInfo
    {
        [Required(ErrorMessage = "(*)"), DisplayName("تلفن")]
        public string Phone { get; set; }

        [Required(ErrorMessage = "(*)"), DisplayName("پیش شماره")]
        public string Ext { get; set; }
    }
}
همانطور که ملاحظه می‌کنید، خاصیت PhoneInfo، تو در تو یا به نوعی Complex است. اگر از ابزارهای Scafolding توکار VS.NET برای تولید View متناظر استفاده کنیم، فیلد تو در توی PhoneInfo را لحاظ نخواهد کرد، اما ... مهم نیست. تعریف دستی آن هم کار می‌کند.


کدهای کنترلر برنامه

using System.Web.Mvc;
using jQueryMvcSample01.Models;
using jQueryMvcSample01.Security;

namespace jQueryMvcSample01.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            return View(); //نمایش فرم
        }

        [HttpPost]
        [AjaxOnly] //فقط در حالت ای‌جکس قابل دسترسی باشد
        [ValidateAntiForgeryToken]
        public ActionResult Index(User user)
        {
            if (this.ModelState.IsValid)
            {
                // ذخیره سازی در بانک اطلاعاتی ...
                System.Threading.Thread.Sleep(3000);

                return Content("ok");//اعلام موفقیت آمیز بودن کار
            }

            return Content(null);//ارسال خطا
        }
    }
}
در اینجا در متد Index، اطلاعات شیء User به صورت Ajaxایی دریافت شده و پس از آن برای مثال قابلیت ذخیره سازی را خواهد داشت.
چند نکته در اینجا حائز اهمیت هستند:
الف) استفاده از ویژگی AjaxOnly (که کدهای آن‌را در پروژه پیوست می‌توانید مشاهده نمائید)، جهت صرفا پردازش درخواست‌های Ajaxایی.
ب) استفاده از ویژگی ValidateAntiForgeryToken در حین اعمال اجکسی. اگر سایت‌های مختلف را در اینباره جستجو کنید، عموما برای پردازش آن در حین استفاده از jQuery Ajax بسیار مشکل دارند.
ج) استفاده از return Content برای اعلام نتیجه کار. اگر اطلاعات ثبت شد، یک ok یا هر عبارت دیگری که علاقمند بودید ارسال گردیده و در غیراینصورت null بازگشت داده می‌شود.


کدهای افزونه PostMvcFormAjax

// <![CDATA[
(function ($) {
    $.fn.PostMvcFormAjax = function (options) {
        var defaults = {
            postUrl: '/',
            loginUrl: '/login',
            beforePostHandler: null,
            completeHandler: null,
            errorHandler: null
        };
        var options = $.extend(defaults, options);

        var validateForm = function (form) {
            //فعال سازی دستی اعتبار سنجی جی‌کوئری
            var val = form.validate();
            val.form();
            return val.valid();
        };

        return this.each(function () {
            var form = $(this);
            //اگر فرم اعتبار سنجی نشده، اطلاعات آن ارسال نشود
            if (!validateForm(form)) return;

            //در اینجا می‌توان مثلا دکمه‌ای را غیرفعال کرد
            if (options.beforePostHandler)
                options.beforePostHandler(this);

            //اطلاعات نباید کش شوند
            $.ajaxSetup({ cache: false });

            $.ajax({
                type: "POST",
                url: options.postUrl,
                data: form.serialize(), //تمام فیلدهای فرم منجمله آنتی فرجری توکن آن‌را ارسال می‌کند
                complete: function (xhr, status) {
                    var data = xhr.responseText;
                    if (xhr.status == 403) {
                        window.location = options.loginUrl; //در حالت لاگین نبودن شخص اجرا می‌شود
                    }
                    else if (status === 'error' || !data) {
                        if (options.errorHandler)
                            options.errorHandler(this);
                    }
                    else {
                        if (options.completeHandler)
                            options.completeHandler(this);
                    }
                }

            });
        });
    };
})(jQuery);
// ]]>
چند نکته مهم در تهیه این افزونه رعایت شده:
الف) فعال سازی دستی اعتبار سنجی جی‌کوئری، از این جهت که این نوع اعتبار سنجی به صورت پیش فرض تنها در حالت postback و ارسال کامل صفحه به سرور فعال می‌شود.
ب) استفاده از متد serialize جهت پردازش یکباره کل اطلاعات و فیلدهای یک فرم.
نکته مهم این متد ارسال فیلد مخفی anti forgery token نیز می‌باشد. فقط باید دقت داشت که این فیلد در حالتی که dataType به json تنظیم شود و همچنین از متد serialize استفاده گردد، در ASP.NET MVC پردازش نمی‌گردد (خیلی مهم!). به همین جهت در اینجا dataType تنظیمات jQuery Ajax حذف شده است.
ج) تنظیم cache به false در تنظیمات ابتدایی jQuery Ajax تا اطلاعات ارسالی و دریافتی کش نشوند و مشکل ساز نگردند.
د) بررسی xhr.status == 403 که توسط SiteAuthorizeAttribute (جایگزین بهتر فیلتر Authorize توکار ASP.NET MVC که کدهای آن در پروژه پیوست قابل دریافت است) و هدایت کاربر به صفحه لاگین


تعریف View ایی که از اشیاء تو در تو استفاده می‌کند و همچنین از افزونه فوق برای ارسال اطلاعات بهره خواهد برد:

@model jQueryMvcSample01.Models.User
@{
    ViewBag.Title = "تعریف کاربر";
    var postUrl = Url.Action(actionName: "Index", controllerName: "Home");
}
@using (Html.BeginForm(actionName: "Index", controllerName: "Home",
                       method: FormMethod.Post,
                       htmlAttributes: new { id = "UserForm" }))
{
    @Html.ValidationSummary(true)
    @Html.AntiForgeryToken()

    <fieldset>
        <legend>تعریف کاربر</legend>
        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Name)
            @Html.ValidationMessageFor(model => model.Name)
        </div>
        <div class="editor-label">
            @Html.LabelFor(model => model.PhoneInfo.Ext)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.PhoneInfo.Ext)
            @Html.ValidationMessageFor(model => model.PhoneInfo.Ext)
        </div>
        <div class="editor-label">
            @Html.LabelFor(model => model.PhoneInfo.Phone)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.PhoneInfo.Phone)
            @Html.ValidationMessageFor(model => model.PhoneInfo.Phone)
        </div>
        <p>
            <input type="submit" id="btnSave" value="ارسال" />
        </p>
    </fieldset>
}
@section JavaScript
{
    <script type="text/javascript">
        $(document).ready(function () {
            $("#btnSave").click(function (event) {
                //جلوگیری از پست بک به سرور
                event.preventDefault();

                var button = $(this);

                $("#UserForm").PostMvcFormAjax({
                    postUrl: '@postUrl',
                    loginUrl: '/login',
                    beforePostHandler: function () {
                        //غیرفعال سازی دکمه ارسال
                        button.attr('disabled', 'disabled');
                        button.val("...");
                    },
                    completeHandler: function () {
                        //فعال سازی مجدد دکمه ارسال
                        alert('انجام شد');
                        button.removeAttr('disabled');
                        button.val("ارسال");
                    },
                    errorHandler: function () {
                        alert('خطایی رخ داده است');
                    }
                });
            });
        });
    </script>
}
همانطور که عنوان شد، مهم نیست که اشیاء تو در تو توسط ابزار Scafolding پشتیبانی نمی‌شود. این نوع خواص را به همان نحو متداول ذکر زنجیره وار خواص می‌توان معرفی و استفاده کرد:
 @Html.EditorFor(model => model.PhoneInfo.Phone)
هم اعتبار سنجی سمت کلاینت آن کار می‌کند و هم اطلاعات آن به اشیاء و خواص متناظر به خوبی نگاشت خواهد شد:


در ادامه نحوه استفاده از افزونه PostMvcFormAjax را مشاهده می‌کنید. چند نکته نیز در اینجا حائز اهمیت هستند:
الف) توسط htmlAttributes یک id برای فرم تعریف کرده‌ایم تا در افزونه PostMvcFormAjax مورد استفاده قرار گیرد.
ب) postUrl و loginUrl را همانند متغیر تعریف شده در ابتدای View توسط Url.Action باید تعریف کرد تا در صورتیکه سایت ما در ریشه اصلی قرار نداشت، باز هم به صورت خودکار مسیر صحیحی محاسبه و ارائه گردد.
ج) نحوه غیرفعال سازی و فعال سازی دکمه submit را در روال‌های beforePostHandler و completeHandler ملاحظه می‌کنید. این مساله برای جلوگیری از کلیک‌های مجدد یک کاربر ناشکیبا و جلوگیری از ثبت اطلاعات تکراری بسیار مهم است.
د) کل این اطلاعات، در یک section به نام JavaScript ثبت شده است. این section در فایل layout برنامه به صورت زیر مورد استفاده قرار خواهد گرفت و به این ترتیب مقدار دهی خواهد شد:
<head>
    <title>@ViewBag.Title</title>    
    <link href="@Url.Content("Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.9.1.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.PostMvcFormAjax.js")" type="text/javascript"></script>
    @RenderSection("JavaScript", required: false)
</head>

دریافت کدهای کامل این قسمت
jQueryMvcSample01.zip