مطالب دوره‌ها
اصل معکوس سازی وابستگی‌ها
پیش از شروع این سری نیاز است با تعدادی از واژه‌های بکار رفته در آن به اختصار آشنا شویم؛ از این واژه‌ها به کرات استفاده شده و در طول دوره به بررسی جزئیات آن‌ها خواهیم پرداخت:

1) Dependency inversion principle یا DIP (اصل معکوس سازی وابستگی‌ها)
DIP یکی از اصول طراحی نرم افزار است و D آن همان D معروف SOLID است (اصول پذیرفته شده شیءگرایی).

2) Inversion of Control یا IOC (معکوس سازی کنترل)
الگویی است که نحوه پیاده سازی DIP را بیان می‌کند.

3) Dependency injection یا DI (تزریق وابستگی‌ها)
یکی از روش‌های پیاده سازی IOC است.

4) IOC container
به فریم ورک‌هایی که کار DI را انجام می‌دهند گفته می‌شود.


Dependency inversion principle چیست؟

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


هر کدام از این‌ها، رابط‌های اتصالی متفاوتی دارند. یکی USB2، یکی USB3 دیگری Mini USB و بعضی‌ها هم از پورت‌های دیگری استفاده می‌کنند. چون هر کدام از لایه‌های زیرین سیستم (در اینجا وسایل قابل شارژ) رابط‌های اتصالی مختلفی را ارائه داده‌اند، برای اتصال آن‌ها به منبع قدرت که در سطحی بالاتر قرار دارد، نیاز به تبدیلگرها و درگاه‌های مختلفی خواهد بود.
اگر در این نوع طراحی‌ها، اصل معکوس سازی وابستگی‌ها رعایت می‌شد، درگاه و رابط اتصال به منبع قدرت باید تعیین کننده نحوه طراحی اینترفیس‌های لایه‌های زیرین می‌بود تا با این آشفتگی نیاز به انواع و اقسام تبدیلگرها، روبرو نمی‌شدیم.


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


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


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



تاریخچه اصل معکوس سازی وابستگی‌ها

اصل معکوس سازی وابستگی‌ها در نشریه C++ Report سال 1996 توسط شخصی به نام Bob Martin (معروف به Uncle Bob!) برای اولین بار مطرح گردید. ایشان همچنین یکی از آغاز کنندگان گروهی بود که مباحث Agile را ارائه کردند. به علاوه ایشان برای اولین بار مباحث SOLID را در دنیای شیءگرایی معرفی کردند (همان مباحث معروف هر کلاس باید تک مسئولیتی باشد، باز باشد برای توسعه، بسته برای تغییر و امثال آن که ما در این سری مباحث قسمت D آن‌را در حالت بررسی هستیم).

مطابق تعاریف Uncle Bob:
الف) ماژول‌های سطح بالا نباید به ماژول‌های سطح پایین وابسته باشند. هر دوی این‌ها باید به Abstraction وابسته باشند.
ب) Abstraction نباید وابسته به جزئیات باشد. جزئیات (پیاده سازی‌ها) باید وابسته به Abstraction باشند.


مثال برنامه کپی

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


حال اگر به این مجموعه، ذخیره سازی اطلاعات بر روی دیسک سخت را اضافه کنیم چطور؟ به این ترتیب سیستم با افزایش وابستگی‌ها، پیچیدگی و if و elseهای بیشتری را خواهد یافت؛ از این جهت که سطح بالایی سیستم به صورت مستقیم وابسته خواهد بود به ماژول‌های سطح پایین آن.
روشی را که ایشان برای حل این مشکل ارائه داده‌اند، معکوس کردن وابستگی‌ها است:


در اینجا سطح بالایی سیستم وابسته است به یک سری تعاریف Abstract خواندن و یا نوشتن؛ بجای وابستگی مستقیم به پیاده سازی‌های سطح پایین آن‌ها.
در این حالت اگر تعداد Readers و یا Writers افزایش یابند، باز هم سطح بالایی سیستم نیازی نیست تغییر کند زیرا وابسته است به یک اینترفیس و نه پیاده سازی آن که محول شده است به لایه‌های زیرین سیستم.
این مساله بر روی لایه بندی سیستم نیز تاثیرگذار است. در روش متداول برنامه نویسی، لایه بالایی به صورت مستقیم متدهای لایه‌های زیرین را صدا زده و مورد استفاده قرار می‌دهد. به این ترتیب هر تغییری در لایه‌های مختلف، بر روی سایر لایه‌ها به شدت تاثیرگذار خواهد بود. اما در حالت معکوس سازی وابستگی‌ها، هر کدام از لایه‌های بالاتر، از طریق اینترفیس از لایه زیرین خود استفاده خواهد کرد. در این حالت هرگونه تغییری در لایه‌های زیرین برنامه تا زمانیکه اینترفیس تعریف شده را پیاده سازی کنند، اهمیتی نخواهد داشت.


مثال برنامه دکمه و لامپ

مثال دیگری که در مقاله Uncle Bob ارائه شده، مثال برنامه دکمه و لامپ است. در حالت متداول، یک دکمه داریم که وابسته است به لامپ. برای مثال وهله‌ای از لامپ به دکمه ارسال شده و سپس دکمه آن‌را کنترل خواهد کرد (خاموش یا روشن). مشکلی که در اینجا وجود دارد وابستگی دکمه به نوعی خاص از لامپ است و تعویض یا استفاده مجدد از آن به سادگی میسر نیست.
راه حلی که برای این مساله ارائه شده، ارائه یک اینترفیس بین دکمه و لامپ است که خاموش و روشن کردن در آن تعریف شده‌اند. اکنون هر لامپی (یا هر وسیله الکتریکی دیگری) که بتواند این متدها را ارائه دهد، در سیستم قابل استفاده خواهد بود.

مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت دوم - ایجاد ساختار اولیه‌ی مثال این سری
در این قسمت قصد داریم ساختار مقدماتی مثال این سری را که لیستی از تصاویر آپلود شده‌ی توسط کاربران مختلف را نمایش می‌دهد، بدون افزودن مباحث امنیتی و سطوح دسترسی کاربران وارد شده‌ی به سیستم، تشکیل دهیم. در قسمت‌های بعدی، به تدریج آن‌را با قابلیت‌های مختلف IdentityServer 4x یکپارچه خواهیم کرد. در اینجا فرض بر این است که حداقل SDK نگارش 2.1.401 را پیشتر نصب کرده‌اید.


بررسی ساختار WebAPI مقدماتی مثال این سری


این پروژه‌ی مقدماتی که هنوز قسمت‌های اعتبارسنجی به آن اضافه نشده‌اند، از دو قسمت WebApi و MvcClient تشکیل می‌شود.
کار قسمت WebApi، ارائه‌ی یک Restful-API برای کار با گالری تصاویر است. برای اجرای آن وارد پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp شده و ابتدا فایل restore.bat و سپس dotnet_run.bat را اجرا کنید.
در این حالت برنامه بر روی پورت 7001 در دسترس خواهد بود:


این پورت نیز در فایل Properties\launchSettings.json تنظیم شده‌است تا با پورت کلاینت MVC تهیه شده، تداخل نکند.
کار این سرویس، ارائه‌ی ImagesController است که توسط آن می‌توان لیستی از تصاویر موجود، اطلاعات یک تصویر و همچنین کار ثبت، ویرایش و حذف تصاویر را انجام داد.
در این Solution، رکوردهای تصاویری که در بانک اطلاعاتی ذخیره می‌شوند، یک چنین ساختاری را دارند:
using System;
using System.ComponentModel.DataAnnotations;

namespace ImageGallery.WebApi.DomainClasses
{
    public class Image
    {
        [Key]
        public Guid Id { get; set; }

        [Required]
        [MaxLength(150)]
        public string Title { get; set; }

        [Required]
        [MaxLength(200)]
        public string FileName { get; set; }

        [Required]
        [MaxLength(50)]
        public string OwnerId { get; set; }
    }
}
همانطور که ملاحظه می‌کنید در اینجا OwnerId نیز در نظر گرفته شده‌است تا بتوان پس از اعمال مباحث اعتبارسنجی، تصاویر را از کاربری خاص دریافت و همچنین صرفا تصاویر متعلق به او را به آن کاربر خاص نمایش داد.
در این قسمت توسط کلاس ImageConfiguration کار مقدار دهی اولیه‌ی بانک اطلاعاتی به کمک متد HasData مربوط به EF Core 2.1 صورت گرفته‌است و به این ترتیب می‌توان برنامه را برای نمایش مقدماتی جاری، بدون داشتن سیستم اعتبارسنجی و مفاهیم کاربران، نمایش داد.
این قسمت از Solution، به نحو زیر تشکیل شده‌است:
- ImageGallery.WebApi.DataLayer
در اینجا کار تشکیل DbContext برنامه و همچنین مقدار دهی اولیه‌ی بانک اطلاعاتی و تنظیمات Migrations قرار گرفته‌اند.
- ImageGallery.WebApi.DomainClasses
در این پروژه کلاس‌های موجودیت‌های متناظر با جداول بانک اطلاعاتی قرار می‌گیرند.
- ImageGallery.WebApi.Mappings
این پروژه کار تهیه نگاشت‌های AutoMapper برنامه را انجام می‌دهد؛ نگاشت‌هایی بین Models و DomainClasses که در ImagesController از آن‌ها استفاده می‌شود.
- ImageGallery.WebApi.Models
در این پروژه همان DTO's پروژه قرار گرفته‌اند. جهت رعایت مسایل امنیتی نباید کلاس موجودیت Image فوق را مستقیما در معرض دید API عمومی قرار داد. به همین جهت تعدادی Model در اینجا تعریف شده‌اند که هم برای ثبت، ویرایش و حذف اطلاعات بکار می‌روند و هم جهت گزارشگیری از رکوردهای موجود جدول تصاویر.
- ImageGallery.WebApi.Services
در این پروژه کار با DbContext انجام شده و توسط آن اطلاعات تصاویر به بانک اطلاعاتی اضافه شده و یا واکشی می‌شوند.
- ImageGallery.WebApi.WebApp
این پروژه، همان پروژه‌ی اصلی است که سایر قسمت‌های یاد شده را در کنار هم قرار داده و به صورت یک Restful-API ارائه می‌دهد.



بررسی ساختار MvcClient مقدماتی مثال این سری

پس از اجرای WebAPI و آماده بودن آن جهت استفاده، اکنون یک کلاینت ASP.NET Core MVC را جهت کار با امکانات ImagesController آن، تدارک دیده‌ایم.
برای اجرای آن وارد پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp شده و ابتدا فایل restore.bat و سپس dotnet_run.bat را اجرا کنید.
در این حالت برنامه بر روی پورت 5001 در دسترس خواهد بود:


