اگر با MVC کار کرده باشید حتما با ModelBinding آن آشنا هستید؛ DefaultModelBinder توکار آن که در اکثر مواقع، باری زیادی را از روی دوش برنامه نویسان بر میدارد و کار را برای آنان راحتتر میکند.
اما در بعضی مواقع این مدل بایندر پیش فرض ممکن است پاسخگوی نیاز ما در
بایند کردن یک خصوصیت از یک مدل خاص نباشد، برای همین ما نیاز داریم که کمی آن را سفارشی سازی کنیم.
برای این کار ما دو راه داریم:
1) یک مدل بایندر جدید را با پیاده سازی IModelBinder تهیه کنیم. (در این حالت ما مجبوریم که مدل بایندر را از ابتدا جهت بایند
کردن کلیه مقادیر شی مدل خود، بازنویسی کنیم و در واقع امکان انتساب آنرا در سطح فقط یک خصوصیت نداریم.) (نحوه پیاده سازی قبلا در اینجا مطرح شده)
2) ModelBinder پیش فرض را جهت پاسخگویی به نیازمان توسعه دهیم. (که در این مطلب قصد آموزشش را داریم.)
فرض کنید که میخواهید بر اساس یک Enum در صفحه، یک DropDownFor معادل را قرار بدید که به طور خودکار رشته انتخاب شده را به یک خصوصیت مدل که از نوع بایت هست بایند بکند.
@Html.DropDownListFor(model => model.AccountType, new SelectList(Enum.GetNames(typeof(Enums.AccountType))))
public enum AccountType : byte { مدیر = 0, کاربر_حقیقی = 1, کاربر_حقوقی = 2, }
namespace MvcApplication1.Models { [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public abstract class PropertyBindAttribute : Attribute { public abstract bool BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor); } public class ExtendedModelBinder : DefaultModelBinder { protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) { if (propertyDescriptor.Attributes.OfType<PropertyBindAttribute>().Any()) { var modelBindAttr = propertyDescriptor.Attributes.OfType<PropertyBindAttribute>().FirstOrDefault(); if (modelBindAttr.BindProperty(controllerContext, bindingContext, propertyDescriptor)) return; } base.BindProperty(controllerContext, bindingContext, propertyDescriptor); } } }
در کد بالا ما تمام کلاس هایی را که از PropertyBindAttribute مشتق شده باشند را به DefaultModelBinder اضافه میکنیم. این کد فقط یک بار نوشته میشود و از این به بعد هر بایندر سفارشی که بسازیم به بایندر پیشفرض اضافه خواهد شد.
public class AccountTypeBindAttribute : PropertyBindAttribute { public override bool BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) { if (propertyDescriptor.PropertyType == typeof(byte)) { HttpRequestBase request = controllerContext.HttpContext.Request; byte accountType = (byte)Enum.Parse(typeof(Enums.AccountType), request.Form["AccountType"]); propertyDescriptor.SetValue(bindingContext.Model, accountType); return true; } return false; } }
[AccountTypeBindAttribute] public byte AccountType { get; set; }
ModelBinders.Binders.DefaultBinder = new ExtendedModelBinder();
The field نوع کاربر : must be a number.
@Html.DropDownListFor(model => model.AccountType, new SelectList(Enum.GetNames(typeof(Enums.AccountType))),new Dictionary<string, object>() {{ "data-val", "false" }})
public partial class CustomUserCtrl : System.Web.UI.UserControl { private System.Delegate _delWithParam; private System.Delegate _delNoParam; // برای فراخوانی متدهایی از صفحه که دارای پارامتر هستند public Delegate PageMethodWithParamRef { set { _delWithParam = value; } } // برای فراخوانی متدهایی از صفحه که بدون پارامتر هستند public Delegate PageMethodWithNoParamRef { set { _delNoParam = value; } } protected void Page_Load(object sender, EventArgs e) { } protected void BtnMethodWithParam_Click(object sender, System.EventArgs e) { //Parameter to a method is being made ready object[] obj = new object[1]; obj[0] = “Parameter Value” as object; _delWithParam.DynamicInvoke(obj); } protected void BtnMethowWithoutParam_Click(object sender, System.EventArgs e) { //Invoke a method with no parameter _delNoParam.DynamicInvoke(); } }
public partial class _Default : System.Web.UI.Page { delegate void DelMethodWithParam(string strParam); delegate void DelMethodWithoutParam();
protected void Page_Load(object sender, EventArgs e) { DelMethodWithParam delParam = new DelMethodWithParam(MethodWithParam); // عامل صفحه را به عامل عمومی تعریف شده در یوزر کنترل تخصیص میدهیم this.UserCtrl.PageMethodWithParamRef = delParam; DelMethodWithoutParam delNoParam = new DelMethodWithoutParam(MethodWithNoParam); // عامل صفحه را به عامل عمومی تعریف شده در یوزر کنترل تخصیص میدهیم this.UserCtrl.PageMethodWithNoParamRef = delNoParam; }
// متد دارای پارامتری که قرار است در کنترل فراخوانی شود private void MethodWithParam(string strParam) { Response.Write(“It has parameter: ” + strParam); } // متد بدون پارامتری که قرار است در کنترل فراخوانی شود private void MethodWithNoParam() { Response.Write(“It has no parameter.”); }
public partial class _Default : System.Web.UI.Page { delegate void DelMethodWithParam(string strParam); delegate void DelMethodWithoutParam(); protected void Page_Load(object sender, EventArgs e) { DelMethodWithParam delParam = new DelMethodWithParam(MethodWithParam); // عامل صفحه را به عامل عمومی تعریف شده در یوزر کنترل تخصیص میدهیم this.UserCtrl.PageMethodWithParamRef = delParam; DelMethodWithoutParam delNoParam = new DelMethodWithoutParam(MethodWithNoParam); // عامل صفحه را به عامل عمومی تعریف شده در یوزر کنترل تخصیص میدهیم this.UserCtrl.PageMethodWithNoParamRef = delNoParam; } // متد دارای پارامتری که قرار است در کنترل فراخوانی شود private void MethodWithParam(string strParam) { Response.Write(“It has parameter: ” + strParam); } // متد بدون پارامتری که قرار است در کنترل فراخوانی شود private void MethodWithNoParam() { Response.Write(“It has no parameter.”); } }
یک نکتهی تکمیلی: روش استفاده از خواص راهبری، در بهروزسانیهای دستهای
در همین مثال مطلب جاری، فرض کنید موجودیت کاربر، به همراه خاصیت محاسباتی تعداد کتابها (NumberOfBooks) هم هست:
public class User { // ... public virtual List<Book> Books { get; set; } = new(); public int NumberOfBooks { get; set; } }
اگر سعی کنیم این خاصیت را به صورت زیر بهروز رسانی کنیم که در آن مقدار تعداد کتابها به کمک خاصیت راهبری Books محاسبه شود:
await context.Users.Where(x=> x.Id == 1).ExecuteUpdateAsync(s => s.SetProperty(user => user.NumberOfBooks, user => user.Books.Count()));
به خطای زیر میرسیم:
... could not be translated. Additional information: Translation of member 'Books' on entity type 'User' failed. This commonly occurs when the specified member is unmapped.
عنوان میکند که این خاصیت راهبری، نگاشت نشدهاست. برای رفع این مشکل باید به صورت زیر عمل کرد:
await context.Users .Where(user => user.Id == 1) .Select(user => new { user.NumberOfBooks, BooksCount = user.Books.Count() }) .ExecuteUpdateAsync(s => s.SetProperty(arg => arg.NumberOfBooks, arg => arg.BooksCount));
در اینجا تنها کاری که انجام شده، انجام محاسبات و نگاشتهای لازم، پیش از فراخوانی متد ExecuteUpdateAsync است.
No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale.
public class Movie { public int ID { get; set; } [Column(TypeName = "decimal(18, 2)")] public decimal Price { get; set; } }
protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); foreach (var property in builder.Model.GetEntityTypes() .SelectMany(t => t.GetProperties()) .Where(p => p.ClrType == typeof(decimal) || p.ClrType == typeof(decimal?))) { property.SetColumnType("decimal(18, 6)"); } }
UI
در نهایت نوبت به طراحی و کدنویسی UI میرسد تا بتوانیم محصولات را به کاربر نمایش دهیم. اما قبل از شروع باید موضوعی را یادآوری کنم. اگر به یاد داشته باشید، در کلاس ProductService موجود در لایهی Domain، از طریق یکی از روشهای الگوی Dependency Injection به نام Constructor Injection، فیلدی از نوع IProductRepository را مقداردهی نمودیم. حال زمانی که بخواهیم نمونه ای را از ProductService ایجاد نماییم، باید به عنوان پارامتر ورودی سازنده، شی ایجاد شده از جنس کلاس ProductRepository موجود در لایه Repository را به آن ارسال نماییم. اما از آنجایی که میخواهیم تفکیک پذیری لایهها از بین نرود و UI بسته به نیاز خود، نمونه مورد نیاز را ایجاد نموده و به این کلاس ارسال کند، از ابزارهایی برای این منظور استفاده میکنیم. یکی از این ابزارها StructureMap میباشد که یک Inversion of Control Container یا به اختصار IoC Container نامیده میشود. با Inversion of Control در مباحث بعدی بیشتر آشنا خواهید شد. StructureMap ابزاری است که در زمان اجرا، پارامترهای ورودی سازندهی کلاسهایی را که از الگوی Dependency Injection استفاده نموده اند و قطعا پارامتر ورودی آنها از جنس یک Interface میباشد را، با ایجاد شی مناسب مقداردهی مینماید.
به منظور استفاده از StructureMap در Visual Studio 2012 باید بر روی پروژه UI خود کلیک راست نموده و گزینهی Manage NuGet Packages را انتخاب نمایید. در پنجره ظاهر شده و از سمت چپ گزینهی Online و سپس در کادر جستجوی سمت راست و بالای پنجره واژهی structuremap را جستجو کنید. توجه داشته باشید که باید به اینترنت متصل باشید تا بتوانید Package مورد نظر را نصب نمایید. پس از پایان عمل جستجو، در کادر میانی structuremap ظاهر میشود که میتوانید با انتخاب آن و فشردن کلید Install آن را بر روی پروژه نصب نمایید.
جهت آشنایی بیشتر با NuGet و نصب آن در سایر نسخههای Visual Studio میتوانید به لینکهای زیر رجوع کنید:
کلاسی با نام BootStrapper را با کد زیر به پروژه UI خود اضافه کنید:
using StructureMap; using StructureMap.Configuration.DSL; using SoCPatterns.Layered.Repository; using SoCPatterns.Layered.Model; namespace SoCPatterns.Layered.WebUI { public class BootStrapper { public static void ConfigureStructureMap() { ObjectFactory.Initialize(x => x.AddRegistry<ProductRegistry>()); } } public class ProductRegistry : Registry { public ProductRegistry() { For<IProductRepository>().Use<ProductRepository>(); } } }
ممکن است یک WinUI ایجاد کرده باشید که در این صورت به جای فضای نام SoCPatterns.Layered.WebUI از SoCPatterns.Layered.WinUI استفاده نمایید.
هدف کلاس BootStrapper این است که تمامی وابستگیها را توسط StructureMap در سیستم Register نماید. زمانی که کدهای کلاینت میخواهند به یک کلاس از طریق StructureMap دسترسی داشته باشند، Structuremap وابستگیهای آن کلاس را تشخیص داده و بصورت خودکار پیاده سازی واقعی (Concrete Implementation) آن کلاس را، براساس همان چیزی که ما برایش تعیین کردیم، به کلاس تزریق مینماید. متد ConfigureStructureMap باید در همان لحظه ای که Application آغاز به کار میکند فراخوانی و اجرا شود. با توجه به نوع UI خود یکی از روالهای زیر را انجام دهید:
در WebUI:
فایل Global.asax را به پروژه اضافه کنید و کد آن را بصورت زیر تغییر دهید:
namespace SoCPatterns.Layered.WebUI { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { BootStrapper.ConfigureStructureMap(); } } }
در WinUI:
در فایل Program.cs کد زیر را اضافه کنید:
namespace SoCPatterns.Layered.WinUI { static class Program { [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); BootStrapper.ConfigureStructureMap(); Application.Run(new Form1()); } } }
سپس برای طراحی رابط کاربری، با توجه به نوع UI خود یکی از روالهای زیر را انجام دهید:
در WebUI:
صفحه Default.aspx را باز نموده و کد زیر را به آن اضافه کنید:
<asp:DropDownList AutoPostBack="true" ID="ddlCustomerType" runat="server"> <asp:ListItem Value="0">Standard</asp:ListItem> <asp:ListItem Value="1">Trade</asp:ListItem> </asp:DropDownList> <asp:Label ID="lblErrorMessage" runat="server" ></asp:Label> <asp:Repeater ID="rptProducts" runat="server" > <HeaderTemplate> <table> <tr> <td>Name</td> <td>RRP</td> <td>Selling Price</td> <td>Discount</td> <td>Savings</td> </tr> <tr> <td colspan="5"><hr /></td> </tr> </HeaderTemplate> <ItemTemplate> <tr> <td><%# Eval("Name") %></td> <td><%# Eval("RRP")%></td> <td><%# Eval("SellingPrice") %></td> <td><%# Eval("Discount") %></td> <td><%# Eval("Savings") %></td> </tr> </ItemTemplate> <FooterTemplate> </table> </FooterTemplate> </asp:Repeater>
در WinUI:
فایل Form1.Designer.cs را باز نموده و کد آن را بصورت زیر تغییر دهید:
#region Windows Form Designer generated code /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { this.cmbCustomerType = new System.Windows.Forms.ComboBox(); this.dgvProducts = new System.Windows.Forms.DataGridView(); this.colName = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.colRrp = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.colSellingPrice = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.colDiscount = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.colSavings = new System.Windows.Forms.DataGridViewTextBoxColumn(); ((System.ComponentModel.ISupportInitialize)(this.dgvProducts)).BeginInit(); this.SuspendLayout(); // // cmbCustomerType // this.cmbCustomerType.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; this.cmbCustomerType.FormattingEnabled = true; this.cmbCustomerType.Items.AddRange(new object[] { "Standard", "Trade"}); this.cmbCustomerType.Location = new System.Drawing.Point(12, 90); this.cmbCustomerType.Name = "cmbCustomerType"; this.cmbCustomerType.Size = new System.Drawing.Size(121, 21); this.cmbCustomerType.TabIndex = 3; // // dgvProducts // this.dgvProducts.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; this.dgvProducts.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { this.colName, this.colRrp, this.colSellingPrice, this.colDiscount, this.colSavings}); this.dgvProducts.Location = new System.Drawing.Point(12, 117); this.dgvProducts.Name = "dgvProducts"; this.dgvProducts.Size = new System.Drawing.Size(561, 206); this.dgvProducts.TabIndex = 2; // // colName // this.colName.DataPropertyName = "Name"; this.colName.HeaderText = "Product Name"; this.colName.Name = "colName"; this.colName.ReadOnly = true; // // colRrp // this.colRrp.DataPropertyName = "Rrp"; this.colRrp.HeaderText = "RRP"; this.colRrp.Name = "colRrp"; this.colRrp.ReadOnly = true; // // colSellingPrice // this.colSellingPrice.DataPropertyName = "SellingPrice"; this.colSellingPrice.HeaderText = "Selling Price"; this.colSellingPrice.Name = "colSellingPrice"; this.colSellingPrice.ReadOnly = true; // // colDiscount // this.colDiscount.DataPropertyName = "Discount"; this.colDiscount.HeaderText = "Discount"; this.colDiscount.Name = "colDiscount"; // // colSavings // this.colSavings.DataPropertyName = "Savings"; this.colSavings.HeaderText = "Savings"; this.colSavings.Name = "colSavings"; this.colSavings.ReadOnly = true; // // Form1 // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(589, 338); this.Controls.Add(this.cmbCustomerType); this.Controls.Add(this.dgvProducts); this.Name = "Form1"; this.Text = "Form1"; ((System.ComponentModel.ISupportInitialize)(this.dgvProducts)).EndInit(); this.ResumeLayout(false); } #endregion private System.Windows.Forms.ComboBox cmbCustomerType; private System.Windows.Forms.DataGridView dgvProducts; private System.Windows.Forms.DataGridViewTextBoxColumn colName; private System.Windows.Forms.DataGridViewTextBoxColumn colRrp; private System.Windows.Forms.DataGridViewTextBoxColumn colSellingPrice; private System.Windows.Forms.DataGridViewTextBoxColumn colDiscount; private System.Windows.Forms.DataGridViewTextBoxColumn colSavings;
سپس در Code Behind، با توجه به نوع UI خود یکی از روالهای زیر را انجام دهید:
در WebUI:
وارد کد نویسی صفحه Default.aspx شده و کد آن را بصورت زیر تغییر دهید:
using System; using System.Collections.Generic; using SoCPatterns.Layered.Model; using SoCPatterns.Layered.Presentation; using SoCPatterns.Layered.Service; using StructureMap; namespace SoCPatterns.Layered.WebUI { public partial class Default : System.Web.UI.Page, IProductListView { private ProductListPresenter _productListPresenter; protected void Page_Init(object sender, EventArgs e) { _productListPresenter = new ProductListPresenter(this,ObjectFactory.GetInstance<Service.ProductService>()); this.ddlCustomerType.SelectedIndexChanged += delegate { _productListPresenter.Display(); }; } protected void Page_Load(object sender, EventArgs e) { if(!Page.IsPostBack) _productListPresenter.Display(); } public void Display(IList<ProductViewModel> products) { rptProducts.DataSource = products; rptProducts.DataBind(); } public CustomerType CustomerType { get { return (CustomerType) int.Parse(ddlCustomerType.SelectedValue); } } public string ErrorMessage { set { lblErrorMessage.Text = String.Format("<p><strong>Error:</strong><br/>{0}</p>", value); } } } }
در WinUI:
وارد کدنویسی Form1 شوید و کد آن را بصورت زیر تغییر دهید:
using System; using System.Collections.Generic; using System.Windows.Forms; using SoCPatterns.Layered.Model; using SoCPatterns.Layered.Presentation; using SoCPatterns.Layered.Service; using StructureMap; namespace SoCPatterns.Layered.WinUI { public partial class Form1 : Form, IProductListView { private ProductListPresenter _productListPresenter; public Form1() { InitializeComponent(); _productListPresenter = new ProductListPresenter(this, ObjectFactory.GetInstance<Service.ProductService>()); this.cmbCustomerType.SelectedIndexChanged += delegate { _productListPresenter.Display(); }; dgvProducts.AutoGenerateColumns = false; cmbCustomerType.SelectedIndex = 0; } public void Display(IList<ProductViewModel> products) { dgvProducts.DataSource = products; } public CustomerType CustomerType { get { return (CustomerType)cmbCustomerType.SelectedIndex; } } public string ErrorMessage { set { MessageBox.Show( String.Format("Error:{0}{1}", Environment.NewLine, value)); } } } }
با توجه به کد فوق، نمونه ای را از کلاس ProductListPresenter، در لحظهی نمونه سازی اولیهی کلاس UI، ایجاد نمودیم. با استفاده از متد ObjectFactory.GetInstance مربوط به StructureMap، نمونه ای از کلاس ProductService ایجاد شده است و به سازندهی کلاس ProductListPresenter ارسال گردیده است. در مورد Structuremap در مباحث بعدی با جزئیات بیشتری صحبت خواهم کرد. پیاده سازی معماری لایه بندی در اینجا به پایان رسید.
اما اصلا نگران نباشید، شما فقط پرواز کوتاه و مختصری را بر فراز کدهای معماری لایه بندی داشته اید که این فقط یک دید کلی را به شما در مورد این معماری داده است. این معماری هنوز جای زیادی برای کار دارد، اما در حال حاضر شما یک Applicaion با پیوند ضعیف (Loosely Coupled) بین لایهها دارید که دارای قابلیت تست پذیری قوی، نگهداری و پشتیبانی آسان و تفکیک پذیری قدرتمند بین اجزای آن میباشد. شکل زیر تعامل بین لایهها و وظایف هر یک از آنها را نمایش میدهد.
در این مطلب نحوهی یکپارچه سازی Windows Authentication دومینهای ویندوزی را با IdentityServer بررسی میکنیم.
کار با تامین کنندههای هویت خارجی
اغلب کاربران، دارای اکانت ثبت شدهای در جای دیگری نیز هستند و شاید آنچنان نسبت به ایجاد اکانت جدیدی در IDP ما رضایت نداشته باشند. برای چنین حالتی، امکان یکپارچه سازی IdentityServer با انواع و اقسام IDPهای دیگر نیز پیش بینی شدهاست. در اینجا تمام اینها، روشهای مختلفی برای ورود به سیستم، توسط یک کاربر هستند. کاربر ممکن است توسط اکانت خود در شبکهی ویندوزی به سیستم وارد شود و یا توسط اکانت خود در گوگل، اما در نهایت از دیدگاه سیستم ما، یک کاربر مشخص بیشتر نیست.
نگاهی به شیوهی پشتیبانی از تامین کنندههای هویت خارجی توسط Quick Start UI
Quick Start UI ای را که در «قسمت چهارم - نصب و راه اندازی IdentityServer» به IDP اضافه کردیم، دارای کدهای کار با تامین کنندههای هویت خارجی نیز میباشد. برای بررسی آن، کنترلر DNT.IDP\Controllers\Account\ExternalController.cs را باز کنید:
[HttpGet] public async Task<IActionResult> Challenge(string provider, string returnUrl) [HttpGet] public async Task<IActionResult> Callback()
در اکشن متد Callback، اطلاعات کاربر از کوکی رمزنگاری شدهی متد Challenge استخراج میشود و بر اساس آن هویت کاربر در سطح IDP شکل میگیرد.
فعالسازی Windows Authentication برای ورود به IDP
در ادامه میخواهیم برنامه را جهت استفادهی از اکانت ویندوزی کاربران جهت ورود به IDP تنظیم کنیم. برای این منظور باید نکات مطلب «فعالسازی Windows Authentication در برنامههای ASP.NET Core 2.0» را پیشتر مطالعه کرده باشید.
پس از فعالسازی Windows Authentication در برنامه، اگر برنامهی IDP را توسط IIS و یا IIS Express و یا HttpSys اجرا کنید، دکمهی جدید Windows را در قسمت External Login مشاهده خواهید کرد:
یک نکته: برچسب این دکمه را در حالت استفادهی از مشتقات IIS، به صورت زیر میتوان تغییر داد:
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.Configure<IISOptions>(iis => { iis.AuthenticationDisplayName = "Windows Account"; iis.AutomaticAuthentication = false; });
اتصال کاربر وارد شدهی از یک تامین کنندهی هویت خارجی به کاربران بانک اطلاعاتی برنامه
سازندهی کنترلر DNT.IDP\Controllers\Account\ExternalController.cs نیز همانند کنترلر Account که آنرا در قسمت قبل تغییر دادیم، از TestUserStore استفاده میکند:
public ExternalController( IIdentityServerInteractionService interaction, IClientStore clientStore, IEventService events, TestUserStore users = null) { _users = users ?? new TestUserStore(TestUsers.Users); _interaction = interaction; _clientStore = clientStore; _events = events; }
private readonly IUsersService _usersService; public ExternalController( // ... IUsersService usersService) { // ... _usersService = usersService; }
الف) در متد FindUserFromExternalProvider
سطر قدیمی
var user = _users.FindByExternalProvider(provider, providerUserId);
var user = await _usersService.GetUserByProviderAsync(provider, providerUserId);
private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims)> FindUserFromExternalProvider(AuthenticateResult result)
private TestUser AutoProvisionUser(string provider, string providerUserId, IEnumerable<Claim> claims) { var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList()); return user; }
مفهوم «Provisioning a user» در اینجا به معنای درخواست از کاربر، جهت ورود اطلاعاتی مانند نام و نام خانوادگی او است که پیشتر صفحهی ثبت کاربر جدید را برای این منظور در قسمت قبل ایجاد کردهایم و از آن میشود در اینجا استفادهی مجدد کرد. بنابراین در ادامه، گردش کاری ورود کاربر از طریق تامین کنندهی هویت خارجی را به نحوی اصلاح میکنیم که کاربر جدید، ابتدا به صفحهی ثبت نام وارد شود و اطلاعات تکمیلی خود را وارد کند؛ سپس به صورت خودکار به متد Callback بازگشته و ادامهی مراحل را طی نماید:
در اکشن متد نمایش صفحهی ثبت نام کاربر جدید، متد RegisterUser تنها آدرس بازگشت به صفحهی قبلی را دریافت میکند:
[HttpGet] public IActionResult RegisterUser(string returnUrl)
namespace DNT.IDP.Controllers.UserRegistration { public class RegistrationInputModel { public string ReturnUrl { get; set; } public string Provider { get; set; } public string ProviderUserId { get; set; } public bool IsProvisioningFromExternal => !string.IsNullOrWhiteSpace(Provider); } }
namespace DNT.IDP.Controllers.Account { [SecurityHeaders] [AllowAnonymous] public class ExternalController : Controller { public async Task<IActionResult> Callback() { var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme); var returnUrl = result.Properties.Items["returnUrl"] ?? "~/"; var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result); if (user == null) { // user = AutoProvisionUser(provider, providerUserId, claims); var returnUrlAfterRegistration = Url.Action("Callback", new { returnUrl = returnUrl }); var continueWithUrl = Url.Action("RegisterUser", "UserRegistration" , new { returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId }); return Redirect(continueWithUrl); }
returnUrl ارسالی به اکشن متد RegisterUser، به همین اکشن متد جاری اشاره میکند. یعنی کاربر پس از تکمیل اطلاعات و اینبار نال نبودن user او، گردش کاری جاری را ادامه خواهد داد.
در ادامه نیاز است امضای متد نمایش صفحهی ثبت نام را نیز بر این اساس اصلاح کنیم:
namespace DNT.IDP.Controllers.UserRegistration { public class UserRegistrationController : Controller { [HttpGet] public IActionResult RegisterUser(RegistrationInputModel registrationInputModel) { var vm = new RegisterUserViewModel { ReturnUrl = registrationInputModel.ReturnUrl, Provider = registrationInputModel.Provider, ProviderUserId = registrationInputModel.ProviderUserId }; return View(vm); }
namespace DNT.IDP.Controllers.UserRegistration { public class RegisterUserViewModel : RegistrationInputModel {
اکنون نیاز است RegisterUser.cshtml را اصلاح کنیم:
- ابتدا دو فیلد مخفی دیگر Provider و ProviderUserId را نیز به این فرم اضافه میکنیم؛ از این جهت که در حین postback به سمت سرور به مقادیر آنها نیاز داریم:
<inputtype="hidden"asp-for="ReturnUrl"/> <inputtype="hidden"asp-for="Provider"/> <inputtype="hidden"asp-for="ProviderUserId"/>
@if (!Model.IsProvisioningFromExternal) { <div> <label asp-for="Password"></label> <input type="password" placeholder="Password" asp-for="Password" autocomplete="off"> </div> }
پس از آن نیاز است اطلاعات اکانت خارجی این کاربر را در حین postback و ارسال اطلاعات به اکشن متد RegisterUser، ثبت کنیم:
namespace DNT.IDP.Controllers.UserRegistration { public class UserRegistrationController : Controller { [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> RegisterUser(RegisterUserViewModel model) { // ... if (model.IsProvisioningFromExternal) { userToCreate.UserLogins.Add(new UserLogin { LoginProvider = model.Provider, ProviderKey = model.ProviderUserId }); } // add it through the repository await _usersService.AddUserAsync(userToCreate); // ... } }
همچنین در ادامهی این اکشن متد، کار لاگین خودکار کاربر نیز انجام میشود. با توجه به اینکه پس از ثبت اطلاعات کاربر نیاز است مجددا گردش کاری اکشن متد Callback طی شود، این لاگین خودکار را نیز برای حالت ورود از طریق تامین کنندهی خارجی، غیرفعال میکنیم:
if (!model.IsProvisioningFromExternal) { // log the user in // issue authentication cookie with subject ID and username var props = new AuthenticationProperties { IsPersistent = false, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; await HttpContext.SignInAsync(userToCreate.SubjectId, userToCreate.Username, props); }
بررسی ورود به سیستم توسط دکمهی External Login -> Windows
پس از این تغییرات، اکنون در حین ورود به سیستم (تصویر ابتدای بحث در قسمت فعالسازی اعتبارسنجی ویندوزی)، گزینهی External Login -> Windows را انتخاب میکنیم. بلافاصله به صفحهی ثبتنام کاربر هدایت خواهیم شد:
همانطور که مشاهده میکنید، IDP اکانت ویندوزی جاری را تشخیص داده و فعال کردهاست. همچنین در اینجا خبری از ورود کلمهی عبور هم نیست.
پس از تکمیل این فرم، بلافاصله کار ثبت اطلاعات کاربر و هدایت خودکار به برنامهی MVC Client انجام میشود.
در ادامه از برنامهی کلاینت logout کنید. اکنون در صفحهی login مجددا بر روی دکمهی Windows کلیک نمائید. اینبار بدون پرسیدن سؤالی، لاگین شده و وارد برنامهی کلاینت خواهید شد؛ چون پیشتر کار اتصال اکانت ویندوزی به اکانتی در سمت 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 وارد کنید.
یک نکته: برای آزمایش برنامه جهت فعالسازی Windows Authentication بهتر است برنامهی IDP را توسط IIS Express اجرا کنید و یا اگر از IIS Express استفاده نمیکنید، نیاز است UseHttpSys فایل program.cs را مطابق توضیحات «یک نکتهی تکمیلی: UseHttpSys و استفادهی از HTTPS» فعال کنید.
MVC Scaffolding #1
کل سری ASP.NET MVC
به همراه کل سری EF Code First
MVC Scaffolding چیست؟
MVC Scaffolding ابزاری است برای تولید خودکار کدهای «اولیه» برنامه، جهت بالا بردن سرعت تولید برنامههای ASP.NET MVC مبتنی بر EF Code First.
بررسی مقدماتی MVC Scaffolding
امکان اجرای ابزار MVC Scaffolding از دو طریق دستورات خط فرمان Powershell و یا صفحه دیالوگ افزودن یک کنترلر در پروژههای ASP.NET MVC وجود دارد. در ابتدا حالت ساده و ابتدایی استفاده از صفحه دیالوگ افزودن یک کنترلر را بررسی خواهیم کرد تا با کلیات این فرآیند آشنا شویم. سپس در ادامه به خط فرمان Powershell که اصل توانمندیها و قابلیتهای سفارشی MVC Scaffolding در آن قرار دارد، خواهیم پرداخت.
برای این منظور یک پروژه جدید MVC را آغاز کنید؛ ابزارهای مقدماتی MVC Scaffolding از اولین به روز رسانی ASP.NET MVC3 به بعد با VS.NET یکپارچه هستند.
ابتدا کلاس زیر را به پوشه مدلهای برنامه اضافه کنید:
using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace MvcApplication1.Models { public class Task { public int Id { set; get; } [Required] public string Name { set; get; } [DisplayName("Due Date")] public DateTime? DueDate { set; get; } [DisplayName("Is Complete")] public bool IsComplete { set; get; } [StringLength(450)] public string Description { set; get; } } }
همانطور که ملاحظه میکنید در قسمت قالبها، تولید کنترلرهایی با اکشن متدهای ثبت و نمایش اطلاعات مبتنی بر EF Code First انتخاب شده است. کلاس مدل نیز به کلاس Task فوق تنظیم گردیده و در زمان انتخاب DbContext مرتبط، گزینه new data context را انتخاب کرده و نام پیش فرض آنرا پذیرفتهایم. زمانیکه بر روی دکمه Add کلیک کنیم، اتفاقات ذیل رخ خواهند داد:
الف) کنترلر جدید TasksController.cs به همراه تمام کدهای Insert/Update/Delete/Display مرتبط تولید خواهد شد.
ب) کلاس DbContext خودکاری به نام MvcApplication1Context.cs در پوشه مدلهای برنامه ایجاد میگردد تا کلاس Task را در معرض دید EF Code first قرار دهد. (همانطور که عنوان شد یکی از پیشنیازهای بحث Scaffolding آشنایی با EF Code first است)
ج) در پوشه Views\Tasks، پنج View جدید را جهت مدیریت فرآیندهای نمایش صفحات Insert، حذف، ویرایش، نمایش و غیره تهیه میکند.
د) فایل وب کانفیگ برنامه جهت درج رشته اتصالی به بانک اطلاعاتی تغییر کرده است. حالت پیش فرض آن استفاده از SQL CE است و برای استفاده از آن نیاز است قسمت 15 سری EF سایت جاری را پیشتر مطالعه کرده باشید (به چه اسمبلیهای دیگری مانند System.Data.SqlServerCe.dll برای اجرا نیاز است و چطور باید اتصال به بانک اطلاعاتی را تنظیم کرد)
معایب:
کیفیت کد تولیدی پیش فرض قابل قبول نیست:
- DbContext در سطح یک کنترلر وهله سازی شده و الگوی Context Per Request در اینجا بکارگرفته نشده است. واقعیت یک برنامه ASP.NET MVC کامل، داشتن چندین Partial View تغدیه شونده از کنترلرهای مختلف در یک صفحه واحد است. اگر قرار باشد به ازای هر کدام یکبار DbContext وهله سازی شود یعنی به ازای هر صفحه چندین بار اتصال به بانک اطلاعاتی باید برقرار شود که سربار زیادی را به همراه دارد. (قسمت 12 سری EF سایت جاری)
- اکشن متدها حاوی منطق پیاده سازی اعمال CRUD یا همان Create/Update/Delete هستند. به عبارتی از یک لایه سرویس برای خلوت کردن اکشن متدها استفاده نشده است.
- از ViewModel تعریف شدهای به نام Task هم به عنوان Domain model و هم ViewModel استفاده شده است. یک کلاس متناظر با جداول بانک اطلاعاتی میتواند شامل فیلدهای بیشتری باشد و نباید آنرا مستقیما در معرض دید یک View قرار داد (خصوصا از لحاظ مسایل امنیتی).
مزیتها:
قسمت عمدهای از کارهای «اولیه» تهیه یک کنترلر و همچنین Viewهای مرتبط به صورت خودکار انجام شدهاند. کارهای اولیهای که با هر روش و الگوی شناخته شدهای قصد پیاده سازی آنها را داشته باشید، وقت زیادی را به خود اختصاص داده و نهایتا آنچنان تفاوت عمدهای هم با کدهای تولیدی در اینجا نخواهند داشت. حداکثر فرمهای آنرا بخواهید با jQuery Ajax پیاده سازی کنید یا کنترلهای پیش فرض را با افزونههای jQuery غنی سازی نمائید. اما شروع کار و کدهای اولیه چیزی بیشتر از این نیست.
نصب بسته اصلی MVC Scaffolding توسط NuGet
بسته اصلی MVC Scaffolding را با استفاده از دستور خط فرمان Powershell ذیل، از طریق منوی Tools، گزینه Library package manager و انتخاب Package manager console میتوان به پروژه خود اضافه کرد:
Install-Package MvcScaffolding
Attempting to resolve dependency 'T4Scaffolding'. Attempting to resolve dependency 'T4Scaffolding.Core'. Attempting to resolve dependency 'EntityFramework'. Successfully installed 'T4Scaffolding.Core 1.0.0'. Successfully installed 'T4Scaffolding 1.0.8'. Successfully installed 'MvcScaffolding 1.0.9'. Successfully added 'T4Scaffolding.Core 1.0.0' to MvcApplication1. Successfully added 'T4Scaffolding 1.0.8' to MvcApplication1. Successfully added 'MvcScaffolding 1.0.9' to MvcApplication1.
پس از اینکه بسته MvcScaffolding به پروژه جاری اضافه شد، همان مراحل قبل را که توسط صفحه دیالوگ افزودن یک کنترلر انجام دادیم، اینبار به کمک دستور ذیل نیز میتوان پیاده سازی کرد:
Scaffold Controller Task
نکته و مزیت مهم دیگری که در اینجا در دسترس میباشد، سوئیچهای خط فرمانی است که به همراه صفحه دیالوگ افزودن یک کنترلر وجود ندارند. برای مثال دستور Scaffold Controller را تایپ کرده و سپس یک خط تیره را اضافه کنید. اکنون دکمه tab را مجددا بفشارید. منویی ظاهر خواهد شد که بیانگر سوئیچهای قابل استفاده است.
برای مثال اگر بخواهیم دستور Scaffold Controller Task را با جزئیات اولیه کاملتری ذکر کنیم، مانند تعیین نام دقیق کلاس مدل و کنترلر تولیدی به همراه نام دیگری برای DbContext مرتبط، خواهیم داشت:
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext
بهبود مقدماتی کیفیت کد تولیدی MVC Scaffolding
در همان کنسول پاروشل NuGet، کلید up arrow را فشار دهید تا مجددا دستور قبلی اجرا شده ظاهر شود. اینبار دستور قبلی را با سوئیچ جدید Repository (استفاده از الگوی مخزن) اجرا کنید:
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository -Force
public class TasksController : Controller { private readonly ITaskRepository taskRepository; // If you are using Dependency Injection, you can delete the following constructor public TasksController() : this(new TaskRepository()) { } public TasksController(ITaskRepository taskRepository) { this.taskRepository = taskRepository; }
public interface ITaskRepository : IDisposable { IQueryable<Task> All { get; } IQueryable<Task> AllIncluding(params Expression<Func<Task, object>>[] includeProperties); Task Find(int id); void InsertOrUpdate(Task task); void Delete(int id); void Save(); }
پیاده سازی این اینترفیس در حالت متد Save آن شامل فراخوانی context.SaveChanges است. این مورد باید به الگوی واحد کار (که در اینجا تعریف نشده) منتقل شود. زیرا در یک دنیای واقعی حاصل کار بر روی چندین موجودیت باید در یک تراکنش ذخیره شوند و قرارگیری متد Save داخل کلاس مخزن یا سرویس برنامه، مخزنهای تعریف شده را تک موجودیتی میکند.
اما در کل با توجه به اینکه پیاده سازی منطق کار با موجودیتها به کلاسهای مخزن واگذار شدهاند و کنترلرها به این نحو خلوتتر گردیدهاند، یک مرحله پیشرفت محسوب میشود.
@Number @code { [Parameter] public string Number { get; set; } protected override Task OnInitializedAsync() { var persianDic = new Dictionary<char, char> { {'0','۰'}, {'1','۱'}, {'2','۲'}, {'3','۳'}, {'4','۴'}, {'5','۵'}, {'6','۶'}, {'7','۷'}, {'8','۸'}, {'9','۹'}, }; var number = Number.ToString(); var ech = number.ToCharArray(); for (int i = 0; i < ech.Length; i++) { persianDic.TryGetValue(ech[i], out char pch); if (pch == null) continue; ech[i] = pch; } Number = new string(ech); return base.OnInitializedAsync(); } }
... @foreach (var item in _items) { <tr> <td class="h6 text-color-1">@item.Title</td> <td> <PersianNumber Number="@item.Price.ToString()"/> ریال</td> </tr> } ...
برای بهبود این وضعیت میتوان مرحلهی دومی را نیز به این فرآیند لاگین افزود؛ پس از اینکه مشخص شد کاربر وارد شدهی به سایت، دارای اکانتی در IDP ما است، کدی را به آدرس ایمیل او ارسال میکنیم. اگر این ایمیل واقعا متعلق به این شخص است، بنابراین قادر به دسترسی به آن، خواندن و ورود آن به برنامهی ما نیز میباشد. این اعتبارسنجی دو مرحلهای را میتوان به عملیات لاگین متداول از طریق ورود نام کاربری و کلمهی عبور در IDP ما نیز اضافه کرد.
تنظیم میانافزار Cookie Authentication
مرحلهی اول ایجاد گردش کاری اعتبارسنجی دو مرحلهای، فعالسازی میانافزار Cookie Authentication در برنامهی IDP است. برای این منظور به کلاس Startup آن مراجعه کرده و AddCookie را اضافه میکنیم:
namespace DNT.IDP { public class Startup { public const string TwoFactorAuthenticationScheme = "idsrv.2FA"; public void ConfigureServices(IServiceCollection services) { // ... services.AddAuthentication() .AddCookie(authenticationScheme: TwoFactorAuthenticationScheme) .AddGoogle(authenticationScheme: "Google", configureOptions: options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; options.ClientId = Configuration["Authentication:Google:ClientId"]; options.ClientSecret = Configuration["Authentication:Google:ClientSecret"]; }); }
اصلاح اکشن متد Login برای هدایت کاربر به صفحهی ورود اطلاعات کد موقتی
تا این مرحله، در اکشن متد Login کنترلر Account، اگر کاربر، اطلاعات هویتی خود را صحیح وارد کند، به سیستم وارد خواهد شد. برای لغو این عملکرد پیشفرض، کدهای HttpContext.SignInAsync آنرا حذف کرده و با Redirect به اکشن متد نمایش صفحهی ورود کد موقتی ارسال شدهی به آدرس ایمیل کاربر، جایگزین میکنیم.
namespace DNT.IDP.Controllers.Account { [SecurityHeaders] [AllowAnonymous] public class AccountController : Controller { [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(LoginInputModel model, string button) { // ... if (ModelState.IsValid) { if (await _usersService.AreUserCredentialsValidAsync(model.Username, model.Password)) { var user = await _usersService.GetUserByUsernameAsync(model.Username); var id = new ClaimsIdentity(); id.AddClaim(new Claim(JwtClaimTypes.Subject, user.SubjectId)); await HttpContext.SignInAsync(scheme: Startup.TwoFactorAuthenticationScheme, principal: new ClaimsPrincipal(id)); await _twoFactorAuthenticationService.SendTemporaryCodeAsync(user.SubjectId); var redirectToAdditionalFactorUrl = Url.Action("AdditionalAuthenticationFactor", new { returnUrl = model.ReturnUrl, rememberLogin = model.RememberLogin }); // request for a local page if (Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(redirectToAdditionalFactorUrl); } if (string.IsNullOrEmpty(model.ReturnUrl)) { return Redirect("~/"); } // user might have clicked on a malicious link - should be logged throw new Exception("invalid return URL"); } await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials")); ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage); } // something went wrong, show form with error var vm = await BuildLoginViewModelAsync(model); return View(vm); }
- سپس بر اساس Id این کاربر، یک ClaimsIdentity تشکیل میشود.
- در ادامه با فراخوانی متد SignInAsync بر روی این ClaimsIdentity، یک کوکی رمزنگاری شده را با scheme تعیین شده که با authenticationScheme تنظیم شدهی در کلاس آغازین برنامه تطابق دارد، ایجاد میکنیم.
await HttpContext.SignInAsync(scheme: Startup.TwoFactorAuthenticationScheme, principal: new ClaimsPrincipal(id));
public interface ITwoFactorAuthenticationService { Task SendTemporaryCodeAsync(string subjectId); Task<bool> IsValidTemporaryCodeAsync(string subjectId, string code); }
- متد IsValidTemporaryCodeAsync، کد دریافت شدهی از کاربر را با نمونهی موجود در بانک اطلاعاتی مقایسه و اعتبار آنرا اعلام میکند.
ایجاد اکشن متد AdditionalAuthenticationFactor و View مرتبط با آن
پس از ارسال کد موقتی به کاربر، کاربر را به صورت خودکار به اکشن متد جدید AdditionalAuthenticationFactor هدایت میکنیم تا این کد موقتی را که به صورت ایمیل (و یا در اینجا با مشاهدهی لاگ برنامه)، دریافت کردهاست، وارد کند. همچنین returnUrl را نیز به این اکشن متد جدید ارسال میکنیم تا بدانیم پس از ورود موفق کد موقتی توسط کاربر، او را باید در ادامهی این گردش کاری به کجا هدایت کنیم. بنابراین قسمت بعدی کار، ایجاد این اکشن متد و تکمیل View آن است.
ViewModel ای که بیانگر ساختار View مرتبط است، چنین تعریفی را دارد:
using System.ComponentModel.DataAnnotations; namespace DNT.IDP.Controllers.Account { public class AdditionalAuthenticationFactorViewModel { [Required] public string Code { get; set; } public string ReturnUrl { get; set; } public bool RememberLogin { get; set; } } }
سپس اکشن متد AdditionalAuthenticationFactor در حالت Get، این View را نمایش میدهد و در حالت Post، اطلاعات آنرا از کاربر دریافت خواهد کرد:
namespace DNT.IDP.Controllers.Account { public class AccountController : Controller { [HttpGet] public IActionResult AdditionalAuthenticationFactor(string returnUrl, bool rememberLogin) { // create VM var vm = new AdditionalAuthenticationFactorViewModel { RememberLogin = rememberLogin, ReturnUrl = returnUrl }; return View(vm); } [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> AdditionalAuthenticationFactor( AdditionalAuthenticationFactorViewModel model) { if (!ModelState.IsValid) { return View(model); } // read identity from the temporary cookie var info = await HttpContext.AuthenticateAsync(Startup.TwoFactorAuthenticationScheme); var tempUser = info?.Principal; if (tempUser == null) { throw new Exception("2FA error"); } // ... check code for user if (!await _twoFactorAuthenticationService.IsValidTemporaryCodeAsync(tempUser.GetSubjectId(), model.Code)) { ModelState.AddModelError("code", "2FA code is invalid."); return View(model); } // login the user AuthenticationProperties props = null; if (AccountOptions.AllowRememberLogin && model.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; } // issue authentication cookie for user var user = await _usersService.GetUserBySubjectIdAsync(tempUser.GetSubjectId()); await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username)); await HttpContext.SignInAsync(user.SubjectId, user.Username, props); // delete temporary cookie used for 2FA await HttpContext.SignOutAsync(Startup.TwoFactorAuthenticationScheme); if (_interaction.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(model.ReturnUrl); } return Redirect("~/"); }
- فراخوانی HttpContext.SignInAsync با اسکیمای مشخص شده، یک کوکی رمزنگاری شده را در اکشن متد Login ایجاد میکند. اکنون در اینجا با استفاده از متد HttpContext.AuthenticateAsync و ذکر همان اسکیما، میتوانیم به محتوای این کوکی رمزنگاری شده دسترسی داشته باشیم و از طریق آن، Id کاربر را استخراج کنیم.
- اکنون که این Id را داریم و همچنین Code موقتی نیز از طرف کاربر ارسال شدهاست، آنرا به متد IsValidTemporaryCodeAsync که پیشتر در مورد آن توضیح دادیم، ارسال کرده و اعتبارسنجی میکنیم.
- در آخر این کوکی رمزنگاری شده را با فراخوانی متد HttpContext.SignOutAsync، حذف و سپس یک کوکی جدید را بر اساس اطلاعات هویت کاربر، توسط متد HttpContext.SignInAsync ایجاد و ثبت میکنیم تا کاربر بتواند بدون مشکل وارد سیستم شود.
View متناظر با آن نیز در فایل src\IDP\DNT.IDP\Views\Account\AdditionalAuthenticationFactor.cshtml، به صورت زیر تعریف شدهاست تا کد موقتی را به همراه آدرس بازگشت پس از ورود آن، به سمت سرور ارسال کند:
@model AdditionalAuthenticationFactorViewModel <div> <div class="page-header"> <h1>2-Factor Authentication</h1> </div> @Html.Partial("_ValidationSummary") <div class="row"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">Input your 2FA code</h3> </div> <div class="panel-body"> <form asp-route="Login"> <input type="hidden" asp-for="ReturnUrl" /> <input type="hidden" asp-for="RememberLogin" /> <fieldset> <div class="form-group"> <label asp-for="Code"></label> <input class="form-control" placeholder="Code" asp-for="Code" autofocus> </div> <div class="form-group"> <button class="btn btn-primary">Submit code</button> </div> </fieldset> </form> </div> </div> </div> </div>
آزمایش برنامه جهت بررسی اعتبارسنجی دو مرحلهای
پس از طی این مراحل، اعتبارسنجی دو مرحلهای در برنامه فعال شدهاست. اکنون برای آزمایش آن، برنامهها را اجرا میکنیم. پس از لاگین، صفحهی زیر نمایش داده میشود:
همچنین کد موقتی این مرحله را نیز در لاگهای برنامه مشاهده میکنید:
پس از ورود آن، کار اعتبارسنجی نهایی آن انجام شده و سپس بلافاصله به برنامهی MVC Client هدایت میشویم.
اضافه کردن اعتبارسنجی دو مرحلهای به قسمت ورود از طریق تامین کنندههای هویت خارجی
دقیقا همین مراحل را نیز به اکشن متد Callback کنترلر ExternalController اضافه میکنیم. در این اکشن متد، تا قسمت کدهای مشخص شدن user آن که از اکانت خارجی وارد شدهاست، با قبل یکی است. پس از آن تمام کدهای لاگین شخص به برنامه را از اینجا حذف و به اکشن متد جدید AdditionalAuthenticationFactor در همین کنترلر منتقل میکنیم.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.