امن سازی برنامههای ASP.NET Core توسط IdentityServer 4x - قسمت نهم- مدیریت طول عمر توکنها
کار با Kendo UI DataSource
حین کار با کتابخانههای جاوا اسکریپتی باید مدام کنسول developer مرورگر را باز نگه دارید تا بتوانید خطاها را بهتر بررسی و دیباگ کنید.
یک نکتهی تکمیلی: روش لغو صف درخواستهای مکرر fetch ارسالی به سمت سرور
ورودی جستجوی بالای صفحه را درنظر بگیرید که بهازای هربار ورود حرفی، یک درخواست fetch جدید را به سمت سرور ارسال میکند تا نتایج جستجوی حاصل را دریافت کند. مشکل اینجاست که ما تنها به آخرین درخواست fetch ارسال شدهی به سمت سرور نیاز داریم و نه به تمام درخواستهای دیگری که صادر شدهاند. به همین جهت این صف درخواستهای fetch قبلی، غیربهینه بوده و ترافیک بالایی را سبب میشوند. یک روش مواجه شدن با این مساله، استفاده از مفهومی به نام debounce است که در پشت صحنه، از یک تایمر استفاده میکند و فقط هر چند ثانیه یکبار، یک درخواست جدید را به همراه آخرین متن ورودی، به سمت سرور ارسال خواهد کرد. راه دیگری هم برای مواجه شدن با این مشکل، در مرورگرهای جدید پیشبینی شدهاست که AbortController نام دارد. با استفاده از آن میتوان «سیگنالی» را به صف درخواستهای پرتعداد fetch قبلی حاصل از ورود اطلاعات کاربر ارسال کنیم که ... «لغو شوید» و به سمت سرور ارسال نشوید.
برای توضیح بهتر آن، به مثال زیر دقت کنید:
<!DOCTYPE html> <html> <body> <input id="search" type="number" /> <script> const results = []; const search = document.getElementById("search"); let controller = new AbortController(); let signal = controller.signal; const onChange = () => { const value = search.value; if (value) { controller.abort(); controller = new AbortController(); signal = controller.signal; getPost(value, signal); } }; search.onkeyup = onChange; </script> </body> </html>
- در اینجا یک input box را داریم که ابتدا، یافت شده و سپس به رخداد onkeyup آن، متد onChange نسبت داده شدهاست تا هربار که کاربر، اطلاعاتی را وارد میکند، فراخوانی شود.
- در ابتدای اسکریپت هم نحوهی نمونه سازی شیء استاندارد جاوااسکریپتی AbortController و دسترسی به شیء signal آنرا مشاهده میکنید.
- در متد onChange، ابتدا مقدار جدید ورودی کاربر، دریافت میشود، سپس این AbortController، لغو میشود و بعد یک نمونهی جدید از آن ایجاد شده و مجددا به شیء signal آن دسترسی پیدا میکنیم تا آنرا به متد getPost ارسال کنیم. این متد هم چنین پیاده سازی را دارد:
const getPost = (value, signal) => { fetch( `https://site.com/search/${value}`, { signal } ); };
همانطور که مشاهده میکنید، تابع fetch، قابلیت پذیرش شیء signal را هم دارد. زمانیکه با هربار تایپ کاربر، متد ()controller.abort فراخوانی میشود، سیگنالی را به fetch «قبلی» متصل به آن ارسال میکند که ... دیگر به سمت سرور ارسال نشو و متوقف شو. با اینکار فقط آخرین ورودی کاربر، سبب بروز یک fetch موفق میشود و ترافیک ارسالی به سمت سرور کاهش پیدا میکند (چون تمام fetchهای قبلی، سیگنال abort را دریافت کردهاند)؛ مانند مثال زیر که کاربر، 5 بار حروفی را وارد کرده و به ازای هربار ورود حرفی، یک درخواست fetch جدید، ایجاد شده، اما ... فقط آخرین درخواست ارسالی او موفق بوده و نتیجهای را بازگشت داده و مابقی درخواستها ... abort شدهاند. این عملیات abort، در سمت کاربر اعمال میشود؛ یعنی اصلا درخواستی به سمت سرور ارسال نمیشود و این لغو درخواست، توسط برنامهی سمت سرور انجام نشدهاست.
bower install angular-route --save
<script src="bower_components/angular-route/angular-route.min.js" type="text/javascript"></script>
<div class="col-md-9"> <ng-view></ng-view> </div>
model.panel = { title: "Panel Title", items: [ { title: "Home", url: "#/home" }, { title: "Articles", url: "#/articles" }, { title: "Authors", url: "#/authors" } ] };
var module = angular.module("dntModule", ["ngRoute"]);
module.config(function ($routeProvider) { $routeProvider .when("/home", { template: "<app-home></app-home>" }) .when("/articles", { template: "<app-articles></app-articles>" }) .when("/authors", { template: "<app-authors></app-authors>" }) .otherwise({ redirectTo: "/home" }); });
module.component("appHome", { template: ` <hr><div> <div>Panel heading = HomePage</div> <div> HomePage </div> </div>` }); module.component("appArticles", { template: ` <hr><div> <div>Panel heading = Articles</div> <div> Articles </div> </div>` }); module.component("appAuthors", { template: ` <hr><div> <div>Panel heading = Authors</div> <div> Authors </div> </div>` });
اکنون توسط لینکهای تعریف شده میتوانیم به راحتی درون تمپلیتها، پیمایش کنیم. همانطور که عنوان شد تا اینجا مسیریاب پیشفرض Angular هیچ اطلاعی از کامپوننتها ندارد؛ بلکه آنها را با کمک template، به صورت غیر مستقیم، درون صفحه نمایش دادهایم.
معرفی Component Router
مزیت این روتر این است که به صورت اختصاصی برای کار با کامپوننتها طراحی شده است. بنابراین دیگر نیازی به استفاده از template درون route configuration نیست. برای استفاده از این روتر ابتدا باید پکیج آن را نصب کنیم:
bower install angular-component-router --save
سپس وابستگی فوق را با روتر پیشفرضی که در مثال قبل بررسی کردیم، جایگزین خواهیم کرد:
<script src="bower_components/angular-component-router/angular_1_router.js"></script>
همچنین درون فایل module.js به جای وابستگی ngRoute از ngComponentrouter استفاده خواهیم کرد:
var module = angular.module("dntModule", ["ngComponentRouter"]);
در ادامه به جای تمامی route configurations قبلی، اینبار یک کامپوننت جدید را به صورت زیر ایجاد خواهیم کرد:
module.component("appHome", { template: ` <hr> <div> <div>Panel heading = HomePage</div> <div> HomePage </div> </div>` });
همانطور که مشاهده میکنید برای پاسخگویی به تغییرات URL، مقدار routeConfig$ را مقداردهی کردهایم. در اینجا به جای بارگذاری تمپلیت، خود کامپوننت، در هر یک از ruleهای فوق بارگذاری خواهد شد. برای حالت otherwise نیز از سینتکس **/ استفاده کردهایم.
تمپلیت کامپوننت فوق نیز به صورت زیر است:
<div class="container"> <div class="row"> <div class="col-md-3"> <hr> <dnt-widget></dnt-widget> </div> <div class="col-md-9"> <ng-outlet></ng-outlet> </div> </div> </div>
لازم به ذکر است دیگر نباید از دایرکتیو ng-view استفاده کنیم؛ زیرا این دایرکتیو برای استفاده از روتر اصلی طراحی شده است. به جای آن از دایرکتیو ng-outlet استفاده شده است. این کامپوننت به عنوان یک کامپوننت top level عمل خواهد کرد. بنابراین درون صفحهی index.html از کامپوننت فوق استفاده خواهیم کرد:
<html ng-app="dntModule"> <head> <meta charset="UTF-8"> <title>Using Angular 1.5 Component Router</title> <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css"> <link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.min.css"> </head> <body> <dnt-app></dnt-app> <script src="bower_components/angular/angular.js" type="text/javascript"></script> <script src="bower_components/angular-component-router/angular_1_router.js"></script> <script src="scripts/module.js" type="text/javascript"></script> <script src="scripts/dnt-app.component.js"></script> <script src="scripts/dnt-widget.component.js"></script> </body> </html>
در نهایت باید جهت فعالسازی سیستم مسیریابی جدید، سرویس زیر را همراه با نام کامپوننت فوق ریجستر کنیم:
module.value("$routerRootComponent", "dntApp");
اکنون اگر برنامه را اجرا کنید خواهید دید که همانند قبل، کار خواهد کرد. اما اینبار از روتر جدید و سازگار با کامپوننتها استفاده میکند.
کدهای این قسمت را نیز از اینجا میتوانید دریافت کنید.
بررسی ویجت Kendo UI File Upload
remove: function () { var r = confirm("برای حذف اطمینان دارید"); if (r == true) { alert("فایل حذف شد"); } else { } },
چون نخ پردازشی رویدادهای Blazor نظیر Oninitialize و OnAfterRender و ... یکی است بنابراین استفاده از StateHasChanged نتیجه مطلوب را به همراه خواهد داشت اما در رابطه با متدهای خارجی (External method) مانند تایمرها باید از await InvokeAsynck(StateHasChanged) استفاده نمود.
StateHasChanged که برای رندر مجدد کامپوننتها در Blazor Server استفاده میشود، اجازه نمیدهد چندین نخ به طور همزمان به فرآیند رندر دسترسی داشته باشند. در صورتی که StateHasChanged توسط یک نخ ثانویه فراخوانی شود آنگاه استثنایی شبیه زیر رخ خواهد داد:
System.InvalidOperationException: The current thread is not associated with the Dispatcher.
MTOM در WCF
- Text(Xml) Message Encoder(به صورت پیش فرض در تمام Http-Base Bindingها از این Encoder استفاده میشود)
- Binary Message Encoder(به صورت پیش فرض در تمام Net* Bindingها از این encoder استفاده میشود که برای سرویسهای وب مناسب نیست)
- MTOM Message Encoder (در حالت استفاده از Http-Base Bindingها و انتقال اطلاعات به صورت باینری از این گزینه استفاده میشود که به صورت پیش فرض غیر فعال است)
»خاصیتی که نوع آن آرایه ای از بایتها است نباید دارای DataMemberAttribute باشد؛ بلکه به جای آن باید از MessageBodyMember استفاده نمایید.
»به جای []Byte میتوان از نوع Stream نیز استفاده کرد(الزامی نیست).
»مقدار خاصیت MessageEncoding در Binding استفاده شده باید MTOM تعیین شود.
ابتدا کلاس مورد نظر را به صورت زیر تهیه میکنیم:
[MessageContract] public class MyFile { [MessageHeader] public String Filename { get; set; } [MessageBodyMember] public Byte[] Contents { get; set; } }
- به جای DataContract از MessageContract استفاده میشود؛
- تمام خاصیت هایی که نوع آنها غیر از []Byte است باید دارای MessageHeader باشند؛
- خاصیتی که برای انتقال محتوای باینری تهیه شده است، باید از MessageBodyMember استفاده نماید؛
- مجاز به تعریف فیلد یا فیلد هایی که نوع آنها Primitive Type است نمیباشید.
تنظیمات مربوط به Binding نیز به صورت خواهد بود:
<bindings> <wsHttpBinding> <binding name="WsHttpMtomBinding" messageEncoding="Mtom" /> </wsHttpBinding> </bindings>
هدف از استفاده از MTOM برای افزایش کارایی انتقال دادههای باینری در حجم زیاد است. در زیر نتایج مقایسه بررسی انتقال اطلاعات به دو صورت MTOM و Text برای حجم دادههای متفاوت را مشاهده میکنید:
با دقت در نتایج بالا مشخص میشود که این روش در حجم دادههای پایین (مثل 100 بایت یا 1000 بایت) عملکرد مورد انتظار را نخواهد داشت. پس این نکته را نیز در هنگام پیاده سازی به این روش مد نظر داشته باشید.
کامپوننتهای جدید SectionOutlet و SectionContent در Blazor 8x
پیاده سازی Sections در Blazor 8x به کمک دو کامپوننت جدید SectionOutlet و SectionContent میسر شدهاست و برای دسترسی به آنها نیاز است ابتدا به فایل Imports.razor_ پروژه، مراجعه کرد و using زیر را به آن اضافه نمود تا این اشیاء، در کامپوننتهای برنامه قابل شناسایی و استفاده شوند:
@using Microsoft.AspNetCore.Components.Sections
SectionOutlet کامپوننتی است که محتوای ارائه شدهی توسط کامپوننت SectionContent را رندر میکند (این محتوا در اصل یک RenderFragment است). ارتباط بین این دو هم توسط خواص SectionName و یا SectionIdهای متناظر، برقرار میشود. اگر چندین SectionContent دارای نام و یا Id یکسانی باشند، محتوای آخرین آنها در SectionOutlet متناظر، رندر میشود.
برای مثال در فایل MainLayout.razor، تغییر زیر را اعمال میکنیم:
<div class="top-row px-4"> <SectionOutlet SectionName="before-top-row"/> <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a> </div>
<SectionContent SectionName="before-top-row"> <a href="/show-help" target="_blank">Help</a> </SectionContent>
روش تعریف یک محتوای پیشفرض
این محتوا، فقط زمانی تامین خواهد شد که کامپوننت در حال نمایش SectionContent، قابل مشاهده و فعال شده باشد. یعنی اگر از کامپوننت نمایش دهندهی آن به صفحهی دیگری رجوع کنیم، محتوای SectionOutlet مجددا خالی خواهد شد، تا زمانیکه به آدرس نمایش دهندهی کامپوننت دربرگیرندهی SectionContent متناظر با آن رجوع کنیم. به همین جهت اگر علاقمند به نمایش یک «محتوای پیشفرض» هستید، میتوان به صورت زیر عمل کرد:
<div class="top-row px-4"> <SectionOutlet SectionName="before-top-row" /> <SectionContent SectionName="before-top-row"> <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a> </SectionContent> </div>
تفاوت SectionId با SectionName
کامپوننت SectionOutlet، هم میتواند یک SectionName را دریافت کند و هم یک SectionId را. SectionNameها، رشتهای هستند و تغییرات آتی آنها تحت نظارت کامپایلر نیست. به همین جهت اگر نیاز به Refactoring آنها است، شاید بهتر باشد از خاصیت SectionId که یک Id از نوع strongly typed را مشخص میکند، استفاده کنیم.
برای نمونه میتوان مثال قبلی را به صورت زیر با استفاده از یک SectionId، بازنویسی کرد:
<div class="top-row px-4"> <SectionOutlet SectionId="BeforeTopRow" /> <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a> </div> @code{ public static SectionOutlet BeforeTopRow = new(); }
سپس هر کامپوننت دیگری که بخواهد از این Id استاتیک استفاده کند، روش کار آن به صورت زیر است:
<SectionContent SectionId="MainLayout.BeforeTopRow"> <a href="/show-help" target="_blank">Help</a> </SectionContent>
ممکن است پس از ارتقاء به دات نت 6، شاهد خطاهای BadHttpRequestException زیادی در سمت Web API شوید (البته فقط در حالت توسعه):
Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException: Unexpected end of request content.
اگر برنامهی شما اطلاعاتی را در پوشهی wwwroot ثبت کند، برای مثال یک بانک اطلاعاتی SQLite را در آن قرار دادهاید، hot reload حتی این پوشه را هم تحت کنترل خود دارد و با هر تغییری در آن، حتی نوشته شدن یک رکورد در فایل بانک اطلاعاتی، مجددا برنامه را کامپایل میکند. همین کامپایل پشت صحنه، سبب abort شدن درخواستهای HTTP سمت کلاینت آن و مشاهدهی خطاهای BadHttpRequestException سمت سرور میشود که عدم اطلاع از دلیل آن میتواند این تصور را پیش بیاورد که کدهای برنامه مشکلی دارند، اما خیر.
اگر با CLI کار میکنید، دستور dotnet watch run --no-hot-reload کمی وضع را بهتر میکند و معادل افزودن تنظیم زیر به فایل Properties\launchSettings.json است:
"profiles": { "Name.Server": { "hotReloadEnabled": false,
<ItemGroup> <Watch Remove="wwwroot\**\*" /> </ItemGroup>