من از طریق کابل گوشیم رو وصل کردم اما در موارد زیر مشکل دارم میشه توضیح بدید چه باید کرد:
1- آنجا که نوشته اید " اولین بار دو برنامه بر روی گوشی شما نصب میشوند که برای کار دیباگ در Xamarin لازم هستند" باید دو برنامه در گوشیم نصب بشه ولی فقط یه برنامه نصب شده .
2- طبق نوشته " خوشبختانه
به واسطه وجود
Xaml edit and continue احتیاجی به
Stop - Start کردن پروژه و بیلد کردن برای اعمال تغییرات UI نیست و به محض تغییر Xaml ، میتوانید تاثیر آن
را در گوشی خود ببینید. ولی برای هر تغییر CSharp باید Stop - Start و Build کنید."
اگر بصورت Emulator برنامه را اجرا کنم با تغییر کد در xml موارد در
شبیه ساز هم تغییر میکند اما در کدهای سی شارپ اصلا اجازه نمیده کدها رو تغییر بدم
(از طریق stop – continue ) و در حالتی که از طریق گوشیم
بخوام استفاده کنم (بجای شبیه ساز)، با تغییر کدهای xml هم
نمیشه تغییرات را روی گوشیم دید و برای کدهای سی شارپ هم که مثل Emulator عمل میکنه یعنی
اجازه تغییر نمیده. ولی اگر روی کدی break point بذارم برنامه را در
همانجا break point درست عمل میکنه اما باز نمیتوان کد سی شارپ را تغییر داد.
ModelBinder سفارشی در ASP.NET MVC
دریافت و نصب Awesomium
پروژه Awesomium دارای یک SDK است که از اینجا قابل دریافت میباشد. بعد از نصب آن در مسیر Awesomium SDK\1.7.3.0\wrappers\Awesomium.NET\Assemblies\Packed میتوانید محصور کنندهی دات نتی آنرا مشاهده کنید. منظور از Packed در اینجا، استفاده از DLLهای فشرده شدهی native آن است که در مسیر Awesomium SDK\1.7.3.0\build\bin\packed کپی شدهاند. بنابراین برای توزیع این نوع برنامهها نیاز است اسمبلی دات نتی Awesomium.Core.dll به همراه دو فایل بومی icudt.dll و awesomium.dll ارائه شوند.
تهیه تصاویر سایتها به کمک Awesomium.NET
پس از نصب Awesomium اگر به مسیر Documents\Awesomium SDK Samples\1.7.3.0\Awesomium.NET\Samples\Core\CSharp\BasicSample مراجعه کنید، مثالی را در مورد تهیه تصاویر سایتها به کمک Awesomium.NET، مشاهده خواهید کرد. خلاصهی آن چند سطر ذیل است:
try { using (WebSession mywebsession = WebCore.CreateWebSession( new WebPreferences() { CustomCSS = "::-webkit-scrollbar { visibility: hidden; }" })) { using (var view = WebCore.CreateWebView(1240, 1000, mywebsession)) { view.Source = new Uri("https://site.com/"); bool finishedLoading = false; view.LoadingFrameComplete += (s, e) => { if (e.IsMainFrame) finishedLoading = true; }; while (!finishedLoading) { Thread.Sleep(100); WebCore.Update(); } using (var surface = (BitmapSurface)view.Surface) { surface.SaveToJPEG("result.jpg"); } } } } finally { WebCore.Shutdown(); }
مشکل! این روش در برنامههای ASP.NET کار نمیکند!
مثال همراه آن یک مثال کنسول ویندوزی است و به خوبی کار میکند؛ اما در برنامههای وب پس از چند روز سعی و خطا مشخص شد که:
الف) WebCore.Shutdown فقط باید در پایان کار یک برنامه فراخوانی شود. یعنی اصلا نیازی نیست تا در برنامههای وب فراخوانی شود.
System.InvalidOperationException: You are attempting to re-initialize the WebCore. The WebCore must only be initialized once per process and must be shut down only when the process exits.
System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt. at Awesomium.Core.NativeMethods.WebCore_CreateWebView_1(HandleRef jarg1, Int32 jarg2, Int32 jarg3, HandleRef jarg4)
خطای فوق هم از آن نوع خطاهایی است که پروسهی IIS را درجا خاموش میکند.
استفاده از Awesomium در یک ترد پس زمینه
راه حلی که نهایتا پاسخ داد و به خوبی و پایدار کار میکند، شامل ایجاد یک ترد مجزای Awesomium در زمان آغاز برنامهی وب و زنده نگه داشتن آن تا زمان پایان کار برنامه است.
using System; using System.Collections.Concurrent; using System.IO; using System.Threading; using System.Web; using Awesomium.Core; namespace AwesomiumWebModule { public class AwesomiumModule : IHttpModule { private static readonly Thread WorkerThread = new Thread(awesomiumWorker); private static readonly ConcurrentQueue<AwesomiumRequest> TaskQueue = new ConcurrentQueue<AwesomiumRequest>(); private static bool _isRunning = true; static AwesomiumModule() { WorkerThread.Start(); } private static void awesomiumWorker() { while (_isRunning) { if (TaskQueue.Count != 0) { AwesomiumRequest outRequest; if (TaskQueue.TryDequeue(out outRequest)) { var img = AwesomiumThumbnail.FetchWebPageThumbnail(outRequest); File.WriteAllBytes(outRequest.SavePath, img); Thread.Sleep(500); } } Thread.Sleep(5); } } public void Dispose() { _isRunning = false; WebCore.Shutdown(); } public void Init(HttpApplication context) { context.EndRequest += endRequest; } static void endRequest(object sender, EventArgs e) { var url = HttpContext.Current.Items[Constants.AwesomiumRequest] as AwesomiumRequest; if (url!=null) { TaskQueue.Enqueue(url); } } } }
نمونهی استفاده از آن، در سمت یک برنامهی وب نیز به صورت زیر است. ابتدا ماژول تهیه شده باید در برنامه ثبت شود:
<?xml version="1.0"?> <configuration> <system.web> <compilation debug="true" targetFramework="4.0" /> <httpModules> <add name="AwesomiumWebModule" type="AwesomiumWebModule.AwesomiumModule"/> </httpModules> </system.web> <system.webServer> <validation validateIntegratedModeConfiguration="false"/> <modules> <add name="AwesomiumWebModule" type="AwesomiumWebModule.AwesomiumModule"/> </modules> </system.webServer> </configuration>
protected void btnStart_Click(object sender, EventArgs e) { var host = new Uri(txtUrl.Text).Host; HttpContext.Current.Items.Add(Constants.AwesomiumRequest, new AwesomiumRequest { Url = txtUrl.Text, SavePath = Path.Combine(HttpRuntime.AppDomainAppPath, "App_Data\\Thumbnails\\" + host + ".jpg"), TempDir = Path.Combine(HttpRuntime.AppDomainAppPath, "App_Data\\Temp") }); lblInfo.Text = "Please wait. Your request will be served shortly."; }
Url، آدرس وب سایتی است که میخواهید تصویر آن تهیه شود. SavePath مسیر کامل فایل jpg نهایی است که قرار است دریافت و ذخیره گردد. TempDir محل ذخیره سازی فایلهای موقتی Awesomium است. Awesomium یک سری کوکی، تصاویر و فایلهای هر سایت را به این ترتیب کش کرده و در دفعات بعدی سریعتر عمل میکند.
پروژهی کامل آنرا از اینجا میتوانید دریافت کنید:
AwesomiumWebApplication_V1.0.zip
declare @t table(id int, name nvarchar(max), active bit) insert @t values (1, 'Group 1', 1), (2, 'Group 2', 0)
select '[' + STUFF(( select ',{"id":' + cast(id as varchar(max)) + ',"name":"' + name + '"' + ',"active":' + cast(active as varchar(max)) +'}' from @t t1 for xml path(''), type ).value('.', 'varchar(max)'), 1, 1, '') + ']'
[{"id":1,"name":"Group 1","active":1},{"id":2,"name":"Group 2","active":0}]
declare @group table(id int, name nvarchar(max), active bit) insert @group values (1, 'Group 1', 1), (2, 'Group 2', 0) declare @member table(id int, groupid int,name nvarchar(max)) insert @member values (1, 1,'Ali'), (2, 1,'Mojtaba'),(3,2,'Hamid') select '[' + STUFF(( select ',{"id":' + cast(g.id as varchar(max)) + ',"name":"' + g.name + '"' + ',"members": { "children": [' + (select + STUFF(( select ',{"id":' + cast(m.id as varchar(max)) + ',"name":"' + m.name + '"}' from @member m where m.groupid = g.id for xml path(''), type ).value('.', 'varchar(max)'), 1, 1, '') + ']}' + ',"active":' + cast(g.active as varchar(max)) +'}') from @group g for xml path(''), type ).value('.', 'varchar(max)'), 1, 1, '') + ']'
[{"id":1,"name":"Group 1","members": { "children": [{"id":1,"name":"Ali"},{"id":2,"name":"Mojtaba"}]} ,"active":1}, {"id":2,"name":"Group 2","members": { "children": [{"id":3,"name":"Hamid"}]} ,"active":0}]
var user = _documentSession .Include<User>(x => x.Apps[59].AddressId) .Load("Users/131-A"); var address = _documentSession.Load<Address>(user.Apps[59].AddressId)
var user = _documentSession .Include<User>(x => x.Apps.Values.Select(app => app.AddressId)) .Load("Users/131-A"); var addresses = List<Address>(); foreach(app in user.Apps) { addresses.Add(_documentSession.Load<Address>(app.AddressId)); //queryسمت کلاینت انجام اجرا میشود }
- حتما باید Id سند(ها) را داشته باشیم.
- کل سند را بازیابی میکند.
List<User> users = await _documentSession .Query<Users>() .Where(u => u.PhoneNumber.StartsWith("915")) .ToListAsync();
var users = await _documentSession.Query<AppUser>() .Where(u => u.Id.Equals("915")) .Select(u => new { u.Apps[appCode].FirstName, u.Apps [appCode].LastName, }) .ToListAsync();
from Users as user where startsWith(user.PhoneNumber, "915") select { FirstName : user.Apps ["59"].FirstName, LastName : user.Apps ["59"].LastName }
from u in _documentSession.Query<User>() where u.PhoneNumber.StartsWith("915") let app = u.Apps["59"] select new { app.FirstName, app.LastName, };
declare function output(u) { var app = u.Apps["59"]; return { FirstName : app.FirstName, LastName : app.LastName}; } from Users as user where startsWith(user.PhoneNumber, "915") select output(user)
app.FirstName, app.LastName, *key = u.ActiveInApps.Select(a => a.Key)
query = query.Search(u => u.key, "59");
public class User_MyIndex : AbstractIndexCreationTask<User> { Map = users => from u in users from app in u.Apps select new { Id = u.Id, PhoneNumber = u.PhoneNumber, UserName = app.Value.UserName, FirstName = app.Value.FirstName, LastName = app.Value.LastName, IsActive = app.Value.IsActive, key = app.Key }; }
new User_MyIndex().Execute(store);
IndexCreation.CreateIndexes(typeof(User_MyIndex).Assembly, store);
from u in _documentSession.Query<User, User_MyIndex>() ...
select new { ... key = aia.Key, Address = LoadDocument<Address>(aia.Value.AddressId), // City = LoadDocument<Address>(aia.Value.AddressId).City, };
Message = app.Messages.Select(m => LoadDocument<Message>(m).Content)
var users = _documentSession.Advanced.AsyncDocumentQuery<User, User_MyIndex>() .WhereStartsWith(nameof(AppUser.PhoneNumber), "915") .WhereEquals("key", appCode, exact: true) .SelectFields<AppUserModel>(new[] { $"Apps[{appCode}].FirstName", $"Apps[{appCode}].LastName" }) .ToListAsync();
public class Post { public int Id { get; set; } public string Content { get; set; } public string Title { get; set; } public List<string> Tags { get; set; } public string WriterName { get; set; } public string WriterId { get; set; } }
public class Post_ByContent : AbstractIndexCreationTask<Post> { public Post_ByContent() { Map = posts=> from post in posts select new { post.Content }; Analyzers.Add(p => p.Content, "StandardAnalyzer"); } }
List<Post> posts = _documentSession .Query<Post, Post_ByContent>() .MoreLikeThis(builder => builder .UsingDocument(p => p.Id == "posts/59-A") .WithOptions(new MoreLikeThisOptions { Fields = new[] { nameof(Post.Content) }, StopWordsDocumentId = "appConfig/StopWords" })) .ToList();
چون interceptor تعریف شده سراسری است، ممکن است در صفحه چندین درخواست HTTP وجود داشته باشند که از این interceptor رد میشوند. به همین جهت خاتمه کار loading bar باید زمانی باشد که تمام این درخواستها خاتمه یافتهاند. نگارش نهایی LoaderInterceptorService را با درنظر گرفتن این شرط در اینجا میتوانید مشاهده کنید.
دریافت کدهای کامل این پروژه
کدهای کامل پروژهای که نیازمندیهای فوق را پیاده سازی میکند، در اینجا میتوانید مشاهده و دریافت کنید. در این مطلب از قرار دادن مستقیم این کدها صرفنظر شده و سعی خواهد شد بجای آن، نقشهی ذهنی درک کدهای آن توضیح داده شود.
پیشنیازها
در پروژهی فوق برای شروع به کار، از اطلاعات مطرح شدهی در سلسله مطالب زیر استفاده شدهاست:
- «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity»
- «مدیریت مرکزی شماره نگارشهای بستههای NuGet در پروژههای NET Core.»
- «کاهش تعداد بار تعریف usingها در C# 10.0 و NET 6.0.»
- «روش یافتن لیست تمام کنترلرها و اکشن متدهای یک برنامهی ASP.NET Core»
نیاز به علامتگذاری صفحات امن شدهی سمت کلاینت، جهت نمایش خودکار آنها
صفحات امن سازی شدهی سمت کلاینت، با ویژگی Authorize مشخص میشوند. بنابراین قید آن الزامی است، تا صرفا جهت کاربران اعتبارسنجی شده، قابل دسترسی شوند. در اینجا میتوان یک نمونهی سفارشی سازی شدهی ویژگی Authorize را به نام ProtectedPageAttribute نیز مورد استفاده قرار داد. این ویژگی از AuthorizeAttribute ارثبری کرده و دقیقا مانند آن عمل میکند؛ اما این اضافات را نیز به همراه دارد:
- به همراه یک Policy از پیش تعیین شده به نام CustomPolicies.DynamicClientPermission است تا توسط قسمتهای بررسی سطوح دسترسی پویا و همچنین منوساز برنامه، یافت شده و مورد استفاده قرار گیرد.
- به همراه خواص اضافهتری مانند GroupName و Title نیز هست. GroupName نام سرتیتر منوی dropdown نمایش داده شدهی در منوی اصلی برنامهاست و Title همان عنوان صفحه که در این منو نمایش داده میشود. اگر صفحهی محافظت شدهای به همراه GroupName نباشد، یعنی باید به صورت یک آیتم اصلی نمایش داده شود. همچنین در اینجا یک سری Order هم درنظر گرفته شدهاند تا بتوان ترتیب نمایش صفحات را نیز به دلخواه تغییر داد.
نمونهای از استفادهی از ویژگی فوق را در مسیر src\Client\Pages\Feature1 میتوانید مشاهده کنید که خلاصهی آن به صورت زیر است:
@attribute [ProtectedPage(GroupName = "Feature 1", Title = "Page 1", GlyphIcon = "bi bi-dot", GroupOrder = 1, ItemOrder = 1)]
ویژگی ProtectedPage را معادل یک ویژگی Authorize سفارشی، به همراه چند خاصیت بیشتر، جهت منوساز پویای برنامه درنظر بگیرید.
نیاز به لیست کردن صفحات علامتگذاری شدهی با ویژگی ProtectedPage
پس از اینکه صفحات مختلف برنامه را توسط ویژگی ProtectedPage علامتگذاری کردیم، اکنون نوبت به لیست کردن پویای آنها است. اینکار توسط سرویس ProtectedPagesProvider صورت میگیرد. این سرویس با استفاده از Reflection، ابتدا تمام IComponentها یا همان کامپوننتهای تعریف شدهی در برنامه را از اسمبلی جاری استخراج میکند. بنابراین اگر نیاز دارید که این جستجو در چندین اسمبلی صورت گیرد، فقط کافی است ابتدای این کدها را تغییر دهید. پس از یافت شدن IComponent ها، فقط آنهایی که دارای RouteAttribute هستند، پردازش میشوند؛ یعنی کامپوننتهایی که به همراه مسیریابی هستند. پس از آن بررسی میشود که آیا این کامپوننت دارای ProtectedPageAttribute هست یا خیر؟ اگر بله، این کامپوننت در لیست نهایی درج خواهد شد.
نیاز به یک منوساز پویا جهت نمایش خودکار صفحات امن سازی شدهی با ویژگی ProtectedPage
اکنون که لیست صفحات امن سازی شدهی توسط ویژگی ProtectedPage را در اختیار داریم، میتوانیم آنها را توسط کامپوننت سفارشی NavBarDynamicMenus به صورت خودکار نمایش دهیم. این کامپوننت لیست صفحات را توسط کامپوننت NavBarDropdownMenu نمایش میدهد.
تهیهی جداول و سرویسهای ثبت دسترسیهای پویای سمت کلاینت
جداول و فیلدهای مورد استفادهی در این پروژه را در تصویر فوق ملاحظه میکنید که در پوشهی src\Server\Entities نیز قابل دسترسی هستند. در این برنامه نیاز به ذخیره سازی اطلاعات نقشهای کاربران مانند نقش Admin، ذخیره سازی سطوح دسترسی پویای سمت کلاینت و همچنین سمت سرور است. بنابراین بجای اینکه به ازای هر کدام، یک جدول جداگانه را تعریف کنیم، میتوان از همان طراحی ASP.NET Core Identity مایکروسافت با استفاده از جدول UserClaimها ایده گرفت. یعنی هر کدام از این موارد، یک Claim خواهند شد:
در اینجا نقشها با Claim استانداردی به نام http://schemas.microsoft.com/ws/2008/06/identity/claims/role که توسط خود مایکروسافت نامگذاری شده و سیستمهای اعتبارسنجی آن بر همین اساس کار میکنند، قابل مشاهدهاست. همچنین دو Claim سفارشی دیگر ::DynamicClientPermission:: برای ذخیره سازی اطلاعات صفحات محافظت شدهی سمت کلاینت و ::DynamicServerPermission:: جهت ذخیره سازی اطلاعات اکشن متدهای محافظت شدهی سمت سرور نیز تعریف شدهاند. رابطهای این اطلاعات با جدول کاربران، many-to-many است.
به این ترتیب است که مشخص میشود کدام کاربر، به چه claimهایی دسترسی دارد.
برای کار با این جداول، سه سرویس UsersService، UserClaimsService و UserTokensService پیش بینی شدهاند. UserTokens اطلاعات توکنهای صادر شدهی توسط برنامه را ذخیره میکند و توسط آن میتوان logout سمت سرور را پیاده سازی کرد؛ از این جهت که JWTها متکی به خود هستند و تا زمانیکه منقضی نشوند، در سمت سرور پردازش خواهند شد، نیاز است بتوان به نحوی اگر کاربری غیرفعال شد، از آن ثانیه به بعد، توکنهای او در سمت سرور پردازش نشوند که به این نکات در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» پیشتر پرداخته شدهاست.
اطلاعات این سرویسها توسط اکشن متدهای UsersAccountManagerController، در اختیار برنامهی کلاینت قرار میگیرند.
نیاز به قسمت مدیریتی ثبت دسترسیهای پویای سمت کلاینت و سرور
قبل از اینکه بتوان قسمتهای مختلف کامپوننت NavBarDynamicMenus را توضیح داد، نیاز است ابتدا یک قسمت مدیریتی را جهت استفادهی از لیست ProtectedPageها نیز تهیه کرد:
در این برنامه، کامپوننت src\Client\Pages\Identity\UsersManager.razor کار لیست کردن کاربران، که اطلاعات آنرا از کنترلر UsersAccountManagerController دریافت میکند، انجام میدهد. در مقابل نام هر کاربر، دو دکمهی ثبت اطلاعات پویای دسترسیهای سمت کلاینت و سمت سرور وجود دارد. سمت کلاینت آن توسط کامپوننت UserClientSidePermissions.razor مدیریت میشود و سمت سرور آن توسط UserServerSidePermissions.razor.
کامپوننت UserClientSidePermissions.razor، همان لیست صفحات محافظت شدهی توسط ویژگی ProtectedPage را به صورت گروه بندی شده و به همراه یک سری chekmark، ارائه میدهد. اگر در اینجا صفحهای انتخاب شد، اطلاعات آن به سمت سرور ارسال میشود تا توسط Claim ای به نام ::DynamicClientPermission:: به کاربر انتخابی انتساب داده شود.
شبیه به همین عملکرد در مورد دسترسی سمت سرور نیز برقرار است. UserServerSidePermissions.razor، لیست اکشن متدهای محافظت شده را از کنترلر DynamicPermissionsManagerController دریافت کرده و نمایش میدهد. این اطلاعات توسط سرویس ApiActionsDiscoveryService جمع آوری میشود. همچنین این اکشن متدهای ویژه نیز باید با ویژگی Authorize(Policy = CustomPolicies.DynamicServerPermission) مزین شده باشند که نمونه مثال آنها را در مسیر src\Server\Controllers\Tests میتوانید مشاهده کنید. اگر در سمت کلاینت و قسمت مدیریتی آن، اکشن متدی جهت کاربر خاصی انتخاب شد، اطلاعات آن ذیل Claimای به نام ::DynamicServerPermission:: به کاربر انتخابی انتساب داده میشود.
بازگشت اطلاعات پویای دسترسیهای سمت کلاینت از API
تا اینجا کامپوننتهای امن سازی شدهی سمت کلاینت و اکشن متدهای امن سازی شدهی سمت سرور را توسط صفحات مدیریتی برنامه، به کاربران مدنظر خود انتساب دادیم و توسط سرویسهای سمت سرور، اطلاعات آنها را در بانک اطلاعاتی ذخیره کردیم. اکنون نوبت به استفادهی از claims تعریف شده و مرتبط با هر کاربر است. پس از یک لاگین موفقیت آمیز توسط UsersAccountManagerController، سه توکن به سمت کاربر ارسال میشوند:
- توکن دسترسی: اطلاعات اعتبارسنجی کاربر به همراه نام و نقشهای او در این توکن وجود دارند.
- توکن به روز رسانی: هدف از آن، دریافت یک توکن دسترسی جدید، بدون نیاز به لاگین مجدد است. به این ترتیب کاربر مدام نیاز به لاگین مجدد نخواهد داشت و تا زمانیکه refresh token منقضی نشدهاست، برنامه میتواند از آن جهت دریافت یک access token جدید استفاده کند.
- توکن سطوح دسترسی پویای سمت کلاینت: در اینجا لیست ::DynamicClientPermission::ها به صورت یک توکن مجزا به سمت کاربر ارسال میشود. این اطلاعات به توکن دسترسی اضافه نشدهاند تا بیجهت حجم آن اضافه نشود؛ از این جهت که نیازی نیست تا به ازای هر درخواست HTTP به سمت سرور، این لیست حجیم claims پویای سمت کلاینت نیز به سمت سرور ارسال شود. چون سمت سرور از claims دیگری به نام ::DynamicServerPermission:: استفاده میکند.
اگر دقت کنید، هم refresh-token و هم DynamicPermissions هر دو به صورت JWT ارسال شدهاند. میشد هر دو را به صورت plain و ساده نیز ارسال کرد. اما مزیت refresh token ارسال شدهی به صورت JWT، انجام اعتبارسنجی خودکار سمت سرور اطلاعات آن است که دستکاری سمت کلاینت آنرا مشکل میکند.
این سه توکن توسط سرویس BearerTokensStore، در برنامهی سمت کلاینت ذخیره و بازیابی میشوند. توکن دسترسی یا همان access token، توسط ClientHttpInterceptorService به صورت خودکار به تمام درخواستهای ارسالی توسط برنامه الصاق خواهد شد.
مدیریت خودکار اجرای Refresh Token در برنامههای Blazor WASM
دریافت refresh token از سمت سرور تنها قسمتی از مدیریت دریافت مجدد یک access token معتبر است. قسمت مهم آن شامل دو مرحلهی زیر است:
الف) اگر خطاهای سمت سرور 401 و یا 403 رخ دادند، ممکن است نیاز به refresh token باشد؛ چون احتمالا یا کاربر جاری به این منبع دسترسی ندارد و یا access token دریافتی که طول عمر آن کمتر از refresh token است، منقضی شده و دیگر قابل استفاده نیست.
ب) پیش از منقضی شدن access token، بهتر است با استفاده از refresh token، یک access token جدید را دریافت کرد تا حالت الف رخ ندهد.
- برای مدیریت حالت الف، یک Policy ویژهی Polly طراحی شدهاست که آنرا در کلاس ClientRefreshTokenRetryPolicy مشاهده میکنید. در این Policy ویژه، هرگاه خطاهای 401 و یا 403 رخ دهند، با استفاده از سرویس جدید IClientRefreshTokenService، کار به روز رسانی توکن انجام خواهد شد. این Policy در کلاس program برنامه ثبت شدهاست. مزیت کار با Policyهای Polly، عدم نیاز به try/catch نوشتنهای تکراری، در هر جائیکه از سرویسهای HttpClient استفاده میشود، میباشد.
- برای مدیریت حالت ب، حتما نیاز به یک تایمر سمت کلاینت است که چند ثانیه پیش از منقضی شدن access token دریافتی پس از لاگین، کار دریافت access token جدیدی را به کمک refresh token موجود، انجام دهد. پیاده سازی این تایمر را در کلاس ClientRefreshTokenTimer مشاهده میکنید که محل فراخوانی و راه اندازی آن یا پس از لاگین موفق در سمت کلاینت و یا با ریفرش صفحه (فشرده شدن دکمهی F5) و در کلاس آغازین ClientAuthenticationStateProvider میباشد.
نیاز به پیاده سازی Security Trimming سمت کلاینت
از داخل DynamicPermissions دریافتی پس از لاگین، لیست claimهای دسترسی پویای سمت کلاینت کاربر لاگین شده استخراج میشود. بنابراین مرحلهی بعد، استخراج، پردازش و اعمال این سطوح دسترسی پویای دریافت شدهی از سرور است.
سرویس BearerTokensStore، کار ذخیره سازی توکنهای دریافتی پس از لاگین را انجام میدهد و سپس با استفاده از سرویس DynamicClientPermissionsProvider، توکن سوم دریافت شده که مرتبط با لیست claims دسترسی کاربر جاری است را پردازش کرده و تبدیل به یک لیست قابل استفاده میکنیم تا توسط آن بتوان زمانیکه قرار است آیتمهای منوها را به صورت پویا نمایش داد، مشخص کنیم که کاربر، به کدامیک دسترسی دارد و به کدامیک خیر. عدم نمایش قسمتی از صفحه که کاربر به آن دسترسی ندارد را security trimming گویند. برای نمونه کامپوننت ویژهی SecurityTrim.razor، با استفاده از نقشها و claims یک کاربر، میتواند تعیین کند که آیا قسمت محصور شدهی صفحه توسط آن قابل نمایش به کاربر است یا خیر. این کامپوننت از متدهای کمکی AuthenticationStateExtensions که کار با user claims دریافتی از طریق JWTها را ساده میکنند، استفاده میکند. یک نمونه از کاربرد کامپوننت SecurityTrim را در فایل src\Client\Shared\MainLayout.razor میتوانید مشاهده کنید که توسط آن لینک Users Manager، فقط به کاربران دارای نقش Admin نمایش داده میشود.
نحوهی مدیریت security trimming منوی پویای برنامه، اندکی متفاوت است. DynamicClientPermissionsProvider لیست claims متعلق به کاربر را بازگشت میدهد. این لیست پس از لاگین موفقیت آمیز دریافت شدهاست. سپس لیست کلی صفحاتی را که در ابتدای برنامه استخراج کردیم، در طی حلقهای از سرویس ClientSecurityTrimmingService عبور میدهیم. یعنی مسیر صفحه و همچنین دسترسیهای پویای کاربر، مشخص هستند. در این بین هر مسیری که در لیست claims پویای کاربر نبود، در لیست آیتمهای منوی پویای برنامه، نمایش داده نمیشود.
نیاز به قطع دسترسی به مسیرهایی در سمت کلاینت که کاربر به صورت پویا به آنها دسترسی ندارد
با استفاده از ClientSecurityTrimmingService، در حلقهای که آیتمهای منوی سایت را نمایش میدهد، موارد غیرمرتبط با کاربر جاری را حذف کردیم و نمایش ندادیم. اما این حذف، به این معنا نیست که اگر این آدرسها را به صورت مستقیم در مرورگر وارد کند، به آنها دسترسی نخواهد داشت. برای رفع این مشکل، نیاز به پیاده سازی یک سیاست دسترسی پویای سمت کلاینت است. روش ثبت این سیاست را در کلاس DynamicClientPermissionsPolicyExtensions مشاهده میکنید. کلید آن همان CustomPolicies.DynamicClientPermission که در حین تعریف ProtectedPageAttribute به عنوان مقدار Policy پیشفرض مقدار دهی شد. یعنی هرگاه ویژگی ProtectedPage به صفحهای اعمال شد، از این سیاست دسترسی استفاده میکند که پردازشگر آن DynamicClientPermissionsAuthorizationHandler است. این هندلر نیز از ClientSecurityTrimmingService استفاده میکند. در هندلر context.User جاری مشخص است. این کاربر را به متد تعیین دسترسی مسیر جاری به سرویس ClientSecurityTrimming ارسال میکنیم تا مشخص شود که آیا به مسیر درخواستی دسترسی دارد یا خیر؟
نیاز به قطع دسترسی به منابعی در سمت سرور که کاربر به صورت پویا به آنها دسترسی ندارد
شبیه به ClientSecurityTrimmingService سمت کلاینت را در سمت سرور نیز داریم؛ به نام ServerSecurityTrimmingService که کار آن، پردازش claimهایی از نوع ::DynamicServerPermission:: است که در صفحهی مدیریتی مرتبطی در سمت کلاینت، به هر کاربر قابل انتساب است. هندلر سیاست دسترسی پویایی که از آن استفاده میکند نیز DynamicServerPermissionsAuthorizationHandler میباشد. این سیاست دسترسی پویا با کلید CustomPolicies.DynamicServerPermission در کلاس ConfigureServicesExtensions تعریف شدهاست. به همین جهت هر اکشن متدی که Policy آن با این کلید مقدار دهی شده باشد، از هندلر پویای فوق جهت تعیین دسترسی پویا عبور خواهد کرد. منطق پیاده سازی شدهی در اینجا، بسیار شبیه به مطلب «سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاستهای دسترسی پویا» است؛ اما بدون استفادهی از ASP.NET Core Identity.
روش اجرای برنامه
چون این برنامه از نوع Blazor WASM هاست شدهاست، نیاز است تا برنامهی Server آنرا در ابتدا اجرا کنید. با اجرای آن، بانک اطلاعاتی SQLite برنامه به صورت خودکار توسط EF-Core ساخته شده و مقدار دهی اولیه میشود. لیست کاربران پیشفرض آنرا در اینجا میتوانید مشاهده کنید. ابتدا با کاربر ادمین وارد شده و سطوح دسترسی سایر کاربران را تغییر دهید. سپس بجای آنها وارد سیستم شده و تغییرات منوها و سطوح دسترسی پویا را بررسی کنید.