این پورت نیز در فایل Properties\launchSettings.json تنظیم شده‌است.


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

این قسمت از Solution به نحو زیر تشکیل شده‌است:
- ImageGallery.MvcClient.Services
در اینجا یک Typed HTTP Client مخصوص NET Core 2.1. را تهیه کرده‌ایم. این سرویس جهت دسترسی به آدرس https://localhost:7001 که WebAPI برنامه در آن قرار دارد، تشکیل شده‌است. روش ثبت مخصوص آن‌را نیز در فایل آغازین پروژه‌ی MvcClient.WebApp توسط متد services.AddHttpClient ملاحظه می‌کنید.
- ImageGallery.MvcClient.ViewModels
مدل‌های متناظر با ساختار Viewهای Razor برنامه‌ی وب، در اینجا قرار می‌گیرند.
- ImageGallery.MvcClient.WebApp
این پروژه، همان پروژه‌ی اصلی است که سایر قسمت‌های یاد شده را در کنار هم قرار داده و به صورت یک برنامه‌ی MVC قابل مرور در مرورگر، ارائه می‌دهد.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای آن ابتدا باید پروژه‌ی WebApi.WebApp را اجرا کنید و سپس پروژه‌ی MvcClient.WebApp.
مطالب
آشنایی با SQL Server Data Tools
مقدمه
یکی از امکانات اضافه شده در Sql Server 2012 ابزار Sql Server Data Tools یا به اختصار SSDT می‌باشد. این ابزار در واقع جایگزین ابزار Business Intelligence Development Studio می باشد که همه امکانات قبلی را داشته و همچنین قابلیت‌های جدیدی نیز به آن اضافه شد است. اما کاربرد این ابزار ایجاد محیطی یکپارچه برای طراحی و توسعه تمامی نسخه‌های پایگاه داده Sql Server با استفاده از Visual Studio می‌باشد. در این مقاله من به بیشتر بر روی امکانات زیر تمرکز دارم:
  • Installation
  • Creating a SQL Server Database Project
  • Modify Database Schema
  • Schema Compare and Update
  • Snapshot Project
  • Publish
  1. نصب Sql Server Data Tools
    آخرین نسخه این ابزار را از این آدرس دانلود کنید یا زمان نصب Sql Server 2012 گزینه آن را انتخاب کنید


  2. ایجاد یک پروژه جدید از نوع  SQL Server Database Project
    پس از نصب SSDT شما از طریق Visual Studio 2012 Shell که همراه SqlServer 2012 نصب می‌شود یا با Visual Stadio 2012 یک پروژه جدید از نوع SQL Server Database Project ایجاد کنید.

     Server Database Project Project به شما امکان توسعه پایگاه داده Sql Server را با استفاده  از محیط یکپارچه Visual Studio با در اختیار گذاشتن ابزار‌های همچون navigation, intellisense, validation, debugging, declarative editing و غیره را می‌دهد. شی پایگاه داده شما در پروژه و فایل‌های مجزا ذخیره می‌شود مثل آن که شما در حال توسعه برنامه #C یا VB.NET هستید.
  3. ورود پایگاه داده موجود به پروژه SQL Server Database
    شما می‌توانید شمای پایگاه داده موجود یا SQL Server DAC Package File (.dacpac) یا هر T-SQL دیگری را با راست کلیک بر روی پروژه و انتخاب گزینه Import به پروژه خود اضافه و تغییرات لازم را اعمال نماید شکل زیر پنجره Import Database  را نشان می‌دهد:
     

      پنجره 
    Solution Explorer امکانات زیر را در اختیار شما قرارمی دهد:

    - اضافه کردن اشیای همچون Table، View  و غیره با راست کیلک کردن بر روی پروژه و انتخاب گزینه Add
    - ویرایش اشیا موجود با دبل کیلک کردن بر روی اشیا
    - مقایسه شمای پایگاه داده با پایگاه داده دیگر و یا Microsoft SQL Server DAC Package File کافیست بر روی پروژه راست کیلک و گزینه Schema Compare را         انتخاب نمایید.
    - ایجاد یک Snapshot از شمای پایگاه داده در یک Microsoft SQL Server DAC Package File . یک snapshot برای ایجاد یک پایگاه داده یا ورود در پروژه ای دیگر یا در مقایسه دو پایگاه داده کاربرد دارد.
    - انتشار پایگاه داده که امکان ایجاد پایگاه داده یر روی یک سرور SQL Server را فراهم می‌نماید.  

  4. مقایسه شمای دو پایگاه داده با هم
    SSDT ابزاری برای مقایسه دو پایگاه داده و بروز رسانی پایگاه داده مقصد از روی شمای منبع از طریق ایجاد یک Script یا به صورت مستقیم را می‌دهد.



  5. انتشار پروژه با استفاده از SQL Server Data Tools
    این ابزار امکان گسترش پروژه شما بر روی SQL Server 2005, 2008, 2008 R2, 2012, یا SQL Azure instance را می‌دهد. برای انتشار کافیت از پنجره Solution Explorer  بر روی پروژه راست کیلک و گزینه Publish را انتخاب کنید.

  6. پنجره SQL Server Object Explorer
    این پنجره با اتصال به سرور SQL Server  امکان دسترسی و مدیریت پایگاه داده را به ما می‌دهد. علاوه بر دسترسی به اجزای یک پایگاه داده امکان مدیریت پوشه‌های Security , Server Objects نیز فراهم می‌باشد. همچنین امکان اتصال به Sql Server Express , localdb نیز وجود دارد. 


  7.   
مطالب
جلوگیری از ارسال Spam در ASP.NET MVC
در هر وب‌سایتی که فرمی برای ارسال اطلاعات به سرور موجود باشد، آن وب سایت مستعد ارسال اسپم و بمباران درخواست‌های متعدد خواهد بود. در برخی موارد استفاده از کپچا می‌تواند راه خوبی برای جلوگیری از ارسال‌های مکرر و مخرب باشد، ولی گاهی اوقات سناریوی ما به شکلی است که امکان استفاده از کپچا، به عنوان یک مکانیزم امنیتی مقدور نیست.
اگر شما یک فرم تماس با ما داشته باشید استفاده از کپچا یک مکانیزم امنیتی معقول می‌باشد و همچنین اگر فرمی جهت ارسال پست داشته باشید. اما در برخی مواقع مانند فرمهای ارسال کامنت، پاسخ، چت و ... امکان استفاده از این روش وجود ندارد و باید به فکر راه حلی مناسب برای مقابل با درخواست‌های مخرب باشیم.
اگر شما هم به دنبال تامین امنیت سایت خود هستید و دوست ندارید که وب سایت شما (به دلیل کمبود پهنای باند یا ارسال مطالب نامربوط که گاهی اوقات به صدها هزار مورد می‌رسد) از دسترس خارج شود این آموزش را دنبال کنید.
برای این منظور ما از یک ActionFilter برای امضای ActionMethodهایی استفاده می‌کنیم که باید با ارسالهای متعدد از سوی یک کاربر مقابله کنند. این ActionFilter  باید قابلیت تنظیم حداقل زمان بین درخواستها را داشته باشد و اگر درخواستی در زمانی کمتر از مدت مجاز تعیین شده برسد، به نحوی مطلوبی به آن رسیدگی کند.
پس از آن ما نیازمند مکانیزمی هستیم تا درخواست‌های رسیده‌ی از سوی هرکاربر را به شکلی کاملا خاص و یکتا شناسایی کند. راه حلی که قرار است در این ActionFilter  از آن استفاده کنم به شرح زیر است:
ما به دنبال آن هستیم که یک شناسه‌ی منحصر به فرد را برای هر درخواست ایجاد کنیم. لذا از اطلاعات شیئ Request جاری برای این منظور استفاده می‌کنیم.
1) IP درخواست جاری (قابل بازیابی از هدر HTTP_X_FORWARDED_FOR یا REMOTE_ADDR)
2) مشخصات مرورگر کاربر (قابل بازیابی از هدر USER_AGENT)
3) آدرس درخواست جاری (برای اینکه شناسه‌ی تولیدی کاملا یکتا باشد، هرچند می‌توانید آن را حذف کنید)

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

مرحله بعد پیاده سازی مکانیزمی برای نگهداری این اطلاعات و بازیابی آن‌ها در هر درخواست است. ما برای این منظور از سیستم Cache استفاده می‌کنیم؛ هرچند راه حل‌های بهتری هم وجود دارند.
بنابراین پس از ایجاد شناسه یکتای درخواست، آن را در Cache قرار می‌دهیم و زمان انقضای آن را هم پارامتری که ابتدای کار گفتم قرار می‌دهیم. سپس در هر درخواست Cache را برای این مقدار یکتا جستجو می‌کنیم. اگر شناسه پیدا شود، یعنی در کمتر از زمان تعیین شده، درخواست مجددی از سوی کاربر صورت گرفته است و اگر شناسه در Cache موجود نباشد، یعنی درخواست رسیده در زمان معقولی صادر شده است.
باید توجه داشته باشید که تعیین زمان بین هر درخواست به ازای هر ActionMethod خواهد بود و نباید آنقدر زیاد باشد که عملا کاربر را محدود کنیم. برای مثال در یک سیستم چت، زمان معقول بین هر درخواست 5 ثانیه است و در یک سیستم ارسال نظر یا پاسخ، 10 ثانیه. در هر حال بسته به نظر شما این زمان می‌تواند قابل تغییر باشد. حتی می‌توانید کاربر را مجبور کنید که در روز فقط یک دیدگاه ارسال کند!

قبل از پیاده سازی سناریوی فوق، در مورد نقش گزینه‌ی سوم در شناسه‌ی درخواست، لازم است توضیحاتی بدهم. با استفاده از این خصوصیت (یعنی آدرس درخواست جاری) شدت سختگیری ما کمتر می‌شود. زیرا به ازای هر آدرس، شناسه‌ی تولیدی متفاوت خواهد بود. اگر فرد مهاجم، برنامه‌ای را که با آن اسپم می‌کند، طوری طراحی کرده باشد که مرتبا درخواست‌ها را به آدرس‌های متفاوتی ارسال کند، مکانیزم ما کمتر با آن مقابله خواهد کرد.
برای مثال فرد مهاجم می‌تواند در یک حلقه، ابتدا درخواستی را به AddComment بدهد، بعد AddReply و بعد SendMessage. پس همانطور که می‌بینید اگر از پارامتر سوم استفاده کنید، عملا قدرت مکانیزم ما به یک سوم کاهش می‌یابد.
نکته‌ی دیگری که قابل ذکر است اینست که این روش راهی برای تشخیص زمان بین درخواست‌های صورت گرفته از کاربر است و به تنهایی نمی‌تواند امنیت کامل را برای مقابله با اسپم‌ها، مهیا کند و باید به فکر مکانیزم دیگری برای مقابله با کاربری که درخواست‌های نامعقولی در مدت زمان کمی می‌فرستد پیاده کنیم (پیاده سازی مکانیزم تکمیلی را در آینده شرح خواهم داد).
اکنون نوبت پیاده سازی سناریوی ماست. ابتدا یک کلاس ایجاد کنید و آن را از ActionFilterAttribute مشتق کنید و کدهای زیر را وارد کنید:
using System;
using System.Linq;
using System.Web.Mvc;
using System.Security.Cryptography;
using System.Text;
using System.Web.Caching;

