یک کاراکتر کنترلی، یک نقطه کدی است که به وسیله علائم نوشتاری قابل نمایش نباشد. مانند Backspace
تعریف بالا به ما میگوید که در یک متن نوشتاری، به غیر از کد حروفی که مشاهده میکنیم، کدهای دیگری هم هستن که قابل نمایش نیستند ولی بین متون وجود دارند. شاید شما تعدادی از آنها را بشناسید مثل کدهای 10 و 13 برای خط بعد و اول سطر که به line feed و carriage return معروف هستند. در این نوشتار قصد داریم با تعدادی از آنها آشنا شویم.
قبل از آغاز این نوشتار به شما توصیه میکنم یک نگاه اجمالی هم که شده بر نوشتار «داستانی از unicode» داشته باشید تا اطلاعات تکمیلیتری از این نوشتار به دست آورید. مبحث کلیدهای کنترلی از زمانی آغاز شد که کدهای اسکی ایجاد شدند و به دو دستهی c0 و c1 تقسیم شدند. خود کدهای اسکی هم بر اساس کدهای تلگراف ایجاد شدند و بسیاری از کلیدهای کنترلی هم از آنجا به استاندارد اسکی پیوستند و برای ارتباط و کنترل دستگاههایی چون چاپگرها و تهیه اطلاعات متا در مورد طلاعاتی که قرار بود در نوار مغناطیسی ذخیره شوند به کار رفتند. به عنوان نمونه کد 10 به عنوان line feed در چاپگر، یک خط کاغذ را به سمت داخل میکشید و کد 13 هم باعث میشد چاپگر به ابتدای سطر بازگردد. البته بیشتر این کاراکترها در پردازش متون به خصوص امروزه استفاده نمیشوند و فقط یک سری از آنها رایج هستند؛ مثل دو موردی که در بالا و در همین خط به آنها اشاره شد.
دستهی c0 از کد 0 آغاز شده و تا کد 31 ادامه مییابد. دو کد بعدی که کدهای Space و DEL هستند در هیچ گروهی قرار نمیگیرند. گروه c1 از کدهای 128 آغاز شده و تا 159 ادامه مییابند که جدول این گروهها و کلیدها کنترلی را میتوانید مشاهده کنید. برای مثال اولین کلید کنترلی که کد آن 0 است به نام نال است که در قدیم هم برای بستن رشتهها در زبان سی از آن استفاده میکردیم.
هر چند به مرور زمان هم تعدادی از همین کلیدهای کنترلی کاربرد خود را از دست دادند و برای آنها شکلک هایی چون خنده، قلب، نت موسیقی و ... را قرار دادند ولی گاهی اوقات برنامه نویسها هنوز در برنامههای خود از کد آنها برای کارهایی چون انجام عملیات بیتی استفاده میکنند.
استفادههای C0
کلیدهای کنترلی این دسته بیشتر برای منظم ساختن متنهای ساده و همچنین ایجاد ارتباط در پروتکل ارتباطی و دستگاههای مختلف به کار میرفت؛ ارسال فرمانهایی چون آغاز و توقف کار یا انجام عملی خاص توسط هر یک از این کلیدها صورت میگرفت. دستگاههایی چون کارت پانچها، ماشین تایپ و موارد مشابه، از این نوع هستند. با اینکه عمر این دستگاه به سر آمد ولی کلیدهای کنترلی جان سالم به در بردند.
استفادههای C1
این دسته در اواخر سال 1970 اضافه شدند و بیشتر برای ارتباط با چاپگر و صفحهی نمایش به کار میرفتند؛ مثل پیمایشهای افقی و عمودی، تعریف ناحیهای برای پر کردن فرم و Line-Break و کلیدهای انتقالی (شیفت) برای پشتیبانی از کلیدهای کنترلی و قابل چاپ بیشتر. 2 تا از کلیدها هم برای استفادهی خصوصی برنامه نویس کنار گذاشته شدند و 4 تا هم رزرو شده برای استفادهی آینده، تا بعدا استانداردسازی شوند.
کلیدهای کنترلی در سی شارپ
بسیاری از ما از علامت \ در کدهایمان برای قرار دادن کلیدهای کنترلی استفاده میکنیم مثل r\n\ که ترکیب دو کد CR و LF است.
برای شناسایی یک کلید کنترلی در سی شارپ از متد ایستای Char.IsControl استفاده مینماییم. کد زیر در مجموعهی MSDN برای نشان دادن قابلیت این متد نوشته شده است که در طی یک حلقه رنجی از کد پوینتها را بررسی کرده و نتیجه را به صورت شش ستونی در کنسول نمایش میدهد. یا کد مشابه دیگر که بر اساس دسیمال نمایش میدهد.
using System; public class ControlChars { public static void Main() { int charsWritten = 0; for (int ctr = 0x00; ctr <= 0xFFFF; ctr++) { char ch = Convert.ToChar(ctr); if (char.IsControl(ch)) { Console.Write(@"\U{0:X4} ", ctr); charsWritten++; if (charsWritten % 6 == 0) Console.WriteLine(); } } } } // The example displays the following output to the console: // \U0000 \U0001 \U0002 \U0003 \U0004 \U0005 // \U0006 \U0007 \U0008 \U0009 \U000A \U000B // \U000C \U000D \U000E \U000F \U0010 \U0011 // \U0012 \U0013 \U0014 \U0015 \U0016 \U0017 // \U0018 \U0019 \U001A \U001B \U001C \U001D // \U001E \U001F \U007F \U0080 \U0081 \U0082 // \U0083 \U0084 \U0085 \U0086 \U0087 \U0088 // \U0089 \U008A \U008B \U008C \U008D \U008E // \U008F \U0090 \U0091 \U0092 \U0093 \U0094 // \U0095 \U0096 \U0097 \U0098 \U0099 \U009A // \U009B \U009C \U009D \U009E \U009F
آیا هنوز برنامه نویسها از کلیدهای کنترلی استفاده میکنند؟
این سوال بستگی به برنامهای دارد که شما مینویسید. باید گفت هنوز بسیاری از آنها در بسیاری از برنامهها استفاده میشوند. مانند بعضی از درایورها برای ارسال اطلاعات به سمت یک قطعه یا دستگاه یا حتی از شما میخواهند برنامهای بنویسید که با دستگاههای قدیمی ارتباط برقرار کند. برنامههایی که نیاز به کار با رشتهها دارند و ...
لیست زیر مشخص میکند که کدامیک از کلیدهای کنترلی تا چه اندازه امروزه توسط برنامه نویسان استفاده میشوند.
Null | استفاده روزمرهای از آن در همهی برنامهها وجود دارد و نیاز به معرفی ندارد. |
Transmission Control | این کلیدها که 10 عدد هستند شامل SOH , ACK , DLE , ENQ , EOT , ETB , ETX , Nak , STX , SYN هستند. کاربردشان در انتقال اطلاعات بود ولی امروزه استفاده از آنها به شدت کم شده است و انتقال دادهها با سوکت TCP/IP و HTTP و FTP و دیگر پروتکولها به سرانجام رسید و گاها برای بعضی کاربردهای ویژه استفاده میشوند. |
BEL | این مورد واقعا کاربردش را از دست داده است. وظیفه قبلیاش ارسال یه هشدار یا یک زنگ اخطار به کاربر بود. مثلا برای اینکه ماشین تایپ به کاربر هشدار بدهد به آخر خط رسیده است، یک کد BELL به سمت آن ارسال میکرد. |
Format Effectors | کدهای این دسته عبارتند از BS , CR , FF , HT , HTJ , HTS , IND , LF , NEL , PLD , PLU , RI , VT , VTS هستند که احتمالا مهمترین کدهایی هستند که امروزه از آنها استفاده میشود. کاربردشان در فرمت بندی یا قالب بندی متون نوشته شده یا همان کلیدهای قابل چاپ میباشد. CR و LF که همیشه معرف حضور ما هستند و بودنشان در سیستم یک امر حیاتی است. HT که همان tab است. BS که همان Backspace است. FF و VT هم که امروزه به ندرت استفاده میشوند. |
Device Control | هنوز برای ارتباط با دستگاههای مختلف مثل کار با پورتها استفاده میشوند. کلیدهای معروف آن DC1 و DC3 هستند که به XON و XOFF هم شناخته میشوند. یکی از کاربردهای آن. |
SUB | یک نماد جایگزین که استفادهی خود را از دست داده است. موقعیکه نمادی نامعتبر بود یا خطایی رخ میداد، این نماد جایگزین آن میشد. امروزه بیشتر از علامت ؟ در متون استفاده میشود. در یک صفحه کلید استاندارد این کد توسط فشرده شدن Ctrl+Z ارسال میشود. |
CAN , EM | کاربردی امروزه ندارد. CAN برای کنترل خطا به کار میرفت و EM در نوارهای مغناطیسی. |
Information Separators | شامل 4 کلید FS ,GS , RS و US میشود که برای جداسازی دادهها از یکدیگر به کار میروند؛ ولی بهخاطر جایگزینی آنها با اسنادی مثل XML یا دیتابیسها، استفاده از آنها تا حدودی به پایان رسیده است. |
SP | همان کلید space است که نیاز به معرفی ندارد و کارش گویای همه چیز هست. |
DELL | همان کلید Delete است. |
NBSP | این کلید همان کاراکتر ;nbsp& است که در کدهای HTML استفاده میشود. |
SHY | علامت - یا Hyphen است که به شدت استفاده از آن کم شده است. |
اجازه بدهید قبل از هر چیزی به دو مفهوم اصلی در IIS بپردزیم :
1. Worker Process
2. Application Pool
پروسههای کارگر w3wp.exe وظیفهی اجرای برنامههای asp.net را در IIS ، به عهده دارند. این پروسهها مسئولیت پردازش تمامی درخواست و پاسخها از/به کلاینت را دارند. هر کاری که باید در asp.net انجام بشود، توسط اینها صورت میگیرد. به بیان سادهتر این پروسهها قلب برنامههای ASP.Net بر روی IIS هستند .
Application Pool:این پولها در واقع ظرفی یا در برگیرنده ای برای پروسههای کارگر به حساب میآیند. این پولها پروسههای کارگر را از هم جدا و دسته بندی میکنند تا قابلیت اعتماد، امنیت و در دسترس بودن بدهند. موقعی که یک پروسه یا حتی یک پول دچار مشکل میشود، این اطمینان داده میشود که تاثیری بر دیگر پولها یا پروسههای کارگر، ندارد. یعنی موقعی که یک web application دچار مشکل شود، هیچ تاثیری بر اجرای web applicationهای دیگر ندارد. به یک application pool با چند پروسه کارگر web garden میگویند.
اصلا این WWW Service چه کاری انجام میدهد و به چه دردی میخورد؟
- HTTP administration and configuration
- Performance monitoring
- Process management
HTTP Administration and Configuration
در نسخههای جدیدتر IIS چکاری بر عهده WWW Service است؟
WAS در قسمت سوم این مقاله توضیح داده خواهد شد.
یک لامپ و سوئیچ برق را درنظر بگیرید. زمانیکه لامپ مشاهده میکند سوئیچ برق در حالت روشن قرار گرفتهاست، روشن خواهد شد و برعکس. در اینجا به سوئیچ، subject و به لامپ، observer گفته میشود. هر زمان که حالت سوئیچ تغییر میکند، از طریق یک callback، وضعیت خود را به observer اعلام خواهد کرد. علت استفاده از callbackها، ارائه راهحلهای عمومی است تا بتواند با انواع و اقسام اشیاء کار کند. به این ترتیب هر بار که شیء observer از نوع متفاوتی تعریف میشود (مثلا بجای لامپ یک خودرو قرار گیرد)، نیازی نخواهد بود تا subject را تغییر داد.
عموما به شیءایی که قرار است وضعیت را مشاهده یا رصد کند، Observer گفته میشود و به شیءایی که قرار است وضعیت آن رصد شود Observable یا Subject گفته میشود.
بد نیست بدانید این الگو یکی از کلیدیترین بخشهای معماری لایه بندی MVC نیز میباشد.
همچنین این نکته حائز اهمیت است که این الگو ممکن است باعث نشتی حافظه هم شود و به این مشکل Lapsed Listener Problem میگویند. یعنی یک listener وجود دارد که تاریخ آن منقضی شده، ولی هنوز در حافظه جا خوش کردهاست. این مشکل برای زبانهای شیءگرایی که با سیستمی مشابه GC پیاده سازی میشوند، رخ میدهد. برای جلوگیری از این حالت، برنامه نویس باید این مشکل را با رجیستر کردنها و عدم رجیستر یک شنوده، در مواقع لزوم حل کند. در غیر این صورت این شنونده بی جهت، یک ارتباط را زنده نگه داشته و حافظهی منبع را به هدر میدهد.
مثال: ما یک کلید داریم که سه کلاس RedLED،GreenLED و BlueLED قرار است آن را مشاهده و وضعیت کلید را رصد کنند.
برای پیاده سازی این الگو، ابتدا یک کلاس انتزاعی را با نام Observer که دارای متدی به نام Update است، ایجاد میکنیم. متغیر از نوع کلاس Observable را بعدا ایجاد میکنیم:
public abstract class Observer { protected Observable Observable; public abstract void Update(); }
public class Observable { private readonly List<Observer> _observers = new List<Observer>(); public void Attach(Observer observer) { _observers.Add(observer); } public void Dettach(Observer observer) { _observers.Remove(observer); } public void NotifyAllObservers() { foreach (var observer in _observers) { observer.Update(); } } }
حال کلاس Switch را با ارث بری از کلاس Observable مینویسیم:
public class Switch:Observable { private bool _state; public bool ChangeState { set { _state = value; NotifyAllObservers(); } get { return _state; } } }
برای هر سه چراغ، رنگی هم داریم:
public class RedLED:Observer { private bool _on = false; public override void Update() { _on = !_on; Console.WriteLine($"Red LED is {((_on) ? "On" : "Off")}"); } }
public class GreenLED:Observer { private bool _on = false; public override void Update() { _on = !_on; Console.WriteLine($"Green LED is {((_on) ? "On" : "Off")}"); } }
public class BlueLED:Observer { private bool _on = false; public override void Update() { _on = !_on; Console.WriteLine($"Blue LED is {((_on) ? "On" : "Off")}"); } }
var greenLed=new GreenLED(); var redLed=new RedLED(); var blueLed=new BlueLED(); var switchKey=new Switch(); switchKey.Attach(greenLed); switchKey.Attach(redLed); switchKey.Attach(blueLed); switchKey.ChangeState = true; switchKey.ChangeState = false;
Green LED is On Red LED is On Blue LED is On Green LED is Off Red LED is Off Blue LED is Off
پیاده سازیهای زیادی را در مورد JSON Web Token با ASP.NET Web API، با کمی جستجو میتوانید پیدا کنید. اما مشکلی که تمام آنها دارند، شامل این موارد هستند:
- چون توکنهای JWT، خودشمول هستند (در پیشنیاز بحث مطرح شدهاست)، تا زمانیکه این توکن منقضی نشود، کاربر با همان سطح دسترسی قبلی میتواند به سیستم، بدون هیچگونه مانعی لاگین کند. در این حالت اگر این کاربر غیرفعال شود، کلمهی عبور او تغییر کند و یا سطح دسترسیهای او کاهش یابند ... مهم نیست! باز هم میتواند با همان توکن قبلی لاگین کند.
- در روش JSON Web Token، عملیات Logout سمت سرور بیمعنا است. یعنی اگر برنامهای در سمت کاربر، قسمت logout را تدارک دیده باشد، چون در سمت سرور این توکنها جایی ذخیره نمیشوند، عملا این logout بیمفهوم است و مجددا میتوان از همان توکن قبلی، برای لاگین به سرور استفاده کرد. چون این توکن شامل تمام اطلاعات لازم برای لاگین است و همچنین جایی هم در سرور ثبت نشدهاست که این توکن در اثر logout، باید غیرمعتبر شود.
- با یک توکن از مکانهای مختلفی میتوان دسترسی لازم را جهت استفادهی از قسمتهای محافظت شدهی برنامه یافت (در صورت دسترسی، چندین نفر میتوانند از آن استفاده کنند).
به همین جهت راه حلی عمومی برای ذخیره سازی توکنهای صادر شده از سمت سرور، در بانک اطلاعاتی تدارک دیده شد که در ادامه به بررسی آن خواهیم پرداخت و این روشی است که میتواند به عنوان پایه مباحث Authentication و Authorization برنامههای تک صفحهای وب استفاده شود. البته سمت کلاینت این راه حل با jQuery پیاده سازی شدهاست (عمومی است؛ برای طرح مفاهیم پایه) و سمت سرور آن به عمد از هیچ نوع بانک اطلاعات و یا ORM خاصی استفاده نمیکند. سرویسهای آن برای بکارگیری انواع و اقسام روشهای ذخیره سازی اطلاعات قابل تغییر هستند و الزامی نیست که حتما از EF استفاده کنید یا از ASP.NET Identity یا هر روش خاص دیگری.
نگاهی به برنامه
در اینجا تمام قابلیتهای این پروژه را مشاهده میکنید.
- امکان لاگین
- امکان دسترسی به یک کنترلر مزین شدهی با فلیتر Authorize
- امکان دسترسی به یک کنترلر مزین شدهی با فلیتر Authorize جهت کاربری با نقش Admin
- پیاده سازی مفهوم ویژهای به نام refresh token که نیاز به لاگین مجدد را پس از منقضی شدن زمان توکن اولیهی لاگین، برطرف میکند.
- پیاده سازی logout
بستههای پیشنیاز برنامه
پروژهای که در اینجا بررسی شدهاست، یک پروژهی خالی ASP.NET Web API 2.x است و برای شروع به کار با JSON Web Tokenها، تنها نیاز به نصب 4 بستهی زیر را دارد:
PM> Install-Package Microsoft.Owin.Host.SystemWeb PM> Install-Package Microsoft.Owin.Security.Jwt PM> Install-Package structuremap PM> Install-Package structuremap.web
از structuremap هم برای تنظیمات تزریق وابستگیهای برنامه استفاده شدهاست. به این صورت قسمت تنظیمات اولیهی JWT ثابت باقی خواهد ماند و صرفا نیاز خواهید داشت تا کمی قسمت سرویسهای برنامه را بر اساس بانک اطلاعاتی و روش ذخیره سازی خودتان سفارشی سازی کنید.
دریافت کدهای کامل برنامه
کدهای کامل این برنامه را از اینجا میتوانید دریافت کنید. در ادامه صرفا قسمتهای مهم این کدها را بررسی خواهیم کرد.
بررسی کلاس AppJwtConfiguration
کلاس AppJwtConfiguration، جهت نظم بخشیدن به تعاریف ابتدایی توکنهای برنامه در فایل web.config، ایجاد شدهاست. اگر به فایل web.config برنامه مراجعه کنید، یک چنین تعریفی را مشاهده خواهید کرد:
<appJwtConfiguration tokenPath="/login" expirationMinutes="2" refreshTokenExpirationMinutes="60" jwtKey="This is my shared key, not so secret, secret!" jwtIssuer="http://localhost/" jwtAudience="Any" />
<configSections> <section name="appJwtConfiguration" type="JwtWithWebAPI.JsonWebTokenConfig.AppJwtConfiguration" /> </configSections>
- در این تنظیمات، دو زمان منقضی شدن را مشاهده میکنید؛ یکی مرتبط است به access tokenها و دیگری مرتبط است به refresh tokenها که در مورد اینها، در ادامه بیشتر توضیح داده خواهد شد.
- jwtKey، یک کلید قوی است که از آن برای امضاء کردن توکنها در سمت سرور استفاده میشود.
- تنظیمات Issuer و Audience هم در اینجا اختیاری هستند.
یک نکته
جهت سهولت کار تزریق وابستگیها، برای کلاس AppJwtConfiguration، اینترفیس IAppJwtConfiguration نیز تدارک دیده شدهاست و در تمام تنظیمات ابتدایی JWT، از این اینترفیس بجای استفادهی مستقیم از کلاس AppJwtConfiguration استفاده شدهاست.
بررسی کلاس OwinStartup
شروع به کار تنظیمات JWT و ورود آنها به چرخهی حیات Owin از کلاس OwinStartup آغاز میشود. در اینجا علت استفادهی از SmObjectFactory.Container.GetInstance انجام تزریق وابستگیهای لازم جهت کار با دو کلاس AppOAuthOptions و AppJwtOptions است.
- در کلاس AppOAuthOptions تنظیماتی مانند نحوهی تهیهی access token و همچنین refresh token ذکر میشوند.
- در کلاس AppJwtOptions تنظیمات فایل وب کانفیگ، مانند کلید مورد استفادهی جهت امضای توکنهای صادر شده، ذکر میشوند.
حداقلهای بانک اطلاعاتی مورد نیاز جهت ذخیره سازی وضعیت کاربران و توکنهای آنها
همانطور که در ابتدای بحث عنوان شد، میخواهیم اگر سطوح دسترسی کاربر تغییر کرد و یا اگر کاربر logout کرد، توکن فعلی او صرفنظر از زمان انقضای آن، بلافاصله غیرقابل استفاده شود. به همین جهت نیاز است حداقل دو جدول زیر را در بانک اطلاعاتی تدارک ببینید:
الف) کلاس User
در کلاس User، بر مبنای اطلاعات خاصیت Roles آن است که ویژگی Authorize با ذکر نقش مثلا Admin کار میکند. بنابراین حداقل نقشی را که برای کاربران، در ابتدای کار نیاز است مشخص کنید، نقش user است.
همچنین خاصیت اضافهتری به نام SerialNumber نیز در اینجا درنظر گرفته شدهاست. این مورد را باید به صورت دستی مدیریت کنید. اگر کاربری کلمهی عبورش را تغییر داد، اگر مدیری نقشی را به او انتساب داد یا از او گرفت و یا اگر کاربری غیرفعال شد، مقدار خاصیت و فیلد SerialNumber را با یک Guid جدید به روز رسانی کنید. این Guid در برنامه با Guid موجود در توکن مقایسه شده و بلافاصله سبب عدم دسترسی او خواهد شد (در صورت عدم تطابق).
ب) کلاس UserToken
در کلاس UserToken کار نگهداری ریز اطلاعات توکنهای صادر شده صورت میگیرد. توکنهای صادر شده دارای access token و refresh token هستند؛ به همراه زمان انقضای آنها. به این ترتیب زمانیکه کاربری درخواستی را به سرور ارسال میکند، ابتدا token او را دریافت کرده و سپس بررسی میکنیم که آیا اصلا چنین توکنی در بانک اطلاعاتی ما وجود خارجی دارد یا خیر؟ آیا توسط ما صادر شدهاست یا خیر؟ اگر خیر، بلافاصله دسترسی او قطع خواهد شد. برای مثال عملیات logout را طوری طراحی میکنیم که تمام توکنهای یک شخص را در بانک اطلاعاتی حذف کند. به این ترتیب توکن قبلی او دیگر قابلیت استفادهی مجدد را نخواهد داشت.
مدیریت بانک اطلاعاتی و کلاسهای سرویس برنامه
در لایه سرویس برنامه، شما سه سرویس را مشاهده خواهید کرد که قابلیت جایگزین شدن با کدهای یک ORM را دارند (نوع آن ORM مهم نیست):
الف) سرویس TokenStoreService
public interface ITokenStoreService { void CreateUserToken(UserToken userToken); bool IsValidToken(string accessToken, int userId); void DeleteExpiredTokens(); UserToken FindToken(string refreshTokenIdHash); void DeleteToken(string refreshTokenIdHash); void InvalidateUserTokens(int userId); void UpdateUserToken(int userId, string accessTokenHash); }
پیاده سازی این کلاس بسیار شبیه است به پیاده سازی ORMهای موجود و فقط یک SaveChanges را کم دارد.
یک نکته:
در سرویس ذخیره سازی توکنها، یک چنین متدی قابل مشاهده است:
public void CreateUserToken(UserToken userToken) { InvalidateUserTokens(userToken.OwnerUserId); _tokens.Add(userToken); }
ب) سرویس UsersService
public interface IUsersService { string GetSerialNumber(int userId); IEnumerable<string> GetUserRoles(int userId); User FindUser(string username, string password); User FindUser(int userId); void UpdateUserLastActivityDate(int userId); }
همچنین متدهای دیگری برای یافتن یک کاربر بر اساس نام کاربری و کلمهی عبور او (جهت مدیریت مرحلهی لاگین)، یافتن کاربر بر اساس Id او (جهت استخراج اطلاعات کاربر) و همچنین یک متد اختیاری نیز برای به روز رسانی فیلد آخرین تاریخ فعالیت کاربر در اینجا پیش بینی شدهاند.
ج) سرویس SecurityService
public interface ISecurityService { string GetSha256Hash(string input); }
پیاده سازی قسمت لاگین و صدور access token
در کلاس AppOAuthProvider کار پیاده سازی قسمت لاگین برنامه انجام شدهاست. این کلاسی است که توسط کلاس AppOAuthOptions به OwinStartup معرفی میشود. قسمتهای مهم کلاس AppOAuthProvider به شرح زیر هستند:
برای درک عملکرد این کلاس، در ابتدای متدهای مختلف آن، یک break point قرار دهید. برنامه را اجرا کرده و سپس بر روی دکمهی login کلیک کنید. به این ترتیب جریان کاری این کلاس را بهتر میتوانید درک کنید. کار آن با فراخوانی متد ValidateClientAuthentication شروع میشود. چون با یک برنامهی وب در حال کار هستیم، ClientId آنرا نال درنظر میگیریم و برای ما مهم نیست. اگر کلاینت ویندوزی خاصی را تدارک دیدید، این کلاینت میتواند ClientId ویژهای را به سمت سرور ارسال کند که در اینجا مدنظر ما نیست.
مهمترین قسمت این کلاس، متد GrantResourceOwnerCredentials است که پس از ValidateClientAuthentication بلافاصله فراخوانی میشود. اگر به کدهای آن دقت کنید، خود owin دارای خاصیتهای user name و password نیز هست.
این اطلاعات را به نحو ذیل از کلاینت خود دریافت میکند. اگر به فایل index.html مراجعه کنید، یک چنین تعریفی را برای متد login میتوانید مشاهده کنید:
function doLogin() { $.ajax({ url: "/login", // web.config --> appConfiguration -> tokenPath data: { username: "Vahid", password: "1234", grant_type: "password" }, type: 'POST', // POST `form encoded` data contentType: 'application/x-www-form-urlencoded'
در متد GrantResourceOwnerCredentials کار بررسی نام کاربری و کلمهی عبور کاربر صورت گرفته و در صورت یافت شدن کاربر (صحیح بودن اطلاعات)، نقشهای او نیز به عنوان Claim جدید به توکن اضافه میشوند.
در اینجا یک Claim سفارشی هم اضافه شدهاست:
identity.AddClaim(new Claim(ClaimTypes.UserData, user.UserId.ToString()));
در انتهای این کلاس، از متد TokenEndpointResponse جهت دسترسی به access token نهایی صادر شدهی برای کاربر، استفاده کردهایم. هش این access token را در بانک اطلاعاتی ذخیره میکنیم (جستجوی هشها سریعتر هستند از جستجوی یک رشتهی طولانی؛ به علاوه در صورت دسترسی به بانک اطلاعاتی، اطلاعات هشها برای مهاجم قابل استفاده نیست).
اگر بخواهیم اطلاعات ارسالی به کاربر را پس از لاگین، نمایش دهیم، به شکل زیر خواهیم رسید:
در اینجا access_token همان JSON Web Token صادر شدهاست که برنامهی کلاینت از آن برای اعتبارسنجی استفاده خواهد کرد.
بنابراین خلاصهی مراحل لاگین در اینجا به ترتیب ذیل است:
- فراخوانی متد ValidateClientAuthenticationدر کلاس AppOAuthProvider . طبق معمول چون ClientID نداریم، این مرحله را قبول میکنیم.
- فراخوانی متد GrantResourceOwnerCredentials در کلاس AppOAuthProvider . در اینجا کار اصلی لاگین به همراه تنظیم Claims کاربر انجام میشود. برای مثال نقشهای او به توکن صادر شده اضافه میشوند.
- فراخوانی متد Protect در کلاس AppJwtWriterFormat که کار امضای دیجیتال access token را انجام میدهد.
- فراخوانی متد CreateAsync در کلاس RefreshTokenProvider. کار این متد صدور توکن ویژهای به نام refresh است. این توکن را در بانک اطلاعاتی ذخیره خواهیم کرد. در اینجا چیزی که به سمت کلاینت ارسال میشود صرفا یک guid است و نه اصل refresh token.
- فرخوانی متد TokenEndpointResponse در کلاس AppOAuthProvider . از این متد جهت یافتن access token نهایی تولید شده و ثبت هش آن در بانک اطلاعاتی استفاده میکنیم.
پیاده سازی قسمت صدور Refresh token
در تصویر فوق، خاصیت refresh_token را هم در شیء JSON ارسالی به سمت کاربر مشاهده میکنید. هدف از refresh_token، تمدید یک توکن است؛ بدون ارسال کلمهی عبور و نام کاربری به سرور. در اینجا access token صادر شده، مطابق تنظیم expirationMinutes در فایل وب کانفیگ، منقضی خواهد شد. اما طول عمر refresh token را بیشتر از طول عمر access token در نظر میگیریم. بنابراین طول عمر یک access token کوتاه است. زمانیکه access token منقضی شد، نیازی نیست تا حتما کاربر را به صفحهی لاگین هدایت کنیم. میتوانیم refresh_token را به سمت سرور ارسال کرده و به این ترتیب درخواست صدور یک access token جدید را ارائه دهیم. این روش هم سریعتر است (کاربر متوجه این retry نخواهد شد) و هم امنتر است چون نیازی به ارسال کلمهی عبور و نام کاربری به سمت سرور وجود ندارند.
سمت کاربر، برای درخواست صدور یک access token جدید بر اساس refresh token صادر شدهی در زمان لاگین، به صورت زیر عمل میشود:
function doRefreshToken() { // obtaining new tokens using the refresh_token should happen only if the id_token has expired. // it is a bad practice to call the endpoint to get a new token every time you do an API call. $.ajax({ url: "/login", // web.config --> appConfiguration -> tokenPath data: { grant_type: "refresh_token", refresh_token: refreshToken }, type: 'POST', // POST `form encoded` data contentType: 'application/x-www-form-urlencoded'
- فرخوانی متد ValidateClientAuthentication در کلاس AppOAuthProvider . طبق معمول چون ClientID نداریم، این مرحله را قبول میکنیم.
- فراخوانی متد ReceiveAsync در کلاس RefreshTokenProvider. در قسمت توضیح مراحل لاگین، عنوان شد که پس از فراخوانی متد GrantResourceOwnerCredentials جهت لاگین، متد CreateAsync در کلاس RefreshTokenProvider فراخوانی میشود. اکنون در متد ReceiveAsync این refresh token ذخیره شدهی در بانک اطلاعاتی را یافته (بر اساس Guid ارسالی از طرف کلاینت) و سپس Deserialize میکنیم. به این ترتیب است که کار درخواست یک access token جدید بر مبنای refresh token موجود آغاز میشود.
- فراخوانی GrantRefreshToken در کلاس AppOAuthProvider . در اینجا اگر نیاز به تنظیم Claim اضافهتری وجود داشت، میتوان اینکار را انجام داد.
- فراخوانی متد Protect در کلاس AppJwtWriterFormat که کار امضای دیجیتال access token جدید را انجام میدهد.
- فراخوانی CreateAsync در کلاس RefreshTokenProvider . پس از اینکه context.DeserializeTicket در متد ReceiveAsync بر مبنای refresh token قبلی انجام شد، مجددا کار تولید یک توکن جدید در متد CreateAsync شروع میشود و زمان انقضاءها تنظیم خواهند شد.
- فراخوانی TokenEndpointResponse در کلاس AppOAuthProvider . مجددا از این متد برای دسترسی به access token جدید و ذخیرهی هش آن در بانک اطلاعاتی استفاده میکنیم.
پیاده سازی فیلتر سفارشی JwtAuthorizeAttribute
در ابتدای بحث عنوان کردیم که اگر مشخصات کاربر تغییر کردند یا کاربر logout کرد، امکان غیرفعال کردن یک توکن را نداریم و این توکن تا زمان انقضای آن معتبر است. این نقیصه را با طراحی یک AuthorizeAttribute سفارشی جدید به نام JwtAuthorizeAttribute برطرف میکنیم. نکات مهم این فیلتر به شرح زیر هستند:
- در اینجا در ابتدا بررسی میشود که آیا درخواست رسیدهی به سرور، حاوی access token هست یا خیر؟ اگر خیر، کار همینجا به پایان میرسد و دسترسی کاربر قطع خواهد شد.
- سپس بررسی میکنیم که آیا درخواست رسیده پس از مدیریت توسط Owin، دارای Claims است یا خیر؟ اگر خیر، یعنی این توکن توسط ما صادر نشدهاست.
- در ادامه شماره سریال موجود در access token را استخراج کرده و آنرا با نمونهی موجود در دیتابیس مقایسه میکنیم. اگر این دو یکی نبودند، دسترسی کاربر قطع میشود.
- همچنین در آخر بررسی میکنیم که آیا هش این توکن رسیده، در بانک اطلاعاتی ما موجود است یا خیر؟ اگر خیر باز هم یعنی این توکن توسط ما صادر نشدهاست.
بنابراین به ازای هر درخواست به سرور، دو بار بررسی بانک اطلاعاتی را خواهیم داشت:
- یکبار بررسی جدول کاربران جهت واکشی مقدار فیلد شماره سریال مربوط به کاربر.
- یکبار هم جهت بررسی جدول توکنها برای اطمینان از صدور توکن رسیده توسط برنامهی ما.
و نکتهی مهم اینجا است که از این پس بجای فیلتر معمولی Authorize، از فیلتر جدید JwtAuthorize در برنامه استفاده خواهیم کرد:
[JwtAuthorize(Roles = "Admin")] public class MyProtectedAdminApiController : ApiController
نحوهی ارسال درخواستهای Ajax ایی به همراه توکن صادر شده
تا اینجا کار صدور توکنهای برنامه به پایان میرسد. برای استفادهی از این توکنها در سمت کاربر، به فایل index.html دقت کنید. در متد doLogin، پس از موفقیت عملیات دو متغیر جدید مقدار دهی میشوند:
var jwtToken; var refreshToken; function doLogin() { $.ajax({ // same as before }).then(function (response) { jwtToken = response.access_token; refreshToken = response.refresh_token; }
function doCallApi() { $.ajax({ headers: { 'Authorization': 'Bearer ' + jwtToken }, url: "/api/MyProtectedApi", type: 'GET' }).then(function (response) {
پیاده سازی logout سمت سرور و کلاینت
پیاده سازی سمت سرور logout را در کنترلر UserController مشاهده میکنید. در اینجا در اکشن متد Logout، کار حذف توکنهای کاربر از بانک اطلاعاتی انجام میشود. به این ترتیب دیگر مهم نیست که توکن او هنوز منقضی شدهاست یا خیر. چون هش آن دیگر در جدول توکنها وجود ندارد، از فیلتر JwtAuthorizeAttribute رد نخواهد شد.
سمت کلاینت آن نیز در فایل index.html ذکر شدهاست:
function doLogout() { $.ajax({ headers: { 'Authorization': 'Bearer ' + jwtToken }, url: "/api/user/logout", type: 'GET'
بررسی تنظیمات IoC Container برنامه
تنظیمات IoC Container برنامه را در پوشهی IoCConfig میتوانید ملاحظه کنید. از کلاس SmWebApiControllerActivator برای فعال سازی تزریق وابستگیها در کنترلرهای Web API استفاده میشود و از کلاس SmWebApiFilterProvider برای فعال سازی تزریق وابستگیها در فیلتر سفارشی که ایجاد کردیم، کمک گرفته خواهد شد.
هر دوی این تنظیمات نیز در کلاس WebApiConfig ثبت و معرفی شدهاند.
به علاوه در کلاس SmObjectFactory، کار معرفی وهلههای مورد استفاده و تنظیم طول عمر آنها انجام شدهاست. برای مثال طول عمر IOAuthAuthorizationServerProvider از نوع Singleton است؛ چون تنها یک وهله از AppOAuthProvider در طول عمر برنامه توسط Owin استفاده میشود و Owin هربار آنرا وهله سازی نمیکند. همین مساله سبب شدهاست که معرفی وابستگیها در سازندهی کلاس AppOAuthProvider کمی با حالات متداول، متفاوت باشند:
public AppOAuthProvider( Func<IUsersService> usersService, Func<ITokenStoreService> tokenStoreService, ISecurityService securityService, IAppJwtConfiguration configuration)
در اینجا سرویس IAppJwtConfiguration با Func معرفی نشدهاست؛ چون طول عمر تنظیمات خوانده شدهی از Web.config نیز Singleton هستند و معرفی آن به همین نحو صحیح است.
اگر علاقمند بودید که بررسی کنید یک سرویس چندبار وهله سازی میشود، یک سازندهی خالی را به آن اضافه کنید و سپس یک break point را بر روی آن قرار دهید و برنامه را اجرا و در این حالت چندبار Login کنید.
آموزش توسعهی میکروسرویسها با Dapr
یک زبان الگو برای میکرو سرویس ها
ساخت Microservices با NET Core.
کتاب NET Microservices.
آغاز کار با الکترون
فرآیندها (Processes) در الکترون به دو بخش تقسیم میشوند:
یک. فرآیند اصلی (Main Process) که همان فایل جاوااسکریپتی است و توسط main، در فایل package.json مشخص شدهاست .فرآیند اصلی تنها فرآیندی است که قابلیت دسترسی به امکانات گرافیکی سیستم عامل را از قبیل نوتیفیکشن ها، دیالوگها ،Tray و ... دارد. فرآیند اصلی میتواند با استفاده از شیء BrowserWindow که در قسمت قبلی کاربرد آن را مشاهده کردیم، render process را ایجاد کند. با هر بار ایجاد یک نمونه از این شیء، یک Render Process ایجاد میشود.
دو. فرآیند رندر (Render Process): از آنجا که الکترون از کرومیوم استفاده میکند و کرومیوم شامل معماری چند پردازشی است، هر صفحهی وب میتواند پردازش خود را داشته باشد که به آن Render Process میگویند. به طور معمول در مرورگرها، صفحات وب در محیطی به نام SandBox اجرا میشوندکه اجازه دسترسی به منابع بومی را ندارند. ولی از آنجا که الکترون میتواند از Node.js استفاده کند، قابلیت دسترسی به تعاملات سطح پایین سیستم عامل را نیز داراست.
در فرآیند اصلی، پنجرهها توسط BrowserWindow ایجاد میشوند و هر پنجرهای که صفحه وبی را برای خودش باز میکند، شامل Render Process خودش است و هر پنجرهای که کارش خاتمه یابد، فرآیند مربوط به خودش به اتمام میرسد. فرآیند اصلی، همه صفحات وب به همراه Render Process مربوط به خودشان را مدیریت میکند و هر فرآیند رندر، از دیگری مجزا و محافظت شده است و تنها تمرکزش بر روی صفحه وبی است که متعلق به خودش است.
در ابتدا قصد داریم یک منو برای برنامهی خود درست کنیم. برای ساخت منو، راههای متفاوتی وجود دارند که فعلا ما راه استفاده از template را بر میگزینیم که به صورت یک آرایه نوشته میشود. کدهای زیر را در فایل index.js یا هر اسمی که برای آن انتخاب کردهاید بنویسید:
const electron = require('electron'); const {app,dialog,BrowserWindow,Menu,shell} = electron; let win; app.on('ready', function () { win = new BrowserWindow({width: 800, height: 600}); win.loadURL(`file://${__dirname}/index.html`); var app_menu=[ { label:'پرونده', submenu:[ { label:'باز کردن', accelerator:'CmdOrCtrl+O', click:()=>{ } }, { label:'ذخیره', accelerator:'CmdOrCtrl+S', click:()=>{ } } ] }, { label:'سیستم', submenu:[ { label:'درباره ما', click:()=> { shell.openExternal('https://www.dntips.ir'); } }, { label:'خروج', accelerator:'CmdOrCtrl+X', click:()=> { win=null; app.quit(); } } ] } ];
در خطوط بعدی، یک کار اضافهتر را جهت آشنایی بیشتر انجام میدهیم. قصد داریم اگر سیستم عامل مکینتاش بود، نام برنامه هم در ابتدای نوار منو نمایش داده شود. به همین جهت در ادامه خطوط زیر را اضافه میکنیم:
if(process.platform=="darwin") { const app_name=app.getName(); app_menu.unshift({ label:app_name }) }
ویندوز | win32 حتی اگر 64 بیتی باشد. |
لینوکس | linux |
مک | darwin |
فری بی اس دی | freebsd |
سولاریس | sunos |
دستو shell در بالا به شما اجازه میدهد با محیط دسکتاپ، یکپارچگی خود
را حفظ کنید و دستوراتی از قبیل باز کردن url، باز کردن یک مسیر دایرکتوری،
باز کردن یک فایل، انتقال فایل به سطل آشغال یا بازیافت و صدای بوق سیستم
(بیپ) را به شما میدهد. مستندات این شیء را در اینجا مطالعه فرمایید.
{ label:'خروج', accelerator:'CmdOrCtrl+X', role:'close' }
در انتها با اجرای دو دستور زیر، منو ساخته میشود:
var menu=Menu.buildFromTemplate(app_menu); Menu.setApplicationMenu(menu);
حال قصد داریم برای زیرمنوی «باز کردن فایل» یک دیالوگ open درخواست کنیم. برای این کار از شیء dialog استفاده میکنیم. پس خطوط زیر را به رویداد کلیک این زیرمنو اضافه میکنیم:
dialog.showOpenDialog({ title:'باز کردن فایل متنی', properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ] ,filters:[ {name:'فایلهای نوشتاری' , extensions:['txt','text']}, {name:'جهت تست' , extensions:['doc','docx']} ] }, (filename)=>{ if(filename===undefined) return; dialog.showMessageBox({title:'پیام اطلاعاتی',type:"info",buttons:['تایید'],message:`the name of file is [${filename}]`}); });
برای قسمت ذخیره هم کد زیر را مینویسیم:
dialog.showSaveDialog({ title:'باز کردن فایل متنی', properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ] ,filters:[ {name:'فایلهای نوشتاری' , extensions:['txt','text']} ] }, (filename)=>{ if(filename===undefined) return; });
حال بهتر است این دیالوگهای جاری را هدفمند کنیم و بتوانیم فایلهای متنی را به کاربر نمایش دهیم، یا آنها را ذخیره کنیم. به همین علت فایل html زیر را نوشته و طبق دستوری که در مقاله «آشنایی با الکترون» فرا گرفتیم، آن را نمایش میدهیم:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <body> Fie Content:<br/> <textarea id="TextFile" cols="100" rows="50"></textarea> </body> </html>
کاری که میخواهیم انجام دهیم این است که فایل متنی را باز کرده و محتوای آن را در کادر متنی نشان دهیم و موقع ذخیره نیز محتوای نوشته شده در کادر متنی را در فایلی ذخیره کنیم. از آنجا که main Process به المانهای DOM یا Render Process دسترسی ندارد، باید از طریقی، ارتباط آن را برقرار کنیم. یکی از راههای برقراری این ارتباط، IPC است. IPC در واقع یک فرستنده و یک شنونده است که هر کدام در یک سمت قرار گرفته اند. فرستنده پیام را تحت یک عنوان ارسال میکند و شنونده منتظر دریافت پیامی تحت همان عنوان میماند و پیام دریافتی را پاسخ میدهد. در این مقاله، ما فقط قسمتی از این نوع ارتباطات را بررسی میکنیم.
در نتیجه محتوای callback کدهای دیالوگ open و save را به شکل زیر تغییر میدهیم:
Open
dialog.showOpenDialog({ title:'باز کردن فایل متنی', properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ] ,filters:[ {name:'فایلهای نوشتاری' , extensions:['txt','text']}, {name:'جهت تست' , extensions:['doc','docx']} ] }, (filename)=>{ if(filename===undefined) return; win.webContents.send('openFile',filename); // dialog.showMessageBox({title:'پیام اطلاعاتی',type:"info",buttons:['تایید'],message:`the name of file is [${filename}]`}); });
dialog.showSaveDialog({ title:'باز کردن فایل متنی', properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ] ,filters:[ {name:'فایلهای نوشتاری' , extensions:['txt','text']} ] }, (filename)=>{ if(filename===undefined) return; win.webContents.send('saveFile',filename); });
برای ایجاد شنونده هم کد زیر را به فایل index.html اضافه میکنیم:
<script> const {ipcRenderer} = require('electron'); var fs=require('fs'); ipcRenderer.on('openFile', (event, arg) => { var content= fs.readFileSync(String(arg),'utf8'); document.getElementById("TextFile").value=content; }); ipcRenderer.on('saveFile', (event, arg) =>{ var content=document.getElementById("TextFile").value; fs.writeFileSync(String(arg),content,'utf8'); alert('ذخیره شد'); }); </script>
در اینجا شوندههایی را از نوع ipcRenderer ایجاد میکنیم و با استفاده از متد on، به پیامهایی تحت عنوانهای مشخص شده گوش فرا میدهیم. پیامهای ارسالی را که حاوی آدرس فایل میباشند، به شیءای که از نوع fs میباشد، میدهند و آنها را میخوانند یا مینویسند. خواندن و نوشتن فایل، به صورت همزمان صورت میگیرد. ولی اگر دوست دارید که به صورت غیر همزمان پیامی را بخوانید یا بنویسید، باید عبارت Sync را از نام متدها حذف کنید و یک callback را به عنوان پارامتر دوم قرار دهید و محتوای آن را از طریق نوشتن یک پارامتر در سازنده دریافت کنید.
فایلهای پروژه