SQL Injection چیست؟
با LINQ نه ولی اگر دستی SP بنویسید ممکنه این SP شما نا امن باشد. یک مثال در اینجا:
در این مطلب نحوهی یکپارچه سازی 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» فعال کنید.
یک نکتهی تکمیلی: روش لغو صف درخواستهای مکرر fetch ارسالی به سمت سرور
ورودی جستجوی بالای صفحه را درنظر بگیرید که بهازای هربار ورود حرفی، یک درخواست fetch جدید را به سمت سرور ارسال میکند تا نتایج جستجوی حاصل را دریافت کند. مشکل اینجاست که ما تنها به آخرین درخواست fetch ارسال شدهی به سمت سرور نیاز داریم و نه به تمام درخواستهای دیگری که صادر شدهاند. به همین جهت این صف درخواستهای fetch قبلی، غیربهینه بوده و ترافیک بالایی را سبب میشوند. یک روش مواجه شدن با این مساله، استفاده از مفهومی به نام debounce است که در پشت صحنه، از یک تایمر استفاده میکند و فقط هر چند ثانیه یکبار، یک درخواست جدید را به همراه آخرین متن ورودی، به سمت سرور ارسال خواهد کرد. راه دیگری هم برای مواجه شدن با این مشکل، در مرورگرهای جدید پیشبینی شدهاست که AbortController نام دارد. با استفاده از آن میتوان «سیگنالی» را به صف درخواستهای پرتعداد fetch قبلی حاصل از ورود اطلاعات کاربر ارسال کنیم که ... «لغو شوید» و به سمت سرور ارسال نشوید.
برای توضیح بهتر آن، به مثال زیر دقت کنید:
<!DOCTYPE html> <html> <body> <input id="search" type="number" /> <script> const results = []; const search = document.getElementById("search"); let controller = new AbortController(); let signal = controller.signal; const onChange = () => { const value = search.value; if (value) { controller.abort(); controller = new AbortController(); signal = controller.signal; getPost(value, signal); } }; search.onkeyup = onChange; </script> </body> </html>
- در اینجا یک input box را داریم که ابتدا، یافت شده و سپس به رخداد onkeyup آن، متد onChange نسبت داده شدهاست تا هربار که کاربر، اطلاعاتی را وارد میکند، فراخوانی شود.
- در ابتدای اسکریپت هم نحوهی نمونه سازی شیء استاندارد جاوااسکریپتی AbortController و دسترسی به شیء signal آنرا مشاهده میکنید.
- در متد onChange، ابتدا مقدار جدید ورودی کاربر، دریافت میشود، سپس این AbortController، لغو میشود و بعد یک نمونهی جدید از آن ایجاد شده و مجددا به شیء signal آن دسترسی پیدا میکنیم تا آنرا به متد getPost ارسال کنیم. این متد هم چنین پیاده سازی را دارد:
const getPost = (value, signal) => { fetch( `https://site.com/search/${value}`, { signal } ); };
همانطور که مشاهده میکنید، تابع fetch، قابلیت پذیرش شیء signal را هم دارد. زمانیکه با هربار تایپ کاربر، متد ()controller.abort فراخوانی میشود، سیگنالی را به fetch «قبلی» متصل به آن ارسال میکند که ... دیگر به سمت سرور ارسال نشو و متوقف شو. با اینکار فقط آخرین ورودی کاربر، سبب بروز یک fetch موفق میشود و ترافیک ارسالی به سمت سرور کاهش پیدا میکند (چون تمام fetchهای قبلی، سیگنال abort را دریافت کردهاند)؛ مانند مثال زیر که کاربر، 5 بار حروفی را وارد کرده و به ازای هربار ورود حرفی، یک درخواست fetch جدید، ایجاد شده، اما ... فقط آخرین درخواست ارسالی او موفق بوده و نتیجهای را بازگشت داده و مابقی درخواستها ... abort شدهاند. این عملیات abort، در سمت کاربر اعمال میشود؛ یعنی اصلا درخواستی به سمت سرور ارسال نمیشود و این لغو درخواست، توسط برنامهی سمت سرور انجام نشدهاست.
مشکلات دیگری هم که داره این هست که در سشن یک شی زمان دار هست و اگر کاربر 15مثلا دقیقه کار نکند بعد از آن سبد خالی میشود که بهتر هست انتخابها همواره حفظ گردد . پس بهترین راهها استفاده از کوکی (+ +) است که در این مقاله نحوه ذخیره هر نوع داده( به همراه فشرده سازی) ذکر شده است و مورد دوم استفاده از local storage و indexedDB است که به زودی قسمت دوم هم منتشر میشود.
مورد بعدی اینکه در صورت به روز رسانی وب سایت به خصوص وب کانفیگ سشنها ریست خواهند شد و سبدی که کاربر پر کرده است در آن لحظه از بین خواهد رفت که با استفاده روشهای کلاینتی چنین مشکلی ایجاد نمیشود.
مورد بعدی اینکه با استفاده از جاوااسکریپت و توابع تحت کلاینت سرعت بهتری در اجرا و رسم سبد کالا میتوانید داشته باشید بدون اینکه بار زیادی را به سرور تحمیل کنید.
نکته تکمیلی اینکه فقط باید در رسم مجدد سبد کالا تحت کلاینت باید موجودیها و قیمتها و تخفیفها و ... هم مجددا بررسی شوند که اطلاعات قبلی نمایش داده نشود که البته این بر میگرده به طراحی ساختاری که ترتیب داده اید.
استفاده از جدول جداگانه برای نگهداری اطلاعات هم روش مناسبی حساب میشود که انتخاب بین اینها به نظر برنامه نویس مربوط میشود.
یکی از نیازهای طراحان این است که مرتبا نیاز به تبدیل نوعها را به یکدیگر دارند. CLR به شما اجازه میدهد که هر آبجکتی را به نوع مربوط به خودش یا والدینش تبدیل کنید. بسته به زبانی که انتخاب میکنید، این تبدیل شکل متفاوتی دارد و در سی شارپ نیاز به سینتکس خاصی نیست.
سی شارپ برای تبدیل یک شیء به نوعهای والدش، نیازی به ذکر نوع ندارد ولی اگر قرار است از سمت والد به سمت فرزند Cast شود نیاز است که صریحا نوع آن را اعلام کنید. در این روش اگر نوع تبدیلات با شیء ما سازگاری نداشته باشد، در زمان اجرا، با خطای
InvalidCastExceptio
internal class Employee { ... } public sealed class Program { public static void Main() { // بدون ذکر نام والد تبدیل صورت میگیرد Object o = new Employee(); // برای تبدیل والد به یکی از مشتقات آن نیاز است // نوع آن به طور صریح ذکر گردد // در بعضی زبانهای مثل ویژوال بیسیک نیازی به ذکر آن نیست Employee e = (Employee) o; } }
استفاده از کلمات as و is در تبدیلات
Object o = new Object(); Boolean b1 = (o is Object); // b1 is true. Boolean b2 = (o is Employee); // b2 is false.
پی نوشت :در این بررسی اگر شیء نال باشد، مقدار برگشتی همیشه false است. چون به هیچ نوعی قابل تبدیل نیست.نحوهی استفادهی از کلمه کلیدی is در این تبدیل به شکل زیر است:
if (o is Employee) { Employee e = (Employee) o; }
برای بهبود کد بالا، سی شارپ کلمهی کلیدی as را ارائه میکند. کلمه کلیدی as باعث میشود اگر شیء به آن نوع قابل تبدیل باشد، ارجاعی صورت بگیرد؛ در غیر این صورت مقدار نال بازگشت داده میشود. شاید شما بگویید که در خط بعدی ما نیز دوباره مجددا یک عبارت شرطی داریم و دوباره داریم عمل تاییدیه را انجام میدهیم. ولی باید گفت این if به مراتب هزینهی کمتری نسبت به بررسیهای تبدیل یا Cast به شیوهی بالاست.
Employee e = o as Employee; if (e != null) { ..... }
فضاهای نام و اسمبلی ها
همانطور که مطلع هستید، فضاهای نام به ما این اجازه را میدهند تا نوعها را به صورت منطقی گروه بندی کنیم تا دسترسی به آنان راحتتر باشد. برای مثال مطمئنا با نگاه به اسم فضای نام
System.Text
public sealed class Program { public static void Main() { System.IO.FileStream fs = new System.IO.FileStream(...); System.Text.StringBuilder sb = new System.Text.StringBuilder(); } }
using System.IO; // Try prepending "System.IO." using System.Text; // Try prepending "System.Text." public sealed class Program { public static void Main() { FileStream fs = new FileStream(...); StringBuilder sb = new StringBuilder(); } }
موقعیکه شما نوعی را در یک فضای نام استفاده میکنید، این نوع به ترتیب بررسی میکند که نوع، در کدام فضای نام و کدام اسمبلی مورد استفاده قرار گرفته است. این اسمبلیها شامل FCL و اسمبلیهای خارجی است که به آن لینک کردهاید. حال ممکن است این سؤال پیش بیاید که ممکن است نام دو نوع، در دو فضای نام متفاوت، یکی باشد و در یک جا مورد استفاده قرار گرفتهاند. چگونه میتوان تشخیص داد که کدام نوع، متعلق به دیگری است؟ نظر مایکروسافت این است که تا میتوانید سعی کنید از اسامی متفاوت استفاده کنید. ولی در بعضی شرایط این مورد ممکن نیست. به همین علت باید هر دو کلاس یا به طور کامل، به همراه فضای نام نوشته شوند؛ یا اینکه یکی از آنها بدین شکل باشد و فضای نام نوع دیگر، با using صدا زده شود.
using Microsoft; using Dotnettips ; public sealed class Program { public static void Main() { Widget w = new Widget();// An ambiguous reference } }
'Widget' is an ambiguous reference between 'Microsoft.Widget' and 'Dotnettips.Widget
using Microsoft; using Dotnettips; public sealed class Program { public static void Main() { Dotnettips.Widget w = new Dotnettips.Widget(); // Not ambiguous } }
using Microsoft; using Dotnettips; using DotnettipsWidget = Dotnettips.Widget; public sealed class Program { public static void Main() { DotnettipsWidget w = new DotnettipsWidget (); // No error now } }
اکثر کنترلهای تعیین اعتبار ASP.Net بر اساس جاوا اسکریپت کار میکنند (مانند RangeValidator و امثال آن). حال اگر کاربری افزونه no script فایرفاکس را نصب کرده بود چه باید کرد؟
با استفاده از این افزونه، این نوع کنترلها از کار خواهند افتاد (چون دیگر کدهای جاوا اسکریپتی آنها اجرا نخواهند شد).
خوشبختانه برای بررسی صحت عملکرد این کنترلها در ASP.Net امکان بررسی خاصیت Page.IsValid نیز وجود دارد که در ادامه به آن خواهیم پرداخت.
صفحهی بسیار ساده ASP.Net زیر را در نظر بگیرید:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm3.aspx.cs" Inherits="testWebForms87.WebForm3" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:TextBox ID="txtData" runat="server"></asp:TextBox>
<asp:RangeValidator ID="RangeValidator1" runat="server" ControlToValidate="txtData"
ErrorMessage="لطفا یک عدد وارد کنید" MaximumValue="100000" MinimumValue="0" SetFocusOnError="True"
Type="Integer"></asp:RangeValidator>
<asp:RequiredFieldValidator ID="RequiredFieldValidator1" runat="server" ControlToValidate="txtData"
ErrorMessage="لطفا مقداری را وارد نمائید" SetFocusOnError="True"></asp:RequiredFieldValidator>
<br />
<asp:Button ID="btnSubmit" runat="server" OnClick="btnSubmit_Click" Text="Submit" />
<br />
<asp:Label ID="lblValue" runat="server"></asp:Label>
</div>
</form>
</body>
</html>
سپس برای بررسی سمت سرور عملکرد کنترلهای تعیین اعتبار در ASP.Net میتوان به صورت زیر عملکرد:
protected void btnSubmit_Click(object sender, EventArgs e)
{
if (btnSubmit.CausesValidation)
{
// Validate the Page
Page.Validate();
// Ensure Page is Valid
if (!Page.IsValid)
{
lblValue.Text = "لطفا جاوا اسکریپت را در مرورگر خود فعال نمائید";
return;
}
}
lblValue.Text = txtData.Text;
}
راه دیگر بررسی غیرفعال بودن جاوا اسکریپت در یک صفحه استفاده از روش سنتی تگ noscript است:
<noscript>
<meta http-equiv="refresh" content="0;url=http://www.google.com">
</noscript>
در پایان باید خاطر نشان کرد که هیچگاه به کنترلهای تعیین اعتبار سمت کاربر اطمینان نکنید و حتما یا از روش فوق استفاده نمائید، یا در روال submit صفحه به سرور، یکبار دیگر دادهها را به صورت دستی نیز بررسی کنید. برای مثال اگر کاربر قرار است آدرس ایمیلی را وارد کند، حتما یکبار دیگر با استفاده از regular expressions ، در سمت سرور نیز عبارت ورودی را بررسی کنید.
برای قسمت backend، از همان برنامهی تکمیل شدهی قسمت قبل استفاده میکنیم که به آن تولید مقدماتی JWTها نیز اضافه شدهاست. البته این سری، مستقل از قسمت سمت سرور آن تهیه خواهد شد و صرفا در حد دریافت توکن از سرور و یا ارسال مشخصات کاربر جهت لاگین، نیاز بیشتری به قسمت سمت سرور آن ندارد و تاکید آن بر روی مباحث سمت کلاینت React است. بنابراین اینکه چگونه این توکن را تولید میکنید، در اینجا اهمیتی ندارد و کلیات آن با تمام روشهای پیاده سازی سمت سرور سازگار است (و مختص به فناوری خاصی نیست). پیشنیاز درک کدهای سمت سرور قسمت JWT آن، مطالب زیر هستند:
- «معرفی JSON Web Token»
- «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity»
- «پیاده سازی JSON Web Token با ASP.NET Web API 2.x»
- « آزمایش Web APIs توسط Postman - قسمت ششم - اعتبارسنجی مبتنی بر JWT»
ثبت یک کاربر جدید
فرم ثبت نام کاربران را در قسمت 21 این سری، در فایل src\components\registerForm.jsx، ایجاد و تکمیل کردیم. البته این فرم هنوز به backend server متصل نیست. برای کار با آن هم نیاز است شیءای را با ساختار زیر که ذکر سه خاصیت اول آن اجباری است، به endpoint ای با آدرس https://localhost:5001/api/Users به صورت یک HTTP Post ارسال کنیم:
{ "name": "string", "email": "string", "password": "string", "isAdmin": true, "id": 0 }
اکنون نوبت به اتصال کامپوننت registerForm.jsx، به سرویس backend است. تا اینجا دو سرویس src\services\genreService.js و src\services\movieService.js را در قسمت قبل، به برنامه جهت کار کردن با endpointهای backend server، اضافه کردیم. شبیه به همین روش را برای کار با سرویس سمت سرور api/Users نیز در پیش میگیریم. بنابراین فایل جدید src\services\userService.js را با محتوای زیر، به برنامهی frontend اضافه میکنیم:
import http from "./httpService"; import { apiUrl } from "../config.json"; const apiEndpoint = apiUrl + "/users"; export function register(user) { return http.post(apiEndpoint, { email: user.username, password: user.password, name: user.name }); }
اطلاعات شیء user ای که در اینجا دریافت میشود، از خاصیت data کامپوننت RegisterForm تامین میگردد:
class RegisterForm extends Form { state = { data: { username: "", password: "", name: "" }, errors: {} };
پس از تعریف userService.js، به registerForm.jsx بازگشته و ابتدا امکانات آنرا import میکنیم:
import * as userService from "../services/userService";
import { register } userService from "../services/userService";
doSubmit = async () => { try { await userService.register(this.state.data); } catch (ex) { if (ex.response && ex.response.status === 400) { const errors = { ...this.state.errors }; // clone an object errors.username = ex.response.data; this.setState({ errors }); } } };
در این حالت برای ارسال اطلاعات یک کاربر، در بار اول، یک چنین خروجی را از سمت سرور میتوان شاهد بود که id جدیدی را به این رکورد نسبت دادهاست:
اگر مجددا همین رکورد را به سمت سرور ارسال کنیم، اینبار خطای زیر را دریافت خواهیم کرد:
که از نوع 400 یا همان BadRequest است:
بنابراین نیاز است بدنهی response را در یک چنین مواردی که خطایی از سمت سرور صادر میشود، دریافت کرده و با به روز رسانی خاصیت errors در state فرم (همان قسمت بدنهی catch کدهای فوق)، سبب درج و نمایش خودکار این خطا شویم:
پیشتر در قسمت بررسی «کار با فرمها» آموختیم که برای مدیریت خطاهای پیش بینی شدهی دریافتی از سمت سرور، نیاز است قطعه کدهای مرتبط با سرویس http را در بدنهی try/catchها محصور کنیم. برای مثال در اینجا اگر ایمیل شخصی تکراری وارد شود، سرویس یک return BadRequest("Can't create the requested record.") را بازگشت میدهد که در اینجا status code معادل BadRequest، همان 400 است. بنابراین انتظار داریم که خطای 400 را از سمت سرور، تحت شرایط خاصی دریافت کنیم. به همین دلیل است که در اینجا تنها مدیریت status code=400 را در بدنهی catch نوشته شده ملاحظه میکنید.
سپس برای نمایش آن، نیاز است خاصیت متناظری را که این خطا به آن مرتبط میشود، با پیام دریافت شدهی از سمت سرور، مقدار دهی کنیم که در اینجا میدانیم مرتبط با username است. به همین جهت سطر errors.username = ex.response.data، کار انتساب بدنهی response را به خاصیت جدید errors.username انجام میدهد. در این حالت اگر به کمک متد setState، کار به روز رسانی خاصیت errors موجود در state را انجام دهیم، رندر مجدد فرم، در صف انجام قرار گرفته و در رندر بعدی آن، پیام موجود در errors.username، نمایش داده میشود.
پیاده سازی ورود به سیستم
فرم ورود به سیستم را در قسمت 18 این سری، در فایل src\components\loginForm.jsx، ایجاد و تکمیل کردیم که این فرم نیز هنوز به backend server متصل نیست. برای کار با آن نیاز است شیءای را با ساختار زیر که ذکر هر دو خاصیت آن اجباری است، به endpoint ای با آدرس https://localhost:5001/api/Auth/Login به صورت یک HTTP Post ارسال کنیم:
{ "email": "string", "password": "string" }
var jwt = _tokenFactoryService.CreateAccessToken(user); return Ok(new { access_token = jwt });
در ادامه برای تعامل با منبع api/Auth/Login سمت سرور، ابتدا یک سرویس مختص آنرا در فایل جدید src\services\authService.js، با محتوای زیر ایجاد میکنیم:
import { apiUrl } from "../config.json"; import http from "./httpService"; const apiEndpoint = apiUrl + "/auth"; export function login(email, password) { return http.post(apiEndpoint + "/login", { email, password }); }
import * as auth from "../services/authService"; class LoginForm extends Form { state = { data: { username: "", password: "" }, errors: {} }; // ... doSubmit = async () => { try { const { data } = this.state; const { data: { access_token } } = await auth.login(data.username, data.password); console.log("JWT", access_token); localStorage.setItem("token", access_token); this.props.history.push("/"); } catch (ex) { if (ex.response && ex.response.status === 400) { const errors = { ...this.state.errors }; errors.username = ex.response.data; this.setState({ errors }); } } };
- ابتدا تمام خروجیهای ماژول authService را با نام شیء auth دریافت کردهایم.
- سپس در متد doSubmit، اطلاعات خاصیت data موجود در state را که معادل فیلدهای فرم لاگین هستند، به متد auth.login برای انجام لاگین سمت سرور، ارسال کردهایم. این متد چون یک Promise را باز میگرداند، باید await شود و پس از آن متد جاری نیز باید به صورت async معرفی گردد.
- همانطور که عنوان شد، خروجی نهایی متد auth.login، یک شیء JSON دارای خاصیت access_token است که در اینجا از خاصیت data خروجی نهایی دریافت شدهاست.
- سپس نیاز است برای استفادههای آتی، این token دریافتی از سرور را در جایی ذخیره کرد. یکی از مکانهای متداول اینکار، local storage مرورگرها است (اطلاعات بیشتر).
- در آخر کاربر را توسط شیء history سیستم مسیریابی برنامه، به صفحهی اصلی آن هدایت میکنیم.
- در اینجا قسمت catch نیز ذکر شدهاست تا خطاهای حاصل از return BadRequestهای دریافتی از سمت سرور را بتوان ذیل فیلد نام کاربری نمایش داد. روش کار آن، دقیقا همانند روشی است که برای فرم ثبت یک کاربر جدید استفاده کردیم.
اکنون اگر برنامه را ذخیره کرده و اجرا کنیم، توکن دریافتی، در کنسول توسعه دهندگان مرورگر لاگ شده و سپس کاربر به صفحهی اصلی برنامه هدایت میشود. همچنین این token ذخیره شده را میتوان در ذیل قسمت application->storage آن نیز مشاهده کرد:
لاگین خودکار کاربر، پس از ثبت نام در سایت
پس از ثبت نام یک کاربر در سایت، بدنهی response بازگشت داده شدهی از سمت سرور، همان شیء user است که اکنون Id او مشخص شدهاست. بنابراین اینبار جهت ارائهی token از سمت سرور، بجای response body، از یک هدر سفارشی در فایل Controllers\UsersController.cs استفاده میکنیم (کدهای کامل آن در انتهای بحث پیوست شدهاست):
var jwt = _tokenFactoryService.CreateAccessToken(user); this.Response.Headers.Add("x-auth-token", jwt);
در ادامه در کدهای سمت کلاینت src\components\registerForm.jsx، برای استخراج این هدر سفارشی، اگر شیء response دریافتی از سرور را لاگ کنیم:
const response = await userService.register(this.state.data); console.log(response);
برای اینکه در کدهای سمت کلاینت بتوان این هدر سفارشی را خواند، نیاز است هدر مخصوص access-control-expose-headers را نیز به response اضافه کرد:
var jwt = _tokenFactoryService.CreateAccessToken(data); this.Response.Headers.Add("x-auth-token", jwt); this.Response.Headers.Add("access-control-expose-headers", "x-auth-token");
اکنون میتوان این هدر سفارشی را در متد doSubmit کامپوننت RegisterForm، از طریق شیء response.headers خواند و در localStorage ذخیره کرد. سپس کاربر را توسط شیء history سیستم مسیریابی، به ریشهی سایت هدایت نمود:
class RegisterForm extends Form { // ... doSubmit = async () => { try { const response = await userService.register(this.state.data); console.log(response); localStorage.setItem("token", response.headers["x-auth-token"]); this.props.history.push("/"); } catch (ex) { if (ex.response && ex.response.status === 400) { const errors = { ...this.state.errors }; // clone an object errors.username = ex.response.data; this.setState({ errors }); } } };
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-26-backend.zip و sample-26-frontend.zip