namespace Parsnet.Core
{
    public class StopSpamAttribute : ActionFilterAttribute
    {
        // حداقل زمان مجاز بین درخواست‌ها برحسب ثانیه
        public int DelayRequest = 10;

        // پیام خطایی که در صورت رسیدن درخواست غیرمجاز باید صادر کنیم
        public string ErrorMessage = "درخواست‌های شما در مدت زمان معقولی صورت نگرفته است.";

        //خصوصیتی برای تعیین اینکه آدرس درخواست هم به شناسه یکتا افزوده شود یا خیر
        public bool AddAddress = true;


        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            // درسترسی به شئی درخواست
            var request = filterContext.HttpContext.Request;

            // دسترسی به شیئ کش
            var cache = filterContext.HttpContext.Cache;

            // کاربر IP بدست آوردن
            var IP = request.ServerVariables["HTTP_X_FORWARDED_FOR"] ?? request.UserHostAddress;

            // مشخصات مرورگر
            var browser = request.UserAgent;

            // در اینجا آدرس درخواست جاری را تعیین می‌کنیم
            var targetInfo = (this.AddAddress) ? (request.RawUrl + request.QueryString) : "";

            // شناسه یکتای درخواست
            var Uniquely = String.Concat(IP, browser, targetInfo);


            //در اینجا با کمک هش یک امضا از شناسه‌ی درخواست ایجاد می‌کنیم
            var hashValue = string.Join("", MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(Uniquely)).Select(s => s.ToString("x2")));

            // ابتدا چک می‌کنیم که آیا شناسه‌ی یکتای درخواست در کش موجود نباشد
            if (cache[hashValue] != null)
            {
                // یک خطا اضافه می‌کنیم ModelState اگر موجود بود یعنی کمتر از زمان موردنظر درخواست مجددی صورت گرفته و به
                filterContext.Controller.ViewData.ModelState.AddModelError("ExcessiveRequests", ErrorMessage);
            }
            else
            {
                // اگر موجود نبود یعنی درخواست با زمانی بیشتر از مقداری که تعیین کرده‌ایم انجام شده
                // پس شناسه درخواست جدید را با پارامتر زمانی که تعیین کرده بودیم به شیئ کش اضافه می‌کنیم
                cache.Add(hashValue, true, null, DateTime.Now.AddSeconds(DelayRequest), Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
            }

            base.OnActionExecuting(filterContext);
        }
    }
}
و حال برای استفاده از این مکانیزم امنیتی ActionMethod مورد نظر را با آن امضا می‌کنیم:
[HttpPost]
        [StopSpam(DelayRequest = 5)]
        [ValidateAntiForgeryToken]
        public virtual async Task<ActionResult> SendFile(HttpPostedFileBase file, int userid = 0)
        { 
        
        }

[HttpPost]
        [StopSpam(DelayRequest = 30, ErrorMessage = "زمان لازم بین ارسال هر مطلب 30 ثانیه است")]
        [ValidateAntiForgeryToken]
        public virtual async Task<ActionResult> InsertPost(NewPostModel model)
        {
     
        }

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

به توجه به دیدگاه‌های مطرح شده اصلاحاتی در کلاس صورت گرفت و قابلیتی به آن اضافه گردید که بتوان مکانیزم اعتبارسنجی را کنترل کرد.
برای این منظور خصوصیتی به این ActionFilter افزوده شد تا هنگامیکه داده‌های فرم معتبر نباشند و در واقع هنوز چیزی ثبت نشده است این مکانیزم را بتوان کنترل کرد. خصوصیت CheckResult باعث میشود تا اگر داده‌های مدل ما در اعتبارسنجی، معتبر نبودند کلید افزوده شده به کش را حذف تا کاربر بتواند مجدد فرم را ارسال کند. مقدار آن به طور پیش فرض true است و اگر برابر false قرار بگیرد تا اتمام زمان تعیین شده در مکانیزم ما، کاربر امکان ارسال مجدد فرم را ندارد.
همچنین باید بعد از اتمام عملیات در صورت عدم موفقیت آمیز بودن آن به ViewBag یک خصوصیت به نام ExecuteResult اضافه کنید و مقدار آن را برابر false قرار دهید. تا کلید از کش حذف گردد.
نحوه استفاده آن هم به شکل زیر می‌باشد:
        [HttpPost]
        [StopSpam(AddAddress = true, DelayRequest = 20)]
        [ValidateAntiForgeryToken]
        public Task<ActionResult> InsertPost(NewPostModel model)
        {
            if (ModelState.IsValid)
            {
                var newPost = dbContext.InsertPost(model);
                if (newPost != null)
                {
                    ViewBag.ExecuteResult = true;
                }
            }

            if (ModelState.IsValidField("ExcessiveRequests") == true)
{
ViewBag.ExecuteResult = false;
}
return View(); }

فایل ضمیمه را می‌توانید از زیر دانلود کنید:
StopSpamAttribute.rar
نظرات مطالب
محدود کردن کاربر‌ها به آپلود فایل‌هایی خاص در ASP.NET MVC
برای بررسی محتوایی خاص، نیاز به parser مخصوص این نوع فایل‌ها است تا بتواند بررسی کند محتوای دریافتی معتبر است یا نه و عموما کسی محتوای این نوع فایل‌ها را بررسی نمی‌کند. بنابراین بررسی پسوند در اکثر موارد کافی است.
مشکل این نیست که فایل rar واقعا rar است یا نه. مشکل این است که این فایل ارسالی، قابلیت اجرا را از طریق فراخوانی آدرس آن در مرورگر، نداشته باشد. بحث بررسی پسوند هم به همین دلیل است. فایل aspx را می‌شود از طریق فراخوانی در مرورگر بر روی سرور اجرا کرد ولی فایلی با پسوند rar این قابلیت را ندارد.
نظرات مطالب
غیرمعتبر شدن کوکی‌های برنامه‌های ASP.NET Core هاست شده‌ی در IIS پس از ری‌استارت آن
یک نکته‌ی تکمیلی: پیاده سازی IXmlRepository مایکروسافت برای EF Core

از زمان ارائه‌ی NET Core 2.2.، بسته‌ی نیوگت جدید Microsoft.AspNetCore.DataProtection.EntityFrameworkCore ارائه شده‌است که کار آن دقیقا شبیه به پیاده سازی «یک نکته‌ی تکمیلی: روش ذخیره سازی کلید موقتی تولید شده در بانک اطلاعاتی بجای حافظه‌ی سرور» است که در نظرات فوق ارائه شد.
برای استفاده‌ی از آن، ابتدا بسته‌ی نیوگت آن‌را به برنامه اضافه کنید:
dotnet add package Microsoft.AspNetCore.DataProtection.EntityFrameworkCore

سپس Context ای را که بر اساس اینترفیس IDataProtectionKeyContext آن پیاده سازی شده‌است و دارای DbSet جدید از نوع DataProtectionKey است، تعریف کنید:
    public class MyKeysContext : DbContext, IDataProtectionKeyContext
    {
        // A recommended constructor overload when using EF Core 
        // with dependency injection.
        public MyKeysContext(DbContextOptions<MyKeysContext> options) 
            : base(options) { }

       // This maps to the table that stores keys.
        public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
    }
که با اجرای مهاجرت‌ها، یک جدول جدید را با سه فیلد زیر، ایجاد می‌کند:
public int Id { get; set; }
public string FriendlyName { get; set; }
public string XmlData { get; set; }

در آخر روش معرفی این Context به سیستم DataProtection به صورت زیر است:
public void ConfigureServices(IServiceCollection services)
{
   // using Microsoft.AspNetCore.DataProtection;
    services.AddDataProtection()
        .PersistKeysToDbContext<MyKeysContext>();
}
به این ترتیب، به صورت خودکار، اطلاعات موقتی کلیدهای رمزنگاری سیستم data-protection در بانک اطلاعاتی ذخیره شده و یا بازیابی می‌شوند.
مطالب
خلاصه‌ای از روش‌های ارسال داده‌های سمت سرور به کدهای جاوا اسکریپتی در ASP.NET MVC
روش‌های زیادی برای ارسال داده‌های سمت سرور تهیه شده در یک برنامه‌ی ASP.NET به کدهای سمت کاربر JavaScript ایی وجود دارند که تعدادی از مهم‌ترین‌های آن‌ها را در این مطلب بررسی خواهیم کرد.

روش اول: دریافت اطلاعات سمت سرور به کمک درخواست‌های Ajax

استفاده از Ajax یکی از روش‌های کلاسیک دریافت اطلاعات سمت سرور در کدهای جاوا اسکریپتی است.
<script type="text/javascript">
var products = [];
$(function() {
    $.getJSON("/home/products", function(response) {
        products = response.products;
    });
});
</script>
برای نمونه در اینجا با استفاده از امکانات jQuery، درخواست Ajax ایی به سرور ارسال شده و سپس نتیجه‌ی دریافتی، به آرایه‌ی جاوا اسکریپتی products نسبت داده شده‌است.
- مزایا: استفاده از Ajax، روشی بسیار متداول و شناخته شده‌است و به کمک انواع و اقسام روش‌های بازگشت JSON از سرور، می‌توان با آن کار کرد.
- معایب: درخواست Ajax، صرفا پس از بارگذاری اولیه‌ی صفحه به سمت سرور ارسال خواهد شد و در این بین، کاربر وقفه‌ای را مشاهده خواهد کرد. همچنین در اینجا بجای یک درخواست از سرور، حداقل دو درخواست باید ارسال شوند؛ یکی برای بارگذاری صفحه‌ی اصلی و دیگری برای دریافت اطلاعات Ajax ایی از سرور به صورت غیرهمزمان.


روش دوم: دریافت اطلاعات از یک فایل جاوا اسکریپتی خارجی

اطلاعات سمت کاربر را از یک فایل جاوا اسکریپتی خارجی الحاق شده‌ی به صفحه‌ی جاری نیز می‌توان تهیه کرد:
 <script src="/file.js"></script>
