try { //انتخاب فایل اکسل OpenFileDialog ofd = new OpenFileDialog(); ofd.Title = "انتخاب فایل اکسل حاوی اطلاعات"; ofd.Filter = "فایل اکسل 2003 (*.xls)|*.xls|فایل اکسل 2007 به بعد(*.xlsx)|*.xlsx"; if (ofd.ShowDialog() == DialogResult.OK) { string excelConnectionString = ""; string SourceFilePath = ofd.FileName; //ایجاد کانکشن استرینگ برا خواندن کل اطلاعات از فایل اکسل و ریختن آنها در یک دیتاتیبل به نام dt if (System.IO.Path.GetExtension(ofd.FileName) == ".xlsx") //تشخیص نوع فایل اکسل برای ایجاد کانکشن استرینگ برای نسخههای مختلف اکسل excelConnectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + SourceFilePath + ";Extended Properties=Excel 12.0"; else if (System.IO.Path.GetExtension(ofd.FileName) == ".xls") excelConnectionString = "Provider=Microsoft.Jet.Oledb.4.0;Data Source=" + SourceFilePath + ";Extended Properties=Excel 8.0"; DataTable dt = new DataTable("tblinfos"); using (System.Data.OleDb.OleDbConnection connection = new System.Data.OleDb.OleDbConnection(excelConnectionString)) { connection.Open(); System.Data.OleDb.OleDbDataAdapter da = new System.Data.OleDb.OleDbDataAdapter("SELECT * FROM [List$]", connection);//List نام شیت در فایل اکسل است da.Fill(dt); connection.Close(); } using (System.Data.OleDb.OleDbConnection connection = new System.Data.OleDb.OleDbConnection(excelConnectionString)) { this.Cursor = Cursors.WaitCursor; connection.Open(); //ایجاد ارتباط با بانک اس کیو ال string sqlConnectionString = @"Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\BankDPR.mdf;Max Pool Size=6000; Connection Timeout=50;Integrated Security=True;User Instance=True"; Dataaccess db = new Dataaccess(); DataTable dtr = db.select("Select Top(1) * From tblinfos");//بدست آورن نام ستونهای جدول مورد نظر برای تطبیق با ستونهای فایل اکسل //******************* Fast Copy using (System.Data.SqlClient.SqlBulkCopy bulkCopy = new System.Data.SqlClient.SqlBulkCopy(sqlConnectionString, System.Data.SqlClient.SqlBulkCopyOptions.KeepIdentity))//تعریف یک شی از کلاس SqlBulkCopy { for (int i = 1; i < dtr.Columns.Count; i++) { bulkCopy.ColumnMappings.Add(dt.Columns[i - 1].Caption, dtr.Columns[i].Caption);//با استفاده از خاصیت ColumnMappings نام ستونهای فایل اکسل با نام ستونهای جدول مورد نظر بانک sql تطبیق داده میشود } bulkCopy.DestinationTableName = "tblinfos";//مشخص نمودن نام جدول که قرار است اطلاعات درون ان کپی گردد bulkCopy.WriteToServer(dt);//انجام عملیات کپی اطلاعات از دیتاتیبل با نام dt به بانک } this.Cursor = Cursors.Arrow; MessageBox.Show("اطلاعات از فایل شما خوانده شد"); } } } catch (Exception ex) { this.Cursor = Cursors.Arrow; MessageBox.Show(ex.Message, "error"); }
public virtual User User { get; set; } public virtual Role Role { get; set; }
public virtual ICollection<UserRole> UserRoles { get; set; } = new HashSet<UserRole>();
معرفی Xamarin و مزیتهای استفاده از آن
سیستم ASP.NET Membership بهمراه ASP.NET 2.0 در سال 2005 معرفی شد، و از آن زمان تا بحال تغییرات زیادی در چگونگی مدیریت احزار هویت و اختیارات کاربران توسط اپلیکیشنهای وب بوجود آمده است. ASP.NET Identity نگاهی تازه است به آنچه که سیستم Membership هنگام تولید اپلیکیشنهای مدرن برای وب، موبایل و تبلت باید باشد.
پیش زمینه: سیستم عضویت در ASP.NET
- الگوی پایگاه داده آن برای SQL Server طراحی شده است، و قادر به تغییرش هم نیستید. میتوانید اطلاعات پروفایل را اضافه کنید، اما تمام دادهها در یک جدول دیگر ذخیره میشوند، که دسترسی به آنها نیز مشکلتر است، تنها راه دسترسی Profile Provider API خواهد بود.
- سیستم تامین کننده (Provider System) امکان تغییر منبع دادهها را به شما میدهد، مثلا میتوانید از بانکهای اطلاعاتی MySQL یا Oracle استفاده کنید. اما تمام سیستم بر اساس پیش فرض هایی طراحی شده است که تنها برای بانکهای اطلاعاتی relational درست هستند. میتوانید تامین کننده (Provider) ای بنویسید که دادههای سیستم عضویت را در منبعی به غیر از دیتابیسهای relational ذخیره میکند؛ مثلا Windows Azure Storage Tables. اما در این صورت باید مقادیر زیادی کد بنویسید. مقادیر زیادی هم System.NotImplementedException باید بنویسید، برای متد هایی که به دیتابیسهای NoSQL مربوط نیستند.
- از آنجایی که سیستم ورود/خروج سایت بر اساس مدل Forms Authentication کار میکند، سیستم عضویت نمیتواند از OWIN استفاده کند. OWIN شامل کامپوننت هایی برای احراز هویت است که شامل سرویسهای خارجی هم میشود (مانند Microsoft Accounts, Facebook, Google, Twitter). همچنین امکان ورود به سیستم توسط حسابهای کاربری سازمانی (Organizational Accounts) نیز وجود دارد مانند Active Directory و Windows Azure Active Directory. این کتابخانه از OAuth 2.0، JWT و CORS نیز پشتیبانی میکند.
- ذخیره دادههای سیستم عضویت در بانکهای اطلاعاتی non-relational مشکل است.
- نمی توانید از آن در کنار OWIN استفاده کنید.
- با فراهم کنندههای موجود ASP.NET Membership بخوبی کار نمیکند. توسعه پذیر هم نیست.
ASP.NET Universal Providers
ASP.NET Universal Providers برای ذخیره سازی اطلاعات سیستم عضویت در Windows Azure SQL Database توسعه پیدا کردند. با SQL Server Compact هم بخوبی کار میکنند. این تامین کنندهها بر اساس Entity Framework Code First ساخته شده بودند و بدین معنا بود که دادههای سیستم عضویت را میتوان در هر منبع داده ای که توسط EF پشتیبانی میشود ذخیره کرد. با انتشار این تامین کنندهها الگوی دیتابیس سیستم عضویت نیز بسیار سبکتر و بهتر شد. اما این سیستم بر پایه زیر ساخت ASP.NET Membership نوشته شده است، بنابراین محدودیتهای پیشین مانند محدودیتهای SqlMembershipProvider هنوز وجود دارند. به بیان دیگر، این سیستمها همچنان برای بانکهای اطلاعاتی relational طراحی شده اند، پس سفارشی سازی اطلاعات کاربران و پروفایلها هنوز مشکل است. در آخر آنکه این تامین کنندهها هنوز از مدل احراز هویت فرم استفاده میکنند.
ASP.NET Identity
- یک سیستم هویت واحد (One ASP.NET Identity system)
- سیستم ASP.NET Identity میتواند در تمام فریم ورکهای مشتق از ASP.NET استفاده شود. مانند ASP.NET MVC, Web Forms, Web Pages, Web API و SignalR
- از این سیستم میتوانید در تولید اپلیکیشنهای وب، موبایل، استور (Store) و یا اپلیکیشنهای ترکیبی استفاده کنید.
- سادگی تزریق دادههای پروفایل درباره کاربران
- روی الگوی دیتابیس برای اطلاعات کاربران و پروفایلها کنترل کامل دارید. مثلا میتوانید به سادگی یک فیلد، برای تاریخ تولد در نظر بگیرید که کاربران هنگام ثبت نام در سایت باید آن را وارد کنند.
- کنترل ذخیره سازی/واکشی اطلاعات
- بصورت پیش فرض ASP.NET Identity تمام اطلاعات کاربران را در یک دیتابیس ذخیره میکند. تمام مکانیزمهای دسترسی به دادهها توسط EF Code First کار میکنند.
- از آنجا که روی الگوی دیتابیس، کنترل کامل دارید، تغییر نام جداول و یا نوع داده فیلدهای کلیدی و غیره ساده است.
- استفاده از مکانیزمهای دیگر برای مدیریت دادههای آن ساده است، مانند SharePoint, Windows Azure Storage Table و دیتابیسهای NoSQL.
- تست پذیری
- ASP.NET Identity تست پذیری اپلیکیشن وب شما را بیشتر میکند. میتوانید برای تمام قسمت هایی که از ASP.NET Identity استفاده میکنند تست بنویسید.
- تامین کننده نقش (Role Provider)
- تامین کننده ای وجود دارد که به شما امکان محدود کردن سطوح دسترسی بر اساس نقوش را میدهد. بسادگی میتوانید نقشهای جدید مانند "Admin" بسازید و بخشهای مختلف اپلیکیشن خود را محدود کنید.
- Claims Based
- ASP.NET Identity از امکان احراز هویت بر اساس Claims نیز پشتیبانی میکند. در این مدل، هویت کاربر بر اساس دسته ای از اختیارات او شناسایی میشود. با استفاده از این روش توسعه دهندگان برای تعریف هویت کاربران، آزادی عمل بیشتری نسبت به مدل Roles دارند. مدل نقشها تنها یک مقدار منطقی (bool) است؛ یا عضو یک نقش هستید یا خیر، در حالیکه با استفاده از روش Claims میتوانید اطلاعات بسیار ریز و دقیقی از هویت کاربر در دست داشته باشید.
- تامین کنندگان اجتماعی
- به راحتی میتوانید از تامین کنندگان دیگری مانند Microsoft, Facebook, Twitter, Google و غیره استفاده کنید و اطلاعات مربوط به کاربران را در اپلیکیشن خود ذخیره کنید.
- Windows Azure Active Directory
- برای اطلاعات بیشتر به این لینک مراجعه کنید.
- یکپارچگی با OWIN
- ASP.NET Identity بر اساس OWIN توسعه پیدا کرده است، بنابراین از هر میزبانی که از OWIN پشتیبانی میکند میتوانید استفاده کنید. همچنین هیچ وابستگی ای به System.Web وجود ندارد. ASP.NET Identity یک فریم ورک کامل و مستقل برای OWIN است و میتواند در هر اپلیکیشنی که روی OWIN میزبانی شده استفاده شود.
- ASP.NET Identity از OWIN برای ورود/خروج کاربران در سایت استفاده میکند. این بدین معنا است که بجای استفاده از Forms Authentication برای تولید یک کوکی، از OWIN CookieAuthentication استفاده میشود.
- پکیج NuGet
- ASP.NET Identity در قالب یک بسته NuGet توزیع میشود. این بسته در قالب پروژههای ASP.NET MVC, Web Forms و Web API که با Visual Studio 2013 منتشر شدند گنجانده شده است.
- توزیع این فریم ورک در قالب یک بسته NuGet این امکان را به تیم ASP.NET میدهد تا امکانات جدیدی توسعه دهند، باگها را برطرف کنند و نتیجه را بصورت چابک به توسعه دهندگان عرضه کنند.
ASP.NET Identity در قالب پروژههای ASP.NET MVC, Web Forms, Web API و SPA که بهمراه Visual Studio 2013 منتشر شده اند استفاده میشود. در ادامه به اختصار خواهیم دید که چگونه ASP.NET Identity کار میکند.
- یک پروژه جدید ASP.NET MVC با تنظیمات Individual User Accounts بسازید.
-
پروژه ایجاد شده شامل سه بسته میشود که مربوط به ASP.NET Identity هستند:
- Microsoft.AspNet.Identity.EntityFramework این بسته شامل پیاده سازی ASP.NET Identity با Entity Framework میشود، که تمام دادههای مربوطه را در یک دیتابیس SQL Server ذخیره میکند.
- Microsoft.AspNet.Identity.Core این بسته محتوی تمام interfaceهای ASP.NET Identity است. با استفاده از این بسته میتوانید پیاده سازی دیگری از ASP.NET Identity بسازید که منبع داده متفاوتی را هدف قرار میدهد. مثلا Windows Azure Storage Table و دیتابیسهای NoSQL.
- Microsoft.AspNet.Identity.OWIN این بسته امکان استفاده از احراز هویت OWIN را در اپلیکیشنهای ASP.NET فراهم میکند. هنگام تولید کوکیها از OWIN Cookie Authentication استفاده خواهد شد.
هنگامیکه بر روی دکمهی Register کلیک شود، کنترلر Account، اکشن متد Register را فراخوانی میکند تا حساب کاربری جدیدی با استفاده از ASP.NET Identity API ساخته شود.
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Register(RegisterViewModel model) { if (ModelState.IsValid) { var user = new ApplicationUser() { UserName = model.UserName }; var result = await UserManager.CreateAsync(user, model.Password); if (result.Succeeded) { await SignInAsync(user, isPersistent: false); return RedirectToAction("Index", "Home"); } else { AddErrors(result); } } // If we got this far, something failed, redisplay form return View(model); }
اگر حساب کاربری با موفقیت ایجاد شود، کاربر توسط فراخوانی متد SignInAsync به سایت وارد میشود.
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Register(RegisterViewModel model) { if (ModelState.IsValid) { var user = new ApplicationUser() { UserName = model.UserName }; var result = await UserManager.CreateAsync(user, model.Password); if (result.Succeeded) { await SignInAsync(user, isPersistent: false); return RedirectToAction("Index", "Home"); } else { AddErrors(result); } } // If we got this far, something failed, redisplay form return View(model); }
private async Task SignInAsync(ApplicationUser user, bool isPersistent) { AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie); var identity = await UserManager.CreateIdentityAsync( user, DefaultAuthenticationTypes.ApplicationCookie); AuthenticationManager.SignIn( new AuthenticationProperties() { IsPersistent = isPersistent }, identity); }
از آنجا که ASP.NET Identity و OWIN Cookie Authentication هر دو Claims-based هستند، فریم ورک، انتظار آبجکتی از نوع ClaimsIdentity را خواهد داشت. این آبجکت تمامی اطلاعات لازم برای تشخیص هویت کاربر را در بر دارد. مثلا اینکه کاربر مورد نظر به چه نقش هایی تعلق دارد؟ و اطلاعاتی از این قبیل. در این مرحله میتوانید Claimهای بیشتری را به کاربر بیافزایید.
کلیک کردن روی لینک Log off در سایت، اکشن متد LogOff در کنترلر Account را اجرا میکند.
// POST: /Account/LogOff [HttpPost] [ValidateAntiForgeryToken] public ActionResult LogOff() { AuthenticationManager.SignOut(); return RedirectToAction("Index", "Home"); }
همانطور که مشاهده میکنید برای ورود/خروج کاربران از AuthenticationManager استفاده میشود که متعلق به OWIN است. متد SignOut همتای متد FormsAuthentication.SignOut است.
کامپوننتهای ASP.NET Identity
تصویر زیر اجزای تشکیل دهنده ASP.NET Identity را نمایش میدهد. بسته هایی که با رنگ سبز نشان داده شده اند سیستم کلی ASP.NET Identity را میسازند. مابقی بستهها وابستگی هایی هستند که برای استفاده از ASP.NET Identity در اپلیکیشنهای ASP.NET لازم اند.
دو پکیج دیگر نیز وجود دارند که به آنها اشاره نشد:
- Microsoft.Security.Owin.Cookies این بسته امکان استفاده از مدل احراز هویت مبتنی بر کوکی (Cookie-based Authentication) را فراهم میکند. مدلی مانند سیستم ASP.NET Forms Authentication.
- EntityFramework که نیازی به معرفی ندارد.
مهاجرت از Membership به ASP.NET Identity
قدمهای بعدی
نگاشت خودکار مجموعهها در Fluent NHibernate ساده است و نیاز به تنظیم خاصی ندارد. برای مثال IList به صورت خودکار به Bag ترجمه میشود و الی آخر.
البته شاید سؤال بپرسید که این Bag از کجا آمده؟ کلا 6 نوع مجموعه در NHibernate پشتیبانی میشوند که شامل Array، Primitive-Array ، Bag ، Set ، List و Map هستند؛ این اسامی هم جهت حفظ سازگاری با جاوا تغییر نکردهاند و گرنه معادلهای آنها در دات نت به این شرح هستند:
Bag=IList
Set=Iesi.Collections.ISet
List=IList
Map=IDictionary
البته در دات نت 4 ، ISet هم به صورت توکار اضافه شده، اما NHibernate از مدتها قبل آنرا از کتابخانهی Iesi.Collections به عاریت گرفته است. مهمترین تفاوتهای این مجموعهها هم در پذیرفتن یا عدم پذیرش اعضای تکراری است. Set و Map اعضای تکراری نمیپذیرند.
در ادامه میخواهیم طرز کار با Map یا همان IDictionary دات نت را بررسی کنیم:
الف) حالتی که نوع کلید و مقدار (در یک عضو Dictionary تعریف شده)، Entity نیستند
using System.Collections.Generic;
namespace Test1.Model12
{
public class User
{
public virtual int Id { set; get; }
public virtual string Name { get; set; }
public virtual IDictionary<string, string> Preferences { get; set; }
}
}
نحوه تعریف نگاشت که مبتنی است بر مشخص سازی تعاریف کلید و مقدار آن جهت تشکیل یک Map یا همان Dictionary :
using FluentNHibernate.Automapping;
using FluentNHibernate.Automapping.Alterations;
namespace Test1.Model12
{
public class UserMapping : IAutoMappingOverride<User>
{
public void Override(AutoMapping<User> mapping)
{
mapping.Id(x => x.Id);
mapping.HasMany(x => x.Preferences)
.AsMap<string>("FieldKey")
.Element("FieldValue", x => x.Type<string>().Length(500));
}
}
}
خروجی SQL متناظر:
create table "User" (
Id INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
primary key (Id)
)
create table Preferences (
User_id INT not null,
FieldValue NVARCHAR(500) null,
FieldKey NVARCHAR(255) not null,
primary key (User_id, FieldKey)
)
alter table Preferences
add constraint FKD6CB18523B1FD789
foreign key (User_id)
references "User"
ب) حالتی که مقدار، Entity است
using System.Collections.Generic;
namespace Test1.Model13
{
public class User
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IDictionary<string, Property> Properties { get; set; }
}
public class Property
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string Value { get; set; }
public virtual User User { get; set; }
}
}
نحوه تعریف نگاشت:
using FluentNHibernate.Automapping;
using FluentNHibernate.Automapping.Alterations;
namespace Test1.Model13
{
public class UserMapping : IAutoMappingOverride<User>
{
public void Override(AutoMapping<User> mapping)
{
mapping.Id(x => x.Id);
mapping.HasMany<Property>(x => x.Properties)
.AsMap<string>("FieldKey")
.Component(x => x.Map(c => c.Id));
}
}
}
create table "Property" (
Id INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
Value NVARCHAR(255) null,
User_id INT null,
primary key (Id)
)
create table "User" (
Id INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
primary key (Id)
)
create table Properties (
User_id INT not null,
Id INT null,
FieldKey NVARCHAR(255) not null,
primary key (User_id, FieldKey)
)
alter table "Property"
add constraint FKF9F4D85A3B1FD7A2
foreign key (User_id)
references "User"
alter table Properties
add constraint FK63646D853B1FD7A2
foreign key (User_id)
references "User"
ج) حالتی که کلید، Entity است
using System;
using System.Collections.Generic;
namespace Test1.Model14
{
public class FormData
{
public virtual int Id { get; set; }
public virtual DateTime? DateTime { get; set; }
public virtual IDictionary<FormField, string> FormPropertyValues { get; set; }
}
public class FormField
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
}
نحوه تعریف نگاشت:
using FluentNHibernate.Automapping;
using FluentNHibernate.Automapping.Alterations;
namespace Test1.Model14
{
public class FormDataMapping : IAutoMappingOverride<FormData>
{
public void Override(AutoMapping<FormData> mapping)
{
mapping.Id(x => x.Id);
mapping.HasMany<FormField>(x => x.FormPropertyValues)
.AsEntityMap("FieldId")
.Element("FieldValue", x => x.Type<string>().Length(500))
.Cascade.All();
}
}
}
create table "FormData" (
Id INT IDENTITY NOT NULL,
DateTime DATETIME null,
primary key (Id)
)
create table FormPropertyValues (
FormData_id INT not null,
FieldValue NVARCHAR(500) null,
FieldId INT not null,
primary key (FormData_id, FieldId)
)
create table "FormField" (
Id INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
primary key (Id)
)
alter table FormPropertyValues
add constraint FKB807B9C090849E
foreign key (FormData_id)
references "FormData"
alter table FormPropertyValues
add constraint FKB807B97165898A
foreign key (FieldId)
references "FormField"
یک مثال عملی:
امکانات فوق جهت طراحی قسمت ثبت اطلاعات یک برنامه «فرم ساز» مبتنی بر Key-Value بسیار مناسب هستند؛ برای مثال:
برنامهای را در نظر بگیرید که میتواند تعدادی خدمات داشته باشد که توسط مدیر برنامه قابل اضافه شدن است؛ برای نمونه خدمات درخواست نصب نرم افزار، خدمات درخواست تعویض کارت پرسنلی، خدمات درخواست مساعده، خدمات ... :
public class Service
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<ServiceFormField> Fields { get; set; }
public virtual IList<ServiceFormData> Forms { get; set; }
}
برای هر خدمات باید بتوان یک فرم طراحی کرد. هر فرم هم از یک سری فیلد خاص آن خدمات تشکیل شده است. برای مثال:
public class ServiceFormField
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual bool IsRequired { get; set; }
public virtual Service Service { get; set; }
}
در اینجا نیازی نیست به ازای هر فیلد جدید واقعا یک فیلد متناظر به دیتابیس اضافه شود و ساختار آن تغییر کند (برخلاف حالت dynamic components که پیشتر در مورد آن بحث شد).
اکنون با داشتن یک خدمات و فیلدهای پویای آن که توسط مدیربرنامه تعریف شدهاند، میتوان اطلاعات وارد کرد. مهمترین نکتهی آن هم IDictionary تعریف شده است که حاوی لیستی از فیلدها به همراه مقادیر وارد شده توسط کاربر خواهد بود:
public class ServiceFormData
{
public virtual int Id { get; set; }
public virtual IDictionary<ServiceFormField, string> FormPropertyValues { get; set; }
public virtual DateTime? DateTime { get; set; }
public virtual Service Service { get; set; }
}
در مورد نحوه نگاشت آن هم در حالت «ج» فوق توضیح داده شد.
Blogger auto poster
لطفا آخرین نسخه را از سایت CodePlex دریافت کنید.
توضیحات تکمیلی جهت تنظیمات برنامه، مخصوص ورد پرس:
اینبار BlogUrl شما چیزی شبیه به https://mysite.wordpress.com/xmlrpc.php خواهد بود که به API وردپرس جهت ارسال خودکار مطالب اشاره میکند.
Username و password شما هم همان نام کاربری و کلمه عبور ورود به وبلاگ است.
در وردپرس برخلاف بلاگر، اگر تگی از پیش تعریف نشده باشد، به صورت خودکار ایجاد نمیشود. بنابراین اول باید categories مناسب را در پنل مدیریتی وبلاگ ایجاد کنید.
زمان را هم در کنترل پنل وبلاگ اصلاح کنید. UTC آن مهم است. اگر تنظیم نباشد احتمالا مطالب به صورت scheduled در کنترل پنل نمایش داده میشود و سر ساعت که رسید عمومی خواهد شد.
حال اگر بخواهیم همین پروژه را به صورت سورس باز ارائه دهیم، استفاده کنندگان نهایی به مشکل برخواهند خورد؛ زیرا فایل pfx حاصل، توسط کلمه عبور محافظت میشود و در سایر سیستمها بدون درنظر گرفتن این ملاحظات قابل استفاده نخواهد بود.
معادل فایلهای pfx، فایلهایی هستند با پسوند snk که تنها تفاوت مهم آنها با فایلهای pfx، عدم محافظت توسط کلمه عبور است و ... برای کارهای خصوصا سورس باز انتخاب مناسبی به شمار میروند. اگر دقت کنید، اکثر پروژههای سورس باز دات نتی موجود در وب (مانند NHibernate، لوسین، iTextSharp و غیره) از فایلهای snk برای اضافه کردن امضای دیجیتال به کتابخانه نهایی تولیدی استفاده میکنند و نه فایلهای pfx محافظت شده.
در اینجا اگر فایل pfx ایی دارید و میخواهید معادل snk آنرا تولید کنید، قطعه کد زیر چنین امکانی را مهیا میسازد:
using System.IO; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; namespace PfxToSnk { class Program { /// <summary> /// Converts .pfx file to .snk file. /// </summary> /// <param name="pfxData">.pfx file data.</param> /// <param name="pfxPassword">.pfx file password.</param> /// <returns>.snk file data.</returns> public static byte[] Pfx2Snk(byte[] pfxData, string pfxPassword) { var cert = new X509Certificate2(pfxData, pfxPassword, X509KeyStorageFlags.Exportable); var privateKey = (RSACryptoServiceProvider)cert.PrivateKey; return privateKey.ExportCspBlob(true); } static void Main(string[] args) { var pfxFileData = File.ReadAllBytes(@"D:\Key.pfx"); var snkFileData = Pfx2Snk(pfxFileData, "my-pass"); File.WriteAllBytes(@"D:\Key.snk", snkFileData); } } }
در تهیه مثال Auto Mapping به کمک امکانات توکار NH 3.2 به این مورد نیاز پیدا کردم:
بتوان نوع متد جنریک را به صورت متغیر تعریف کرد و این نوع در زمان کامپایل برنامه مشخص نباشد. مثلا چیزی شبیه به این مثال:
using System;
namespace GenericsSample
{
class TestGenerics
{
public static void Print<T>(T data)
{
Console.WriteLine("Print<T>");
}
}
class Program
{
static void Main(string[] args)
{
var type = typeof(Nullable<int>);
TestGenerics.Print<type>(1);
}
}
}
این نوع فراخوانی متد Print در دات نت به صورت پیش فرض غیرمجاز است و نوع جنریک را نمیتوان به صورت متغیر معرفی کرد.
که البته این هم راه حل دارد و به کمک Reflection قابل حل است:
using System;
namespace GenericsSample
{
class TestGenerics
{
public static void Print<T>(T data)
{
Console.WriteLine("Print<T>");
}
}
class Program
{
static void Main(string[] args)
{
var nullableIntType = typeof(Nullable<>).MakeGenericType(typeof(int));
var method = typeof(TestGenerics).GetMethod("Print");
var genericMethod = method.MakeGenericMethod(new[] { nullableIntType });
genericMethod.Invoke(null, new object[] { 1 });
}
}
}
دو متد MakeGenericType و MakeGenericMethod برای ساخت پویای نوعهای جنریک و همچنین ارسال آنها به متدهای جنریک در دات نت وجود دارند که مثالی از نحوه استفاده از آنها را در بالا ملاحظه میکنید.
مثال دوم:
اگر کلاس TestGenerics نسخه غیرجنریک متد Print را هم داشت، چطور؟ مثلا:
class TestGenerics
{
public static void Print<T>(T data)
{
Console.WriteLine("Print<T>");
}
public static void Print(object data)
{
Console.WriteLine("Print");
}
}
اینبار اگر برنامه فوق را اجرا کنیم، پیغام Ambiguous match found را حین فراخوانی GetMoethod دریافت خواهیم کرد؛ چون دو متد با یک نام در کلاس یاد شده وجود دارند. برای حل این مشکل باید به نحو زیر عمل کرد:
using System;
using System.Linq;
namespace GenericsSample
{
class TestGenerics
{
public static void Print<T>(T data)
{
Console.WriteLine("Print<T>");
}
public static void Print(object data)
{
Console.WriteLine("Print");
}
}
class Program
{
static void Main(string[] args)
{
var nullableIntType = typeof(Nullable<>).MakeGenericType(typeof(int));
var method = typeof(TestGenerics).GetMethods()
.First(x => x.Name == "Print" && (x.GetParameters()[0]).ParameterType.IsGenericParameter);
var genericMethod = method.MakeGenericMethod(new[] { nullableIntType });
genericMethod.Invoke(null, new object[] { 1 });
}
}
}
GetMethods تمام متدها را بازگشت داده و سپس بر اساس متادیتای متدها، میتوان تشخیص داد که کدام یک جنریک است.
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); FluentValidationModelValidatorProvider.Configure(); }
[Validator(typeof(PersonValidator))] public class Person { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } public int Age { get; set; } } public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { RuleFor(x => x.Id).NotNull(); RuleFor(x => x.Name).Length(0, 10); RuleFor(x => x.Email).EmailAddress(); RuleFor(x => x.Age).InclusiveBetween(18, 60); } }
public class PeopleController : Controller { public ActionResult Create() { return View(); } [HttpPost] public ActionResult Create(Person person) { if(! ModelState.IsValid) { // re-render the view when validation failed. return View("Create", person); } TempData["notice"] = "Person successfully created"; return RedirectToAction("Index"); } }
@Html.ValidationSummary() @using (Html.BeginForm()) { Id: @Html.TextBoxFor(x => x.Id) @Html.ValidationMessageFor(x => x.Id) <br /> Name: @Html.TextBoxFor(x => x.Name) @Html.ValidationMessageFor(x => x.Name) <br /> Email: @Html.TextBoxFor(x => x.Email) @Html.ValidationMessageFor(x => x.Email) <br /> Age: @Html.TextBoxFor(x => x.Age) @Html.ValidationMessageFor(x => x.Age) <input type="submit" value="submit" /> }
- NotNull/NotEmpty
- Matches
- InclusiveBetween
- CreditCard
- Email
- EqualTo
- Length
public ActionResult Save([CustomizeValidator(RuleSet="MyRuleset")] Customer cust) { // ... }
public interface IValidatorFactory { IValidator<T> GetValidator<T>(); IValidator GetValidator(Type type); }
ObjectFactory.Configure(cfg => cfg.AddRegistry(new MyRegistry())); public class MyRegistry : Registry { public MyRegistry() { For<IValidator<Person>>() .Singleton() .Use<PersonValidator>(); } }
public class MyRegistry : Registry { public MyRegistry() { AssemblyScanner.FindValidatorsInAssemblyContaining<MyValidator>() .ForEach(result => { For(result.InterfaceType) .Singleton() .Use(result.ValidatorType); }); } }
protected void Application_Start() { RegisterRoutes(RouteTable.Routes); //Configure structuremap ObjectFactory.Configure(cfg => cfg.AddRegistry(new MyRegistry())); ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory()); //Configure FV to use StructureMap var factory = new StructureMapValidatorFactory(); //Tell MVC to use FV for validation ModelValidatorProviders.Providers.Add(new FluentValidationModelValidatorProvider(factory)); DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false; }
بررسی طول عمر توکنها
اگر مرورگر خود را پس از لاگین به سیستم، برای مدتی به حال خود رها کنید، پس از شروع به کار مجدد، مشاهده خواهید کرد که دیگر نمیتوانید به 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 وارد کنید.