معرفی JSON Web Token
امروزه یکی از بزرگترین دغدغههای فعالان حوزه آی تی، برقراری امنیت اطلاعات میباشد. با پدید آمدن بانکهای دادهای آماری و مالی، حساسیت مسئله صد چندان میشود. در ادامه چک لیستی را ارائه مینمایم که با کمک آن میتوانید تا حدود بسیار خوبی امنیت نرم افزار تحت وب خود را برقرار نمایید. در برخی از موارد مثالهایی از تکنولوژی مایکروسافت آورده شده است که این بدلیل تخصص نویسنده در تکنولوژیهای مایکروسافت میباشد. در صورتیکه شما از تکنولوژیها و زبانهای سورس باز بهره میبرید، میبایست معادل مورد ذکر شده را در زبان مورد استفاده خود بیابید .
ابتدا اجازه دهید مقداری با حملات آشنا شویم و سپس راه مقابله را در کنار هم بررسی نماییم.
مهمترین و خطرناکترین حملات سطح وب :
حمله XSS
این نوع حملات بدین صورت است که هکر با استفاده از فرمهای عمومی یا خصوصی (پنلهای سایت) اقدام به ثبت کدهای مخرب جاوااسکریپت درون دیتابیس شما مینماید. همانطور که میدانید پایه اصلی سیستمهای احراز هویت، ساخت فایل کوکی بر روی کامپیوتر کاربران میباشد. زمانی که مطلب ثبت شدهی هکر برای کاربران شما نمایش داده میشود، کدهای جاوا اسکریپت هکر روی مرورگر کاربر، اجرا شده و اطلاعات کوکیهای کاربر به راحتی برای سایت هکر ارسال میشود (معمولا هکر یک صفحه روی وب میسازد تا بتواند اطلاعات دریافتی از کدهای جاوا اسکریپت خود را دریافت و در جایی ذخیره کند).
حال هکر به راحتی کوکی را بر روی مرورگر خودش تنظیم میکند و بعد وارد سایت شما میشود. سیستم شما او را با کاربر شما اشتباه میگیرد و به راحتی هکر به اطلاعات پنل کاربری کاربر(ان) شما دست پیدا میکند.
حمله SQL Injection
این حمله معروفترین حمله است که تقریبا با قدرت میتوانم بگویم که درتکنولوژی ASP.Net با امکانات فوق العادهای که بصورت توکار در دات نت در نظر گرفته شده است، بصورت کامل به فراموشی سپرده شده است. فقط 2 تا نکتهی ریز هست که باید در کدهایتان رعایت کنید و تمام.
این حمله بدین صورت است که هکر یک سری دستورات SQL را در کوئری استرینگ، به صفحات تزریق میکند و بدین صورت میتواند در کدهای کوئری TSQL شما اختلال ایجاد کند و اطلاعات جداول شما را بدست بیاورد. در این نوع حمله، هکر از طریق باگ سطح کد نویسی کدهای نرم افزار، به دیتابیس حمله میکند و اطلاعاتی مثل نام کاربری و کلمهی عبور ادمین یا کاربران را میدزد و بعد میرود داخل پنل و خرابکاری میکند.
حمله CSRF
این حمله یکی از جالبترین و جذابترین نوع حملات است که هوش بالای دوستان هکر را نشون میدهد. عبارت CSRF مخفف Cross Site Request Forgery است (احتمالا دوستان ام وی سی کار، این عبارت برایشان آشناست).
در این نوع حمله هکر یک فایل برای کاربر شما از طریق ایمیل یا روشهای دیگر ارسال میکند و کاربر را به این سمت سوق میدهد که فایل را باز کند. کاربر یک فایل به ظاهر معمولی مثل عکس یا ... را میبیند و فایل را باز میکند. وقتی فایل باز میشود دیتای خاصی دیده نمیشود و گاهی هم اروری مبنی بر ناقص بودن فایل یا ... به کاربر نمایش داده میشود و کاربر فکر میکند که فایل، ناقص برای ارسال شده ...
اما در حقیقت با کلیک بر روی فایل و باز کردن آن یک درخواست POST از کامپیوتر کاربر برای سایت شما ارسال میشود و در صورتیکه کاربر در آن زمان در سایت شما لاگین باشد، سایت درخواست را با روی باز میپذیرد و درخواست را اجرا میکند. بدین صورت هکر میتواند درخواستهایی را به سرویسهای سایت شما که مثلا برای حذف یک سری داده است، ارسال کند و اطلاعات کاربر را حذف کند.
حمله Brute Force
در این حمله، هکر از یک سری برنامه برای ارسال درخواستهای مکرر به فرمهای سایت شما استفاده میکند و بدین صورت فرمهای عمومی سایت شما مورد حجوم انبوهی از درخواستها قرار میگیرد که این امر در بهترین حالت موجب ثبت کلی دیتای اسپم در دیتابیس شما و در بدترین حالت موجب داون شدن سایت شما میشود.
حمله DDOS
این نوع حمله مانند حمله Brute Force است؛ با این تفاوت که درخواست به همهی صفحات شما ارسال میشود و معمولا درخواستها از چندین سرور مختلف برای سایت شما ارسال میشوند و حجم درخواستها به قدری زیاد است که عملا سرور شما هنگ میکند و کاملا از دسترس خارج میشود. این نوع حمله در سطح کد راه حل زیادی ندارد و در سطح سرور و فایروال باید حل شود و حل آن هم بدین صورت است که درخواستهای بیش از حد طبیعی از یک آی پی خاص تشخیص داده شده و به سرعت، آی پی بلاک میشود و از آن به بعد درخواستهای آن آی پی در فایروال از بین میرود و دیگه به سرور نمیرسد.
حمله SHELL
شل فایلی است خطرناک که اگر بر روی سرور سایت شما آپلود و اجرا شود، هکر از طریق آن دسترسی کاملی به کل سرور سایت شما خواهد داشت. فایلهای دیگری با نام بکدور [1] نیز وجود دارند که نویسنده تمایل دارد آنها را نیز از نوع حمله SHELL معرفی نماید. این نوع از فایلها به مراتب بسیار خطرناکتر از فایلهای شل میباشند؛ تا جایی که ممکن است سالها هکر به سروی دسترسی داشته باشد و مدیر سرور کاملا از آن بی خبر باشد. اینجاست که باید شدیدا مراقب فایلهایی که روی سایت شما آپلود میشوند باشید. نویسنده به تمامی خوانندگان پیشنهاد مینماید، در صورتیکه نرم افزار حساسی دارند، حتما از سرور اختصاصی استفاده نمایند؛ چرا که در هاستهای اشتراکی که در آنها فضا و امکانات یک سرور بصورت اشتراکی در اختیار چندین سایت قرار میگیرد، وجود باگ امنیتی در سایر سایتهای موجود بر روی سرور اشتراکی میتواند امنیت سایت شما را نیز به مخاطره بیاندازد. نویسنده تهیهی سرور اختصاصی را شدیدا به توسعه دهندگان سایتهای دارای تراکنشهای بانکی بالا (داخلی یا خارجی) پیشنهاد مینماید. زیرا درگاه تراکنشهای بانکی بر روی آی پی هاست شما قفل میشوند و در صورتیکه سرور بصورت اختصاصی تهیه شده باشد، آی پی سرور شما فقط و فقط در اختیار شماست و هکر نمیتواند با تهیه هاستی بر روی سرور اشتراکی شما، به راحتی آی پی قفل شده در درگاه بانکی شما را در اختیار داشته باشد. بدیهی است تنها در اختیار داشتن آی پی سرور شما جهت انجام خرابکاری در درگاه بانکی شما کافی نیست. ولی به نظر نویسنده این مورد در بدترین حالت ممکن 30% کار هکر میباشد. البته بحث حمله شل به سطح مهارت متخصصان سرورها نیز بستگی دارد. نویسنده اظهار میدارد اطلاعات دقیقی از تنظیماتی که بتواند جلوی اجرای انواع شل و یا جلوی دسترسی فایلهای شل را بگیرد، ندارد. بنابراین از متخصصان این حوزه دعویت مینماید اطلاعاتی درباره این موضوع ارائه نمایند.
حمله SNIFF
در این نوع حملات، هکر پکتهای رد و بدل شدهی بین کاربران و سرور شما را شنود مینماید و به راحتی میتواند اطلاعات مهمی مثل نام کاربری و رمز عبور کاربران شما را بدست آورد.
چک لیست امنیتی پروژههای نرم افزاری تحت وب
- بررسی کامل ورودیهای دریافتی از فرمهای سایت؛ هم در سمت کلاینت و هم در سطح سرور .
- در تکنولوژی دات نت به منظور تمیز سازی ورودیها و حذف تگهای خطرناکی همچون تگ script، کتابخانهای با نام Microsoft.Security.Application وجود دارد. کتابخانههای سورس باز دیگری نیز وجود دارند که نمونه آن کتابخانه AntiXss [2] سایت نوگت [3] میباشد.
- بررسی کامل ورودیهای دریافتی از کوئری استرینگهای [4] سایت. اگر از ASP.Net MVC استفاده مینمایید، تا حدی زیادی نیاز به نگرانی نخواهد داشت، زیرا تبدیلات [5] در سیستم Model Binding انجام میپذیرد و این موضوع تا حد زیادی شما را در برابر حملات SQL Injection مقاوم مینماید.
- حتما در فرمهای عمومی سایتتان از تصویر کپچا با امنیت بالا استفاده نمایید. این موضوع جهت شناخت روباتها از انسانها میباشد و شما را در برابر حملات Brute Force مقاوم مینماید.
- حتما سیستم شخصی سازی صفحات ارور را فعال نمایید و از نمایش صفحات ارور حاوی اطلاعات مهمی مانند صفحات ارور ASP.Net جلوگیری نمایید. این موضوع بسیار حساس میباشد و میتواند نقاط ضعف نرم افزار شما را برای هکر نمایان کند. حتی ممکن است اطلاعات حساسی مانند نام بانک اطلاعاتی، نام کاربری اتصال به بانک اطلاعاتی و نام جداول بانک اطلاعاتی شما را در اختیار هکر قرار دهد.
- استفاده از ORM ها یا استفاده از پروسیجرهای پارامتریک. این موضوع کاملا شما را در برابر حملات SQL Injection مقاوم مینماید. کما اینکه ORM ها، سطحی از کش را بصورت توکار دارا میباشند و این موضوع در سرعت دستیابی به دادهها نیز بسیار تاثیر گذار است. از طرف دیگر بانک اطلاعاتی SQL نیز امکانات توکاری جهت کش نمودن پرس و جوهای [6] پارامتریک دارد.
- لاگ کردن ارورهای سطح کد و سطح روتینگ [7] . یکی از مهمترین خصیصههای پروژههای با کیفیت، لاگ شدن خطاهای سطح کد میباشد. این امر شما را با نقاط حساس و ضعفهای نرم افزار آگاه میسازد و به شما اجازه میدهد به سرعت در جهت رفع آنها اقدام نمایید. لاگ نمودن خطاهای سطح روتینگ شما را از فعالیتهای هکرها جهت یافتن صفحات لاگین و صفحات مدیریتی پنل مدیریتی سایت اگاه مینماید، همچنین شما را از حملات SQL Injection نیز آگاه مینماید.
- جلوگیری از ایندکس شدن صفحات لاگین پنل مدیریت سایت در موتورهای جستجو. بخش مهمی از عملیات هکر ها، قرار دادن روباتهای تشخیص رمز بر روی صفحات لاگین میباشد که به نوعی میتوان این نوع حملات را در دسته حملات Brute Force قرار داد. موتورهای جستجو یکی از ابزارهای مهم هکرها میباشد. عملیات هایی مانند یافتن صفحات لاگین پنل مدیریتی یکی از کاربردهای موتورهای جستجو برای هکرها میباشد.
- لاگ کردن ورود و خروج افراد به همراه تاریخ، زمان، آی پی افراد و وضعیت لاگین. با کمک این موضوع شما میتوانید ورود و خروج کاربران نرم افزار خود را کنترل نمایید و موارد غیر طبیعی و مشکوک را در سریعترین زمان مورد بررسی قرار دهید.
- استفاده از روالهای استاندارد جهت بخش "فراموشی کلمه عبور". همیشه از استاندارهای نرم افزارهای بزرگ پیروی نمایید. بدیهی است استاندارهای استفاده شده در این نرم افزارها بارها و بارها تست شده و سپس بعنوان یک روال استاندارد در همهی نرم افزارهای بزرگ بکار گرفته شده است. استاندارد جهانی بخش "فراموشی کلمه عبور" که در اغلب نرم افزارهای معروف جهان بکار گرفته شده است، عبارت است از دریافت آدرس ایمیل کاربر، احراز هویت ایمیل وارد شده، ارسال یک نامهی الکترونیکی [8] حاوی نام کاربری و لینک تنظیم کلمه عبور جدید به ایمیل کاربر. بهتر است لینک ارسال شده به ایمیل کاربر بصورت یکبار مصرف باشد. کاربر پس از کلیک بر روی لینک تنظیم کلمه عبور جدید، وارد یکی از صفحات سایت شده و میتواند کلمهی عبور جدیدی را برای خود ثبت نماید. در پایان، کاربر به صفحهی ورود سایت هدایت شده و پیامی مبنی بر موفقیت آمیز بودن عملیات تغییر کلمهی عبور به او نمایش داده میشود. البته روال ذکر شده حداقل رول استانداردی میباشد و میتوان در کنار آن از روالهای تکمیل کنندهای مانند پرسشهای امنیتی و غیره نیز استفاده نمود.
- قراردادن امکاناتی جهت بلاک نمودن آی پیها و غیر فعال نمودن حساب کاربری اعضای سایت. در نرم افزار باید این امکان وجود داشته باشد که آی پی هایی که بصورت غیر طبیعی در سایت فعالیت مینمایند و یا مکررا اقدام به ورود به پنل مدیریتی و پنل کاربران مینمایند را بلاک نماییم. همچنین در صورت تخلف کاربران باید بتوان حساب کاربری کاربر خاطی را مسدود نمود. این موضوع میتواند بسته به اندازه پروژه و یا سلیقه تیم توسعه بصورت خودکار، دستی و یا هر دو روش در نرم افزار در تعبیه شود.
- امن سازی سرویسهای ای جکس و چک کردن ای جکس بودن درخواست ها. حتما جلوی اجرای سرویسهای درون نرم افزاری از بیرون از نرم افزار را بگیرید. سرویسهای ای جکس یکی از این نوع سرویسها میباشند که در نرم افزارها جهت استفادههای داخلی در نظر گرفته میشوند. در این نوع سرویسها حتما نوع درخواست را بررسی نمایید و از پاسخگویی سرویسها به درخواستهای غیر ای جکسی جلوگیری نمایید. در ASP.Net MVC این امر توسط متد Request.IsAjaxRequest انجام میپذیرد .
- محدود کردن سرویسهای حساس به درخواستهای POST. حتما از دسترسی به سرویس هایی از نوع Insert,Update و Delete از طریق فعل GET جلوگیری نمایید. در ASP.Net MVC این سرویسها را به فعل POST محدود نموده و در ASP.Net Web API این سرویسها را به افعال POST,PUT و DELETE محدود نمایید.
- عدم استفاده از آی دی در پنلهای کاربران بالاخص در آدرس صفحات (کوئری استرینگ) و استفاده از کد غیر قابل پیش بینی مثل GUID به جای آن. حتی الامکان بررسی مالکیت دادهها در همه بخشهای پنلهای کاربری سایت را جهت محکم کاری بیشتر انجام دهید تا خدای نکرده کاربر با تغییر اطلاعات کوئری استرینگ صفحات نتوانند به دادههای یک کاربر دیگه دسترسی داشته باشند.
- حتی الامکان پنل مدیران را از کاربران بصورت فیزیکی جدا نمایید. این مورد جهت جلوگیری از خطاهایی است که ممکن است توسط توسعه دهنده در سطح سیستم مدیریت نقش رخ دهد و موجب دسترسی داشتن کاربران به بخش هایی از پنل مدیریتی شود.
- استفاده از الگوریتمهای کدگذاری ترکیبی و کد کردن اطلاعات حساس قبل از ذخیره سازی در بانک اطلاعاتی. اطلاعات حساسی مانند کلمات عبور را حتما توسط چند الگوریتم کدگذاری، کدگذاری نمایید و سپس درون بانک اطلاعاتی ذخیره نمایید.
- تنظیمات حساس نرم افزار را درون فایل web.config قرار دهید و حتی الامکان آنها را نیز کدگذاری نمایید. بصورتی که اطلاعات قابلیت دیکد شدن را داشته باشند.
- ساخت پروژه بصورت چند لایه. این موضوع جهت جلوگیری از دستیابی هکر به ساختار لایههای پروژههای شما میباشد. به بیان دیگر اگر نهایتا هکر بتواند به اطلاعات FTP هاست شما دست یابد، استفاده از تکنولوژی چند لایه در بدترین حالت هکر را از دستیابی به اطاعات لایههای زیرین نرم افزار باز میدارد. البته این کار برای هکرها غیر ممکن نیست، اما بسیار سخت و زمان بر میباشد.
- اشتراک گذاری اینترفیس در سرویسهای خارج برنامه ای و عدم اشتراک گذاری کلاس اصلی. این موضوع از دستیابی هکر به بدنه سرویسها و پیاده سازیهای آنها جلوگیری مینماید.
- استفاده از تکنیکهای مقابله با CSRF در همه سرویسهای POST. در ASP.NET MVC اتریبیوتی با نام AntiForgery جهت مقاوم سازی سرویسها از حملات CSRF وجود دارد. مکانیزم بدین صورت است که در تمامی فرمهای سایت یک کد منحصر به فرد تولید میگردد که همراه درخواست GET به کامپیوتر کاربر ارسال میشود و در هنگام ارسال درخواست POST به سرور، صحت کد مورد نظر بررسی شده و در صورت صحت، اجازهی اجرای سرویس به درخواست داده میشود. بدین صورت وقتی کاربر سایت شما فایل آلودهای را باز مینماید، در خواست ارسالی هکر که توسط فایل باز شده، به سرور سایت ما ارسال میگردد، فاقد کد منحصر به فرد بوده و از اجرای سرویس جلو گیری میشود.
- استفاده از سیستمهای مدیریت نقش امن مانند IDENTITY در ASP.Net MVC و یا استفاده از امکانات توکار دات نت در سیستمهای مدیریت نقش شخصی سازی شده [9] . بدیهی است امنیت این سیستمها بارها و بارها تست شده است.
- بررسی فرمت و پسوند فایلهای آپلود شده. توجه نمایید که بررسی پسوند فایلها کافی نبوده و فرمت فایلها نیز میبایست بررسی شود. حتی نویسنده پیشنهاد مینماید فایلها را به نوعهای مرتبطشان تبدیل [10] نمایید. در حوزه هک بایند نمودن انواع ویروس، تروجان، شل و بک دور [11] به فایلهای تصویری و متنی یک امر بسیار رایج است. بنابراین حساسیت زیادی روی این موضوع قرار دهید. نویسنده توصیه مینماید کتابخانههای کاملی برای این موضوع تدارک ببینید تا در تمامی پروژهها نیاز به ایجاد مجدد آنها نداشته باشید و سعی نمایید در هر پروژه این کتابخانهها را تکمیلتر و بهتر نمایید.
- تنظیم IIS جهت جلوگیری از اجرای فایلهای اجرایی در مسیر آپلود فایلها. شاید جمله بیان شده به نظر ترسناک و یا سخت برسد، اما این کار با نوشتن چند تگ ساده در فایل Web.Config به راحتی قابل انجام است و نیاز به هیچ نوع کدنویسی ندارد.
- آپلود فایلها در پوشه App_Data و دسترسی به فایلها از طریق سرویسهای خود شما. پوشه App_Data پوشهای امن است و دسترسی مستقیم از طریق آدرس بار مرورگر به فایلهای درون آن توسط IIS داده نمیشود و افراد فقط از طریق سرویسهای خود شما میتوانند به فایلهای داخل این پوشه دسترسی داشته باشند. بدین صورت در سرویسهای خود میتوانید با تبدیل نمودن [12] فایلها به نوع خودشان (تصویر. پی دی اف یا ...) هکر را نا امید نمایید. این موضوع شما را در مقابل حملات SHELL مقاوم مینماید.
- استفاده از تکنیکهای لاگین چند سطحی برای پنل ادمین. در این روش شما حتی با داشتن نام کاربری و کلمهی عبور ادمین، قادر نخواهید بود وارد پنل ادمین شوید. نویسنده ابزار میدارد که این روش، یک روش ابداعی میباشد که از ترکیبی از احرا هویت ساده توسط نام کاربری و کلمهی عبور به همراه تکنیکهای احراز هویت ایمیل و موبایل مدیریت سایت میباشد.
- استفاده از SSL بسیار اهمیت دارد. بالاخص اگر نرم افزار شما Service Oriented باشد و نرم افزار شما سرویس هایی جهت اتصال به اپلیکیشنهای خارجی مثل اپلیکیشن اندروید دارد. این مورد در صفحات لاگین نیز بسیار مهم است و موجب میشود نام کاربری و کلمه عبور کاربران شما بصورت هش شده بین کامپیوتر کاربر و سرور شما رد و بدل شود و عملا شنود پکتها فایده ای برای هکر نخواهد داشت، زیرا دادهها توسط الگوریتمهای امنیتی که بین سرور و مرورگر کاربران توافق میشود کدگذاری شده و سپس رد و بدل میشوند.
[1] Back Door
[2] https://www.nuget.org/packages/AntiXss/
[3] www. Nuget.org
[4] Query String
[5] Casting
[6] Procedure
[7] Routing
[8] Email
[9] Custom Role Provider
[10] Cast
[11] Back Door
[12] Cast
روش افزودن میانافزار RateLimiter به برنامههای ASP.NET Core
شبیه به سایر میانافزارها، جهت فعالسازی میانافزار RateLimiter، ابتدا باید سرویسهای متناظر با آنرا به برنامه معرفی کرد و پس از فعالسازی میانافزار مسیریابی، آنرا به زنجیرهی مدیریت یک درخواست معرفی نمود. برای نمونه در مثال زیر، امکان دسترسی به تمام درخواستها، به 10 درخواست در دقیقه، محدود میشود که پارتیشن بندی آن (در مورد پارتیشن بندی در قسمت قبل بیشتر بحث شد)، بر اساس username کاربر اعتبارسنجی شده و یا hostname یک کاربر غیراعتبارسنجی شدهاست:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddRateLimiter(options => { options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext => RateLimitPartition.GetFixedWindowLimiter( partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(), factory: partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 10, QueueLimit = 0, Window = TimeSpan.FromMinutes(1) })); }); // ... var app = builder.Build(); // ... app.UseRouting(); app.UseRateLimiter(); app.MapGet("/", () => "Hello World!"); app.Run();
- فراخوانی builder.Services.AddRateLimiter، سبب معرفی سرویسهای میانافزار rate limiter به سیستم تزریق وابستگیهای ASP.NET Core میشود.
- در اینجا میتوان برای مثال خاصیت options.GlobalLimiter تنظیمات آنرا نیز مقدار دهی کرد. GlobalLimiter، سبب تنظیم یک محدود کنندهی سراسری نرخ، برای تمام درخواستهای رسیدهی به برنامه میشود.
- GlobalLimiter را میتوان با هر نوع PartitionedRateLimiter مقدار دهی کرد که در اینجا از نوع FixedWindowLimiter انتخاب شدهاست تا بتوان «الگوریتمهای بازهی زمانی مشخص» را به برنامه اعمال نمود تا برای مثال فقط امکان پردازش 10 درخواست در هر دقیقه برای هر کاربر، وجود داشته باشد.
- در پایان کار، فراخوانی app.UseRateLimiter را نیز مشاهده میکنید که سبب فعالسازی میانافزار، بر اساس تنظیمات صورت گرفته میشود.
برای آزمایش برنامه، آنرا اجرا کرده و سپس به سرعت شروع به refresh کردن صفحهی اصلی آن کنید. پس از 10 بار ریفرش، پیام 503 Service Unavailable را مشاهده خواهید کرد که به معنای مسدود شدن دسترسی به برنامه توسط میانافزار rate limiter است.
بررسی تنظیمات رد درخواستها توسط میانافزار rate limiter
اگر پس از محدود شدن دسترسی به برنامه توسط میان افزار rate limiter از status code = 503 دریافتی راضی نیستید، میتوان آنرا هم تغییر داد:
builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = 429; // ... });
علاوه بر آن در اینجا گزینهی OnRejected نیز پیش بینی شدهاست تا بتوان response ارائه شده را در حالت رد درخواست، سفارشی سازی کرد تا بتوان پیام بهتری را به کاربری که هم اکنون دسترسی او محدود شدهاست، ارائه داد:
builder.Services.AddRateLimiter(options => { options.OnRejected = async (context, token) => { context.HttpContext.Response.StatusCode = 429; if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) { await context.HttpContext.Response.WriteAsync( $"Too many requests. Please try again after {retryAfter.TotalMinutes} minute(s). " + $"Read more about our rate limits at https://example.org/docs/ratelimiting.", cancellationToken: token); } else { await context.HttpContext.Response.WriteAsync( "Too many requests. Please try again later. " + "Read more about our rate limits at https://example.org/docs/ratelimiting.", cancellationToken: token); } }; // ... });
یک نکته: باتوجه به اینکه در اینجا به HttpContext دسترسی داریم، یعنی به context.HttpContext.RequestServices نیز دسترسی خواهیم داشت که توسط آن میتوان برای مثال سرویس ILogger را از آن درخواست کرد و رخداد واقع شده را برای بررسی بیشتر لاگ نمود؛ برای مثال چه کاربری مشکل پیدا کردهاست؟
context.HttpContext.RequestServices.GetService<ILoggerFactory>()? .CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware") .LogWarning("OnRejected: {RequestPath}", context.HttpContext.Request.Path);
طراحی فعلی میانافزار rate limiter، کمی محدود است. برای مثال «retry after»، تنها metadata مفیدی است که جهت بازگشت ارائه میدهد و همچنین مانند GitHub مشخص نمیکند که در لحظهی جاری چند درخواست دیگر را میتوان ارسال کرد و امکان دسترسی به اطلاعات آماری درونی آن وجود ندارد. اگر نیاز به یک چنین اطلاعاتی دارید شاید استفاده از میانافزار ثالث دیگری به نام AspNetCoreRateLimit برای شما مفیدتر باشد!
الگوریتمهای پشتیبانی شدهی توسط میانافزار rate limiter
در قسمت قبل با چند الگوریتم استاندارد طراحی میانافزارهای rate limiter آشنا شدیم که میانافزار توکار rate limiter موجود در ASP.NET Core 7x، اکثر آنها را پشتیبانی میکند:
- Concurrency limit: سادهترین نوع محدود سازی نرخ درخواستها است و کاری به زمان ندارد و فقط برای آن، تعداد درخواستهای همزمان مهم است. برای مثال پیاده سازی «مجاز بودن تنها 10 درخواست همزمان».
- Fixed window limit: توسط آن میتوان محدودیتهایی مانند «مجاز بودن تنها 60 درخواست در دقیقه» را اعمال کرد که به معنای امکان ارسال یک درخواست در هر ثانیه در هر دقیقه و یا حتی ارسال یکجای 60 درخواست در یک ثانیه است.
- Sliding window limit: این محدودیت بسیار شبیه به حالت قبل است اما به همراه قطعاتی که کنترل بیشتری را بر روی محدودیتها میسر میکند؛ مانند مجاز بودن 60 درخواست در هر دقیقه که فقط در این حالت یک درخواست در هر ثانیه مجاز باشد.
- Token bucket limit: امکان کنترل نرخ سیلان را میسر کرده و همچنین از درخواستهای انفجاری نیز پشتیبانی میکند (این مفاهیم در قسمت قبل بررسی شدند).
علاوه بر اینها امکان ترکیب گزینههای فوق توسط متد کمکی PartitionedRateLimiter.CreateChained نیز میسر است:
builder.Services.AddRateLimiter(options => { options.GlobalLimiter = PartitionedRateLimiter.CreateChained( PartitionedRateLimiter.Create<HttpContext, string>(httpContext => RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 600, Window = TimeSpan.FromMinutes(1) })), PartitionedRateLimiter.Create<HttpContext, string>(httpContext => RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 6000, Window = TimeSpan.FromHours(1) }))); // ... });
در این مثال فرضی، متد الحاقی ResolveClientIpAddress اهمیتی ندارد. بهتر است برای برنامهی خود از کلید پارتیشن بندی بهتر و معقولتری استفاده کنید.
امکان در صف قرار دادن درخواستها بجای رد کردن آنها
در تنظیمات مثالهای فوق، در کنار PermitLimit، میتوان QueueLimit را نیز مشخص کرد. به این ترتیب با رسیدن به PermitLimit، به تعداد QueueLimit، درخواستها در صف قرار میگیرند، بجای اینکه کاملا رد شوند:
PartitionedRateLimiter.Create<HttpContext, string>(httpContext => RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 10, QueueLimit = 6, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, Window = TimeSpan.FromSeconds(1) })));
این تنظیم، تجربهی کاربری بهتری را برای استفاده کنندگان از برنامهی شما به همراه خواهد داشت؛ بجای رد قاطع درخواستهای ارسالی توسط آنها.
یک نکته: بهتر است QueueLimitهای بزرگی را انتخاب نکنید؛ خصوصا برای بازههای زمانی طولانی. چون یک مصرف کننده نیاز دارد تا سریع، پاسخی را دریافت کند و اگر اینطور نباشد، دوباره سعی خواهد کرد. تنها چند ثانیهی کوتاه در صف بودن برای کاربران معنا دارد.
امکان ایجاد سیاستهای محدود سازی سفارشی
اگر الگوریتمهای توکار میانافزار rate limiter برای کار شما مناسب نیستند، میتوانید با پیاده سازی <IRateLimiterPolicy<TPartitionKey، یک نمونهی سفارشی را ایجاد کنید. پیاده سازی این اینترفیس، نیاز به دو متد را دارد:
الف) متد GetPartition که بر اساس HttpContext جاری، یک rate limiter مخصوص را باز میگرداند.
ب) متد OnRejected که امکان سفارشی سازی response رد درخواستها را میسر میکند.
در مثال زیر پیاده سازی یک rate limiter سفارشی را مشاهده میکنید که نحوهی پارتیشن بندی آن بر اساس user-name کاربر اعتبارسنجی شده و یا host-name کاربر وارد نشدهی به سیستم است. در اینجا کاربر وارد شدهی به سیستم، محدودیت بیشتری دارد:
public class ExampleRateLimiterPolicy : IRateLimiterPolicy<string> { public RateLimitPartition<string> GetPartition(HttpContext httpContext) { if (httpContext.User.Identity?.IsAuthenticated == true) { return RateLimitPartition.GetFixedWindowLimiter(httpContext.User.Identity.Name!, partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 1_000, Window = TimeSpan.FromMinutes(1), }); } return RateLimitPartition.GetFixedWindowLimiter(httpContext.Request.Headers.Host.ToString(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 100, Window = TimeSpan.FromMinutes(1), }); } public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; } = (context, _) => { context.HttpContext.Response.StatusCode = 418; // I'm a 🫖 return new ValueTask(); }; }
options.AddPolicy<string, ExampleRateLimiterPolicy>("myPolicy");
امکان تعریف سیاستهای محدود سازی نرخ دسترسی به گروهی از endpoints
تا اینجا روشهای سراسری محدود سازی دسترسی به منابع برنامه را بررسی کردیم؛ اما ممکن است در برنامهای بخواهیم محدودیتهای متفاوتی را به گروههای خاصی از endpoints اعمال کنیم و یا شاید اصلا نخواهیم تعدادی از آنها را محدود کنیم:
builder.Services.AddRateLimiter(options => { options.AddFixedWindowLimiter("Api", options => { options.AutoReplenishment = true; options.PermitLimit = 10; options.Window = TimeSpan.FromMinutes(1); }); options.AddFixedWindowLimiter("Web", options => { options.AutoReplenishment = true; options.PermitLimit = 10; options.Window = TimeSpan.FromMinutes(1); }); // ... });
البته باید درنظر داشت که متدهای الحاقی Add داری را که در اینجا ملاحظه میکنید، محدود سازی را بر اساس نام درنظر گرفته شده انجام میدهند. یعنی درحقیقت یک محدودسازی سراسری بر اساس گروهی از endpoints هستند و امکان تعریف پارتیشنی را به ازای یک کاربر یا آدرس IP خاص، ندارند. اگر نیاز به اعمال این نوع پارتیشن بندی را دارید، باید از متدهای AddPolicy استفاده کنید:
options.AddPolicy("Api", httpContext => RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 10, Window = TimeSpan.FromSeconds(1) }));
محدود سازی نرخ دسترسی به منابع در ASP.NET Core Minimal API
پس از تعریف نامی برای سیاستهای دسترسی، اکنون میتوان از آنها به صورت زیر جهت محدود سازی یک endpoint و یا گروهی از آنها استفاده کرد:
// Endpoint app.MapGet("/api/hello", () => "Hello World!").RequireRateLimiting("Api"); // Group app.MapGroup("/api/orders").RequireRateLimiting("Api");
// Endpoint app.MapGet("/api/hello", () => "Hello World!").DisableRateLimiting(); // Group app.MapGroup("/api/orders").DisableRateLimiting();
محدود سازی نرخ دسترسی به منابع در ASP.NET Core MVC
میتوان سیاستهای نرخ دسترسی تعریف شده را بر اساس نام آنها به کنترلرها و یا اکشن متدها اعمال نمود:
[EnableRateLimiting("Api")] public class Orders : Controller { [DisableRateLimiting] public IActionResult Index() { return View(); } [EnableRateLimiting("ApiListing")] public IActionResult List() { return View(); } }
و یا حتی میتوان این سیاستهای محدود سازی نرخ دسترسی را به تمام کنترلرها و صفحات razor نیز به صورت زیر اعمال کرد:
app.UseConfiguredEndpoints(endpoints => { endpoints.MapRazorPages() .DisableRateLimiting(); endpoints.MapControllers() .RequireRateLimiting("UserBasedRateLimiting"); });
معرفی Bit Platform
وب اسمبلی چیست؟
<BlazorMode> ... </BlazorMode> <WebAppDeploymentType> ... </WebAppDeploymentType>
- وجود سیستم Exception handling در سرور و کلاینت (این موضوع به گونه ای بر اساس Best Practiceها پیاده سازی شده که اپلیکیشن را از بروز هر خطایی که بخواهد موجب Crash کردن برنامه شود ایزوله کرده)
- وجود سیستم User Authentication بر اساس JWT که شما در همان ابتدا که از این تمپلیت پروژه جدیدی میسازید صفحات SignIn ، SignUp را خواهید داشت.
- پکیج Bit Blazor UI که بالاتر درمورد آن صحبت کرده ایم از همان ابتدا در TodoTemplate نصب و تنظیم شده تا بتوانید به راحتی صفحات جدید با استفاده از آن بسازید.
- کانفیگ استاندارد Swagger در سمت سرور.
- ارسال ایمیل در روند SignUp.
- وجود خاصیت AutoInject برای سادهسازی تزریق وابستگی ها.
- و بسیاری موراد دیگر که در داکیومنتهای پروژه میتوانید آنهارا ببینید.
- شما میتوانید این پروژه را در گیتهاب مشاهده کنید.
- برای اشکالات یا قابلیت هایی که میخواهید برطرف شود Issue ثبت کنید.
- پروژه را Fork کنید و Star دهید.
- ایشوهایی که وجود دارد را برطرف کنید و Pull Request ارسال کنید.
- برای در جریان بودن از روند توسعه در جلسات برنامه ریزی (Planning Meeting) و گزارشات هفتگی (Standup Meeting ) که همه اینها در Microsoft Teams برگزار میشود شرکت کنید.
- بسیار سبک ورزن است و تنها نیاز به نصب بستهی npm آنرا دارد.
- با فایلهای متنی معمولی کار میکند که ویرایش و copy/paste در آنها بسیار سادهاست.
- قرار دادن فایلهای نهایی متنی آن در ورژن کنترل بسیار سادهاست.
- امکان نوشتن درخواستهای به هم وابسته و آزمودن نتایج حاصل را دارا است.
- چون یک ابزار خط فرمان است، امکان استفادهی از آن به سادگی در فرآیندههای توسعهی مداوم وجود دارد.
- ابزارهای npm، چندسکویی هستند.
نصب strest
در ادامه قصد داریم مطلب «آزمایش Web APIs توسط Postman - قسمت ششم - اعتبارسنجی مبتنی بر JWT» را با استفاده از strest بازنویسی کنیم. به همین جهت در ابتدا نیاز است بستهی npm آنرا به صورت سراسری نصب کنیم:
npm i -g @strest/cli
مرحله 1: خاموش کردن بررسی مجوز SSL برنامه
مرحله 2: ایجاد درخواست login و دریافت توکنها
مجوز SSL آزمایشی برنامهی ASP.NET Core ما، از نوع خود امضاء شدهاست. به همین جهت اگر سعی در اجرای strest را با درخواستهای ارسالی به آن داشته باشیم، باشکست مواجه خواهند شد. بنابراین در ابتدا، خاصیت allowInsecure را به true تنظیم میکنیم:
version: 2 variables: baseUrl: https://localhost:5001/api logResponse: false allowInsecure: true
- همچنین در ابتدای این تنظیمات، روش تعریف متغیرها را نیز مشاهده میکنید که برای مثال توسط آنها baseUrl تعریف شدهاست.
درست در سطر پس از این تنظیمات، دستور اجرا و اعتبارسنجی درخواست Login را مینویسیم:
requests: loginRequest: request: url: <$ baseUrl $>/account/login method: POST postData: mimeType: application/json text: username: "Vahid" password: "1234" log: <$ logResponse $> validate: - jsonpath: content.access_token type: [string] - jsonpath: content.refresh_token type: [string]
- درخواستها با requests شروع میشوند. سپس ذیل آن میتوان نام چندین درخواست یا request را ذکر کرد که برای مثال نام درخواست تعریف شدهی در اینجا loginRequest است. این نام مهم است؛ از این جهت که با اشارهی به آن میتوان به فیلدهای خروجی response حاصل، در درخواستهای بعدی، دسترسی یافت.
- سپس، آدرس درخواست مشخص شدهاست. در اینجا روش کار با متغیرها را نیز مشاهده میکنید.
- نوع درخواست POST است.
- در ادامه جزئیات اطلاعات ارسالی به سمت سرور باید مشخص شوند. برای مثال در اینجا با فرمت application/json قرار است یک شیء تشکیل شدهی از username و password ارسال شوند.
- در سطر بعدی، خاصیت log با متغیر logResponse مقدار دهی شدهاست. اگر به true تنظیم شود، اصل خروجی response را توسط برنامهی خط فرمان strest میتوان مشاهده کرد. اگر اینکار خروجی را شلوغ کرد، میتوان آنرا به false تنظیم کرد و این خروجی را در فایل strest_history.json نهایی که حاصل از اجرای آزمایشهای تعریف شدهاست، در کنار فایل JWT.strest.yml خود یافت و مشاهده کرد.
- سپس به قسمت آزمودن نتیجهی درخواست میرسیم. در اینجا انتظار داریم که درخواست حاصل که با فرمت json است، دارای دو خاصیت رشتهای access_token و refresh_token باشد.
مرحلهی 3: ذخیره سازی توکنهای دریافتی در متغیرهای سراسری
مرحلهی 3: ذخیره سازی مراحل انجام شده
در حین کار با strest نیازی به ذخیره سازی نتیجهی حاصل از response، در متغیرهای خاصی نیست. برای مثال اگر بخواهیم به نتیجهی حاصل از عملیات لاگین فوق در درخواستهای بعدی دسترسی پیدا کنیم، میتوان نوشت <$ loginRequest.content.access_token $>
در اینجا درج متغیرها توسط <$ $> صورت میگیرد. سپس loginRequest به نام درخواست مرتبط اشاره میکند. خاصیت content.access_token نیز مقدار خاصیت access_token شیء response را بر میگرداند.
همچنین ذخیره سازی مراحل انجام شده نیز نکتهی خاصی را به همراه ندارد. یک تک فایل متنی JWT.strest.yml وجود دارد که آزمایشهای ما در آن درج میشوند.
مرحلهی 4: دسترسی به منابع محافظت شدهی سمت سرور
در ادامه روش تعریف دو درخواست جدید دیگر را در فایل JWT.strest.yml مشاهده میکنید که از نوع Get هستند و به اکشن متدهای محافظت شده ارسال میشوند:
myProtectedApiRequest: request: url: <$ baseUrl $>/MyProtectedApi method: GET headers: - name: Authorization value: Bearer <$ loginRequest.content.access_token $> log: <$ logResponse $> validate: - jsonpath: content.title expect: "Hello from My Protected Controller! [Authorize]" mProtectedAdminApiRequest: request: url: <$ baseUrl $>/MyProtectedAdminApi method: GET headers: - name: Authorization value: Bearer <$ loginRequest.content.access_token $> log: <$ logResponse $> validate: - jsonpath: content.title expect: "Hello from My Protected Admin Api Controller! [Authorize(Policy = CustomRoles.Admin)]"
- چون نیاز است به همراه درخواست خود، هدر اعتبارسنجی مبتنی بر JWT را که به صورت Bearer value است نیز به سمت سرور ارسال کنیم، خاصیت headers را توسط یک name/value مشخص کردهایم. همانطور که عنوان شد در فایلهای yaml، فاصلهها و تو رفتگیها مهم هستند و حتما باید رعایت شوند.
- سپس دومین آزمون نوشته شده را نیز مشاهده میکنید. در قسمت validate، مشخص کردهایم که خاصیت title دریافتی از response باید مساوی مقدار خاصی باشد.
دقیقا همین نکات برای درخواست دوم به MyProtectedAdminApi تکرار شدهاند.
مرحلهی 5: ارسال Refresh token و دریافت یک سری توکن جدید
اکشن متد account/RefreshToken در سمت سرور، نیاز دارد تا یک شیء جیسون با خاصیت refreshToken را دریافت کند. مقدار این خاصیت از طریق response متناظر با درخواست نامدار loginRequest استخراج میشود که در قسمت postData مشخص شدهاست:
refreshTokenRequest: request: url: <$ baseUrl $>/account/RefreshToken method: POST postData: mimeType: application/json text: refreshToken: <$ loginRequest.content.refresh_token $> log: <$ logResponse $> validate: - jsonpath: content.access_token type: [string] - jsonpath: content.refresh_token type: [string]
مرحلهی 6: آزمایش توکن جدید دریافتی از سرور
در قسمت قبل، توکنهای جدیدی صادر شدند که اکنون برای کار با آنها میتوان از متغیر refreshTokenRequest.content.access_toke استفاده کرد:
myProtectedApiRequestWithNewToken: request: url: <$ baseUrl $>/MyProtectedApi method: GET headers: - name: Authorization value: Bearer <$ refreshTokenRequest.content.access_token $> log: <$ logResponse $> validate: - jsonpath: content.title expect: "Hello from My Protected Controller! [Authorize]"
مرحلهی 7: آزمایش منقضی شدن توکنی که در ابتدای کار پس از لاگین دریافت کردیم
اکنون که refresh token صورت گرفتهاست، دیگر نباید بتوانیم از توکن دریافتی پس از لاگین استفاده کنیم و برنامه باید آنرا برگشت بزند:
myProtectedApiRequestWithOldToken: request: url: <$ baseUrl $>/MyProtectedApi method: GET headers: - name: Authorization value: Bearer <$ loginRequest.content.access_token $> log: <$ logResponse $> validate: - jsonpath: status expect: 401
مرحلهی 8: آزمایش خروج از سیستم
در اینجا نیاز است به آدرس account/logout، یک کوئری استرینگ را با کلید refreshToken و مقدار ریفرشتوکن دریافتی از درخواست نامدار refreshTokenRequest، به سمت سرور ارسال کنیم:
logoutRequest: request: url: <$ baseUrl $>/account/logout method: GET headers: - name: Authorization value: Bearer <$ refreshTokenRequest.content.access_token $> queryString: - name: refreshToken value: <$ refreshTokenRequest.content.refresh_token $> log: <$ logResponse $> validate: - jsonpath: content expect: true
مرحلهی 9: بررسی عدم امکان دسترسی به منابع محافظت شدهی سمت سرور، پس از logout
در مرحلهی قبل، از سیستم خارج شدیم. اکنون میخواهیم بررسی کنیم که آیا توکن دریافتی پیشین هنوز معتبر است یا خیر؟ آیا میتوان هنوز هم به منابع محافظت شده دسترسی یافت یا خیر:
myProtectedApiRequestWithNewTokenAfterLogout: request: url: <$ baseUrl $>/MyProtectedApi method: GET headers: - name: Authorization value: Bearer <$ refreshTokenRequest.content.access_token $> log: <$ logResponse $> validate: - jsonpath: status expect: 401
مرحلهی 10: اجرای تمام آزمونهای واحد نوشته شده
همانطور که در ابتدای بحث نیز عنوان شد فقط کافی است دستور strest JWT.strest.yml را در خط فرمان اجرا کنیم تا آزمونهای ما به ترتیب اجرا شوند:
فایل نهایی این آزمایش را در اینجا میتوانید مشاهده میکنید.
امن سازی برنامههای ASP.NET Core توسط IdentityServer 4x - قسمت سوم - بررسی مفاهیم OpenID Connect
پروتکل OpenID Connect چگونه کار میکند؟
در انتهای قسمت اول این سری، پروتکل OpenID Connect معرفی شد. در ادامه جزئیات بیشتری از این پروتکل را بررسی میکنیم.
هر برنامهی کلاینت متصل به WebAPI مثال قسمت قبل، نیاز به دانستن هویت کاربر وارد شدهی به آنرا دارد. در اینجا به این برنامهی کلاینت، اصطلاحا relying party هم گفته میشود؛ از این جهت که این برنامهی کلاینت، به برنامهی Identity provider و یا به اختصار IDP، جهت دریافت نتیجهی اعتبارسنجی کاربر، وابستهاست. برنامهی کلاینت یک درخواست Authentication را به سمت IDP ارسال میکند. به این ترتیب کاربر به صورت موقت از برنامهی جاری خارج شده و به برنامهی IDP منتقل میشود. در برنامهی IDP است که کاربر مشخص میکند کیست؛ برای مثال با ارائهی نام کاربری و کلمهی عبور. پس از این مرحله، در صورت تائید هویت کاربر، برنامهی IDP یک Identity Token را تولید و امضاء میکند. سپس برنامهی IDP کاربر را مجددا به برنامهی کلاینت اصلی هدایت میکند و Identity Token را در اختیار آن کلاینت قرار میدهد. در اینجا برنامهی کلاینت، این توکن هویت را دریافت و اعتبارسنجی میکند. اگر این اعتبارسنجی با موفقیت انجام شود، اکنون کاربر تعیین اعتبار شده و هویت او جهت استفادهی از قسمتهای مختلف برنامه مشخص میشود. در برنامههای ASP.NET Core، این توکن هویت، پردازش و بر اساس آن یکسری Claims تولید میشوند که در اغلب موارد به صورت یک کوکی رمزنگاری شده در سمت کلاینت ذخیره میشوند.
به این ترتیب مرورگر با هر درخواستی از سمت کاربر، این کوکی را به صورت خودکار به سمت برنامهی کلاینت ارسال میکند و از طریق آن، هویت کاربر بدون نیاز به مراجعهی مجدد به IDP، استخراج و استفاده میشود.
مراحل انتقال کاربر به IDP، صدور توکن هویت، بازگشت مجدد به برنامهی کلاینت، اعتبارسنجی، استخراج Claims و ذخیرهی آن به صورت یک کوکی رمزنگاری شده را در تصویر فوق ملاحظه میکنید. بنابراین در حین کار با یک IDP، مرحلهی لاگین به سیستم، دیگر در برنامه یا برنامههای کلاینت قرار ندارد. در اینجا دو فلش به سمت IDP و سپس به سمت کلاینت را بخاطر بسپارید. در ادامه از آنها برای توضیح Flow و Endpoints استفاده خواهیم کرد.
البته OpenID Connect برای کار همزمان با انواع و اقسام برنامههای کلاینت طراحی شدهاست؛ مانند برنامهی سمت سرور MVC، برنامههای سمت کلاینت جاوا اسکریپتی مانند Angular و برنامههای موبایل. برای این منظور باید در IDP نوع کلاینت و یکسری از تنظیمات مرتبط با آنرا مشخص کرد.
کلاینتهای عمومی و محرمانه
زمانیکه قرار است با یک IDP کار کنیم، این IDP باید بتواند بین یک برنامهی حسابداری و یک برنامهی پرسنلی که از آن برای احراز هویت استفاده میکنند، تفاوت قائل شود و آنها را شناسایی کند.
- کلاینت محرمانه (Confidential Client)
هر کلاینت با یک client-id و یک client-secret شناخته میشود. کلاینتی که بتواند محرمانگی این اطلاعات را حفظ کند، کلاینت محرمانه نامیده میشود.
در اینجا هر کاربر، اطلاعات هویت خود را در IDP وارد میکند. اما اطلاعات تعیین هویت کلاینتها در سمت کلاینتها ذخیره میشوند. برای مثال برنامههای وب ASP.NET Core میتوانند هویت کلاینت خود را به نحو امنی در سمت سرور ذخیره کنند و این اطلاعات، قابل دسترسی توسط کاربران آن برنامه نیستند.
- کلاینت عمومی (Public Client)
این نوع کلاینتها نمیتوانند محرمانگی هویت خود را حفظ و تضمین کنند؛ مانند برنامههای جاوا اسکریپتی Angular و یا برنامههای موبایل که بر روی وسایل الکترونیکی کاربران اجرا میشوند. در این حالت هرچقدر هم سعی کنیم، چون کاربران به اصل این برنامهها دسترسی دارند، نمیتوان محرمانگی اطلاعات ذخیره شدهی در آنها را تضمین کرد.
مفهوم OpenID Connect Endpoints
در تصویر ابتدای بحث، دو فلش را مشاهده میکنید؛ برای مثال چگونه میتوان به Identity token دسترسی یافت (Authentication) و همچنین زمانیکه صحبت از Authorization میشود، چگونه میتوان Access tokens را دریافت کرد. اینکه این مراحله چگونه کار میکنند، توسط Flow مشخص میشود. Flow مشخص میکند که چگونه باید توکنها از سمت IDP به سمت کلاینت بازگشت داده شوند. بسته به نوع کلاینتها که در مورد آنها بحث شد و نیازمندیهای برنامه، باید از Flow مناسبی استفاده کرد.
هر Flow با Endpoint متفاوتی ارتباط برقرار میکند. این Endpointها در حقیقت جایگزین راهحلهای خانگی تولید برنامههای IDP هستند.
- در ابتدا یک Authorization Endpoint وجود دارد که در سطح IDP عمل میکند. این مورد همان انتهای فلش اول ارسال درخواست به سمت IDP است؛ در تصویر ابتدای بحث. کار این Endpoint، بازگشت Identity token جهت انجام عملیات Authentication و بازگشت Access token برای تکمیل عملیات Authorization است. این عملیات نیز توسط Redirection کلاینت انجام میشود (هدایت کاربر به سمت برنامهی IDP، دریافت توکنها و سپس هدایت مجدد به سمت برنامهی کلاینت اصلی).
نکتهی مهم: استفادهی از TLS و یا همان پروتکل HTTPS برای کار با OpenID Connect Endpoints اجباری است و بدون آن نمیتوانید با این سیستم کار کنید. به عبارتی در اینجا ترافیک بین کلاینت و IDP، همواره باید رمزنگاری شده باشد.
البته مزیت کار با ASP.NET Core 2.1، یکپارچگی بهتر و پیشفرض آن با پروتکل HTTPS است؛ تا حدی که مثال پیشفرض local آن به همراه یک مجوز موقتی SSL نصب شدهی توسط SDK آن کار میکند.
- پس از Authorization Endpoint که در مورد آن توضیح داده شد، یک Redirection Endpoint وجود دارد. در ابتدای کار، کلاینت با یک Redirect به سمت IDP هدایت میشود و پس از احراز هویت، مجددا کاربر به سمت کلاینت Redirect خواهد شد. به آدرسی که IDP کاربر را به سمت کلاینت Redirect میکند، Redirection Endpoint میگویند و در سطح کلاینت تعریف میشود. برنامهی IDP، اطلاعات تولیدی خود را مانند انواع توکنها، به سمت این Endpoint که در سمت کلاینت قرار دارد، ارسال میکند.
- پس از آن یک Token Endpoint نیز وجود دارد که در سطح IDP تعریف میشود. این Endpoint، آدرسی است در سمت IDP، که برنامهی کلاینت میتواند با برنامه نویسی، توکنهایی را از آن درخواست کند. این درخواست عموما از نوع HTTP Post بدون Redirection است.
مفهوم OpenID Connect Flow
- اولین Flow موجود، Authorization Code Flow است. کار آن بازگشت کدهای Authorization از Authorization Endpoint و همچنین توکنها از طریق Token Endpoint میباشد. در ایجا منظور از «کدهای Authorization»، اطلاعات دسترسی با طول عمر کوتاه است. هدف Authorization Code این است که مشخص کند، کاربری که به IDP لاگین کردهاست، همانی است که Flow را از طریق برنامهی وب کلاینت، شروع کردهاست. انتخاب این نوع Flow، برای کلاینتهای محرمانه مناسب است. در این حالت میتوان مباحث Refresh token و داشتن توکنهایی با طول عمر بالا را نیز پیاده سازی کرد.
- Implicit Flow، تنها توکنهای تولیدی را توسط Authorization Endpoint بازگشت میدهد و در اینجا خبری از بازگشت «کدهای Authorization» نیست. بنابراین از Token Endpoint استفاده نمیکند. این نوع Flow، برای کلاینتهای عمومی مناسب است. در اینجا کار client authentication انجام نمیشود؛ از این جهت که کلاینتهای عمومی، مناسب ذخیره سازی client-secret نیستند. به همین جهت در اینجا امکان دسترسی به Refresh token و توکنهایی با طول عمر بالا میسر نیست. این نوع از Flow، ممکن است توسط کلاینتهای محرمانه نیز استفاده شود.
- Hybrid Flow، تعدادی از توکنها را توسط Authorization Endpoint و تعدادی دیگر را توسط Token Endpoint بازگشت میدهد؛ بنابراین ترکیبی از دو Flow قبلی است. انتخاب این نوع Flow، برای کلاینتهای محرمانه مناسب است. در این حالت میتوان مباحث Refresh token و داشتن توکنهایی با طول عمر بالا را نیز پیاده سازی کرد. از این نوع Flow ممکن است برای native mobile apps نیز استفاده شود.
آگاهی از انواع Flowها، انتخاب نوع صحیح آنها را میسر میکند که در نتیجه منتهی به مشکلات امنیتی نخواهند شد. برای مثال Hybrid Flow توسط پشتیبانی از Refresh token امکان تمدید توکن جاری و بالا بردن طول عمر آنرا دارد و این طول عمر بالا بهتر است به کلاینتهای اعتبارسنجی شده ارائه شود. برای اعتبارسنجی یک کلاینت، نیاز به client-secret داریم و برای مثال برنامههای جاوا اسکریپتی نمیتوانند محل مناسبی برای ذخیره سازی client-secret باشند؛ چون از نوع کلاینتهای عمومی محسوب میشوند. بنابراین نباید از Hybrid Flow برای برنامههای Angular استفاده کرد. هرچند انتخاب این مساله صرفا به شما بر میگردد و چیزی نمیتواند مانع آن شود. برای مثال میتوان Hybrid Flow را با برنامههای Angular هم بکار برد؛ هرچند ایدهی خوبی نیست.
انتخاب OpenID Connect Flow مناسب برای یک برنامهی کلاینت از نوع ASP.NET Core
برنامههای ASP.NET Core، از نوع کلاینتهای محرمانه بهشمار میروند. بنابراین در اینجا میتوان تمام Flowهای یاد شده را انتخاب کرد. در برنامههای سمت سرور وب، به ویژگی به روز رسانی توکن نیاز است. بنابراین باید دسترسی به Refresh token را نیز داشت که توسط Implicit Flow پشتیبانی نمیشود. به همین جهت از Implicit Flow در اینجا استفاده نمیکنیم. پیش از ارائهی OpenID Connect، تنها Flow مورد استفادهی در برنامههای سمت سرور وب، همان Authorization Code Flow بود. در این Flow تمام توکنها توسط Token Endpoint بازگشت داده میشوند. اما Hybrid Flow نسبت به آن این مزیتها را دارد:
- ابتدا اجازه میدهد تا Identity token را از IDP دریافت کنیم. سپس میتوان آنرا بدون دریافت توکن دسترسی، تعیین اعتبار کرد.
- در ادامه OpenID Connect این Identity token را به یک توکن دسترسی، متصل میکند.
به همین جهت OpenID Connect نسبت به OAuth 2 ارجحیت بیشتری پیدا میکند.
پس از آشنایی با این مقدمات، در قسمت بعدی، کار نصب و راه اندازی IdentityServer را انجام خواهیم داد.
پَرباد چیست؟
آنچه که شما در این مطلب یاد خواهید گرفت:
- طریقه نصب
- ایجاد صورتحساب و ارسال کاربر به درگاه پرداخت
- تایید صورتحساب
- مردود کردن صورتحساب قبل از انتقال وجه از مشتری به فروشنده
- برگشت وجه به حساب مشتری پس از تأیید صورتحساب
- درگاه مجازی پرداخت (برای تست وب اپلیکیشن، بدون داشتن حساب واقعی در درگاههای بانکی)
- تنظیمات
- ذخیره سازی اطلاعات پرداخت
طریقه نصب
PM> Install-Package Parbad
PM> Install-Package Parbad.MVC5
ایجاد صورتحساب و ارسال کاربر به درگاه پرداخت
var invoice = new Invoice( [Order Number], [Amount], [Verify URL]);
var invoice = new Invoice(1, 30000, "http://www.mywebsite.com/payment/verify" );
var result = Payment.Request(Gateways.Mellat, invoice);
if (result.Status == RequestResultStatus.Success) { // این متد، کاربر را به سمت وب سایت درگاه پرداخت هدایت میکند result.Process(Context); } else { // در صورت تمایل میتوانید پیغام مورد نظر از درگاه پرداخت را نمایش دهید var msg = result.Message; }
در وب سایتهای MVC میتوانید به روش زیر عمل کنید
if (result.Status == RequestResultStatus.Success) { // کاربر را به سمت وب سایت درگاه پرداخت هدایت میکند return new RequestActionResult(result); } else { return View("Error"); }
تأیید صورتحساب
var result = Payment.Verify(System.Web.HttpContext.Current);
if(result.Status == VerifyResultStatus.Success) { // کاربر، صورتحساب را پرداخت کرده است و شما میتوانید ادامه عملیات خرید را انجام دهید } else { // کاربر بنا به دلایلی صورتحساب را پرداخت نکرده است // شما همچنین میتوانید علت را در قالب یک پیام از پراپرتی پیام مشاهده کنید // بنابراین شما میتوانید این صورتحساب را در پایگاه داده خود مردود اعلام کنید }
مردود کردن صورتحساب قبل از انتقال وجه از مشتری به فروشنده
همانطور که در تصویر میبینید، در هنگام بازگشت مشتری به وب سایت شما و تأیید کردن صورتحساب، شما میتوانید اطلاعات تراکنش مورد نظر را که شامل، درگاه پرداخت بانکی، شماره سفارش و شماره رجوع است را دریافت کنید و سپس با استفاده از این اطلاعات، پایگاه داده خود را بررسی کرده و در صورت لزوم، متد Cancel را فراخوانی کنید. به این ترتیب به درگاه بانکی، هیچگونه تأییدیه ای اعلام نمیشود و این بدان معناست که اگر وجهی به حساب فروشگاه واریز شده باشد، پس از چند دقیقه (معمولا ۱۵ دقیقه) به حساب مشتری برگشت داده خواهد شد.
برگشت وجه به حساب مشتری پس از تأیید صورتحساب
var refundResult = Payment.Refund(new RefundInvoice([Order Number], [Amount]));
درگاه مجازی پرداخت
var result = Payment.Request(Gateways.ParbadVirtualGateway, invoice);
در نتیجه در هنگام هدایت کاربر به درگاه پرداخت، کاربر به درگاه مجازی هدایت خواهد شد.
<system.webServer> <handlers> <add name="ParbadGatewayPage" verb="*" path="Parbad.axd" type="Parbad.Web.Gateway.ParbadVirtualGatewayHandler" /> </handlers> </system.webServer>
ParbadConfiguration.Gateways.ConfigureParbadVirtualGateway(new ParbadVirtualGatewayConfiguration("Parbad.axd"));
تنظیمات پَرباد
public class Global : HttpApplication { void Application_Start(object sender, EventArgs e) { // configurations } }
public class Startup { public void Configuration(IAppBuilder app) { // configurations } }
تنظیمات درگاههای پرداخت
تنظیمات ذخیره سازی اطلاعات پرداخت
ParbadConfiguration.Storage = new SqlServerStorage("Connection String", "MyPaymentTableName");
public class MyStorage : Storage { // Implement methods here... }
ParbadConfiguration.Storage = new MyStorage();
در صورتیکه هر گونه پیشنهاد یا انتقاد نسبت به کارکرد این سیستم دارید، صمیمانه منتظر شنیدن آن در راستای توسعه این سیستم هستم.همچنین در صورت تمایل به توسعه آن، میتوانید آن را در گیت هاب دنبال کنید و یا لطفا پیشنهادات، بحثها و نظرات خود را در صفحه مخصوص این پروژه ارسال کنید.با تشکر.
اولا، اجازه بدهید نگاهی به تنظیم یک دامنه محلی داشته باشیم. زمانیکه شما برنامه محلی را اجرا میکنید IIS Express به صورت محلی، پورتی خاص را به برنامه اختصاص میدهد:
فرض کنید میخواهیم برای سایت خود، درگاه بانک را راه اندازی کنیم. برنامه را به صورت محلی اجرا کرده و زمانیکه قصد ارتباط با بانک را دارید، با پیامی که دامنه شما در سیستم پرداخت بانکی ثبت نشده، مواجه میشوید. در اینجا بانک انتظار دارد که ما از طریق دامنهای که قبلا در سیستم پرداخت بانک ثبت کردهایم برای مثال (www.elemarket.ir) با آن ارتباط برقرار کنیم؛ ولی به دلیل ارتباط به صورت محلی با یکچنین دامنهای (localhost:59395) روبهرو شده و پیغام عدم امکان برقراری ارتباط را میدهد.
حال قصد داریم با تغییر دامنه به صورت سفارشی، این مشکل را برطرف کرده، تا درحقیقت قبل از قرار دادن سایت بر روی سرور و تست عملیات بتوانیم به صورت محلی نتیجه را دریافت کنیم.
استفاده از Telerik Fiddler برای ایجاد یک دامنهی سفارشی
برای این کار میتوانید از برنامهی سبک وزن Telerik Fiddler استفاده کنید و تنها کافیست به قسمت Tools>Host برنامه بروید و آدرس محلی برنامه (localhost:59395 ) و آدرس دامنهی مورد نظر را وارد کنید تا برنامه هم به صورت local و هم توسط یک دامنهی سفارشی، در دسترس باشد. برنامهی Fiddler را باز نگه داشته و به ویژوال استودیو بازگرید.
در نهایت، پیکربندی IIS Express خود را با اتصالهای جدید به روز کنید. شما معمولا میتوانید پیکربندی IIS Express Application خود را در این مسیر پیدا کنید .
C: \ Users \ YOUR_USERNAME \ Documents \ IISExpress \ config / applicationhost.config
در دسترس است.
فایل را باز کرده و گره <sites> را جستجو کنید. شما باید بتوانید درخواست خود را در فهرست سایتها مشاهده کنید. ما قصد داریم HTTP binding را با تغییر localhost به نام دامنهی سفارشی خود بهروز رسانی کنیم. در اینجا HTTP binding به صورت پیشفرض بر روی localhost میباشد:
پس از تغییر localhost و ذخیره کردن تنظیمات، بررسی کنید تا IIS Express در حال اجرا نباشد. حال برنامه را اجرا کنید.
نکته: اگر هنگام اجرای برنامه با خطای
“Unable to create the virtual directory. You must use specify ‘localhost’ for the server name”
" Invalid URL:the hostame could not be parsed "
بعد از اجرای برنامه به طور پیشفرض بر روی همان پورت localhost اجرا شده، حال به دامنهی مورد نظر که ایجاد کردهاید بروید:
در اینجا بعد از تلاش برای ارتباط با بانک، دیگر با پیام «دامنه شما در سیستم پرداخت بانکی ثبت نشدهاست» مواجه نشده و با موفقیت امکان تست برنامه را داریم:
افزودن و اعتبارسنجی خودکار Anti-Forgery Tokens در برنامههای Angular مبتنی بر ASP.NET Core
var xsrfTokenCookie = new HttpCookie("XSRF-TOKEN") { Value = ComputeXsrfTokenValue(), HttpOnly = false, // Now JavaScript is able to read the cookie Path = "/", Domain = "localhost" };