یک چنین فایلی را می‌توان توسط کدهای سمت سرور نیز بازگشت داد و اطلاعات آن‌را تهیه و پر کرد. در این حالت فقط کافی است که ContentType شیء Response را از نوع application/javascript مشخص کنیم و سپس یک خروجی متنی جاوا اسکریپتی را از سمت سرور بازگشت دهیم.
این روش نیز تقریبا مانند حالت یک درخواست Ajax ایی کار می‌کند و اطلاعات مورد نیاز را در طی یک درخواست جداگانه، پس از بارگذاری صفحه‌ی اصلی، از سرور دریافت خواهد کرد. البته در حالت کار با Ajax، می‌توان در طی یک callback، نتیجه را دریافت کرد و سپس عکس العمل نشان داد؛ اما در اینجا callback ایی وجود ندارد.


روش سوم: استفاده از SignalR

در SignalR ابتدا سعی می‌شود تا با استفاده از Web Sockets ارتباطی ماندگار بین کلاینت و سرور برقرار شود و سپس در این حالت، سرور می‌تواند مدام اطلاعاتی، مانند تغییرات داده‌های خود را به سمت کاربر، جهت نمایش و یا محاسبات خاص خود ارسال کند. اگر حالت Web Socket میسر نباشد (توسط سرور یا کلاینت پشتیبانی نشود)، به حالت‌های دیگری مانند server events, forever frames, long polling سوئیچ خواهد کرد. اطلاعات بیشتر


روش چهارم: قرار دادن اطلاعات سمت سرور در کدهای HTML صفحه

روش متداول دیگری جهت تامین اطلاعات جاوا اسکریپتی سمت کاربر، قرار دادن آن‌ها در ویژگی‌های data-* ارائه شده در HTML5 است.
<ul>
@foreach (var product in products)
{
  <li id="product@product.Id" data-rank="@product.Rank">@product.Name</li>
}
</ul>
در اینجا لیستی از محصولات، به صورت متداولی از کنترلر دریافت گردیده و سپس ویژگی data-rank هر ردیف نمایش داده شده نیز توسط این اطلاعات سمت سرور مقدار دهی شده‌اند.
اکنون برای دسترسی به مقدار data-rank سطری مانند product1، در کدهای جاوا اسکریپتی صفحه می‌توان نوشت:
<script type="text/javascript">
var product1Rank = $("#product1").data("rank");
</script>
این روش برای قرار دادن اطلاعات ثابت اشیاء، روش مرسومی است.


روش پنجم: قرار دادن اطلاعات سمت سرور در کدهای جاوا اسکریپتی صفحه

این روش همانند روش چهارم است، با این تفاوت که اینبار اطلاعات مورد نیاز، مستقیما به یک متغیر جاوا اسکریپتی انتساب داده شده‌است:
 <script type="text/javascript">
var product1Name = "@product1.Name";
</script>
مزیت این روش، عدم ارسال درخواست اضافه‌تری به سرور برای دریافت اطلاعات مورد نیاز است. مقدار product1Name در همان بار اول رندر صفحه از سمت سرور، مشخص می‌گردد.


روش ششم: انتساب یک شیء دات نتی به یک متغیر جاوا اسکریپتی

این روش همانند روش پنجم است، با این تفاوت که اینبار قصد داریم بجای یک مقدار ثابت رشته‌ای یا عددی، برای مثال، آرایه‌ای از اشیاء را به یک متغیر جاوا اسکریپتی انتساب دهیم. در اینجا ابتدا اطلاعات مورد نظر را به فرمت JSON تبدیل می‌کنیم:
//سمت سرور
[HttpGet]
public ActionResult Index()
{
    var array = new[]
    {
        "Afghanistan",
        "Albania",
        "Algeria",
        "American Samoa",
        "Andorra",
        "Angola",
        "Anguilla",
        "Antarctica",
        "Antigua and/or Barbuda"
     };
     ViewBag.JsonString = new JavaScriptSerializer().Serialize(array);
     return View();
}
سپس توسط متد Html.Raw می‌توان این رشته‌ی JSON را که اکنون حاوی آرایه جاوا اسکریپتی سمت سرور است، به یک متغیر جاوا اسکریپتی نسبت داد:
 //سمت کلاینت
<script type="text/javascript">
var jsonArray = @Html.Raw(@ViewBag.JsonString);
</script>
استفاده از Html.Raw در این حالت از این جهت ضروری است که اطلاعاتی مانند [] به صورت encode شده در سمت کاربر نمایش داده نشوند؛ چون Razor به صورت پیش فرض اطلاعات را Encode می‌کند.
و یا اینکار را به صورت خلاصه به شکل زیر نیز می‌توان در سمت کاربر انجام داد:
 <script type="text/javascript">
  var model = @Html.Raw(Json.Encode(Model));
  // your js code here
</script>
در اینجا کار تبدیل اطلاعات مدل به JSON، در همان سمت RazorView انجام شده‌است.
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 18 - کار با ASP.NET Web API
در ASP.NET Core، برخلاف نگارش‌های قبلی ASP.NET که ASP.NET Web API مجزای از ASP.NET MVC و همچنین وب فرم‌ها ارائه شده بود، اکنون جزئی از ASP.NET MVC است و با آن یکپارچه می‌باشد. بنابراین پیشنیازهای راه اندازی Web API با ASP.NET Core شامل سه مورد ذیل هستند که پیشتر آن‌ها را بررسی کردیم:
الف) فعال سازی ارائه‌ی فایل‌های استاتیک
ب) فعال سازی ASP.NET MVC
ج) آشنایی با تغییرات مسیریابی

و مابقی آن صرفا یک سری نکات تکمیلی هستند که در ادامه آن‌ها را بررسی خواهیم کرد.


تعریف مسیریابی کلی کنترلر

در اینجا همانند مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 9 - بررسی تغییرات مسیریابی»، می‌توان در صورت نیاز، مسیریابی کلی کنترلر را توسط ویژگی Route بازنویسی کرد و برای مثال درخواست‌های آن‌را محدود به درخواست‌هایی کرد که با api/ شروع شوند:
[Route("api/[controller]")] // http://localhost:7742/api/test
public class TestController : Controller
{
    private readonly ILogger<TestController> _logger;
 
    public TestController(ILogger<TestController> logger)
    {
        _logger = logger;
    }
[controller] هم در اینجا یک توکن پیش فرض است که با نام کنترلر جاری یا همان Test، به صورت خودکار جایگزین می‌شود.
در مورد سرویس ثبت وقایع نیز در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 17 - بررسی فریم ورک Logging» بحث کردیم و از آن می‌توان برای ثبت استثناءهای رخ داده استفاده کرد.


یک کنترلر ، اما با قابلیت‌های متعدد

همانطور که ملاحظه می‌کنید، اینبار کلاس پایه‌ی این کنترلر Test، همان Controller متداول ASP.NET MVC ذکر شده‌است و نه Api Controller سابق. تمام قابلیت‌های موجود در این‌دو توسط همان Controller ارائه می‌شوند.


هنوز پیش فرض‌های سابق Web API برقرار هستند

در مثال ذیل که به نظر یک کنترلر ASP.NET MVC است،
- هنوز متد Get مربوط به Web API که به صورت پیش فرض به درخواست‌های Get ختم شده‌ی به نام کنترلر پاسخ می‌دهد، برقرار است (متد IEnumerable<string> Get). برای مثال اگر شخصی در مرورگر، آدرس http://localhost:7742/api/test را درخواست دهد، متد Get اجرا می‌شود.
- در اینجا می‌توان نوع خروجی متد را دقیقا از همان نوع اشیاء مدنظر، تعیین کرد؛ برای نمونه تعریف  <IEnumerable<string در مثال زیر.
- مهم نیست که از return Json استفاده کنید و یا خروجی را مستقیما با فرمت <IEnumerable<string ارائه دهید.
- اگر نیاز به کنترل بیشتری بر روی HTTP Response Status بازگشتی داشتید، می‌توانید از متدهایی مانند return Ok و یا return BadRequest در صورت بروز مشکلی استفاده نمائید. برای مثال در متد IActionResult GetEpisodes2، استثنای فرضی حاصل، ابتدا توسط سرویس ثبت وقایع ذخیره شده و در آخر یک BadRequest بازگشت داده می‌شود.
- تمام مسیریابی‌ها را توسط ویژگی Route و یا نوع‌های درخواستی مانند HttpGet، می‌توان بازنویسی کرد؛ مانند مسیر /api/path1
- امکان محدود ساختن نوع پارامترهای دریافتی همانند متد Get(int page) ذیل، توسط ویژگی‌های مسیریابی وجود دارد.
[Route("api/[controller]")] // http://localhost:7742/api/test
public class TestController : Controller
{
    private readonly ILogger<TestController> _logger;
 
    public TestController(ILogger<TestController> logger)
    {
        _logger = logger;
    }
 
    [HttpGet]
    public IEnumerable<string> Get() // http://localhost:7742/api/test
    {
        return new [] { "value1", "value2" };
    }
 
    [HttpGet("{page:int}")]
    public IActionResult Get(int page) // http://localhost:7742/api/test/1
    {
        return Json(new[] { "value3", "value4" });
    }
 
    [HttpGet("/api/path1")]
    public IActionResult GetEpisodes1() // http://localhost:7742/api/path1
    {
        return Json(new[] { "value5", "value6" });
    }
 
    [HttpGet("/api/path2")]
    public IActionResult GetEpisodes2() // http://localhost:7742/api/path2
    {
        try
        {
            // get data from the DB ...
            return Ok(new[] { "value7", "value8" });
        }
        catch (Exception ex)
        {
            _logger.LogError("Failed to get data from the API", ex);
            return BadRequest();
        }
    } 
}
بنابراین در اینجا اگر می‌خواهید یک کنترلر ASP.NET Web API 2.x را به ASP.NET Core 1.0 ارتقاء دهید، تمام متدهای Get و Put و امثال آن هنوز معتبر هستند و مانند سابق عمل می‌کنند:
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET: api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }
// GET api/values/5
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "value";
        }
// POST api/values
        [HttpPost]
        public void Post([FromBody]string value)
        {
        }
// PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
        }
// DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}
در مورد ویژگی FromBody در ادامه بیشتر بحث خواهد شد.

یک نکته: اگر می‌خواهید خروجی Web API شما همواره JSON باشد، می‌توانید ویژگی جدید Produces را به شکل ذیل به کلاس کنترلر اعمال کنید:
 [Produces("application/json")]
[Route("api/[controller]")] // http://localhost:7742/api/test
public class TestController : Controller


تغییرات Model binding پیش فرض، برای پشتیبانی از ASP.NET MVC و ASP.NET Web API

