تمامی مراحل رو چک کردم .ممنون میشم اگر کسی بطور کامل تونسته زامارین رو راه اندازی کنه یه آموزش تصویری برای بقیه از مراحل راه اندازی قرار بده.
اشتراکها
بررسی تغییرات Blazor در دات نت 8
What's New in Blazor for .NET 8
Come find out about the future of Blazor in .NET 8! We'll explore all the upcoming features and improvements, including our effort to create a unified full stack web UI programming model that combines the strengths of client and server. We hope to see you there!
You will learn:
How Blazor is becoming the best option for full stack web development
How Blazor in .NET 8 will provide full flexibility to build web apps however works best for you
How to try out the latest Blazor features in .NET 8
به صورت پیش فرض SQL Server از روش write-ahead log - WAL استفاده میکند. به این معنا که کلیه تغییرات، پیش از commit نهایی باید در لاگ فایل آن نوشته شوند. این مساله با تعداد بالای تراکنشها تا حدودی بر روی سرعت سیستم میتواند تاثیرگذار باشد. برای بهبود این وضعیت، در SQL Server 2014 قابلیتی به نام delayed_durability اضافه شدهاست که با فعال سازی آن، کلیه اعمال مرتبط با لاگهای تراکنشها به صورت غیرهمزمان انجام میشوند. به این ترتیب تراکنشها زودتر از معمول به پایان خواهد رسید؛ با این فرض که نوشته شدن تغییرات در لاگ فایلها، در آیندهای محتمل انجام خواهند شد. این مساله به معنای فدا کردن D در ACID است (Atomicity, Consistency, Isolation, Durability). البته باید دقت داشت که رسیدن به ACID کامل هزینهبر است و شاید خیلی از اوقات تمام اجزای آن نیازی نباشند یا حتی بتوان با اندکی تخفیف آنها را اعمال کرد؛ مانند D به تاخیر افتاده.
برای اینکار SQL Server از یک بافر 60 کیلوبایتی برای ذخیره سازی اطلاعات لاگهایی که قرار است به صورت غیرهمزمان با تراکنشها نوشته شوند، استفاده میکند. هر زمان که این 60KB پر شد، آنرا flush کرده و ثبت خواهد نمود. به این ترتیب به دو مزیت خواهیم رسید:
- پردازش تراکنشها بدون منتظر شدن جهت commit نهایی در دیسک سخت ادامه خواهند یافت. صبر کمتر به معنای امکان پردازش تراکنشهای بیشتری در یک سیستم پر ترافیک است.
- با توجه به بافری که از آن صحبت شد، اینبار اعمال Write به صورت یک سری batch اعمال میشوند که کارآیی و سرعت بیشتری نسبت به حالت تکی دارند.
اندکی تاریخچه
ایده یک چنین عملی 28 سال قبل توسط Hal Berenson ارائه شدهاست! اوراکل آنرا در سال 2006 تحت عنوان Asynchronous Commit پیاده سازی کرد و مایکروسافت در سال 2014 آنرا ارائه دادهاست.
فعال سازی ماندگاری غیرهمزمان در SQL Server
فعال سازی این قابلیت در سطح بانک اطلاعاتی، در سطح یک تراکنش مشخص و یا در سطح رویههای ذخیره شده کامپایل شده مخصوص OLTP درون حافظهای، میسر است.
برای فعال سازی ماندگاری با تاخیر در سطح یک دیتابیس، خواهیم داشت:
در اینجا اگر ALLOWED را انتخاب کنید، به این معنا است که لاگ کلیه تراکنشهای مرتبط با این بانک اطلاعاتی به صورت غیرهمزمان نوشته میشوند. حالت FORCED نیز دقیقا به همین معنا است با این تفاوت که اگر حالت ALLOWED انتخاب شود، تراکنشهای ماندگار (آنهایی که به صورت دستی DELAYED_DURABILITY را غیرفعال کردهاند)، سبب flush کلیه تراکنشهایی با ماندگاری به تاخیر افتاده خواهند شد و سپس اجرا میشوند. در حالت Forced تنظیم دسترسی DELAYED_DURABILITY = OFF در سطح تراکنشها تاثیری نخواهد داشت؛ اما در حالت ALLOWED این مساله به صورت دستی در سطح یک تراکنش قابل لغو است.
البته باید توجه داشت، صرفنظر از این تنظیمات، یک سری از تراکنشها همیشه ماندگار هستند و بدون تاخیر؛ مانند تراکنشهای سیستمی، تراکنشهای بین دو یا چند بانک اطلاعاتی و کلیه تراکنشهایی که با FileTable، Change Data Capture و Change Tracking سر و کار دارند.
در سطح تراکنشهای میتوان نوشت:
و یا در رویههای ذخیره شده کامپایل شده مخصوص OLTP درون حافظهای خواهیم داشت:
سؤال: آیا فعال سازی DELAYED_DURABILITY بر روی مباحث locking و isolation levels تاثیر دارند؟
پاسخ: خیر. کلیه تنظیمات قفل گذاریها همانند قبل و بر اساس isolation levels تعیین شده، رخ خواهند داد. تنها تفاوت در اینجا است که با فعال سازی DELAYED_DURABILITY، کار commit بدون صبر کردن برای پایان نوشته شدن اطلاعات در لاگ سیستم صورت میگیرد. به این ترتیب قفلهای انجام شده زودتر آزاد خواهند شد.
سؤال: میزان از دست دادن اطلاعات احتمالی در این روش چقدر است؟
در صورتیکه سرور کرش کند یا ریاستارت شود، حداکثر به اندازهی 60KB اطلاعات را از دست خواهید داد (اندازهی بافری که برای اینکار درنظر گرفته شدهاست). البته عنوان شدهاست که اگر ریاستارت یا خاموشی سرور، از پیش تعیین شده باشد، ابتدا کلیه لاگهای flush نشده، ذخیره شده و سپس ادامهی کار صورت خواهد گرفت؛ ولی زیاد به آن اطمینان نکنید. اما همواره با فراخوانی sys.sp_flush_log، میتوان به صورت دستی بافر لاگهای سیستم را flush کرد.
یک آزمایش
در ادامه قصد داریم یک جدول جدید را در بانک اطلاعاتی آزمایشی testdb2 ایجاد کنیم. سپس یکبار تنظیم DELAYED_DURABILITY = FORCED را انجام داده و 10 هزار رکورد را ثبت میکنیم و بار دیگر DELAYED_DURABILITY = DISABLED را تنظیم کرده و همین عملیات را تکرار خواهیم کرد:
با این خروجی:
در این آزمایش، سرعت insertها در حالت DELAYED_DURABILITY = FORCED حدود 4 برابر است نسبت به حالت معمولی.
برای مطالعه بیشتر
Control Transaction Durability
SQL Server 2014 Delayed Durability/Lazy Commit
Delayed Durability in SQL Server 2014 – Part 1
Is In-Memory OLTP Always a silver bullet for achieving better transactional speed
Delayed Durability in SQL Server 2014
برای اینکار SQL Server از یک بافر 60 کیلوبایتی برای ذخیره سازی اطلاعات لاگهایی که قرار است به صورت غیرهمزمان با تراکنشها نوشته شوند، استفاده میکند. هر زمان که این 60KB پر شد، آنرا flush کرده و ثبت خواهد نمود. به این ترتیب به دو مزیت خواهیم رسید:
- پردازش تراکنشها بدون منتظر شدن جهت commit نهایی در دیسک سخت ادامه خواهند یافت. صبر کمتر به معنای امکان پردازش تراکنشهای بیشتری در یک سیستم پر ترافیک است.
- با توجه به بافری که از آن صحبت شد، اینبار اعمال Write به صورت یک سری batch اعمال میشوند که کارآیی و سرعت بیشتری نسبت به حالت تکی دارند.
اندکی تاریخچه
ایده یک چنین عملی 28 سال قبل توسط Hal Berenson ارائه شدهاست! اوراکل آنرا در سال 2006 تحت عنوان Asynchronous Commit پیاده سازی کرد و مایکروسافت در سال 2014 آنرا ارائه دادهاست.
فعال سازی ماندگاری غیرهمزمان در SQL Server
فعال سازی این قابلیت در سطح بانک اطلاعاتی، در سطح یک تراکنش مشخص و یا در سطح رویههای ذخیره شده کامپایل شده مخصوص OLTP درون حافظهای، میسر است.
برای فعال سازی ماندگاری با تاخیر در سطح یک دیتابیس، خواهیم داشت:
ALTER DATABASE dbname SET DELAYED_DURABILITY = DISABLED | ALLOWED | FORCED;
در اینجا اگر ALLOWED را انتخاب کنید، به این معنا است که لاگ کلیه تراکنشهای مرتبط با این بانک اطلاعاتی به صورت غیرهمزمان نوشته میشوند. حالت FORCED نیز دقیقا به همین معنا است با این تفاوت که اگر حالت ALLOWED انتخاب شود، تراکنشهای ماندگار (آنهایی که به صورت دستی DELAYED_DURABILITY را غیرفعال کردهاند)، سبب flush کلیه تراکنشهایی با ماندگاری به تاخیر افتاده خواهند شد و سپس اجرا میشوند. در حالت Forced تنظیم دسترسی DELAYED_DURABILITY = OFF در سطح تراکنشها تاثیری نخواهد داشت؛ اما در حالت ALLOWED این مساله به صورت دستی در سطح یک تراکنش قابل لغو است.
البته باید توجه داشت، صرفنظر از این تنظیمات، یک سری از تراکنشها همیشه ماندگار هستند و بدون تاخیر؛ مانند تراکنشهای سیستمی، تراکنشهای بین دو یا چند بانک اطلاعاتی و کلیه تراکنشهایی که با FileTable، Change Data Capture و Change Tracking سر و کار دارند.
در سطح تراکنشهای میتوان نوشت:
COMMIT TRANSACTION WITH (DELAYED_DURABILITY = ON);
BEGIN ATOMIC WITH (DELAYED_DURABILITY = ON, ...)
سؤال: آیا فعال سازی DELAYED_DURABILITY بر روی مباحث locking و isolation levels تاثیر دارند؟
پاسخ: خیر. کلیه تنظیمات قفل گذاریها همانند قبل و بر اساس isolation levels تعیین شده، رخ خواهند داد. تنها تفاوت در اینجا است که با فعال سازی DELAYED_DURABILITY، کار commit بدون صبر کردن برای پایان نوشته شدن اطلاعات در لاگ سیستم صورت میگیرد. به این ترتیب قفلهای انجام شده زودتر آزاد خواهند شد.
سؤال: میزان از دست دادن اطلاعات احتمالی در این روش چقدر است؟
در صورتیکه سرور کرش کند یا ریاستارت شود، حداکثر به اندازهی 60KB اطلاعات را از دست خواهید داد (اندازهی بافری که برای اینکار درنظر گرفته شدهاست). البته عنوان شدهاست که اگر ریاستارت یا خاموشی سرور، از پیش تعیین شده باشد، ابتدا کلیه لاگهای flush نشده، ذخیره شده و سپس ادامهی کار صورت خواهد گرفت؛ ولی زیاد به آن اطمینان نکنید. اما همواره با فراخوانی sys.sp_flush_log، میتوان به صورت دستی بافر لاگهای سیستم را flush کرد.
یک آزمایش
در ادامه قصد داریم یک جدول جدید را در بانک اطلاعاتی آزمایشی testdb2 ایجاد کنیم. سپس یکبار تنظیم DELAYED_DURABILITY = FORCED را انجام داده و 10 هزار رکورد را ثبت میکنیم و بار دیگر DELAYED_DURABILITY = DISABLED را تنظیم کرده و همین عملیات را تکرار خواهیم کرد:
CREATE TABLE tblData( ID INT IDENTITY(1, 1), Data1 VARCHAR(50), Data2 INT ); CREATE CLUSTERED INDEX PK_tblData ON tblData(ID); CREATE NONCLUSTERED INDEX IX_tblData_Data2 ON tblData(Data2); ------------------------- alter database testdb2 SET DELAYED_DURABILITY = FORCED; ------------------------- SET NOCOUNT ON Print 'DELAYED_DURABILITY = FORCED' DECLARE @counter AS INT = 0 DECLARE @start datetime = getdate() WHILE (@counter < 10000) BEGIN INSERT INTO tblData (Data1, Data2) VALUES('My Data', @counter) SET @counter += 1 END Print DATEDIFF(ms,@start,getdate()); GO ------------------------- alter database testdb2 SET DELAYED_DURABILITY = DISABLED; truncate table tblData; ------------------------- SET NOCOUNT ON Print 'DELAYED_DURABILITY = DISABLED' DECLARE @counter AS INT = 0 DECLARE @start datetime = getdate() WHILE (@counter < 10000) BEGIN INSERT INTO tblData (Data1, Data2) VALUES('My Data', @counter) SET @counter += 1 END Print DATEDIFF(ms,@start,getdate()); GO -----------------------
DELAYED_DURABILITY = FORCED 666 DELAYED_DURABILITY = DISABLED 2883
برای مطالعه بیشتر
Control Transaction Durability
SQL Server 2014 Delayed Durability/Lazy Commit
Delayed Durability in SQL Server 2014 – Part 1
Is In-Memory OLTP Always a silver bullet for achieving better transactional speed
Delayed Durability in SQL Server 2014
وبلاگها ، سایتها و مقالات ایرانی (داخل و خارج از ایران)
- بهبود در توابع Table-Valued
- ظاهر جدید برای ویژوال استودیو 2010
- سورس نرم افزار اشتراک
- فریم ورک های سی اس اس را بهتر بشناسیم
- غزال مایکروسافت در راه است
- بررسی سایت ماهواره امید
- مروری بر سافاری 4
- صدا زدن یک Web service از طریق jquery
- نصب OTRS روی ویندوز ویستا
- آموزش کامل اسکریپت نویسی nsis - ساخت برنامه نصب
- توضیحی اجمالی در مورد singleton pattern
- MySQL Storage Engines
- مشکل بهم ریختگی متون فارسی انگلیسی در کامپیوتر
- ۴۸ نکته و اصل مهم در برنامه نویسی پی اچ پی
- فشرده سازی صفحات در ASP.net
امنیت
Visual Studio
ASP. Net
طراحی و توسعه وب
- ورود به دنیای jQuery plugins
- لیستی از 240 افزونهی جیکوئری
- و همچنین 20 مورد دیگر
- معرفی 13 screengrab webservices
- jQuery Chart Plugins
- برگههای تقلب وبی!
- خودتان را برای IE8 آماده کنید!
PHP
اسکیوال سرور
سی شارپ
عمومی دات نت
- Maestro ، زبان جدید دات نتی برای برنامه نویسی موازی
- چگونه تشخیص دهیم که یک اسمبلی دات نت به چه زبانی نوشته شده است؟
- ADO.NET Data Services v1.5 CTP1
ویندوز
مسایل اجتماعی و انسانی برنامه نویسی
متفرقه
توکنهای صادر شدهی توسط 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 شروع میکنیم.
- در اینجا در تنظیمات یک کلاینت جدید، خاصیت 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 به خواص یک کلاینت است:
- در اینجا میتوان خاصیت 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 را صادر کنیم:
همین اندازه تنظیم در سمت برنامهی کلاینت برای دریافت refresh token و ذخیره سازی آن جهت استفادههای آتی کفایت میکند.
در ادامه نیاز است به سرویس ImageGalleryHttpClient مراجعه کرده و کدهای آنرا به صورت زیر تغییر داد:
تفاوت این کلاس با نمونهی قبلی آن در اضافه شدن متد 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 هر کلاینت است:
مقدار پیشفرض AccessTokenType، همان Jwt یا توکنهای متکی به خود است که در اینجا به Reference Token تغییر یافتهاست.
اینبار اگر برنامه را اجرا کنید و در کلاس ImageGalleryHttpClient برنامهی کلاینت، بر روی سطر httpClient.SetBearerToken یک break-point قرار دهید، مشاهده خواهید کرد فرمت این توکن ارسالی به سمت Web API تغییر یافته و اینبار تنها یک Id سادهاست که دیگر قابل decode شدن و استخراج اطلاعات دیگری از آن نیست. با ادامه جریان برنامه و رسیدن این توکن به سمت Web API، درخواست رسیده برگشت خواهد خورد و اجرا نمیشود.
علت اینجا است که هنوز تنظیمات کار با token introspection endpoint انجام نشده و این توکن رسیدهی در سمت Web API قابل اعتبارسنجی و استفاده نیست. برای تنظیم آن نیاز است یک ApiSecret را در سطح Api Resource مربوط به IDP تنظیم کنیم:
اکنون فایل startup در سطح API را جهت معرفی این تغییرات به صورت زیر ویرایش میکنیم:
در اینجا نیاز است 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 آنرا تکمیل میکنیم:
در اینجا در متد جدید 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 وارد کنید.
بررسی طول عمر توکنها
اگر مرورگر خود را پس از لاگین به سیستم، برای مدتی به حال خود رها کنید، پس از شروع به کار مجدد، مشاهده خواهید کرد که دیگر نمیتوانید به 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 } }; } } }
- مقدار خاصیت 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, // ... } }; } } }
- البته 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");
در ادامه نیاز است به سرویس 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; } } }
پیشتر در قسمت ششم، روش کار مستقیم با 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 } }; } } }
اینبار اگر برنامه را اجرا کنید و در کلاس 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()) } } }; }
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"; });
اکنون اگر برنامه را اجرا کنید، ارتباط با 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); } } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.
در این قسمت نحوهی فعال سازی قابلیت FileStream را بررسی خواهیم کرد و در قسمت بعدی نحوهی دسترسی به آنرا از طریق برنامه نویسی مرور مینمائیم.
فعال سازی قابلیت FileStream
همانند اکثر قابلیتهای اس کیوال سرور، فعال سازی FileStream نیز حداقل به دو صورت استفاده از GUI و قابلیتهای management studio میسر است و یا استفاده از دستورات T-SQL (و البته کتابخانهی SMO یا همان محصور کنندهی تواناییهای management studio نیز قابل استفاده است).
روش اول) استفاده از management studio
قابلیت FileStream به صورت پیش فرض غیرفعال است. برای فعال سازی آن به مسیر زیر مراجعه نمائید:
Start > All Programs > Microsoft SqlServer 2008 > Configuration Tools > SQL Server Configuration Manager
سپس در قسمت SQL Server services ، وهله مربوط به SQL Server را یافته، کلیک راست و به برگه خواص آن مراجعه کرده (شکل زیر) و قابلیت FileStream را فعال کنید:
گزینههای مختلف آن به شرح زیر هستند:
• Enable FileStream for transact-sql access : امکان استفاده از دستورات T-SQL را جهت دسترسی به فایلها فعال میسازد (یا برعکس)
• Enable FileStream for File I/O streaming access : امکان دسترسی به فایلها با استفاده از Win32 streaming access
• All remote clients to have streaming access to file stream data : اجازهی دسترسی به کلاینتهای راه دور جهت استفاده از قابلیت FileStream
مرحله بعد، فعال سازی سطح دسترسی به سرور است. به management studio مراجعه نمائید. سپس بر روی وهله سرور مورد نظر کلیک راست نموده و به خواص آن مراجعه کنید (شکل زیر). سپس در قسمت advanced سطح دسترسی را بر روی Full قرار دهید.
پس از این تنظیم به شما پیغام داده خواهد شد که باید دیتابیس سرور را یکبار راه اندازی مجدد نمائید تا تنظیمات مورد نظر، اعمال شوند.
در ادامه باید دیتابیسی را که نیاز است نوع داده FileStream را بپذیرد، تنظیم نمود.
بر روی دیتابیس مورد نظر کلیک راست کرده و در برگه خواص آن به گزینهی Filegroups مراجعه کنید. سپس در اینجا یک گروه جدید را اضافه کرده ، نامی دلخواه را وارد نموده و سپس تیک مربوط به default بودن آنرا نیز قرار دهید (شکل زیر):
سپس در همین برگهی خواص دیتابیس که باز است، به گزینهی Files مراجعه کنید. در اینجا سه کار را باید انجام دهید. ابتدا بر روی دکمه Add کلیک کرده و در قسمت logical name ردیف اضافه شده، نامی دلخواه را وارد کنید. سپس file type آن را بر روی FileStream قرار دهید. در ادامه به قسمت path در همین ردیف مراجعه نموده و مسیر ذخیره سازی را مشخص کنید. در پایان بر روی دکمهی OK کلیک نمائید تا کار تنظیم دیتابیس به پایان رسد (شکل زیر):
روش دوم) استفاده از دستورات T-SQL
منهای قسمت تنظیمات SQL Server Configuration Manager که باید از طریق روش عنوان شده صورت گیرد، سایر موارد فوق را با استفاده از دستورات T-SQL نیز میتوان انجام داد:
الف) تنظیم سطح دسترسی بر روی سرور
EXEC sp_configure filestream_access_level, 2 -- 0 : Disable , 1 : Transact Sql Access , 2 : Win IO Access
GO
RECONFIGURE
GO
اگر نیاز باشد دیتابیس جدیدی ایجاد شود: (ایجاد گروه فایل مربوطه و سپس تنظیمات مسیر آن)
CREATE DATABASE Test_Db
ON
PRIMARY ( NAME = TestDb1,
FILENAME = 'C:\DATA\Test_Db.mdf'),
FILEGROUP FileStreamGroup1 CONTAINS FILESTREAM( NAME = Testfsg1,
FILENAME = 'C:\DATA\Learning_DbStream')
LOG ON ( NAME = TestDbLog1,
FILENAME = 'C:\DATA\Test_Db.ldf')
GO
و یا ایجاد تغییرات بر روی دیتابیسی موجود: (ایجاد گروه فایل مخصوص و سپس افزودن فایل مربوطه و تنظیمات آن)
--add filegroup
alter database TestDb
Add FileGroup FileStreamFileGroup1 contains FileStream
go
--Add FileGroup To DB
alter database TestDB
add file
(
name = 'UserDocuments' ,
filename = 'C:\FileStream\UserDocuments'
) to filegroup FileStreamFileGroup1
تعریف جدولی آزمایشی به همراه فیلدی از نوع FileStream :
تا اینجا سرور و همچنین دیتابیس جهت پذیرش این نوع داده آماده شدند. اکنون نوبت به استفاده از آن است:
CREATE TABLE [tblFiles]
(
FileId UNIQUEIDENTIFIER NOT NULL ROWGUIDCOL UNIQUE DEFAULT(NEWID()),
Title NVARCHAR(255) NOT NULL,
SystemFile VARBINARY(MAX) FILESTREAM NULL
)
ON [PRIMARY] FILESTREAM_ON [fsg1]
توسط دستور T-SQL فوق جدولی که از نوع داده FileStream استفاده میکند، ایجاد خواهد شد. این جدول همانطور که مشخص است حتما باید دارای یک فیلد منحصربفرد باشد (ر.ک. مقاله قبل) و همچنین برچسب فایل استریم به فیلدی از نوع VARBINARY(MAX) نیز الصاق شده است. به علاوه گروه فایل آن نیز باید به صورت صریح مشخص گردد؛ که در مثال ما مطابق تصاویر به fsg1 تنظیم شده بود.
ادامه دارد ...
سلام و عرض ادب .
اما بعد از وارد شدن در صفحه اصلی پروژام وقتی از طریق Rasor اطلاعات ایدنتی رو فراخوانی میکنم خالی نشان میدهد. که کد فراخوانی به شکل زیر میباشد در بالای صفحه. البته در کنترل هام (چه از نوع Web Api چه از نوع MVC) نیز دریافت نمیکنم.
بنده این تکنولوژی رو در پروژه web api راه اندازی کردم . اما مشکلی که به آن برمیخورم این است که در لاگین بعد از اینکه نام کاربری و کلمه عبور اعتبارسنجی شد و توکن دریافت شد به صفحه ایندکس ارجاع میدم یعنی بعد از دریافت توکن در سمت کلاینت (جی کوئری) از کد زیر استفاده مینمایم :
window.location = 'Index';
var claimsIdentity = this.User.Identity as System.Security.Claims.ClaimsIdentity; var userId = claimsIdentity.FindFirst(System.Security.Claims.ClaimTypes.UserData).Value;
مطالب
ASP.NET MVC #7
آشنایی با Razor Views
قبل از اینکه بحث جاری ASP.NET MVC را بتوانیم ادامه دهیم و مثلا مباحث دریافت اطلاعات از کاربر، کار با فرمها و امثال آنرا بررسی کنیم، نیاز است حداقل به دستور زبان یکی از View Engineهای ASP.NET MVC آشنا باشیم.
MVC3 موتور View جدیدی را به نام Razor معرفی کرده است که به عنوان روش برگزیده ایجاد Viewها در این سیستم به شمار میرود و فوق العاده نسبت به ASPX view engine سابق، زیباتر، سادهتر و فشردهتر طراحی شده است و یکی از اهداف آن تلفیق code و markup میباشد. در این حالت دیگر پسوند فایلهای Viewها همانند سابق ASPX نخواهد بود و به cshtml و یا vbhtml تغییر یافته است. همچنین برخلاف web forms view engine از System.Web.Page مشتق نشده است. و باید دقت داشت که Razor یک زبان برنامه نویسی جدید نیست. در اینجا از مخلوط زبانهای سی شارپ و یا ویژوال بیسیک به همراه تگهای html استفاده میشود.
البته این را هم باید عنوان کرد که این مسایل سلیقهای است. اگر با web forms view engine راحت هستید، با همان کار کنید. اگر با هیچکدام از اینها راحت نیستید (!) نمونههای دیگر هم وجود دارند، مثلا:
Razor Views یک سری قابلیت جالب را هم به همراه دارند:
1) امکان کامپایل آنها به درون یک DLL وجود دارد. مزیت: استفاده مجدد از کد، عدم نیاز به وجود صریح فایل cshtml یا vbhtml بر روی دیسک سخت.
2) آزمون پذیری: از آنجائیکه Razor viewها به صورت یک کلاس کامپایل میشوند و همچنین از System.Web.Page مشتق نخواهند شد، امکان بررسی HTML نهایی تولیدی آنهابدون نیاز به راه اندازی یک وب سرور وجود دارد.
3) IntelliSense ویژوال استودیو به خوبی آنرا پوشش میدهد.
4) با توجه به مواردی که ذکر شد، یک اتفاق جالب هم رخ داده است: امکان استفاده از Razor engine خارج از ASP.NET MVC هم وجود دارد. برای مثال یک سرویس ویندوز NT طراحی کردهاید که قرار است ایمیل فرمت شدهای به همراه اطلاعات مدلهای شما را در فواصل زمانی مشخص ارسال کند؟ میتوانید برای طراحی آن از Razor engine استفاده کنید و تهیه خروجی نهایی HTML آن نیازی به راه اندازی وب سرور و وهله سازی HttpContext ندارد.
ساختار پروژه مثال جاری
در ادامه مرور سریعی خواهیم داشت بر دستور زبان Razor engine و جهت نمایش این قابلیتها، یک مثال ساده را در ابتدا با مشخصات زیر ایجاد خواهیم کرد:
الف) یک empty ASP.NET MVC 3 project را ایجاد کنید و نوع View engine را هم در ابتدای کار Razor انتخاب نمائید.
ب) دو کلاس زیر را به پوشه مدلهای برنامه اضافه کنید:
namespace MvcApplication3.Models
{
public class Product
{
public Product(string productNumber, string name, decimal price)
{
Name = name;
Price = price;
ProductNumber = productNumber;
}
public string ProductNumber { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
}
using System.Collections.Generic;
namespace MvcApplication3.Models
{
public class Products : List<Product>
{
public Products()
{
this.Add(new Product("D123", "Super Fast Bike", 1000M));
this.Add(new Product("A356", "Durable Helmet", 123.45M));
this.Add(new Product("M924", "Soft Bike Seat", 34.99M));
}
}
}
کلاس Products صرفا یک منبع داده تشکیل شده در حافظه است. بدیهی است هر نوع ORM ایی که یک ToList را بتواند در اختیار شما قرار دهد، توانایی تشکیل لیست جنریکی از محصولات را نیز خواهد داشت و تفاوتی نمیکند که کدامیک مورد استفاده قرار گیرد.
ج) سپس یک کنترلر جدید به نام ProductsController را به پوشه Controllers برنامه اضافه میکنیم:
using System.Web.Mvc;
using MvcApplication3.Models;
namespace MvcApplication3.Controllers
{
public class ProductsController : Controller
{
public ActionResult Index()
{
var products = new Products();
return View(products);
}
}
}
د) بر روی نام متد Index کلیک راست کرده، گزینه Add view را جهت افزودن View متناظر آن، انتخاب کنید. البته میشود همانند قسمت پنجم گزینه Create a strongly typed view را انتخاب کرد و سپس Product را به عنوان کلاس مدل انتخاب نمود و در آخر خیلی سریع یک لیست از محصولات را نمایش داد، اما فعلا از این قسمت صرفنظر نمائید، چون میخواهیم آن را دستی ایجاد کرده و توضیحات و نکات بیشتری را بررسی کنیم.
ه) برای اینکه حین اجرای برنامه در VS.NET هربار نخواهیم که آدرس کنترلر Products را دستی در مرورگر وارد کنیم، فایل Global.asax.cs را گشوده و سپس در متد RegisterRoutes، در سطر Parameter defaults، مقدار پیش فرض کنترلر را مساوی Products قرار دهید.
مرجع سریع Razor
ابتدا کدهای View متد Index را به شکل زیر وارد نمائید:
@model List<MvcApplication3.Models.Product>
@{
ViewBag.Title = "Index";
var number = 12;
var data = "some text...";
<h2>line1: @data</h2>
@:line-2: @data <br />
<text>line-3:</text> @data
}
<br />
site@(data)
<br />
@@name
<br />
@(number/10)
<br />
First product: @Model.First().Name
<br />
@if (@number>10)
{
<span>@data</span>
}
else
{
<text>Plain Text</text>
}
<br />
@foreach (var item in Model)
{
<li>@item.Name, $@item.Price </li>
}
@*
A Razor Comment
*@
<br />
@("First product: " + Model.First().Name)
<br />
<img src="@(number).jpg" />
در ادامه توضیحات مرتبط با این کدها ارائه خواهد شد:
1) نحوه معرفی یک قطعه کد
@model List<MvcApplication3.Models.Product>
@{
ViewBag.Title = "Index";
var number = 12;
var data = "some text...";
<h2>line1: @data</h2>
@:line-2: @data <br />
<text>line-3:</text> @data
}
این کدها متعلق به Viewایی است که در قسمت (د) بررسی ساختار پروژه مثال جاری، ایجاد کردیم. در ابتدای آن هم نوع model مشخص شده تا بتوان سادهتر به اطلاعات شیء Model به کمک IntelliSense دسترسی داشت.
برای ایجاد یک قطعه کد در Viewایی از نوع Razor به این نحو عمل میشود:
@{ ...Code Block.... }
در اینجا مجاز هستیم کدهای سی شارپ را وارد کنیم. یک نکته جالب را هم باید درنظر داشت: امکان نوشتن تگهای html هم در این بین وجود دارد (بدون اینکه مجبور باشیم قطعه کد شروع شده را خاتمه دهیم، به حالت html معمولی سوئیچ کرده و دوباره یک قطعه کد دیگر را شروع نمائیم). مانند line1 مثال فوق. اگر کمی پایینتر از این سطر مثلا بنویسیم line2 (به عنوان یک برچسب) کامپایلر ایراد خواهد گرفت، زیرا این مورد نه متغیر است و نه از پیش تعریف شده است. به عبارتی نباید فراموش کنیم که اینجا قرار است کد نوشته شود. برای رفع این مشکل دو راه حل وجود دارد که در سطرهای دو و سه ملاحظه میکنید. یا باید از تگی به نام text برای معرفی یک برچسب در این میان استفاده کرد (سطر سه) یا اگر قرار است اطلاعاتی به شکل یک متن معمولی پردازش شود ابتدای آن مانند سطر دوم باید یک @: قرار گیرد.
کمی پایینتر از قطعه کد معرفی شده در بالا بنویسید:
<br />
site@data
اکنون اگر خروجی این View را در مرورگر بررسی کنید، دقیقا همین site@data خواهد بود. چون در این حالت Razor تصور خواهد کرد که قصد داشتهاید یک آدرس ایمیل را وارد کنید. برای این حالت خاص باید نوشت:
<br />
site@(data)
به این ترتیب data از متغیر data تعریف شده در code block قبلی برنامه دریافت و نمایش داده خواهد شد.
شبیه به همین حالت در مثال زیر هم وجود دارد:
<img src="@(number).jpg" />
در اینجا اگر پرانتزها را حذف کنیم، Razor فرض را بر این خواهد گذاشت که شیء number دارای خاصیت jpg است. بنابراین باید به نحو صریحی، بازه کاری را مشخص نمائیم.
بکار گیری این علامت @ یک نکته جنبی دیگر را هم به همراه دارد. فرض کنید در صفحه قصد دارید آدرس توئیتری شخصی را وارد کنید. مثلا:
<br />
@name
در این حالت View کامپایل نخواهد شد و Razor تصور خواهد کرد که قرار است اطلاعات متغیری به نام name را نمایش دهید. برای نمایش این اطلاعات به همین شکل، یک @ دیگر به ابتدای سطر اضافه کنید:
<br />
@@name
2) نحوه معرفی عبارات
عبارات پس از علامت @ معرفی میشوند و به صورت پیش فرض Html Encoded هستند (در قسمت 5 در اینباره بیشتر توضیح داده شد):
First product: @Model.First().Name
در این مثال با توجه به اینکه نوع مدل در ابتدای View مشخص شده است، شیء Model به لیستی از Products اشاره میکند.
یک نکته:
مشخص سازی حد و مرز صریح یک متغیر در مثال زیر نیز کاربرد دارد:
<br />
@number/10
اگر خروجی این مثال را بررسی کنید مساوی 12/10 خواهد بود و محاسبهای انجام نخواهد شد. برای حل این مشکل باز هم از پرانتز میتوان کمک گرفت:
<br />
@(number/10)
@if (@number>10)
{
<span>@data</span>
}
else
{
<text>Plain Text</text>
}
یک عبارت شرطی در اینجا با @if شروع میشود و سپس نکاتی که در «نحوه معرفی یک قطعه کد» بیان شد، در مورد عبارات داخل {} صادق خواهد بود. یعنی در اینجا نیز میتوان عبارات سی شارپ مخلوط با تگهای html را نوشت.
یک نکته: عبارت شرطی زیر نادرست است. حتما باید سطرهای کدهای سی شارپ بین {} محصور شوند؛ حتی اگر یک سطر باشند:
@if( i < 1 ) int myVar=0;
4) نحوه استفاده از حلقه foreach
@foreach (var item in Model)
{
<li>@item.Name, $@item.Price </li>
}
حلقه foreach نیز مانند عبارات شرطی با یک @ شروع شده و داخل {} بدنه آن نکات «نحوه معرفی یک قطعه کد» برقرار هستند (امکان تلفیق code و markup با هم).
کسانی که پیشتر با web forms کار کرده باشند، احتمالا الان خواهند گفت که این یک پس رفت است و بازگشت به دوران ASP کلاسیک دهه نود! ما به ندرت داخل صفحات aspx وب فرمها کد مینوشتیم. مثلا پیشتر یک GridView وجود داشت و یک دیتاسورس که به آن متصل میشد؛ مابقی خودکار بود و ما هیچ وقت حلقهای ننوشتیم. در اینجا هم این مساله با نوشتن برای مثال «html helpers» قابل کنترل است که در قسمتهای بعدی به آن پرداخته خواهد شد. به عبارتی قرار نیست به این نحو با Viewهای Razor رفتار کنیم. این قسمت فقط یک آشنایی کلی با Syntax است.
5) امکان تعریف فضای نام در ابتدای View
@using namespace;
6) نحوه نوشتن توضیحات سمت سرور:
@*
A Razor Comment / Server side Comment
*@
7) نحوه معرفی عبارات چند جزئی:
@("First product: " + Model.First().Name)
همانطور که ملاحظه میکنید، ذکر یک پرانتز برای معرفی عبارات چندجزئی کفایت میکند.
استفاده از موتور Razor خارج از ASP.NET MVC
پیشتر مطلبی را در مورد «تهیه قالب برای ایمیلهای ارسالی یک برنامه ASP.Net» در این سایت مطالعه کردهاید. اولین سؤالی هم که در ذیل آن مطلب مطرح شده این است: «در برنامههای ویندوز چطور؟» پاسخ این است که کل آن مثال بر مبنای HttpContext.Current.Server.Execute کار میکند. یعنی باید مراحل وهله سازی HttpContext و شیء Server توسط یک وب سرور و درخواست رسیده طی شود و ... شبیه سازی آن آنچنان مرسوم و کار سادهای نیست.
اما این مشکل با Razor وجود ندارد. به عبارتی در اینجا برای رندر کردن یک Razor View به html نهایی، نیازی به HttpContext نیست. بنابراین از این امکانات مثلا در یک سرویس ویندوز ان تی یا یک برنامه کنسول، WinForms، WPF و غیره هم میتوان استفاده کرد.
برای اینکه بتوان از Razor خارج از ASP.NET MVC استفاده کرد، نیاز به اندکی کدنویسی هست مثلا استفاده از کامپایلر سی شارپ یا وی بی و کامپایل پویای کد و یک سری ست آپ دیگر. پروژهای به نام RazorEngine این کپسوله سازی رو انجام داده و از اینجا http://razorengine.codeplex.com/ قابل دریافت است.
سلام؛ در متد CanUserAccess کلاس SecurityTrimmingService:
ابتدا بررسی میشه که کاربر دارای دسترسی مورد نظر میباشد و سپس چک میشه که کاربر احراز هویت شده و دارای دسترسی admin میباشد.
public bool CanUserAccess(ClaimsPrincipal user, string area, string controller, string action) { var currentClaimValue = $"{area}:{controller}:{action}"; var securedControllerActions = _mvcActionsDiscoveryService.GetAllSecuredControllerActionsWithPolicy(ConstantPolicies.DynamicPermission); if (!securedControllerActions.SelectMany(x => x.MvcActions).Any(x => x.ActionId == currentClaimValue)) { throw new KeyNotFoundException($@"The `secured` area={area}/controller={controller}/action={action} with `ConstantPolicies.DynamicPermission` policy not found. Please check you have entered the area/controller/action names correctly and also it's decorated with the correct security policy."); } if (!user.Identity.IsAuthenticated) { return false; } if (user.IsInRole(ConstantRoles.Admin)) { // Admin users have access to all of the pages. return true; } // Check for dynamic permissions // A user gets its permissions claims from the `ApplicationClaimsPrincipalFactory` class automatically and it includes the role claims too. return user.HasClaim(claim => claim.Type == ConstantPolicies.DynamicPermissionClaimType && claim.Value == currentClaimValue); }
آیا کاربری که دارای دسترسی Admin میباشد نیازی به چک کردن HasClaim دارد؟
مثلا اگر این متد به این صورت نوشته شود مشکلی پیش میاد؟
public bool CanUserAccess(ClaimsPrincipal user, string area, string controller, string action) { if (!user.Identity.IsAuthenticated) { return false; } if (user.IsInRole(ConstantRoles.Admin)) { // Admin users have access to all of the pages. return true; } var currentClaimValue = $"{area}:{controller}:{action}"; var securedControllerActions = _mvcActionsDiscoveryService.GetAllSecuredControllerActionsWithPolicy(ConstantPolicies.DynamicPermission); if (!securedControllerActions.SelectMany(x => x.MvcActions).Any(x => x.ActionId == currentClaimValue)) { throw new KeyNotFoundException($@"The `secured` area={area}/controller={controller}/action={action} with `ConstantPolicies.DynamicPermission` policy not found. Please check you have entered the area/controller/action names correctly and also it's decorated with the correct security policy."); } // Check for dynamic permissions // A user gets its permissions claims from the `ApplicationClaimsPrincipalFactory` class automatically and it includes the role claims too. return user.HasClaim(claim => claim.Type == ConstantPolicies.DynamicPermissionClaimType && claim.Value == currentClaimValue); }