قابلیت جالبی از SQL Server 2005 به بعد به این محصول اضافه شده است که امکان ایجاد یک وب سرویس بومی را بر اساس رویههای ذخیره شده و یا توابع تعریف شده در دیتابیسهای موجود، فراهم میسازد. این قابلیت نیازی به IIS یا هر هاست دیگری برای اجرا ندارد و توسط خود اس کیوال سرور راه اندازی و مدیریت میشود.
توضیحات مفصل آنرا در MSDN میتوانید ملاحظه کنید و در اینجا یک مثال عملی از آن را با هم مرور خواهیم کرد:
الف) ایجاد یک جدول آزمایشی به همراه تعدادی رکورد دلخواه در آن
CREATE TABLE [tblWSTest](
[id] [int] IDENTITY(1,1) NOT NULL,
[f1] [nvarchar](50) NULL,
[f2] [nvarchar](500) NULL,
CONSTRAINT [PK_tblWSTest] PRIMARY KEY CLUSTERED
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
SET IDENTITY_INSERT [tblWSTest] ON
INSERT [tblWSTest] ([id], [f1], [f2]) VALUES (1, N'a1', N'a2')
INSERT [tblWSTest] ([id], [f1], [f2]) VALUES (2, N'b1', N'b2')
INSERT [tblWSTest] ([id], [f1], [f2]) VALUES (3, N'c1', N'c2')
INSERT [tblWSTest] ([id], [f1], [f2]) VALUES (4, N'd1', N'd2')
INSERT [tblWSTest] ([id], [f1], [f2]) VALUES (5, N'e1', N'e2')
SET IDENTITY_INSERT [dbo].[tblWSTest] OFF
CREATE PROCEDURE GetAllData
AS
SELECT f1,
f2
FROM tblWSTest
CREATE ENDPOINT GetDataService
STATE = STARTED
AS HTTP(
PATH = '/GetData',
AUTHENTICATION = (INTEGRATED),
PORTS = (CLEAR),
CLEAR_PORT = 8080,
SITE = '*'
)
FOR SOAP(
WEBMETHOD 'GetAllData'
(NAME = 'testdb2009.dbo.GetAllData'),
WSDL = DEFAULT,
DATABASE = 'testdb2009',
NAMESPACE = DEFAULT
)
توضیحات:
Ports در حالت clear و یا ssl میتواند باشد. همچنین برای اینکه با IIS موجود بر روی سیستم هم تداخل نکند CLEAR_PORT به 8080 تنظیم شده است. سایر پارامترهای آن بسیار واضح هستند. برای مثال تعیین دیتابیسی که این رویه ذخیره شده در آن قرار دارد و همچنین مسیر کامل دسترسی به آن دقیقا مشخص میگردند.
این وب سرویس هم اکنون آغاز به کار کرده است. برای مشاهده wsdl آن، آدرس زیر را در مرورگر وب خود وارد نمائید (PATH و CLEAR_PORT معرفی شده در endPoint اینجا بکار میرود):
د) استفاده از این وب سرویس در یک برنامه ویندوزی
یک برنامه ساده winForms را شروع کنید. سپس یک DataGridView را بر روی فرم قرار دهید (بدیهی است این مورد میتواند یک برنامه ASP.Net هم باشد و موارد مشابه دیگر). سپس از منوی پروژه، یک service reference را در VS2008 بر اساس آدرس wdsl فوق اضافه کنید (شکل زیر):
برای اینکه این مثال در VS2008 درست کار کند باید فایل app.config ایجاد شده را کمی ویرایش کرد. قسمت security آن را یافته و تغییرات زیر را با توجه به AUTHENTICATION مورد نیاز تغییر دهید:
<security mode="TransportCredentialOnly">
<transport clientCredentialType="Windows" proxyCredentialType="None"
realm="" />
<message clientCredentialType="UserName" algorithmSuite="Default" />
</security>
using System;
using System.Data;
using System.Windows.Forms;
namespace WebServiceTest
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
ServiceReference1.GetDataServiceSoapClient data =
new ServiceReference1.GetDataServiceSoapClient();
dataGridView1.DataSource = (data.GetAllData()[0] as DataSet).Tables[0];
}
}
}
زمانیکه اولین نگارش ASP.NET حدود 10 سال قبل منتشر شد، تنها سیستم عاملی که از آن پشتیبانی میکرد، ویندوز سرور 2000 بود، تنها پروسهی اجرایی آن aspnet_wp نام داشت و تنها معماری پشتیبانی شده هم X86 بود. به پروسهی aspnet_wp محدودیت مصرف حافظهای اعمال شده بود که در حین آغاز آن بر اساس مقدار قابل تغییر processModel memoryLimit محاسبه و اعمال میشد (تعریف شده در فایل ماشین کانفیگ). این عدد به صورت درصدی از ظرفیت RAM فیزیکی سیستم، قابل تعریف و به صورت پیش فرض به 60 درصد تنظیم شده بود. به این ترتیب این پروسه مجاز نبود تا تمام حافظهی فیزیکی مهیا را مصرف کند و در صورت وجود نشتی حافظهای در برنامهای خاص، این پروسه امکان بازیابی مجدد حافظه را پیدا میکرد (recycling). همچنین یک مورد دیگر را هم باید در نظر داشت و آن هم وجود قابلیتی است به نام ASP.NET Cache است که امکان ذخیره سازی مقادیر اشیاء را در حافظهی مصرفی این پروسه مهیا میسازد. هر زمان که میزان این حافظهی مصرفی به حد نزدیکی از محدودیت تعریف شده برسد، این پروسه به صورت خودکار شروع به حذف آنها خواهد کرد.
محدودیت 60 درصدی تعریف شده، برای سیستمهایی با میزان RAM کم بسیار مفید بود اما در سیستمهایی با میزان RAM بیشتر، مثلا 4 گیگ به 2.4GB حافظه مهیا (60 درصد حافظه فیزیکی سیستم) محدود میشد و همچنین باید در نظر داشت که میزان user mode virtual address space مهیا نیز تنها 2 گیگابایت بود. بنابراین هیچگاه استفاده مؤثری از تمام ظرفیت RAM مهیا صورت نمیگرفت و گاها مشاهده میشد که یک برنامه تنها با مصرف 1.5GB RAM میتوانست پیغام OutOfMemoryException را صادر کند. در این حالت مطابق بررسیهای صورت گرفته مشخص شد که اگر مقدار processModel memoryLimit به حدود 800 مگابایت تنظیم شود، بهترین عملکرد را برای سیستمهای مختلف میتوان مشاهده کرد.
با ارائهی ویندوز سرور 2003 و همچنین ارائهی نسخهی 1.1 دات نت فریم ورک و ASP.NET ، این وضعیت تغییر کرد. پروسهی جدید در اینجا w3wp نام دارد و این پروسه تعاریف مرتبط با محدودیت حافظهی خود را از تنظیمات IIS دریافت میکند (قسمت Maximum Used Memory در برگهی Recycling مربوط به خواص Application Pool مرتبط). متاسفانه این عدد به صورت پیش فرض محدودیتی ندارد و به ظاهر برنامه مجاز است تا حد امکان از حافظهی مهیا استفاده کند. به همین جهت یکی از مواردی را که باید در نظر داشت، مقدار دهی Maximum Used Memory ذکر شده است. خصوصا اینکه در نگارش 1.1 ، تنظیمات میزان مصرف RAM مرتبط با ASP.NET Cache نیز با برنامه یکی است.
در نگارش 2.0 دات نت فریم ورک، تنظیمات مرتبط با ASP.NET cache از تنظیمات میزان RAM مصرفی یک برنامهی ASP.NET جدا شد و این مورد توسط قسمت cache privateBytesLimit قابل تنظیم و مدیریت است (در فایل IIS Metabase و همچنین فایل web.config برنامه).
نکته!
اگر process memory limit و همچنین cache memory limit را تنظیم نکنید، باز به همان عدد 60 درصد سابق بازخواهیم گشت و این مورد به صورت خودکار توسط IIS محاسبه و اعمال میشود. البته محدودیت ذکر شده برای پروسههای 64 بیتی در این حالت بسیار بهتر خواهد بود. اگر هر دوی اینها را تنظیم کنید، عدد حداقل بکارگرفته شده، مبنای کار خواهد بود و اگر تنها یکی را تنظیم کنید ، این عدد به هر دو حالت اعمال میگردد. برای بررسی بهتر میتوان به مقدار Cache.EffectivePrivateBytesLimit و Cache.EffectivePercentagePhysicalMemoryLimit مراجعه کرد.
و ... اکنون بهتر میتوانید به این سؤال پاسخ دهید که «سرور ما بیشتر از 4 گیگ رم دارد و برنامهی ASP.NET من الان فقط 850 مگ رم مصرف کرده (که البته این هم نشانی از عدم dispose صحیح منابع است یا عدم تعیین تقدم و تاخر و زمان منقضی شدن، حین تعریف اشیاء کش)، اما پیغام out of memory exception را دریافت میکنم. چرا؟!»
بنابراین ایجاد یک Application pool جدید به ازای هر برنامهی ASP.NET امری است بسیار مهم زیرا:
- به این ترتیب هر برنامهی ASP.NET در پروسهای ایزوله از پروسهی دیگر اجرا خواهد شد (این مساله از لحاظ امنیتی هم بسیار مهم است). در اینجا هر برنامه، از پروسهی w3wp.exe مجزای خاص خود استفاده خواهد کرد (شبیه به مرورگرهایی که هر tab را در یک پروسه جدید اجرا میکنند).
- اگر پروسهای به حد بالای مصرف حافظهی خود رسید با تنظیمات انجام شده در قسمت recycling مرتبط با Application pool اختصاصی آن، به صورت خودکار کار بازیابی حافظه صورت میگیرد و این امر بر روی سایر برنامهها تاثیر نخواهد داشت (کاربران سایر برنامهها مدام شکایت نمیکنند که سشنها پرید. کش خالی شد. زیرا در حالت وجود application pool اختصاصی به ازای هر برنامه، مدیریت حافظه برنامهها از هم ایزوله خواهند بود)
- کرش صورت گرفته در یک برنامه به دلیل عدم مدیریت خطاها، بر روی سایر برنامهها تاثیر منفی نخواهد گذاشت. (زمانیکه ASP.NET worker process به دلیل استثنایی مدیریت نشده خاتمه یابد بلافاصله و به صورت خودکار مجددا «وهلهی دیگری» از آن شروع به کار خواهد کرد؛ یعنی تمام سشنهای قبلی از بین خواهند رفت؛ که در صورت ایزوله سازی ذکر شده، سایر برنامهها در امان خواهند ماند؛ چون در پروسه ایزولهی خود مشغول به کار هستند)
- با وجود application pool اختصاصی به ازای هر برنامه، میتوان برای سایتهای کم ترافیک و پرترافیک، زمانهای recycling متفاوتی را اعمال کرد. به این ترتیب مدیریت حافظهی بهتری قابل پیاده سازی میباشد. همچنین در این حالت میتوان مشخص کرد کدام سایت از تعداد worker process بیشتر یا کمتری استفاده کند.
- کاربری که پروسهی ASP.NET تحت آن اجرا میشود نیز همینجا تعریف میگردد. بنابراین به این ترتیب میتوان به برنامهای دسترسی بیشتر و یا کمتر داد، بدون تاثیر گذاری بر روی سایر برنامههای موجود.
نتیجه گیری:
- از IIS استفاده میکنید؟ آیا میدانید Application pool چیست؟
- آیا میدانید در صورت عدم مقدار دهی پارامترهای حافظهی یک Application pool ، به صورت پیش فرض چند درصد از حافظهی فیزیکی مهیا در اختیار شما است؟
برای مطالعه بیشتر:
var browser = Request.Browser.Browser + " " + Request.Browser.Version;
برای مثال با فایرفاکس، چنین خروجی را دارد:
اما ... با مرورگر جدید Edge مایکروسافت، خروجی کروم را مشاهده خواهیم کرد:
از این جهت که user agent این مرورگر، چنین شکلی را دارد و ختم به edge است:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240
برای رفع این مشکل، نیاز است فایل جدیدی را به مجموعهی «browser definition files» دات نت اضافه کنیم. این فایلها عموما در مسیر زیر یافت میشوند:
<windir>\Microsoft.NET\Framework\<ver>\CONFIG\Browsers
C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config\Browsers
با این محتویات:
<browsers> <browser id="Edge" parentID="Chrome"> <identification> <userAgent match="Edge/(?'version'(?'major'\d+)(?'minor'\.\d+))" /> </identification> <capabilities> <capability name="browser" value="Edge" /> <capability name="version" value="${version}" /> <capability name="majorversion" value="${major}" /> <capability name="minorversion" value="${minor}" /> </capabilities> </browser> </browsers>
اکنون پس از این تنظیمات، برنامه (تفاوتی نمیکند که وب فرم باشد یا MVC) اطلاعات صحیحی را نمایش میدهد:
MVC Scaffolding #3
آشنایی با ساختار اصلی MVC Scaffolding
پس از نصب MVC Scaffolding از طریق NuGet به پوشه Packages مراجعه نمائید. در اینجا پوشههای MvcScaffolding، T4Scaffolding و T4Scaffolding.Core ساختار اصلی این بسته را تشکیل میدهند. برای نمونه اگر پوشه T4Scaffolding\tools را باز کنیم، شاهد تعدادی فایل ps1 خواهیم بود که همان فایلهای پاورشل هستند. مطابق طراحی NuGet، همواره فایلی با نام init.ps1 در ابتدا اجرا خواهد شد. همچنین در اینجا پوشههای T4Scaffolding\tools\EFRepository و T4Scaffolding\tools\EFDbContext نیز قرار دارند که حاوی قالبهای اولیه کدهای مرتبط با الگوی مخزن و DbContext تولیدی میباشند.
در پوشه MvcScaffolding\tools، ساختار قالبهای پیش فرض تولید Viewها و کنترلرهای تولیدی قرار دارند. در اینجا به ازای هر مورد، دو نگارش vb و cs قابل مشاهده است.
سفارشی سازی قالبهای پیش فرض Viewهای MVC Scaffolding
برای سفارشی سازی قالبهای پیش فرض از دستور کلی زیر استفاده میشود:
Scaffold CustomTemplate Name Template
Scaffold CustomTemplate View Index
اگر دستور فوق را اجرا کنیم، فایل جدیدی به نام CodeTemplates\Scaffolders\MvcScaffolding.RazorView\Index.cs.t4 به پروژه جاری اضافه میشود. از این پس کلیه فرامین اجرایی، از نسخه محلی فوق بجای نمونههای پیش فرض استفاده خواهند کرد.
در ادامه قصد داریم اندکی این قالب پیش فرض را جهت اعمال ویژگی DisplayName به هدر جدول تولیدی نمایش اطلاعات Tasks تغییر دهیم. در کلاس Task، خاصیت زمان موعود با ویژگی DisplayName مزین شده است. این نام نمایشی حین تولید فرمهای ثبت و ویرایش اطلاعات بکار گرفته میشود، اما در زمان تولید جدول اطلاعات ثبت شده، به هدر جدول اعمال نمیگردد.
[DisplayName("Due Date")] public DateTime? DueDate { set; get; }
// Describes the information about a property on the model class ModelProperty { public string Name { get; set; } public string DisplayName { get; set; } public string ValueExpression { get; set; } public EnvDTE.CodeTypeRef Type { get; set; } public bool IsPrimaryKey { get; set; } public bool IsForeignKey { get; set; } public bool IsReadOnly { get; set; } }
static string GetDisplayName(EnvDTE.CodeProperty prop) { var displayAttr = prop.Attributes.OfType<EnvDTE80.CodeAttribute2>().Where(x => x.FullName == typeof(System.ComponentModel.DisplayNameAttribute).FullName).FirstOrDefault(); if(displayAttr == null) { return prop.Name; } return displayAttr.Value.Replace("\"",""); }
اکنون برای اعمال متد GetDisplayName، متد GetEligibleProperties را یافته و به نحو زیر تغییر دهید:
results.Add(new ModelProperty { Name = prop.Name, DisplayName = GetDisplayName(prop), ValueExpression = "Model." + prop.Name, Type = prop.Type, IsPrimaryKey = Model.PrimaryKeyName == prop.Name, IsForeignKey = ParentRelations.Any(x => x.RelationProperty == prop), IsReadOnly = !prop.IsWriteable() });
اکنون قسمت هدر جدول تولیدی را در ابتدای فایل t4 یافته و به نحو زیر تغییر میدهیم تا از DisplayName استفاده کند:
<# List<ModelProperty> properties = GetModelProperties(Model.ViewDataType, true); foreach (ModelProperty property in properties) { if (!property.IsPrimaryKey && !property.IsForeignKey) { #> <th> <#= property.DisplayName #> </th> <# } } #>
PM> Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository -Force
سفارشی سازی قالبهای پیش فرض کنترلرهای MVC Scaffolding
در ادامه قصد داریم کدهای الگوی مخزن تهیه شده را اندکی تغییر دهیم. برای مثال با توجه به اینکه از تزریق وابستگیها استفاده خواهیم کرد، نیازی به سازنده اولیه پیش فرض کنترلر که در بالای آن ذکر شده «در صورت استفاده از یک DI این مورد را حذف کنید»، نداریم. برای این منظور دستور زیر را اجرا کنید:
PM> Scaffold CustomTemplate Controller ControllerWithRepository
به این ترتیب فایل جدید CodeTemplates\Scaffolders\MvcScaffolding.Controller\ControllerWithRepository.cs.t4 به پروژه جاری اضافه خواهد شد. در این فایل چند سطر ذیل را یافته و سپس حذف کنید:
// If you are using Dependency Injection, you can delete the following constructor public <#= Model.ControllerName #>() : this(<#= String.Join(", ", Repositories.Values.Select(x => "new " + x.RepositoryTypeName + "()")) #>) { }
PM> Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository -Force -ForceMode ControllerOnly
یا اگر نیاز به تغییر کدهای الگوی مخزن مورد استفاده است میتوان از دستور ذیل استفاده کرد:
Scaffold CustomScaffolder EFRepository
لیست Scaffolderهای مهیا با دستور Get-Scaffolder قابل مشاهده است.
private static bool _isPreGeneratedViewCacheSet; private void InitializationPreGeneratedViews() { if (_isPreGeneratedViewCacheSet) return; var precompiledViewsFilePath = new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName + @”\EF6PrecompiledViews.xml”; InteractiveViews.SetViewCacheFactory(this, new FileViewCacheFactory(precompiledViewsFilePath)); _isPreGeneratedViewCacheSet = true; }
InteractiveViews .SetViewCacheFactory(ctx, new FileViewCacheFactory(new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName + @"\EF6PrecompiledViews.xml"));
public class XamAppExceptionHandler : BitExceptionHandler { public override void OnExceptionReceived(Exception exp, IDictionary<string, string> properties = null) { #if DEBUG System.Diagnostics.Debugger.Break(); #endif base.OnExceptionReceived(exp, properties); } }
BitExceptionHandler.Current = new XamAppExceptionHandler();
حال برای لاگ کردن این خطاها، میتوانید از Microsoft's AppCenter استفاده کنید. استفاده از امکانات App Center رایگان بوده و برای استفادهی در ایران محدودیتی ندارد. ابتدا در سایت مربوطه ثبت نام کنید و سپس سه بار Add New app را بزنید و به نامهای XamApp_Windows، XamApp_iOS و XamApp_Android سه برنامه را بسازید و برای Android و iOS گزینهی Xamarin را انتخاب و برای ویندوز نیز UWP را انتخاب کنید.
سپس پکیجهای Microsoft.AppCenter.Crashes و Microsoft.AppCenter.Analytics را بر روی XamApp نصب نموده و کد زیر را در سه فایل AppDelegate.cs/MainActivity.cs/App.xaml.cs برای iOS/Android/Windows کپی کنید:
AppCenter.Start("copy-your-guid-key-for-iOS-Android-Windows-here", typeof(Crashes), typeof(Analytics));
برای هر یک از برنامههای Android/iOS/Windows یک Guid متفاوت دارید که در قسمت Getting Started در سایت App Center میتوانید آنها را مشاهده کنید. هر بار که این کد را کپی میکنید، مقدار Guid درست را بگذارید.
برای گزارش کردن خطاهای برنامه، کافی است کد زیر را به XamAppExceptionHandler.cs که در ابتدای این قسمت در موردش صحبت کرده بودیم اضافه کنید:
Crashes.TrackError(exp, properties);
حال اگر برنامه را اجرا و شروع به تست کنید و یا آن را در اختیار تسترها و مشتریها بگذارید، نه تنها گزارش تمامی خطاها و کرشها را خواهید داشت که حتی آمار استفاده کنندههای برنامه (شامل کشور و مشخصات دستگاه و ...) را نیز خواهید داشت.
حال در یک کد ده خطی، اگر در خط پنجم خطایی رخ دهد، اگر چه باعث بسته شدن برنامه نمیشود و لاگ نیز میشود، ولی در مواقعی خیلی خاص، شاید بخواهید در صورت رخ دادن خطا، چند خط کد بعدی کماکان اجرا شوند. در این حالت شما Try/Catch مینویسید که برای عبور کردن از خطا از آن استفاده کردهاید. در این صورت، ترجیحا آن را به شکل زیر بنویسید:
// code 1... try { // code 2... } catch (Exception ex) { BitExceptionHandler.Current.OnExceptionReceived(ex, new Dictionary<string, string> { { "SomeData", "2" } }); } // code 3...
در این کد مثال، فرض کنیم که برخی اوقات در code 2 خطایی رخ میدهد که برای ما مهم نیست و میخواهیم حتی در صورت رخ دادن خطا، code 3 اجرا شود. توصیه میکنیم در این موارد که در برنامه خیلی هم نباید متداول باشد، لااقل خطا را با کمک کد BitExceptionHandler.Current.OnExceptionReceived لاگ کنید و همچنین با داشتن یک Dictionary میتوانید حتی دیتای بیشتری را نیز به AppCenter فرستاده و در پرتال مربوطه مشاهده کنید.
به صورت کلی بهتر است از این نوع Try/Catchها پرهیز کنید و حتی اگر جایی Catch ای نوشتید، در نهایت دوباره خطا را throw کنید.
try { // some codes... } catch { // Do something related to exception... // for example, show some alerts to the user. throw; // You don't need to call BitExceptionHandler.Current.OnExceptionReceived... }
در مثال فوق، قصد داریم وقتی خطایی رخ داد، پیامی را به کاربر اطلاع دهیم. در این صورت، پس از نمایش پیام مربوطه، مجددا خطا را throw کنید. در این صورت، نیازی به فراخوانی BitExceptionHandler.Current.OnExceptionReceieved نیز نیست.
البته AppCenter در زمینه پابلیش کردن برنامه و همچنین Push Notification و ... نیز دارای امکاناتی هست که به موضوع این قسمت ارتباطی ندارند.
GUID یا Globally unique identifier یک عدد صحیح 128 بیتی است (بنابراین 2 به توان 128 حالت را میتوان برای آن درنظر گرفت). از لحاظ آماری تولید دو GUID یکسان تقریبا صفر میباشد. به همین جهت از آن با اطمینان میتوان به عنوان یک شناسه منحصربفرد استفاده کرد. برای مثال اگر به لینکهای دانلود فایلها از سایت مایکروسافت دقت کنید، این نوع GUID ها را به وفور میتوانید ملاحظه نمائید. یا زمانیکه قرار است فایلی را که بر روی سرور آپلود شده، ذخیره نمائیم، میتوان نام آنرا یک GUID درنظر گرفت بدون اینکه نگران باشیم آیا فایل آپلود شده بر روی یکی از فایلهای موجود overwrite میشود یا خیر. یا مثلا استفاده از آن در سناریوی بازیابی کلمه عبور در یک سایت. هنگامیکه کاربری درخواست بازیابی کلمه عبور فراموش شده خود را داد، یک GUID برای آن تولید کرده و به او ایمیل میزنیم و در آخر آنرا در کوئری استرینگی دریافت کرده و با مقدار موجود در دیتابیس مقایسه میکنیم. مطمئن هستیم که این عبارت قابل حدس زدن نیست و همچنین یکتا است.
برای تولید GUID ها در دات نت میتوان مانند مثال زیر عمل کرد و خروجیهای دلخواهی را با فرمتهای مختلفی دریافت کرد:
System.Guid.NewGuid().ToString() = 81276701-9dd7-42e9-b128-81c762a172ff
System.Guid.NewGuid().ToString("N") = 489ecfc61ee7403988efe8546806c6a2
System.Guid.NewGuid().ToString("D") = 119201d9-84d9-4126-b93f-be6576eedbfd
System.Guid.NewGuid().ToString("B") = {fd508d4b-cbaf-4f1c-894c-810169b1d20c}
System.Guid.NewGuid().ToString("P") = (eee1fe00-7e63-4632-a290-516bfc457f42)
دو روش برای انجام اینکار وجود دارد
الف) عبارت دریافت شده را به new Guid پاس کنیم. اگر ورودی غیرمعتبر باشد، یک exception تولید خواهد شد.
ب) استفاده از regular expressions جهت بررسی الگوی عبارت وارد شده
پیاده سازی این دو را در کلاس زیر میتوان ملاحظه نمود:
using System;
using System.Text.RegularExpressions;
namespace sample
{
/// <summary>
/// بررسی اعتبار یک گوئید
/// </summary>
public static class CValidGUID
{
/// <summary>
/// بررسی تعیین اعتبار ورودی
/// </summary>
/// <param name="guidString">ورودی</param>
/// <returns></returns>
public static bool IsGuid(this string guidString)
{
if (string.IsNullOrEmpty(guidString)) return false;
bool bResult;
try
{
Guid g = new Guid(guidString);
bResult = true;
}
catch
{
bResult = false;
}
return bResult;
}
/// <summary>
/// بررسی تعیین اعتبار ورودی
/// </summary>
/// <param name="input">ورودی</param>
/// <returns></returns>
public static bool IsValidGUID(this string input)
{
return !string.IsNullOrEmpty(input) &&
new Regex(@"^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$").IsMatch(input);
}
}
}
using NUnit.Framework;
using sample;
namespace TestLibrary
{
[TestFixture]
public class TestCValidGUID
{
/*******************************************************************************/
[Test]
public void TestIsGuid1()
{
Assert.IsTrue("81276701-9dd7-42e9-b128-81c762a172ff".IsGuid());
}
[Test]
public void TestIsGuid2()
{
Assert.IsTrue("489ecfc61ee7403988efe8546806c6a2".IsGuid());
}
[Test]
public void TestIsGuid3()
{
Assert.IsTrue("{fd508d4b-cbaf-4f1c-894c-810169b1d20c}".IsGuid());
}
[Test]
public void TestIsGuid4()
{
Assert.IsTrue("(eee1fe00-7e63-4632-a290-516bfc457f42)".IsGuid());
}
[Test]
public void TestIsGuid5()
{
Assert.IsFalse("81276701;9dd7;42e9-b128-81c762a172ff".IsGuid());
}
/*******************************************************************************/
[Test]
public void TestIsValidGUID1()
{
Assert.IsTrue("81276701-9dd7-42e9-b128-81c762a172ff".IsValidGUID());
}
[Test]
public void TestIsValidGUID2()
{
Assert.IsTrue("489ecfc61ee7403988efe8546806c6a2".IsValidGUID());
}
[Test]
public void TestIsValidGUID3()
{
Assert.IsTrue("{fd508d4b-cbaf-4f1c-894c-810169b1d20c}".IsValidGUID());
}
[Test]
public void TestIsValidGUID4()
{
Assert.IsTrue("(eee1fe00-7e63-4632-a290-516bfc457f42)".IsValidGUID());
}
[Test]
public void TestIsValidGUID5()
{
Assert.IsFalse("81276701;9dd7;42e9-b128-81c762a172ff".IsValidGUID());
}
}
}
همانطور که ملاحظه میکنید حالت دوم یعنی استفاده از عبارات باقاعده دو حالت را نمیتواند بررسی کند (مطابق الگوی بکار گرفته شده که البته قابل اصلاح است)، اما روش معمولی استفاده از new Guid ، تمام فرمتهای تولید شده توسط دات نت را پوشش میدهد.
پیاده سازیهای زیادی را در مورد JSON Web Token با ASP.NET Web API، با کمی جستجو میتوانید پیدا کنید. اما مشکلی که تمام آنها دارند، شامل این موارد هستند:
- چون توکنهای JWT، خودشمول هستند (در پیشنیاز بحث مطرح شدهاست)، تا زمانیکه این توکن منقضی نشود، کاربر با همان سطح دسترسی قبلی میتواند به سیستم، بدون هیچگونه مانعی لاگین کند. در این حالت اگر این کاربر غیرفعال شود، کلمهی عبور او تغییر کند و یا سطح دسترسیهای او کاهش یابند ... مهم نیست! باز هم میتواند با همان توکن قبلی لاگین کند.
- در روش JSON Web Token، عملیات Logout سمت سرور بیمعنا است. یعنی اگر برنامهای در سمت کاربر، قسمت logout را تدارک دیده باشد، چون در سمت سرور این توکنها جایی ذخیره نمیشوند، عملا این logout بیمفهوم است و مجددا میتوان از همان توکن قبلی، برای لاگین به سرور استفاده کرد. چون این توکن شامل تمام اطلاعات لازم برای لاگین است و همچنین جایی هم در سرور ثبت نشدهاست که این توکن در اثر logout، باید غیرمعتبر شود.
- با یک توکن از مکانهای مختلفی میتوان دسترسی لازم را جهت استفادهی از قسمتهای محافظت شدهی برنامه یافت (در صورت دسترسی، چندین نفر میتوانند از آن استفاده کنند).
به همین جهت راه حلی عمومی برای ذخیره سازی توکنهای صادر شده از سمت سرور، در بانک اطلاعاتی تدارک دیده شد که در ادامه به بررسی آن خواهیم پرداخت و این روشی است که میتواند به عنوان پایه مباحث Authentication و Authorization برنامههای تک صفحهای وب استفاده شود. البته سمت کلاینت این راه حل با jQuery پیاده سازی شدهاست (عمومی است؛ برای طرح مفاهیم پایه) و سمت سرور آن به عمد از هیچ نوع بانک اطلاعات و یا ORM خاصی استفاده نمیکند. سرویسهای آن برای بکارگیری انواع و اقسام روشهای ذخیره سازی اطلاعات قابل تغییر هستند و الزامی نیست که حتما از EF استفاده کنید یا از ASP.NET Identity یا هر روش خاص دیگری.
نگاهی به برنامه
در اینجا تمام قابلیتهای این پروژه را مشاهده میکنید.
- امکان لاگین
- امکان دسترسی به یک کنترلر مزین شدهی با فلیتر Authorize
- امکان دسترسی به یک کنترلر مزین شدهی با فلیتر Authorize جهت کاربری با نقش Admin
- پیاده سازی مفهوم ویژهای به نام refresh token که نیاز به لاگین مجدد را پس از منقضی شدن زمان توکن اولیهی لاگین، برطرف میکند.
- پیاده سازی logout
بستههای پیشنیاز برنامه
پروژهای که در اینجا بررسی شدهاست، یک پروژهی خالی ASP.NET Web API 2.x است و برای شروع به کار با JSON Web Tokenها، تنها نیاز به نصب 4 بستهی زیر را دارد:
PM> Install-Package Microsoft.Owin.Host.SystemWeb PM> Install-Package Microsoft.Owin.Security.Jwt PM> Install-Package structuremap PM> Install-Package structuremap.web
از structuremap هم برای تنظیمات تزریق وابستگیهای برنامه استفاده شدهاست. به این صورت قسمت تنظیمات اولیهی JWT ثابت باقی خواهد ماند و صرفا نیاز خواهید داشت تا کمی قسمت سرویسهای برنامه را بر اساس بانک اطلاعاتی و روش ذخیره سازی خودتان سفارشی سازی کنید.
دریافت کدهای کامل برنامه
کدهای کامل این برنامه را از اینجا میتوانید دریافت کنید. در ادامه صرفا قسمتهای مهم این کدها را بررسی خواهیم کرد.
بررسی کلاس AppJwtConfiguration
کلاس AppJwtConfiguration، جهت نظم بخشیدن به تعاریف ابتدایی توکنهای برنامه در فایل web.config، ایجاد شدهاست. اگر به فایل web.config برنامه مراجعه کنید، یک چنین تعریفی را مشاهده خواهید کرد:
<appJwtConfiguration tokenPath="/login" expirationMinutes="2" refreshTokenExpirationMinutes="60" jwtKey="This is my shared key, not so secret, secret!" jwtIssuer="http://localhost/" jwtAudience="Any" />
<configSections> <section name="appJwtConfiguration" type="JwtWithWebAPI.JsonWebTokenConfig.AppJwtConfiguration" /> </configSections>
- در این تنظیمات، دو زمان منقضی شدن را مشاهده میکنید؛ یکی مرتبط است به access tokenها و دیگری مرتبط است به refresh tokenها که در مورد اینها، در ادامه بیشتر توضیح داده خواهد شد.
- jwtKey، یک کلید قوی است که از آن برای امضاء کردن توکنها در سمت سرور استفاده میشود.
- تنظیمات Issuer و Audience هم در اینجا اختیاری هستند.
یک نکته
جهت سهولت کار تزریق وابستگیها، برای کلاس AppJwtConfiguration، اینترفیس IAppJwtConfiguration نیز تدارک دیده شدهاست و در تمام تنظیمات ابتدایی JWT، از این اینترفیس بجای استفادهی مستقیم از کلاس AppJwtConfiguration استفاده شدهاست.
بررسی کلاس OwinStartup
شروع به کار تنظیمات JWT و ورود آنها به چرخهی حیات Owin از کلاس OwinStartup آغاز میشود. در اینجا علت استفادهی از SmObjectFactory.Container.GetInstance انجام تزریق وابستگیهای لازم جهت کار با دو کلاس AppOAuthOptions و AppJwtOptions است.
- در کلاس AppOAuthOptions تنظیماتی مانند نحوهی تهیهی access token و همچنین refresh token ذکر میشوند.
- در کلاس AppJwtOptions تنظیمات فایل وب کانفیگ، مانند کلید مورد استفادهی جهت امضای توکنهای صادر شده، ذکر میشوند.
حداقلهای بانک اطلاعاتی مورد نیاز جهت ذخیره سازی وضعیت کاربران و توکنهای آنها
همانطور که در ابتدای بحث عنوان شد، میخواهیم اگر سطوح دسترسی کاربر تغییر کرد و یا اگر کاربر logout کرد، توکن فعلی او صرفنظر از زمان انقضای آن، بلافاصله غیرقابل استفاده شود. به همین جهت نیاز است حداقل دو جدول زیر را در بانک اطلاعاتی تدارک ببینید:
الف) کلاس User
در کلاس User، بر مبنای اطلاعات خاصیت Roles آن است که ویژگی Authorize با ذکر نقش مثلا Admin کار میکند. بنابراین حداقل نقشی را که برای کاربران، در ابتدای کار نیاز است مشخص کنید، نقش user است.
همچنین خاصیت اضافهتری به نام SerialNumber نیز در اینجا درنظر گرفته شدهاست. این مورد را باید به صورت دستی مدیریت کنید. اگر کاربری کلمهی عبورش را تغییر داد، اگر مدیری نقشی را به او انتساب داد یا از او گرفت و یا اگر کاربری غیرفعال شد، مقدار خاصیت و فیلد SerialNumber را با یک Guid جدید به روز رسانی کنید. این Guid در برنامه با Guid موجود در توکن مقایسه شده و بلافاصله سبب عدم دسترسی او خواهد شد (در صورت عدم تطابق).
ب) کلاس UserToken
در کلاس UserToken کار نگهداری ریز اطلاعات توکنهای صادر شده صورت میگیرد. توکنهای صادر شده دارای access token و refresh token هستند؛ به همراه زمان انقضای آنها. به این ترتیب زمانیکه کاربری درخواستی را به سرور ارسال میکند، ابتدا token او را دریافت کرده و سپس بررسی میکنیم که آیا اصلا چنین توکنی در بانک اطلاعاتی ما وجود خارجی دارد یا خیر؟ آیا توسط ما صادر شدهاست یا خیر؟ اگر خیر، بلافاصله دسترسی او قطع خواهد شد. برای مثال عملیات logout را طوری طراحی میکنیم که تمام توکنهای یک شخص را در بانک اطلاعاتی حذف کند. به این ترتیب توکن قبلی او دیگر قابلیت استفادهی مجدد را نخواهد داشت.
مدیریت بانک اطلاعاتی و کلاسهای سرویس برنامه
در لایه سرویس برنامه، شما سه سرویس را مشاهده خواهید کرد که قابلیت جایگزین شدن با کدهای یک ORM را دارند (نوع آن ORM مهم نیست):
الف) سرویس TokenStoreService
public interface ITokenStoreService { void CreateUserToken(UserToken userToken); bool IsValidToken(string accessToken, int userId); void DeleteExpiredTokens(); UserToken FindToken(string refreshTokenIdHash); void DeleteToken(string refreshTokenIdHash); void InvalidateUserTokens(int userId); void UpdateUserToken(int userId, string accessTokenHash); }
پیاده سازی این کلاس بسیار شبیه است به پیاده سازی ORMهای موجود و فقط یک SaveChanges را کم دارد.
یک نکته:
در سرویس ذخیره سازی توکنها، یک چنین متدی قابل مشاهده است:
public void CreateUserToken(UserToken userToken) { InvalidateUserTokens(userToken.OwnerUserId); _tokens.Add(userToken); }
ب) سرویس UsersService
public interface IUsersService { string GetSerialNumber(int userId); IEnumerable<string> GetUserRoles(int userId); User FindUser(string username, string password); User FindUser(int userId); void UpdateUserLastActivityDate(int userId); }
همچنین متدهای دیگری برای یافتن یک کاربر بر اساس نام کاربری و کلمهی عبور او (جهت مدیریت مرحلهی لاگین)، یافتن کاربر بر اساس Id او (جهت استخراج اطلاعات کاربر) و همچنین یک متد اختیاری نیز برای به روز رسانی فیلد آخرین تاریخ فعالیت کاربر در اینجا پیش بینی شدهاند.
ج) سرویس SecurityService
public interface ISecurityService { string GetSha256Hash(string input); }
پیاده سازی قسمت لاگین و صدور access token
در کلاس AppOAuthProvider کار پیاده سازی قسمت لاگین برنامه انجام شدهاست. این کلاسی است که توسط کلاس AppOAuthOptions به OwinStartup معرفی میشود. قسمتهای مهم کلاس AppOAuthProvider به شرح زیر هستند:
برای درک عملکرد این کلاس، در ابتدای متدهای مختلف آن، یک break point قرار دهید. برنامه را اجرا کرده و سپس بر روی دکمهی login کلیک کنید. به این ترتیب جریان کاری این کلاس را بهتر میتوانید درک کنید. کار آن با فراخوانی متد ValidateClientAuthentication شروع میشود. چون با یک برنامهی وب در حال کار هستیم، ClientId آنرا نال درنظر میگیریم و برای ما مهم نیست. اگر کلاینت ویندوزی خاصی را تدارک دیدید، این کلاینت میتواند ClientId ویژهای را به سمت سرور ارسال کند که در اینجا مدنظر ما نیست.
مهمترین قسمت این کلاس، متد GrantResourceOwnerCredentials است که پس از ValidateClientAuthentication بلافاصله فراخوانی میشود. اگر به کدهای آن دقت کنید، خود owin دارای خاصیتهای user name و password نیز هست.
این اطلاعات را به نحو ذیل از کلاینت خود دریافت میکند. اگر به فایل index.html مراجعه کنید، یک چنین تعریفی را برای متد login میتوانید مشاهده کنید:
function doLogin() { $.ajax({ url: "/login", // web.config --> appConfiguration -> tokenPath data: { username: "Vahid", password: "1234", grant_type: "password" }, type: 'POST', // POST `form encoded` data contentType: 'application/x-www-form-urlencoded'
در متد GrantResourceOwnerCredentials کار بررسی نام کاربری و کلمهی عبور کاربر صورت گرفته و در صورت یافت شدن کاربر (صحیح بودن اطلاعات)، نقشهای او نیز به عنوان Claim جدید به توکن اضافه میشوند.
در اینجا یک Claim سفارشی هم اضافه شدهاست:
identity.AddClaim(new Claim(ClaimTypes.UserData, user.UserId.ToString()));
در انتهای این کلاس، از متد TokenEndpointResponse جهت دسترسی به access token نهایی صادر شدهی برای کاربر، استفاده کردهایم. هش این access token را در بانک اطلاعاتی ذخیره میکنیم (جستجوی هشها سریعتر هستند از جستجوی یک رشتهی طولانی؛ به علاوه در صورت دسترسی به بانک اطلاعاتی، اطلاعات هشها برای مهاجم قابل استفاده نیست).
اگر بخواهیم اطلاعات ارسالی به کاربر را پس از لاگین، نمایش دهیم، به شکل زیر خواهیم رسید:
در اینجا access_token همان JSON Web Token صادر شدهاست که برنامهی کلاینت از آن برای اعتبارسنجی استفاده خواهد کرد.
بنابراین خلاصهی مراحل لاگین در اینجا به ترتیب ذیل است:
- فراخوانی متد ValidateClientAuthenticationدر کلاس AppOAuthProvider . طبق معمول چون ClientID نداریم، این مرحله را قبول میکنیم.
- فراخوانی متد GrantResourceOwnerCredentials در کلاس AppOAuthProvider . در اینجا کار اصلی لاگین به همراه تنظیم Claims کاربر انجام میشود. برای مثال نقشهای او به توکن صادر شده اضافه میشوند.
- فراخوانی متد Protect در کلاس AppJwtWriterFormat که کار امضای دیجیتال access token را انجام میدهد.
- فراخوانی متد CreateAsync در کلاس RefreshTokenProvider. کار این متد صدور توکن ویژهای به نام refresh است. این توکن را در بانک اطلاعاتی ذخیره خواهیم کرد. در اینجا چیزی که به سمت کلاینت ارسال میشود صرفا یک guid است و نه اصل refresh token.
- فرخوانی متد TokenEndpointResponse در کلاس AppOAuthProvider . از این متد جهت یافتن access token نهایی تولید شده و ثبت هش آن در بانک اطلاعاتی استفاده میکنیم.
پیاده سازی قسمت صدور Refresh token
در تصویر فوق، خاصیت refresh_token را هم در شیء JSON ارسالی به سمت کاربر مشاهده میکنید. هدف از refresh_token، تمدید یک توکن است؛ بدون ارسال کلمهی عبور و نام کاربری به سرور. در اینجا access token صادر شده، مطابق تنظیم expirationMinutes در فایل وب کانفیگ، منقضی خواهد شد. اما طول عمر refresh token را بیشتر از طول عمر access token در نظر میگیریم. بنابراین طول عمر یک access token کوتاه است. زمانیکه access token منقضی شد، نیازی نیست تا حتما کاربر را به صفحهی لاگین هدایت کنیم. میتوانیم refresh_token را به سمت سرور ارسال کرده و به این ترتیب درخواست صدور یک access token جدید را ارائه دهیم. این روش هم سریعتر است (کاربر متوجه این retry نخواهد شد) و هم امنتر است چون نیازی به ارسال کلمهی عبور و نام کاربری به سمت سرور وجود ندارند.
سمت کاربر، برای درخواست صدور یک access token جدید بر اساس refresh token صادر شدهی در زمان لاگین، به صورت زیر عمل میشود:
function doRefreshToken() { // obtaining new tokens using the refresh_token should happen only if the id_token has expired. // it is a bad practice to call the endpoint to get a new token every time you do an API call. $.ajax({ url: "/login", // web.config --> appConfiguration -> tokenPath data: { grant_type: "refresh_token", refresh_token: refreshToken }, type: 'POST', // POST `form encoded` data contentType: 'application/x-www-form-urlencoded'
- فرخوانی متد ValidateClientAuthentication در کلاس AppOAuthProvider . طبق معمول چون ClientID نداریم، این مرحله را قبول میکنیم.
- فراخوانی متد ReceiveAsync در کلاس RefreshTokenProvider. در قسمت توضیح مراحل لاگین، عنوان شد که پس از فراخوانی متد GrantResourceOwnerCredentials جهت لاگین، متد CreateAsync در کلاس RefreshTokenProvider فراخوانی میشود. اکنون در متد ReceiveAsync این refresh token ذخیره شدهی در بانک اطلاعاتی را یافته (بر اساس Guid ارسالی از طرف کلاینت) و سپس Deserialize میکنیم. به این ترتیب است که کار درخواست یک access token جدید بر مبنای refresh token موجود آغاز میشود.
- فراخوانی GrantRefreshToken در کلاس AppOAuthProvider . در اینجا اگر نیاز به تنظیم Claim اضافهتری وجود داشت، میتوان اینکار را انجام داد.
- فراخوانی متد Protect در کلاس AppJwtWriterFormat که کار امضای دیجیتال access token جدید را انجام میدهد.
- فراخوانی CreateAsync در کلاس RefreshTokenProvider . پس از اینکه context.DeserializeTicket در متد ReceiveAsync بر مبنای refresh token قبلی انجام شد، مجددا کار تولید یک توکن جدید در متد CreateAsync شروع میشود و زمان انقضاءها تنظیم خواهند شد.
- فراخوانی TokenEndpointResponse در کلاس AppOAuthProvider . مجددا از این متد برای دسترسی به access token جدید و ذخیرهی هش آن در بانک اطلاعاتی استفاده میکنیم.
پیاده سازی فیلتر سفارشی JwtAuthorizeAttribute
در ابتدای بحث عنوان کردیم که اگر مشخصات کاربر تغییر کردند یا کاربر logout کرد، امکان غیرفعال کردن یک توکن را نداریم و این توکن تا زمان انقضای آن معتبر است. این نقیصه را با طراحی یک AuthorizeAttribute سفارشی جدید به نام JwtAuthorizeAttribute برطرف میکنیم. نکات مهم این فیلتر به شرح زیر هستند:
- در اینجا در ابتدا بررسی میشود که آیا درخواست رسیدهی به سرور، حاوی access token هست یا خیر؟ اگر خیر، کار همینجا به پایان میرسد و دسترسی کاربر قطع خواهد شد.
- سپس بررسی میکنیم که آیا درخواست رسیده پس از مدیریت توسط Owin، دارای Claims است یا خیر؟ اگر خیر، یعنی این توکن توسط ما صادر نشدهاست.
- در ادامه شماره سریال موجود در access token را استخراج کرده و آنرا با نمونهی موجود در دیتابیس مقایسه میکنیم. اگر این دو یکی نبودند، دسترسی کاربر قطع میشود.
- همچنین در آخر بررسی میکنیم که آیا هش این توکن رسیده، در بانک اطلاعاتی ما موجود است یا خیر؟ اگر خیر باز هم یعنی این توکن توسط ما صادر نشدهاست.
بنابراین به ازای هر درخواست به سرور، دو بار بررسی بانک اطلاعاتی را خواهیم داشت:
- یکبار بررسی جدول کاربران جهت واکشی مقدار فیلد شماره سریال مربوط به کاربر.
- یکبار هم جهت بررسی جدول توکنها برای اطمینان از صدور توکن رسیده توسط برنامهی ما.
و نکتهی مهم اینجا است که از این پس بجای فیلتر معمولی Authorize، از فیلتر جدید JwtAuthorize در برنامه استفاده خواهیم کرد:
[JwtAuthorize(Roles = "Admin")] public class MyProtectedAdminApiController : ApiController
نحوهی ارسال درخواستهای Ajax ایی به همراه توکن صادر شده
تا اینجا کار صدور توکنهای برنامه به پایان میرسد. برای استفادهی از این توکنها در سمت کاربر، به فایل index.html دقت کنید. در متد doLogin، پس از موفقیت عملیات دو متغیر جدید مقدار دهی میشوند:
var jwtToken; var refreshToken; function doLogin() { $.ajax({ // same as before }).then(function (response) { jwtToken = response.access_token; refreshToken = response.refresh_token; }
function doCallApi() { $.ajax({ headers: { 'Authorization': 'Bearer ' + jwtToken }, url: "/api/MyProtectedApi", type: 'GET' }).then(function (response) {
پیاده سازی logout سمت سرور و کلاینت
پیاده سازی سمت سرور logout را در کنترلر UserController مشاهده میکنید. در اینجا در اکشن متد Logout، کار حذف توکنهای کاربر از بانک اطلاعاتی انجام میشود. به این ترتیب دیگر مهم نیست که توکن او هنوز منقضی شدهاست یا خیر. چون هش آن دیگر در جدول توکنها وجود ندارد، از فیلتر JwtAuthorizeAttribute رد نخواهد شد.
سمت کلاینت آن نیز در فایل index.html ذکر شدهاست:
function doLogout() { $.ajax({ headers: { 'Authorization': 'Bearer ' + jwtToken }, url: "/api/user/logout", type: 'GET'
بررسی تنظیمات IoC Container برنامه
تنظیمات IoC Container برنامه را در پوشهی IoCConfig میتوانید ملاحظه کنید. از کلاس SmWebApiControllerActivator برای فعال سازی تزریق وابستگیها در کنترلرهای Web API استفاده میشود و از کلاس SmWebApiFilterProvider برای فعال سازی تزریق وابستگیها در فیلتر سفارشی که ایجاد کردیم، کمک گرفته خواهد شد.
هر دوی این تنظیمات نیز در کلاس WebApiConfig ثبت و معرفی شدهاند.
به علاوه در کلاس SmObjectFactory، کار معرفی وهلههای مورد استفاده و تنظیم طول عمر آنها انجام شدهاست. برای مثال طول عمر IOAuthAuthorizationServerProvider از نوع Singleton است؛ چون تنها یک وهله از AppOAuthProvider در طول عمر برنامه توسط Owin استفاده میشود و Owin هربار آنرا وهله سازی نمیکند. همین مساله سبب شدهاست که معرفی وابستگیها در سازندهی کلاس AppOAuthProvider کمی با حالات متداول، متفاوت باشند:
public AppOAuthProvider( Func<IUsersService> usersService, Func<ITokenStoreService> tokenStoreService, ISecurityService securityService, IAppJwtConfiguration configuration)
در اینجا سرویس IAppJwtConfiguration با Func معرفی نشدهاست؛ چون طول عمر تنظیمات خوانده شدهی از Web.config نیز Singleton هستند و معرفی آن به همین نحو صحیح است.
اگر علاقمند بودید که بررسی کنید یک سرویس چندبار وهله سازی میشود، یک سازندهی خالی را به آن اضافه کنید و سپس یک break point را بر روی آن قرار دهید و برنامه را اجرا و در این حالت چندبار Login کنید.