فرض کنید مدل زیر را به برنامه اضافه کرده‌اید:
namespace Core1RtmEmptyTest.Models
{
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    }
}
و همچنین قصد دارید اطلاعات آن‌را از کاربر توسط یک عملیات POST دریافت کرده و به شکل JSON نمایش دهید:
using Core1RtmEmptyTest.Models;
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class PersonController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
 
        [HttpPost]
        public IActionResult Index(Person person)
        {
            return Json(person);
        }
    }
}
برای اینکار، از jQuery به نحو ذیل استفاده می‌کنیم (از این جهت که بیشتر ارسال‌های به سرور جهت کار با Web API نیز Ajax ایی هستند):
@section scripts
{
    <script type="text/javascript">
        $(function () {
            $.ajax({
                type: 'POST',
                url: '/Person/Index',
                dataType: 'json',
                contentType: 'application/json; charset=utf-8',
                data: JSON.stringify({
                    FirstName: 'F1',
                    LastName: 'L1',
                    Age: 23
                }),
                success: function (result) {
                    console.log('Data received: ');
                    console.log(result);
                }
            });
        });
    </script>
}


همانطور که مشاهده می‌کنید، اگر در ابتدای این متد یک break-point قرار دهیم، اطلاعاتی را از سمت کاربر دریافت نکرده‌است و مقادیر دریافتی نال هستند.
این مورد یکی از مهم‌ترین تغییرات Model binding این نگارش از ASP.NET MVC با نگارش‌های قبلی آن است. در اینجا اشیاء پیچیده از request body دریافت و bind نمی‌شوند و باید به نحو ذیل، محل دریافت و تفسیر آن‌ها را دقیقا مشخص کرد:
 public IActionResult Index([FromBody]Person person)
زمانیکه ویژگی FromBody را مشخص می‌کنیم، آنگاه اطلاعات دریافتی از request body دریافتی، به شیء Person نگاشت خواهند شد.


نکته‌ی مهم: حتی اگر FromBody را ذکر کنید ولی از JSON.stringify در سمت کاربر استفاده نکنید، باز هم نال دریافت خواهید کرد. بنابراین در این نگارش ذکر JSON.stringify نیز الزامی است.


حالت‌های دیگر تغییرات Model Binding در ASP.NET Core

تا اینجا مشخص شد که اگر یک درخواست Ajax ایی را به سمت سرور یک برنامه‌ی ASP.NET Core ارسال کنیم، به صورت پیش فرض به اشیاء پیچیده‌ی سمت سرور bind نمی‌شود و باید حتما ویژگی FromBody را نیز مشخص کرد تا اطلاعات را از request body واکشی کند (محل دریافت اطلاعات پیش فرض آن نامشخص است).
یک سؤال: اگر به سمت یک چنین اکشن متدی، اطلاعات فرمی را به حالت معمول ارسال کنیم، چه اتفاقی رخ خواهد داد؟
ارسال اطلاعات فرم‌ها به سرور، همواره شامل دو تغییر ذیل است:
  var dataType = 'application/x-www-form-urlencoded; charset=utf-8';
 var data = $('form').serialize();
اطلاعات فرم سریالایز می‌شوند و data type مخصوصی هم برای آن‌ها تنظیم خواهد شد. در این حالت، ارسال یک چنین اطلاعاتی به سمت اکشن متد فوق، با خطای 415 unsupported media type متوقف می‌شود. برای رفع این مشکل باید از ویژگی دیگری به نام FromForm استفاده کرد:
 [HttpPost]
public IActionResult Index([FromForm]Person person)
حالت‌های دیگر ممکن را در تصویر ذیل ملاحظه می‌کنید:


علت این مساله نیز بالا رفتن میزان امنیت سیستم است. در نگارش‌های قبلی، تمام مکان‌ها و حالت‌های میسر جستجو می‌شوند و اگر یکی از آن‌ها قابلیت تطابق با خواص شیء مدنظر را داشته باشد، کار binding به پایان می‌رسد. اما در اینجا با مشخص شدن محل دقیق منبع اطلاعات، دیگر سایر حالات جستجو نشده و سطح حمله کاهش پیدا می‌کند.
در اینجا باید مشخص کرد که دقیقا اطلاعاتی که قرار است به یک شیء پیچیده Bind شوند، آیا از یک Form تامین می‌شوند، یا از Body و یا از هدر، کوئری استرینگ، مسیریابی و یا حتی از یک سرویس.
تمام این حالت‌ها مشخص هستند (برای مثال دریافت اطلاعات از هدر درخواست HTTP و انتساب آن‌ها به خواص متناظری در شیء مشخص شده)، منهای FromService آن که به نحو ذیل عمل می‌کند:
در این حالت می‌توان در سازنده‌ی کلاس مدل خود، سرویسی را تزریق کرد و توسط آن خاصیتی را مقدار دهی نمود:
public class ProductModel
{
    public ProductModel(IProductService prodService)
    {
        Value = prodService.Get(productId);
    }
    public IProduct Value { get; private set; }
}
این تزریق وابستگی‌ها برای اینکه تکمیل شود، نیاز به ویژگی FromServices خواهد داشت:
 public async Task<IActionResult> GetProduct([FromServices]ProductModel product)
{
}
وجود ویژگی FromServices به این معنا است که سرویس‌های مدل یاد شده را از تنظیمات ابتدایی IoC Container خود خوانده و سپس در اختیار مدل جاری قرار بده. به این ترتیب حتی تزریق وابستگی‌ها در مدل‌های برنامه هم میسر می‌شود.


تغییر تنظیمات اولیه‌ی خروجی‌های ASP.NET Web API

در اینجا حالت ارائه‌ی خروجی XML به صورت پیش فرض فعال نیست. اگر علاقمند به افزودن آن نیز باشید، نحوه‌ی کار را در متد ConfigureServices کلاس آغازین برنامه در کدهای ذیل مشاهده می‌کنید:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.FormatterMappings.SetMediaTypeMappingForFormat("xml", new MediaTypeHeaderValue("application/xml")); 
 
    }).AddJsonOptions(options =>
    {
        options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Include;
        options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
    });
همچنین اگر خواستید تنظیمات ابتدایی JSON.NET را تغییر داده و برای مثال خروجی JSON تولیدی را camel case کنید، این‌کار را توسط متد AddJsonOptions به نحو فوق می‌توان انجام داد.
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت نهم- مدیریت طول عمر توکن‌ها
توکن‌های صادر شده‌ی توسط IdentityServer به دلایل امنیتی، طول عمر محدودی دارند. بنابراین اولین سؤالی که در اینجا مطرح خواهد شد، این است: «اگر توکنی منقضی شد، چه باید کرد؟» و یا «اگر خواستیم به صورت دستی طول عمر توکنی را پایان دهیم، چه باید کرد؟»


بررسی طول عمر توکن‌ها

اگر مرورگر خود را پس از لاگین به سیستم، برای مدتی به حال خود رها کنید، پس از شروع به کار مجدد، مشاهده خواهید کرد که دیگر نمی‌توانید به API دسترسی پیدا کنید. علت اینجا است که Access token صادر شده، منقضی شده‌است. تمام توکن‌ها، دارای طول عمر مشخصی هستند و پس از سپری شدن این زمان، دیگر اعتبارسنجی نخواهند شد. زمان انقضای توکن، در خاصیت یا claim ویژه‌ای به نام exp ذخیره می‌شود.
در اینجا ما دو نوع توکن را داریم: Identity token و Access token
از Identity token برای ورود به سیستم کلاینت استفاده می‌شود و به صورت پیش‌فرض طول عمر کوتاه آن به 5 دقیقه تنظیم شده‌است. علت کوتاه بودن این زمان این است که این توکن‌ها تنها یکبار مورد استفاده قرار می‌گیرد و پس از ارائه‌ی آن به کلاینت، از طریق آن Claim Identity تولید می‌شود. پس از آن طول عمر Claim Identity تولید شده صرفا به تنظیمات برنامه‌ی کلاینت مرتبط است و می‌تواند از تنظیمات IDP کاملا مجزا باشد؛ مانند پیاده سازی sliding expiration. در این حالت تا زمانیکه کاربر در برنامه فعال است، در حالت logged in باقی خواهد ماند.

Access tokenها متفاوت هستند. طول عمر پیش‌فرض آن‌ها به یک ساعت تنظیم شده‌است و نسبت به Identity token طول عمر بیشتری دارند. پس از اینکه این زمان سپری شد، تنها با داشتن یک Access token جدید است که دسترسی ما مجددا به Web API برقرار خواهد شد. بنابراین در اینجا ممکن است هنوز در برنامه‌ی کلاینت در حالت logged in قرار داشته باشیم، چون هنوز طول عمر Claim Identity آن به پایان نرسیده‌است، اما نتوانیم با قسمت‌های مختلف برنامه کار کنیم، چون نمی‌توانیم از یک Access token منقضی شده جهت دسترسی به منابع محافظت شده‌ی سمت Web API استفاده نمائیم. در اینجا دیگر برنامه‌ی کلاینت هیچ نقشی بر روی تعیین طول عمر یک Access token ندارد و این طول عمر صرفا توسط IDP به تمام کلاینت‌های آن دیکته می‌شود.
در اینجا برای دریافت یک Access token جدید، نیاز به یک Refresh token داریم که صرفا برای «کلاینت‌های محرمانه» که در قسمت سوم این سری آن‌ها را بررسی کردیم، توصیه می‌شود.


چگونه می‌توان زمان انقضای توکن‌ها را صریحا تنظیم کرد؟

برای تنظیم زمان انقضای توکن‌ها، از کلاس src\IDP\DNT.IDP\Config.cs سمت IDP شروع می‌کنیم.
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientName = "Image Gallery",
                    // IdentityTokenLifetime = ... // defaults to 300 seconds / 5 minutes
                    // AuthorizationCodeLifetime = ... // defaults to 300 seconds / 5 minutes
                    // AccessTokenLifetime = ... // defaults to 3600 seconds / 1 hour
                }
             };
        }
    }
}
- در اینجا در تنظیمات یک کلاینت جدید، خاصیت IdentityTokenLifetime آن، به طول عمر Identity token تولید شده اشاره می‌کند که مقدار پیش‌فرض آن عدد صحیح 300 ثانیه است یا معادل 5 دقیقه.
- مقدار خاصیت AuthorizationCodeLifetime تنظیمات یک کلاینت، عدد صحیحی است با مقدار پیش‌فرض 300 ثانیه یا معادل 5 دقیقه که طول عمر AuthorizationCode را تعیین می‌کند. این مورد، طول عمر توکن خاصی نیست و در حین فراخوانی Token Endpoint مبادله می‌شود و در طی Hybrid flow رخ می‌دهد. بنابراین مقدار پیش‌فرض آن بسیار مناسب بوده و نیازی به تغییر آن نیست.
- مقدار خاصیت AccessTokenLifetime تنظیمات یک کلاینت، عدد صحیحی است با مقدار پیش‌فرض 3600 ثانیه و یا معادل 1 ساعت و طول عمر Access token تولید شده‌ی توسط این IDP را مشخص می‌کند.
البته باید درنظر داشت اگر طول عمر این توکن دسترسی را برای مثال به 120 یا 2 دقیقه تنظیم کنید، پس از سپری شدن این 2 دقیقه ... هنوز هم برنامه‌ی کلاینت قادر است به Web API دسترسی داشته باشد. علت آن وجود بازه‌ی 5 دقیقه‌ای است که در طی آن، انجام این عملیات مجاز شمرده می‌شود و برای کلاینت‌هایی درنظر گرفته شده‌است که ساعت سیستم آن‌ها ممکن است اندکی با ساعت سرور IDP تفاوت داشته باشند.


درخواست تولید یک Access Token جدید با استفاده از Refresh Tokens

زمانیکه توکنی منقضی می‌شود، کاربر باید مجددا به سیستم لاگین کند تا توکن جدیدی برای او صادر گردد. برای بهبود این تجربه‌ی کاربری، می‌توان در کلاینت‌های محرمانه با استفاده از Refresh token، در پشت صحنه عملیات دریافت توکن جدید را انجام داد و در این حالت دیگر کاربر نیازی به لاگین مجدد ندارد. در این حالت برنامه‌ی کلاینت یک درخواست از نوع POST را به سمت IDP ارسال می‌کند. در این حالت عملیات Client Authentication نیز صورت می‌گیرد. یعنی باید مشخصات کامل کلاینت را به سمت IDP ارسال کرد. در اینجا اطلاعات هویت کلاینت در هدر درخواست و Refresh token در بدنه‌ی درخواست به سمت سرور IDP ارسال خواهند شد. پس از آن IDP اطلاعات رسیده را تعیین اعتبار کرده و در صورت موفقیت آمیز بودن عملیات، یک Access token جدید را به همراه Identity token و همچنین یک Refresh token جدید دیگر، صادر می‌کند.
برای صدور مجوز درخواست یک Refresh token، نیاز است scope جدیدی را به نام offline_access معرفی کنیم. به این معنا که امکان دسترسی به برنامه حتی در زمانیکه offline است، وجود داشته باشد. بنابراین offline در اینجا به معنای عدم لاگین بودن شخص در سطح IDP است.
بنابراین اولین قدم پیاده سازی کار با Refresh token، مراجعه‌ی به کلاس src\IDP\DNT.IDP\Config.cs و افزودن خاصیت AllowOfflineAccess با مقدار true به خواص یک کلاینت است:
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientName = "Image Gallery",
                    // IdentityTokenLifetime = ... // defaults to 300 seconds / 5 minutes
                    // AuthorizationCodeLifetime = ... // defaults to 300 seconds / 5 minutes
                    // AccessTokenLifetime = ... // defaults to 3600 seconds / 1 hour
                    AllowOfflineAccess = true,
                    // AbsoluteRefreshTokenLifetime = ... // Defaults to 2592000 seconds / 30 days
                    // RefreshTokenExpiration = TokenExpiration.Sliding
                    UpdateAccessTokenClaimsOnRefresh = true,
                    // ...
                }
             };
        }
    }
}
- در اینجا می‌توان خاصیت AbsoluteRefreshTokenLifetime را که بیانگر طول عمر Refresh token است، تنظیم کرد. مقدار پیش‌فرض آن 2592000  ثانیه و یا معادل 30 روز است.
- البته RefreshToken ضرورتی ندارد که طول عمر Absolute و یا کاملا تعیین شده‌ای را داشته باشد. این رفتار را توسط خاصیت RefreshTokenExpiration می‌توان به TokenExpiration.Sliding نیز تنظیم کرد. البته حالت پیش‌فرض آن بسیار مناسب است.
- در اینجا می‌توان خاصیت UpdateAccessTokenClaimsOnRefresh را نیز به true تنظیم کرد. فرض کنید یکی از Claims کاربر مانند آدرس او تغییر کرده‌است. به صورت پیش‌فرض با درخواست مجدد توکن توسط RefreshToken، این Claims به روز رسانی نمی‌شوند. با تنظیم این خاصیت به true این مشکل برطرف خواهد شد.


پس از تنظیم IDP جهت صدور RefreshToken، اکنون کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامه‌ی MVC Client را به صورت زیر تکمیل می‌کنیم:
ابتدا در متد تنظیمات AddOpenIdConnect، نیاز است صدور درخواست scope جدید offline_access را صادر کنیم:
options.Scope.Add("offline_access");
همین اندازه تنظیم در سمت برنامه‌ی کلاینت برای دریافت refresh token و ذخیره سازی آن جهت استفاده‌های آتی کفایت می‌کند.

در ادامه نیاز است به سرویس ImageGalleryHttpClient مراجعه کرده و کدهای آن‌را به صورت زیر تغییر داد:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

namespace ImageGallery.MvcClient.Services
{
    public interface IImageGalleryHttpClient
    {
        Task<HttpClient> GetHttpClientAsync();
    }

    /// <summary>
    /// A typed HttpClient.
    /// </summary>
    public class ImageGalleryHttpClient : IImageGalleryHttpClient
    {
        private readonly HttpClient _httpClient;
        private readonly IConfiguration _configuration;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly ILogger<ImageGalleryHttpClient> _logger;

        public ImageGalleryHttpClient(
            HttpClient httpClient,
            IConfiguration configuration,
            IHttpContextAccessor httpContextAccessor,
            ILogger<ImageGalleryHttpClient> logger)
        {
            _httpClient = httpClient;
            _configuration = configuration;
            _httpContextAccessor = httpContextAccessor;
            _logger = logger;
        }

        public async Task<HttpClient> GetHttpClientAsync()
        {
            var accessToken = string.Empty;

            var currentContext = _httpContextAccessor.HttpContext;
            var expires_at = await currentContext.GetTokenAsync("expires_at");
            if (string.IsNullOrWhiteSpace(expires_at)
                || ((DateTime.Parse(expires_at).AddSeconds(-60)).ToUniversalTime() < DateTime.UtcNow))
            {
                accessToken = await RenewTokens();
            }
            else
            {
                accessToken = await currentContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            }

            if (!string.IsNullOrWhiteSpace(accessToken))
            {
                _logger.LogInformation($"Using Access Token: {accessToken}");
                _httpClient.SetBearerToken(accessToken);
            }

            _httpClient.BaseAddress = new Uri(_configuration["WebApiBaseAddress"]);
            _httpClient.DefaultRequestHeaders.Accept.Clear();
            _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            return _httpClient;
        }

        private async Task<string> RenewTokens()
        {
            // get the current HttpContext to access the tokens
            var currentContext = _httpContextAccessor.HttpContext;

            // get the metadata
            var discoveryClient = new DiscoveryClient(_configuration["IDPBaseAddress"]);
            var metaDataResponse = await discoveryClient.GetAsync();

            // create a new token client to get new tokens
            var tokenClient = new TokenClient(
                metaDataResponse.TokenEndpoint,
                _configuration["ClientId"],
                _configuration["ClientSecret"]);

            // get the saved refresh token
            var currentRefreshToken = await currentContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);

            // refresh the tokens
            var tokenResult = await tokenClient.RequestRefreshTokenAsync(currentRefreshToken);
            if (tokenResult.IsError)
            {
                throw new Exception("Problem encountered while refreshing tokens.", tokenResult.Exception);
            }

            // update the tokens & expiration value
            var updatedTokens = new List<AuthenticationToken>();
            updatedTokens.Add(new AuthenticationToken
            {
                Name = OpenIdConnectParameterNames.IdToken,
                Value = tokenResult.IdentityToken
            });
            updatedTokens.Add(new AuthenticationToken
            {
                Name = OpenIdConnectParameterNames.AccessToken,
                Value = tokenResult.AccessToken
            });
            updatedTokens.Add(new AuthenticationToken
            {
                Name = OpenIdConnectParameterNames.RefreshToken,
                Value = tokenResult.RefreshToken
            });

            var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResult.ExpiresIn);
            updatedTokens.Add(new AuthenticationToken
            {
                Name = "expires_at",
                Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
            });

            // get authenticate result, containing the current principal & properties
            var currentAuthenticateResult = await currentContext.AuthenticateAsync("Cookies");

            // store the updated tokens
            currentAuthenticateResult.Properties.StoreTokens(updatedTokens);

            // sign in
            await currentContext.SignInAsync("Cookies",
             currentAuthenticateResult.Principal, currentAuthenticateResult.Properties);

            // return the new access token
            return tokenResult.AccessToken;
        }
    }
}
تفاوت این کلاس با نمونه‌ی قبلی آن در اضافه شدن متد RenewTokens آن است.
پیشتر در قسمت ششم، روش کار مستقیم با DiscoveryClient و TokenClient را در حین کار با UserInfo Endpoint جهت دریافت دستی اطلاعات claims از IDP بررسی کردیم. در اینجا به همین ترتیب با TokenEndpoint کار می‌کنیم. به همین جهت توسط DiscoveryClient، متادیتای IDP را که شامل آدرس TokenEndpoint است، استخراج کرده و توسط آن TokenClient را به همراه اطلاعات کلاینت تشکیل می‌دهیم.
سپس مقدار refresh token فعلی را نیاز داریم. زیرا توسط آن است که می‌توانیم درخواست دریافت یکسری توکن جدید را ارائه دهیم. پس از آن با فراخوانی tokenClient.RequestRefreshTokenAsync(currentRefreshToken)، تعدادی توکن جدید را از سمت IDP دریافت می‌کنیم. لیست آن‌ها را تهیه کرده و توسط آن کوکی جاری را به روز رسانی می‌کنیم. در این حالت نیاز است مجددا SignInAsync فراخوانی شود تا کار به روز رسانی کوکی نهایی گردد.
خروجی این متد، مقدار access token جدید است.
پس از آن در متد GetHttpClientAsync بررسی می‌کنیم که آیا نیاز است کار refresh token صورت گیرد یا خیر؟ برای این منظور مقدار expires_at را دریافت و با زمان جاری با فرمت UTC مقایسه می‌کنیم. 60 ثانیه پیش از انقضای توکن، متد RenewTokens فراخوانی شده و توسط آن access token جدیدی برای استفاده‌ی در برنامه صادر می‌شود. مابقی این متد مانند قبل است و این توکن دسترسی را به همراه درخواست از Web API به سمت آن ارسال می‌کنیم.


معرفی Reference Tokens

تا اینجا با توکن‌هایی از نوع JWT کار کردیم. این نوع توکن‌ها، به همراه تمام اطلاعات مورد نیاز جهت اعتبارسنجی آن‌ها در سمت کلاینت، بدون نیاز به فراخوانی مجدد IDP به ازای هر درخواست هستند. اما این نوع توکن‌ها به همراه یک مشکل نیز هستند. زمانیکه صادر شدند، دیگر نمی‌توان طول عمر آن‌ها را کنترل کرد. اگر طول عمر یک Access token به مدت 20 دقیقه تنظیم شده باشد، می‌توان مطمئن بود که در طی این 20 دقیقه حتما می‌توان از آن استفاده کرد و دیگر نمی‌توان در طی این بازه‌ی زمانی دسترسی آن‌را بست و یا آن‌را برگشت زد. اینجاست که Reference Tokens معرفی می‌شوند. بجای قرار دادن تمام اطلاعات در یک JWT متکی به خود، این نوع توکن‌های مرجع، فقط یک Id هستند که به توکن اصلی ذخیره شده‌ی در سطح IDP لینک می‌شوند و به آن اشاره می‌کنند. در این حالت هربار که نیاز به دسترسی منابع محافظت شده‌ی سمت API را با یک چنین توکن دسترسی لینک شده‌ای داشته باشیم، Reference Token در پشت صحنه (back channel) به IDP ارسال شده و اعتبارسنجی می‌شود. سپس محتوای اصلی آن به سمت API ارسال می‌شود. این عملیات از طریق endpoint ویژه‌ای در IDP به نام token introspection endpoint انجام می‌شود. به این ترتیب می‌توان طول عمر توکن صادر شده را کاملا کنترل کرد؛ چون تنها تا زمانیکه در data store مربوط به IDP وجود خارجی داشته باشند، قابل استفاده خواهند بود. بنابراین نسبت به حالت استفاده‌ی از JWTهای متکی به خود، تنها عیب آن زیاد شدن ترافیک به سمت IDP جهت اعتبارسنجی Reference Token‌ها به ازای هر درخواست به سمت Web API است.


چگونه از Reference Token‌ها بجای JWTهای متکی به خود استفاده کنیم؟

برای استفاده‌ی از Reference Tokenها بجای JWTها، ابتدا نیاز به مراجعه‌ی به کلاس src\IDP\DNT.IDP\Config.cs و تغییر مقدار خاصیت AccessTokenType هر کلاینت است:
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientName = "Image Gallery",
// ...
                    AccessTokenType = AccessTokenType.Reference
                }
             };
        }
    }
}
مقدار پیش‌فرض AccessTokenType، همان Jwt یا توکن‌های متکی به خود است که در اینجا به Reference Token تغییر یافته‌است.
اینبار اگر برنامه را اجرا کنید و در کلاس ImageGalleryHttpClient برنامه‌ی کلاینت، بر روی سطر httpClient.SetBearerToken یک break-point قرار دهید، مشاهده خواهید کرد فرمت این توکن ارسالی به سمت Web API تغییر یافته و اینبار تنها یک Id ساده‌است که دیگر قابل decode شدن و استخراج اطلاعات دیگری از آن نیست. با ادامه جریان برنامه و رسیدن این توکن به سمت Web API، درخواست رسیده برگشت خواهد خورد و اجرا نمی‌شود.
علت اینجا است که هنوز تنظیمات کار با token introspection endpoint انجام نشده و این توکن رسیده‌ی در سمت Web API قابل اعتبارسنجی و استفاده نیست. برای تنظیم آن نیاز است یک ApiSecret را در سطح Api Resource مربوط به IDP تنظیم کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        // api-related resources (scopes)
        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource(
                    name: "imagegalleryapi",
                    displayName: "Image Gallery API",
                    claimTypes: new List<string> {"role" })
                {
                  ApiSecrets = { new Secret("apisecret".Sha256()) }
                }
            };
        }
اکنون فایل startup در سطح API را جهت معرفی این تغییرات به صورت زیر ویرایش می‌کنیم:
namespace ImageGallery.WebApi.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(defaultScheme: IdentityServerAuthenticationDefaults.AuthenticationScheme)
               .AddIdentityServerAuthentication(options =>
               {
                   options.Authority = Configuration["IDPBaseAddress"];
                   options.ApiName = "imagegalleryapi";
                   options.ApiSecret = "apisecret";
               });
در اینجا نیاز است ApiSecret تنظیم شده‌ی در سطح IDP معرفی شود.

اکنون اگر برنامه را اجرا کنید، ارتباط با token introspection endpoint به صورت خودکار برقرار شده، توکن رسیده اعتبارسنجی گردیده و برنامه بدون مشکل اجرا خواهد شد.


چگونه می‌توان Reference Tokenها را از IDP حذف کرد؟

هدف اصلی استفاده‌ی از Reference Tokenها به دست آوردن کنترل بیشتری بر روی طول عمر آن‌ها است و حذف کردن آن‌ها می‌تواند به روش‌های مختلفی رخ دهد. برای مثال یک روش آن تدارک یک صفحه‌ی Admin و ارائه‌ی رابط کاربری برای حذف توکن‌ها از منبع داده‌ی IDP است. روش دیگر آن حذف این توکن‌ها از طریق برنامه‌ی کلاینت با برنامه نویسی است؛ برای مثال در زمان logout شخص. برای این منظور، endpoint ویژه‌ای به نام token revocation endpoint در نظر گرفته شده‌است. فراخوانی آن از سمت برنامه‌ی کلاینت، امکان حذف توکن‌های ذخیره شده‌ی در سمت IDP را میسر می‌کند.
به همین جهت به کنترلر ImageGallery.MvcClient.WebApp\Controllers\GalleryController.cs مراجعه کرده و متد Logout آن‌را تکمیل می‌کنیم:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        public async Task Logout()
        {
            await revokeTokens();
            // Clears the  local cookie ("Cookies" must match the name of the scheme)
            await HttpContext.SignOutAsync("Cookies");
            await HttpContext.SignOutAsync("oidc");
        }

        private async Task revokeTokens()
        {
            var discoveryClient = new DiscoveryClient(_configuration["IDPBaseAddress"]);
            var metaDataResponse = await discoveryClient.GetAsync();
            var tokenRevocationClient = new TokenRevocationClient(
                metaDataResponse.RevocationEndpoint,
                _configuration["ClientId"],
                _configuration["ClientSecret"]
            );

            var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            if (!string.IsNullOrWhiteSpace(accessToken))
            {
                var response = await tokenRevocationClient.RevokeAccessTokenAsync(accessToken);
                if (response.IsError)
                {
                    throw new Exception("Problem accessing the TokenRevocation endpoint.", response.Exception);
                }
            }

            var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
            if (!string.IsNullOrWhiteSpace(refreshToken))
            {
                var response = await tokenRevocationClient.RevokeRefreshTokenAsync(refreshToken);
                if (response.IsError)
                {
                    throw new Exception("Problem accessing the TokenRevocation endpoint.", response.Exception);
                }
            }
        }
در اینجا در متد جدید revokeTokens، ابتدا توسط DiscoveryClient، به آدرس RevocationEndpoint دسترسی پیدا می‌کنیم. سپس توسط آن، TokenRevocationClient را تشکیل می‌دهیم. اکنون می‌توان توسط این کلاینت حذف توکن‌ها، دو متد RevokeAccessTokenAsync و RevokeRefreshTokenAsync آن‌را بر اساس مقادیر فعلی این توکن‌ها در سیستم، فراخوانی کرد تا سبب حذف آن‌ها در سمت IDP شویم.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
مطالب
توسعه‌ی Micro Frontends با Webpack
Micro Frontend چیست؟

micro frontend یک الگوی معماری (architecture pattern) می‌باشد؛ جایی که یک front-end app، به چند app  کوچکتر تقسیم می‌شود و هر کدام از آن‌ها به صورت مستقل  توسعه داده و تست می‌شوند.  مفهومی شبیه به مایکروسرویس‌ها است؛ اما برای سورس کد‌های یکپارچه‌ی سمت کلاینت. 


چرا؟
خیلی سخت است که بخواهیم روی سورس کد‌های یکپارچه سمت کلاینت تست نویسی، به‌روز رسانی و هم چنین نگهداری کنیم. این در حالی است که توانایی تیم را به منظور مستقل کار کردن بر روی بخش‌های مختلفی از app، محدود می‌کند. شکستن یک app یکپارچه به micro frontend‌های کوچکتر و قابل مدیریت، این امکان را فراهم میسازد که چندین تیم، به صورت مستقل کار کنند و از فریم ورک‌های ترجیحی خود استفاده کنند.

چگونه؟
اساسا 3 راه برای ادغام کردن ماژول‌های micro frontend  با container app وجود دارد. 

 1-Server integration

هر micro frontend در یک وب سرور  احتمالا جداگانه هاست شده‌است که مسئول رندر کردن و خدمت دادن markup‌های مربوطه می‌باشد. به محض دریافت درخواستی از سمت مرورگر، container app در خواست را برای markup، با برقراری تماس به سرور، برای micro frontend مربوطه انجام میدهد. 
این یک روش ایده آل نمی‌باشد؛ به‌طوریکه چندین تماس به سرور برای رندر کردن محتوا در صفحه برقرار می‌شود و  همچنین مستلزم پیاده سازی استراتژی cache، به منظور کاهش تاخیر است. 

2-Compile time integration

container app دسترسی به کد‌های micro frontend‌ها را در طول توسعه و زمان کامپایل دارد. یکی  از راه‌های انجام این روش این است که micro frontend‌ها را به عنوان بسته‌های npm منتشر کنیم و سپس از آن‌ها به عنوان یک وابستگی در container app استفاده کنیم. 
در حالیکه راه اندازی و پیاده سازی، در این حالت ساده است، اما یک وابستگی محکم بین container app  و micro frontend وجود دارد. هر زمانکه یک micro frontend بروزرسانی می‌شود، نیاز است که container app ،  به منظور یکپارچه کردن بروز رسانی، دوباره استقرار یابد.

3-Run time integration

container app دسترسی به کد‌های micro frontend‌ها را زمانیکه در مرورگر اجرا می‌شوند، دارد. یکی از راه‌های انجام این روش، استفاده از پلاگین Module Federation مربوط به Webpack است که مراقب ساختن، در دسترس قرار دادن و استفاده از وابستگی‌ها در زمان اجرا می‌باشد (در ادامه، این حالت را با جزئیات بیشتری مورد بررسی قرار خواهیم داد). 
در این حالت وابستگی بین container app و micro frontend‌ها وجود ندارد و یکپارچگی در زمان اجرا اتفاق می‌افتد. هر micro frontend می‌تواند به صورت مستقل توسعه داده شود و استقرار یابد، بدون اینکه container app را دوباره استقرار دهیم.
در این حالت راه اندازی در مقایسه با compile-time integration پیچیده‌تر است.



Webpack Module Federation Plugin

پلاگین module federation در Webpack 5.0 معرفی شد که  امکان توسعه app‌های micro frontend را با بارگذاری پویای کد‌های app‌های micro frontend را در container app ، فراهم میسازد. 
همچنین این امکان را فراهم می‌کند که dependency ها را بین remote app‌ها و host app، به منظور جلوگیری از کد‌های تکراری و کاهش دادن سایز build، به اشتراک بگذاریم.
 در این مقاله ما یک مثال را بررسی خواهیم کرد که شامل دو micro frontend ساده می‌باشد و سپس آن‌ها را در یک host app ادغام می‌کنیم. 
ساختار نهایی به‌صورت زیر خواهد بود:



ایجاد کردن اولین Remote app


یک پوشه را به نام remote1 ایجاد کنید و سپس یک فایل را به نام package.json، در پوشه‌ی ایجاد شده، با دستور زیر ایجاد کنید. 
npm init --yes
پوشه‌ی ایجاد شده را در code editor خود باز کنید (من از vs code استفاده می‌کنم) و سپس وابستگی‌های زیر را به فایل package.json اضافه کنید. 
npm install html-webpack-plugin webpack webpack-cli webpack-dev-server --save

1) webpack/ webpack-CLI: برای استفاده از webpack و دستورات webpack CLI
2) html-webpack-plugin/webpack-devserver: سرور توسعه محلی با live reloading

 و همچنین اسکریپت webpack serve را در بخش scripts، به منظور serve کردن application در مرورگر اضافه کنید.
اکنون فایل package.json شما همانند زیر می‌باشد: 
{
  "name": "remote1",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack serve"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "html-webpack-plugin": "^5.3.2",
    "webpack": "^5.57.0",
    "webpack-cli": "^4.8.0",
    "webpack-dev-server": "^4.3.1"
  }
}

در ادامه، یک پوشه‌ی جدید را به نام public  ایجاد کنید و یک فایل را به نام index.html در پوشه‌ی public اضافه کنید سپس HTML markup زیر را درون فایل index.html اضافه کنید، که شامل یک div نگهدارنده می‌باشد، جائیکه خروجی app در زمان توسعه، در آن قرار میگیرد. 
<!DOCTYPE html>
<html>

<head>
    <title>
        Remote1(Localhost:7001)
    </title>
</head>

<body>
    <div id="dev-remote1"></div>
</body>

</html>

در ادامه یک پوشه‌ی جدید را به نام src  ایجاد کنید و دو فایل را با نام‌های index.js و startup.js در آن قرار دهید. در ادامه به این دو فایل برمیگردیم و کد‌های لازم را در آن قرار میدهیم.
یک فایل جدید را به نام webpack.config.js در ریشه‌ی پروژه ایجاد می‌کنیم که در آن تنظیمات webpack  را قرار میدهیم. در این فایل، دو پلاگین را به نام‌های Webpack و Module Federation، اضافه می‌کنیم.
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  mode: 'development',
  devServer: {
    port: 7001,
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'remote1',
      filename: 'remoteEntry.js',
      exposes: {
        './RemoteApp1': './src/startup',
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

فایل remoteEntry.js شامل یک لیست از فایل‌های در معرض قرار داده شده‌ی توسط remote app به همراه مسیر‌های آنها می‌باشد. از این فایل در زمان ادغام کردن remote app در host app استفاده می‌شود.  
index.js نقطه‌ی ورودی remote app ما می‌باشد.  به منظور جلوگیری از eager loading، کد‌های startup را در یک js فایل جداگانه به نام startup.js قرار میدهیم. از این رو فایل index.js  شامل فقط یک import می‌باشد. 
import('./startup');
در درون فایل startup.js ، یک تابع به نام mount را export می‌کنیم. این تابع یک element را دریافت می‌کند؛ جائیکه قرار است خروجی app در آن قرار گیرد. در حالت development، خروجی در یک div نگهدارنده با شناسه dev-remote1 در فایل محلی index.html قرار میگیرد.
const mount = (el) => {
    el.innerHTML = '<div>Remote 1 Content</div>';
  };
  
  if (process.env.NODE_ENV === 'development') {
    const el = document.querySelector('#dev-remote1');
    if (el) {
      mount(el);
    }
  }
  
  export { mount };
فایل‌ها را ذخیره کنید و سپس دستور npm start را جهت مشاهده‌ی خروجی اجرا کنید. در اینجا برنامه بر روی پورت 7001 اجرا می‌شود . ( http://localhost:7001  )


ایجاد کردن دومین Remote app  


در اینجا نیز مراحل، دقیقا شبیه به ایجاد remote app قبلی می‌باشد. یک پوشه را به نام remote2 در کنار پوشه‌ی remote1 ایجاد کنید و یک فایل را به نام package.json، با وابستگی‌های معرفی شده ایجاد کنید و سپس دستور npm install را بزنید تا وابستگی‌ها نصب شوند.
{
  "name": "remote2",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack serve"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "html-webpack-plugin": "^5.3.2",
    "webpack": "^5.57.0",
    "webpack-cli": "^4.8.0",
    "webpack-dev-server": "^4.3.1"
  }
}
سپس یک فایل را با نام index.html و با محتوای زیر، در پوشه‌ی public ایجاد کنید:
<!DOCTYPE html>
<html>

<head>
  <title>
    Remote2(Localhost:7002)
  </title>
</head>

<body>
  <div id="dev-remote2"></div>
</body>

</html>

در ادامه یک فایل را با نام webpack.config.js در ریشه‌ی پروژه‌ی remote2 ایجاد کنید و محتوای زیر را در آن قرار دهید. مطمئن باشید که پورت اجرایی برنامه 7002 می‌باشد.
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  mode: 'development',
  devServer: {
    port: 7002,
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'remote2',
      filename: 'remoteEntry.js',
      exposes: {
        './RemoteApp2': './src/startup',
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

فایل فایل index.js برای remote2
import('./startup');
فایل startup.js  برای remote2
const mount = (el) => {
    el.innerHTML = '<div>Remote 2 Content</div>';
  };
  
  if (process.env.NODE_ENV === 'development') {
    const el = document.querySelector('#dev-remote2');
    if (el) {
      mount(el);
    }
  }
  
  export { mount };

اکنون می‌توانید با اجرای دستور npm start برنامه را بر روی پورت 7002 اجرا کنید . ( http://localhost:7002 ) 

ایجاد کردن Host app و یکپارچه کردن آن با Remote app  ها


 یک پوشه‌ی جدید را به نام container در کنار دو پوشه‌ی قبلی (remote1, remote2) ایجاد کنید. در پوشه‌ی ایجاد شده، یک فایل را به نام package.json ایجاد کنید و محتوای زیر را در آن قرار دهید و سپس دستور npm install را بزنید تا لیست وابستگی‌ها دریافت شود. 
{
  "name": "container",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack serve"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "html-webpack-plugin": "^5.3.2",
    "webpack": "^5.57.0",
    "webpack-cli": "^4.8.0",
    "webpack-dev-server": "^4.3.1"
  }
}

اکنون یک پوشه را به نام public، در ریشه‌ی پروژه ایجاد کنید و یک فایل را به نام index.html، در آن قرار دهید. در این فایل دو div  نگهدارنده به منظور هاست کردن محتوا، از هر remote app  قرار دارد.
<!DOCTYPE html>
<html>
  <head> 
    <title>Host App (Localhost:7000)</title>
  </head>
  <body>
    <div id="remote1-app"></div>
    <div id="remote2-app"></div>
  </body>
</html>
یک پوشه به نام src، در ریشه پروژه ایجاد کنید و دو فایل را با نام‌های index.js و startup.js، در آن قرار دهید. کمی جلوتر به این دو فایل می‌پردازیم. 

یک فایل را با نام  webpack.config.js، با تنظیمات زیر در ریشه‌ی پروژه قرار دهید. در اینجا پورت اجرایی برنامه، 7000 تعیین شده‌است:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  mode: 'development',
  devServer: {
    port: 7000,
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'container',
      remotes: {
        remote1: 'remote1@http://localhost:7001/remoteEntry.js',
        remote2: 'remote2@http://localhost:7002/remoteEntry.js',
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

برای خصوصیت remotes در پلاگین Module Federation باید remote appهای را تعریف کنیم که container app می‌خواهد به آن دسترسی داشته باشد. خصوصیت remotes شامل زوج کلید/مقدارها  (key/value) می‌باشد و در اینجا کلید، رشته و مقدار هم، رشته است. مقدار (value) برای کلید (key) با نام remote app شروع میشود که آن را در بخش تنظیمات پلاگین module federation مربوط به remote app مربوطه قرار داده‌ایم. سپس @ قرار میگیرد و در ادامه آن، آدرس جایی را که remoteEntry.js مربوط به remote app  قرار دارد، می‌نویسیم.

 
 
دقیقا مثل remote app ها، در فایل index.js مربوط به import ، host app زیر را انجام میدهیم: 
import('./startup');
در فایل remote app ، startup.js ‌ها را همانند زیر import می‌کنیم و سپس آن‌ها را در فایل index.html میزبان سوار می‌کنیم.
import { mount as remote1Mount } from 'remote1/RemoteApp1';
import { mount as remote2Mount } from 'remote2/RemoteApp2';

remote1Mount(document.querySelector('#remote1-app'));
remote2Mount(document.querySelector('#remote2-app'));

در ادامه جهت مشاهده خروجی، app میزبان را با دستور npm start اجرا می‌کنیم. اکنون شما می‌توانید خروجی remote app ‌ها را در host app  ببینید. لازم به ذکر است که در هنگام اجرای دستور npm start  برای host app ، هر دو remote app  ایجاد شده باید در حالت اجرا باشند. 



ملاحظات

Module federation در Webpack  نسخه 5 به بالا، در دسترس قرار دارد. شما می‌توانید وابستگی‌های بین remote app و host app را به اشتراک بگذارید. این کار را با اضافه کردن آن‌ها به تنظیمات پلاگین module federation برای remote app و host app  انجام دهید.
new ModuleFederationPlugin({
  name: 'remote1',
  filename: 'remoteEntry.js',
  exposes: {
    './RemoteApp1': './src/startup',
  },
  shared:['react', 'react-dom']
}),

ارتباط بین host و remote app می‌تواند از طریق callback‌ها انجام شود.