ممنون خیلی خوب بود. یهreference خوب واسه ajax معرفی می کنید. مثلا همین پنجره captcha رو چجوری با ajax درست می کنن؟؟؟؟
پاسخ به بازخوردهای پروژهها
نمایش گزارش در پنجره جدید
باز کردن پنجره جدید، ارسال پارامتر به یک صفحهی دیگر و امثال اینها را خودتان مدیریت کنید. این کتابخانه فقط فایل PDF درست میکند.
ممکن است تعدادی از اکشن متدهای API طراحی شده، محافظت شده باشند. بنابراین OpenAPI Specification تولیدی نیز باید به همراه مستندات کافی در این مورد باشد تا استفاده کنندگان از آن بدانند چگونه باید با آن کار کنند. نگارش سوم OpenAPI Specification از اعتبارسنجی و احراز هویت مبتنی بر هدرها مانند basic و یا bearer، همچنین حالت کار با API Keys مانند هدرها، کوئری استرینگها و کوکیها و یا حالت OAuth2 و OpenID Connect پشتیبانی میکند و این موارد ذیل خواص securitySchemes و security در OpenAPI Specification ظاهر میشوند:
- خاصیت securitySchemes انواع حالتهای اعتبارسنجی پشتیبانی شده را لیست میکند.
- خاصیت security کار اعمال Scheme تعریف شده را به کل API یا صرفا قسمتهای خاصی از آن، انجام میدهد.
در ادامه مثالی را بررسی خواهیم کرد که مبتنی بر basic authentication کار میکند و در این حالت به ازای هر درخواست به API، نیاز است یک نام کاربری و کلمهی عبور نیز ارسال شوند. البته روش توصیه شده، کار با JWT و یا OpenID Connect است؛ اما جهت تکمیل سادهتر این قسمت، بدون نیاز به برپایی مقدماتی پیچیده، کار با basic authentication را بررسی میکنیم و اصول کلی آن از دیدگاه مستندات OpenAPI Specification تفاوتی نمیکند.
افزودن Basic Authentication به API برنامه
برای پیاده سازی Basic Authentication نیاز به یک AuthenticationHandler سفارشی داریم:
کار این AuthenticationHandler سفارشی، با بازنویسی متد HandleAuthenticateAsync شروع میشود. در اینجا به دنبال هدر ویژهای با کلید Authorization میگردد. این هدر باید به همراه نام کاربری و کلمهی عبوری با حالت base64 encoded باشد. اگر این هدر وجود نداشت و یا مقدار هدر Authorization، با فرمتی که مدنظر ما است قابل decode و همچنین جداسازی نبود، شکست اعتبارسنجی اعلام میشود.
پس از دریافت مقدار هدر Authorization، ابتدا مقدار آنرا از base64 به حالت معمولی تبدیل کرده و سپس بر اساس حرف ":"، دو قسمت را از آن جداسازی میکنیم. قسمت اول را به عنوان نام کاربری و قسمت دوم را به عنوان کلمهی عبور پردازش خواهیم کرد. در این مثال جهت سادگی، این دو باید مساوی DNT و 123 باشند. اگر اینچنین بود، یک AuthenticationTicket دارای Claim ای حاوی نام کاربری را ایجاد کرده و آنرا به عنوان حاصل موفقیت آمیز بودن عملیات بازگشت میدهیم.
مرحلهی بعد، استفاده و معرفی این BasicAuthenticationHandler تهیه شده به برنامه است:
در اینجا توسط متد services.AddAuthentication، این scheme جدید که نام رسمی آن Basic است، به همراه Handler آن، به برنامه معرفی میشود.
همچنین نیاز است میانافزار اعتبارسنجی را نیز با فراخوانی متد app.UseAuthentication، به برنامه اضافه کرد که باید پیش از فراخوانی app.UseMvc صورت گیرد تا به آن اعمال شود:
همچنین برای اینکه تمام اکشن متدهای موجود را نیز محافظت کنیم، میتوان فیلتر Authorize را به صورت سراسری اعمال کرد:
تکمیل مستندات API جهت انعکاس تنظیمات محافظت از اکشن متدهای آن
پس از تنظیم محافظت دسترسی به اکشن متدهای برنامه، اکنون نوبت به مستند کردن آن است و همانطور که در ابتدای بحث نیز عنوان شد، برای این منظور نیاز به تعریف خواص securitySchemes و security در OpenAPI Specification است:
در اینجا توسط متد setupAction.AddSecurityDefinition، ابتدا یک نام تعریف میشود (از این نام، در قسمت بعدی تنظیمات استفاده خواهد شد). پارامتر دوم آن همان SecurityScheme است که توضیح داده شد. برای حالت basic auth، نوع آن Http است و اسکیمای آن basic. باید دقت داشت که مقدار خاصیت Scheme در اینجا، حساس به بزرگی و کوچکی حروف است.
پس از این تنظیم اگر برنامه را اجرا کنیم، یک دکمهی authorize اضافه شدهاست:
با کلیک بر روی آن، صفحهی ورود نام کاربری و کلمهی عبور ظاهر میشود:
اگر آنرا تکمیل کرده و سپس برای مثال لیست نویسندگان را درخواست کنیم (با کلیک بر روی دکمهی try it out آن و سپس کلیک بر روی دکمهی execute ذیل آن)، تنها خروجی 401 یا unauthorized را دریافت میکنیم:
- بنابراین برای تکمیل آن، مطابق نکات قسمت چهارم، ابتدا باید status code مساوی 401 را به صورت سراسری، مستند کنیم:
- همچنین هرچند با کلیک بر روی دکمهی Authorize در Swagger UI و ورود نام کاربری و کلمهی عبور توسط آن، در همانجا پیام Authorized را دریافت کردیم، اما اطلاعات آن به ازای هر درخواست، به سمت سرور ارسال نمیشود. به همین جهت در حین درخواست لیست نویسندگان، پیام unauthorized را دریافت کردیم. برای رفع این مشکل نیاز است به OpenAPI Spec اعلام کنیم که تعامل با API، نیاز به Authentication دارد:
در اینجا OpenApiSecurityRequirement یک دیکشنری از نوع <<Dictionary<OpenApiSecurityScheme, IList<string است که کلید آن از نوع OpenApiSecurityScheme تعریف میشود و باید آنرا به نمونهای که توسط setupAction.AddSecurityDefinition پیشتر اضافه کردیم، متصل کنیم. این اتصال توسط خاصیت Reference آن و Id ای که به نام تعریف شدهی توسط آن اشاره میکند، صورت میگیرد. مقدار این دیکشنری نیز لیستی از رشتهها میتواند باشد (مانند توکنها و scopes در OpenID Connect) که در اینجا با یک لیست خالی مقدار دهی شدهاست.
پس از این تنظیمات، Swagger UI با افزودن یک آیکن قفل به مداخل APIهای محافظت شده، به صورت زیر تغییر میکند:
در این حالت اگر بر روی آیکن قفل کلیک کنیم، همان صفحهی دیالوگ ورود نام کاربری و کلمهی عبوری که پیشتر با کلیک بر روی دکمهی Authorize ظاهر شد، نمایش داده میشود. با تکمیل آن و کلیک مجدد بر روی آیکن قفل، جهت گشوده شدن پنل API و سپس کلیک بر روی try it out آن، برای مثال میتوان به API محافظت شدهی دریافت لیست نویسندگان، بدون مشکلی، دسترسی یافت:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: OpenAPISwaggerDoc-06.zip
"securitySchemes": { "basicAuth": { "type":"http", "description":"Input your username and password to access this API", "scheme":"basic" } } … "security":[ {"basicAuth":[]} ]
- خاصیت security کار اعمال Scheme تعریف شده را به کل API یا صرفا قسمتهای خاصی از آن، انجام میدهد.
در ادامه مثالی را بررسی خواهیم کرد که مبتنی بر basic authentication کار میکند و در این حالت به ازای هر درخواست به API، نیاز است یک نام کاربری و کلمهی عبور نیز ارسال شوند. البته روش توصیه شده، کار با JWT و یا OpenID Connect است؛ اما جهت تکمیل سادهتر این قسمت، بدون نیاز به برپایی مقدماتی پیچیده، کار با basic authentication را بررسی میکنیم و اصول کلی آن از دیدگاه مستندات OpenAPI Specification تفاوتی نمیکند.
افزودن Basic Authentication به API برنامه
برای پیاده سازی Basic Authentication نیاز به یک AuthenticationHandler سفارشی داریم:
using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Net.Http.Headers; using System.Security.Claims; using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; namespace OpenAPISwaggerDoc.Web.Authentication { public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> { public BasicAuthenticationHandler( IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected override Task<AuthenticateResult> HandleAuthenticateAsync() { if (!Request.Headers.ContainsKey("Authorization")) { return Task.FromResult(AuthenticateResult.Fail("Missing Authorization header")); } try { var authenticationHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]); var credentialBytes = Convert.FromBase64String(authenticationHeader.Parameter); var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':'); var username = credentials[0]; var password = credentials[1]; if (username == "DNT" && password == "123") { var claims = new[] { new Claim(ClaimTypes.NameIdentifier, username) }; var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); return Task.FromResult(AuthenticateResult.Success(ticket)); } return Task.FromResult(AuthenticateResult.Fail("Invalid username or password")); } catch { return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization header")); } } } }
پس از دریافت مقدار هدر Authorization، ابتدا مقدار آنرا از base64 به حالت معمولی تبدیل کرده و سپس بر اساس حرف ":"، دو قسمت را از آن جداسازی میکنیم. قسمت اول را به عنوان نام کاربری و قسمت دوم را به عنوان کلمهی عبور پردازش خواهیم کرد. در این مثال جهت سادگی، این دو باید مساوی DNT و 123 باشند. اگر اینچنین بود، یک AuthenticationTicket دارای Claim ای حاوی نام کاربری را ایجاد کرده و آنرا به عنوان حاصل موفقیت آمیز بودن عملیات بازگشت میدهیم.
مرحلهی بعد، استفاده و معرفی این BasicAuthenticationHandler تهیه شده به برنامه است:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(defaultScheme: "Basic") .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("Basic", null);
همچنین نیاز است میانافزار اعتبارسنجی را نیز با فراخوانی متد app.UseAuthentication، به برنامه اضافه کرد که باید پیش از فراخوانی app.UseMvc صورت گیرد تا به آن اعمال شود:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseStaticFiles(); app.UseAuthentication(); app.UseMvc(); } } }
همچنین برای اینکه تمام اکشن متدهای موجود را نیز محافظت کنیم، میتوان فیلتر Authorize را به صورت سراسری اعمال کرد:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddMvc(setupAction => { setupAction.Filters.Add(new AuthorizeFilter()); // ...
تکمیل مستندات API جهت انعکاس تنظیمات محافظت از اکشن متدهای آن
پس از تنظیم محافظت دسترسی به اکشن متدهای برنامه، اکنون نوبت به مستند کردن آن است و همانطور که در ابتدای بحث نیز عنوان شد، برای این منظور نیاز به تعریف خواص securitySchemes و security در OpenAPI Specification است:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSwaggerGen(setupAction => { // ... setupAction.AddSecurityDefinition("basicAuth", new OpenApiSecurityScheme { Type = SecuritySchemeType.Http, Scheme = "basic", Description = "Input your username and password to access this API" }); });
پس از این تنظیم اگر برنامه را اجرا کنیم، یک دکمهی authorize اضافه شدهاست:
با کلیک بر روی آن، صفحهی ورود نام کاربری و کلمهی عبور ظاهر میشود:
اگر آنرا تکمیل کرده و سپس برای مثال لیست نویسندگان را درخواست کنیم (با کلیک بر روی دکمهی try it out آن و سپس کلیک بر روی دکمهی execute ذیل آن)، تنها خروجی 401 یا unauthorized را دریافت میکنیم:
- بنابراین برای تکمیل آن، مطابق نکات قسمت چهارم، ابتدا باید status code مساوی 401 را به صورت سراسری، مستند کنیم:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddMvc(setupAction => { setupAction.Filters.Add(new ProducesResponseTypeAttribute(StatusCodes.Status401Unauthorized));
- همچنین هرچند با کلیک بر روی دکمهی Authorize در Swagger UI و ورود نام کاربری و کلمهی عبور توسط آن، در همانجا پیام Authorized را دریافت کردیم، اما اطلاعات آن به ازای هر درخواست، به سمت سرور ارسال نمیشود. به همین جهت در حین درخواست لیست نویسندگان، پیام unauthorized را دریافت کردیم. برای رفع این مشکل نیاز است به OpenAPI Spec اعلام کنیم که تعامل با API، نیاز به Authentication دارد:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSwaggerGen(setupAction => { // ... setupAction.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "basicAuth" } }, new List<string>() } }); });
پس از این تنظیمات، Swagger UI با افزودن یک آیکن قفل به مداخل APIهای محافظت شده، به صورت زیر تغییر میکند:
در این حالت اگر بر روی آیکن قفل کلیک کنیم، همان صفحهی دیالوگ ورود نام کاربری و کلمهی عبوری که پیشتر با کلیک بر روی دکمهی Authorize ظاهر شد، نمایش داده میشود. با تکمیل آن و کلیک مجدد بر روی آیکن قفل، جهت گشوده شدن پنل API و سپس کلیک بر روی try it out آن، برای مثال میتوان به API محافظت شدهی دریافت لیست نویسندگان، بدون مشکلی، دسترسی یافت:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: OpenAPISwaggerDoc-06.zip
اشتراکها
dialog element در Chrome 37 Beta
سلام.خسته نباشید.
اگه بخواهیم توی ویرایش به صورت Dialog به جای DropDownList از JQuery AutoComplete استفاده کنیم باید چه تغییری بدیم؟
مرسی
پاسخ به بازخوردهای پروژهها
Helper AjaxDialog چطوری کار میکنه؟
jQuery UI Dialog هست. فراخوانی آن در فایل adminjs.js پوشه Iris.Web\Scripts صورت گرفته.
پیش نیاز
اگر در مورد TortoiseSVN و سورس کنترل اطلاعات پایه ندارید، کتاب مدیریت فایلهای یک پروژه نرم افزاری با استفاده از Subversion آقای نصیری را مطالعه کنید و همچنین سیستم پیگیری خطای YouTrack را نگاهی بیاندازید (البته اگر اطلاعی ندارید).
مقدمه
هنگام کار روی یک پروژه، باگ ها، وظیفهها و موضوعاتی به شما واگذار میشود که باید انها را انجام دهید. هنگام commit کردن تغییرات، برای مشخص شدن اینکه تغییرات مربوط به کدام Bug-Id بوده است بود است باید سیستم Bug/Issue Tracker رو با سورس کنترل یکپارچه کنیم.
یکپارچه سازی TortoiseSVN و YouTrack
1- روی یک نسخه کاری پروژه راست کلیک، از منوی TortoiseSVN گزینه Properties را انتخاب کنید.
bugtraq:url : آدرس YouTrack Sever که به این صورت وارد میشود: %http://localhost:8080/issue/%BUGID
bugtraq:message : درو اقع الگویی پیامی هست که برای نگهداری Bug-Id استفاده میشود و باید شامل کلمه %BUGID% باشد. مثلا: %Issue: %BUGID
bugtraq:number : مقدار این خصلت را false وارد کنید؛ چون Bug-Idهایهای YouTrack میتوانند شامل عدد و حروف باشند.
bugtraq:message : درو اقع الگویی پیامی هست که برای نگهداری Bug-Id استفاده میشود و باید شامل کلمه %BUGID% باشد. مثلا: %Issue: %BUGID
bugtraq:number : مقدار این خصلت را false وارد کنید؛ چون Bug-Idهایهای YouTrack میتوانند شامل عدد و حروف باشند.
بعد از اینکه این سه خصلت را مقداردهی کرید، تغییرات را Commit کنید. همانطور که میبینید یک Textbox (بالا، سمت راست) اضافه شده که محل وارد کردن Bug-Id مربوط به تغییرات است. از این پس، میتوانید Bug-Id یا Issue-Idهای مربوط به هر تغییرات را در آن Textbox وارد کنید.
همچنین تغییرات در پلاگین AnkhSVN در ویژال استودیو نیز اعمال میشود:
اکنون، در متن commitها شماره Bud-Id نیز ذکر شده است.
نکته 1: اگر YouTrack روی یک سرور نصب هست، بجای localhost نام کامپیوتر سرور یا آی پی آن را وارد کنید. پورت 8080 نیز بصورت پیش فرض است و اگر هنگام نصب آن را تغییر داده اید، اینجا نیز آنرا تغییر دهید.
نکته 2: خصلت bugtraq:message یک الگوی پیام از شما میگیرد؛ یعنی الگو را تحت هر شکلی میتوان وارد کرد. بعنوان مثال الگو را به این شکل وارد کنید: "برای مشاهده جزئیات بیشتر به Bug-Id شماره %BUGID% مراجعه کنید."
نکته 3: اگر خصلت bugtraq:number مقدارش true باشد، برای وارد کردن Bug-Id فقط از عدد میتوانید استفاده کنید. بصورت پیش فرض مقدار این خصلت true است.
نکته 4: میتوانید این تنظیمات را در یک فایل Export کنید و در بقیه پروژه ها، با یک مرحله و بسادگی آنرا Import کنید.
خصلتهای دیگری نیز میتوان برروی مخزن کد اعمال کرد که از حوزه این مقاله خارج است. همچنین تنظمیات اختیاری جانبی دیگری نیز برای یکپارچه سازی وجود دارند. برای دیدن این تنظمیات روی نسخه کاری راست کلیک، از منوی TortoiseSVN گزینه Properties را انتخاب کنید و از پنجره باز شده روی دکمه New و گزینه ( Bugtraq (Issue tracker integration را انتخاب کنید.
برای اطلاعات بیشتر در مورد این تنظیمات، داکیومنت یکپارچه سازی با سیستمهای Bug tracking / Issue Tracking را مطالعه کنید.
در این مقاله مفاهیم مختلفی را در ارتباط با DataBinding بررسی خواهیم کرد:
• One Way Binding بخش اول
• INPC بخش اول
• Tow Way Binding بخش اول
• List Binding بخش دوم
• Element Binding بخش دوم
• Data Conversion بخش دوم
در ابتدا مفهوم انقیاد دادهها یا همان DataBinding را مرور میکنیم. به فرآیند مرتبط سازی منابع اطلاعاتی به کنترلها در برنامهها یا به بیان امروزیتر، به Viewها و نمایش اطلاعات در آنها، انقیاد (Databinding) گویند.
One Way Data Binding (انقیاد یک طرفه)
در این حالت اطلاعات را صرفا در یک View و یا یک کنترل نمایش میدهیم و تغییر اطلاعات در View، تاثیری بر روی منبع اطلاعاتی نخواهد داشت.
مثال: یک پروژهی WPF ساده را ایجاد و سپس کلاس Employee را با خصوصیات زیر، تعریف میکنیم:
public class Employee { public string Name { get; set; } public string Title { get; set; } public static Employee GetEmployee() { var emp = new Employee { Name = "Mani", Title = "CEO" }; return emp; } }
در بخش markup فایل MainWindow.xaml کدهای زیر را ایجاد میکنیم:
<Grid> <StackPanel Name="Display"> <StackPanel Orientation="Horizontal"> <TextBlock>First Name :</TextBlock> <TextBlock Margin="5,0,0,0" Text="{Binding Name}"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock>Title :</TextBlock> <TextBlock Margin="5,0,0,0" Text="{Binding Title}"></TextBlock> </StackPanel> </StackPanel> </Grid>
برای تکمیل و انجام عملیات Binding کافی است خصوصیت DataContext را با استفاده از متد استاتیک تعریف شدهی در کلاس Employee پر کنیم.
public MainWindow() { InitializeComponent(); DataContext = Employee.GetEmployee(); }
INPC یا INotifyPropertyChanged Interface
پس از بایند کردن اطلاعات به View مورد نظر، ممکن است منبع داده در طول زمان استفاده تغییر کند. از این رو لازم است که این تغییرات، به View اعمال شوند. بدین منظور میبایست ابتدا Interface ، INotifyPropertyChanged را برای کلاس Employee پیاده سازی کنیم:
public class Employee :INotifyPropertyChanged { public string Name { get; set; } public string Title { get; set; } public static Employee GetEmployee() { var emp = new Employee { Name = "Mani", Title = "CEO" }; return emp; } public event PropertyChangedEventHandler PropertyChanged; [NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged ([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
پس از پیاده سازی Interface، خصوصیات کلاس Employee نبایستی بصورت AutoProperty باشند. پس پیاده سازی خصوصیات را با تعریف فیلدهای مورد نیاز تغییر میدهیم.
علت تغییر پیاده سازی، لزوم فراخوانی رویداد dOnPropertyChange در بلاک Set خصوصیات کلاس Employee میباشد:
private string _name; private string _title; public string Name { get { return _name; } set { _name = value; OnPropertyChanged(); } } public string Title { get { return _title; } set { _title = value; OnPropertyChanged(); } }
در ادامه ، در بخش CodeBehind در سازنده کلاس کدهای زیر را جایگزین کدهای قبلی می کنیم :
private Employee emp; public MainWindow() { InitializeComponent(); emp = new Employee() { Name = "Mani", Title = "CEO" }; DataContext = emp; }
<StackPanel Orientation="Horizontal"> <Button Click="btnClick" Width="70" Height="30" Content="Change"/> </StackPanel>
در بخش CodeBehind، رویداد Click را به شکل زیر پیاده سازی میکنیم:
private void btnClick(object sender, RoutedEventArgs e) { emp.Name = "Amir"; emp.Title = "Manager"; }
در برنامهی فوق، در ابتدا View با اطلاعات ارسالی در بخش سازنده، پر میشود و با کلیک بر روی دکمه، منبع داده بهروز شده (در اینجا شیء emp) و بهصورت اتوماتیک View ما بهروز خواهد شد.
Tow Way Data binding (انقیاد دو طرفه)
در این حالت از Data binding، با تغییر View، تغییرات بر روی منبع داده نیز اعمال میشوند.
ابتدا بخش markup مثال فوق را با اضافه کردن ویژگی Mode در کنار ویژگی Binding به شکل زیر تغییر میدهیم:
<Grid> <StackPanel Name="Display" > <StackPanel Orientation="Horizontal"> <TextBlock Margin="5,0,0,0">Name :</TextBlock> <TextBox Margin="5,0,0,0" Text="{Binding Name, Mode=TwoWay}"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Margin="5,0,0,0">Title :</TextBlock> <TextBox Margin="5,0,0,0" Text="{Binding Title,Mode=TwoWay}"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Margin="5,0,0,0">Name :</TextBlock> <TextBlock Margin="5,0,0,0" Text="{Binding Name}"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Margin="5,0,0,0">Title :</TextBlock> <TextBlock Margin="5,0,0,0" Text="{Binding Title}"/> </StackPanel> </StackPanel> </Grid>
اگر ویژگی Mode نوشته نشود بصورت پیش فرض بهصورت OneWay تعبیر میشود. حالت قبل. همچنین در کد بالا دو Textbox در صفحه قرار داده شدهاند تا با تغییر محتوای آن بتوانیم تاثیر عملیات دوطرفهی انقیاد را بر روی Textblockهای بعدی مشاهده کنیم.
پس از اجرای برنامه (بخش CodeBehind نیازی به اصلاح ندارد) مقداری جدید در Textbox موجود در صفحه تایپ کنید. کافی است Focus از روی کنترلی که محتوای آن را تغییر دادهاید، عوض شود، بلافاصله Textblock متناظر با آن، با محتوای جدیدی که در منبع داده اعمال شده است بهروز میشود.
- پیشنیار بحث «معرفی JSON Web Token»
پیاده سازیهای زیادی را در مورد 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 بستهی زیر را دارد:
بستهی Microsoft.Owin.Host.SystemWeb سبب میشود تا کلاس OwinStartup به صورت خودکار شناسایی و بارگذاری شود. این کلاسی است که کار تنظیمات اولیهی JSON Web token را انجام میدهد و بستهی Microsoft.Owin.Security.Jwt شامل تمام امکاناتی است که برای راه اندازی توکنهای JWT نیاز داریم.
از structuremap هم برای تنظیمات تزریق وابستگیهای برنامه استفاده شدهاست. به این صورت قسمت تنظیمات اولیهی JWT ثابت باقی خواهد ماند و صرفا نیاز خواهید داشت تا کمی قسمت سرویسهای برنامه را بر اساس بانک اطلاعاتی و روش ذخیره سازی خودتان سفارشی سازی کنید.
دریافت کدهای کامل برنامه
کدهای کامل این برنامه را از اینجا میتوانید دریافت کنید. در ادامه صرفا قسمتهای مهم این کدها را بررسی خواهیم کرد.
بررسی کلاس AppJwtConfiguration
کلاس AppJwtConfiguration، جهت نظم بخشیدن به تعاریف ابتدایی توکنهای برنامه در فایل web.config، ایجاد شدهاست. اگر به فایل web.config برنامه مراجعه کنید، یک چنین تعریفی را مشاهده خواهید کرد:
این قسمت جدید بر اساس configSection ذیل که به کلاس AppJwtConfiguration اشاره میکند، قابل استفاده شدهاست (بنابراین اگر فضای نام این کلاس را تغییر دادید، باید این قسمت را نیز مطابق آن ویرایش کنید؛ درغیراینصورت، appJwtConfiguration قابل شناسایی نخواهد بود):
- در اینجا tokenPath، یک مسیر دلخواه است. برای مثال در اینجا به مسیر login تنظیم شدهاست. برنامهای که با Microsoft.Owin.Security.Jwt کار میکند، نیازی ندارد تا یک قسمت لاگین مجزا داشته باشد (مثلا یک کنترلر User و بعد یک اکشن متد اختصاصی Login). کار لاگین، در متد GrantResourceOwnerCredentials کلاس AppOAuthProvider انجام میشود. اینجا است که نام کاربری و کلمهی عبور کاربری که به سمت سرور ارسال میشوند، توسط Owin دریافت و در اختیار ما قرار میگیرند.
- در این تنظیمات، دو زمان منقضی شدن را مشاهده میکنید؛ یکی مرتبط است به 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
کار سرویس TokenStore، ذخیره سازی و تعیین اعتبار توکنهای صادر شدهاست. در اینجا ثبت یک توکن، بررسی صحت وجود یک توکن رسیده، حذف توکنهای منقضی شده، یافتن یک توکن بر اساس هش توکن، حذف یک توکن بر اساس هش توکن، غیرمعتبر کردن و حذف تمام توکنهای یک شخص و به روز رسانی توکن یک کاربر، پیش بینی شدهاند.
پیاده سازی این کلاس بسیار شبیه است به پیاده سازی ORMهای موجود و فقط یک SaveChanges را کم دارد.
یک نکته:
در سرویس ذخیره سازی توکنها، یک چنین متدی قابل مشاهده است:
استفاده از InvalidateUserTokens در اینجا سبب خواهد شد با لاگین شخص و یا صدور توکن جدیدی برای او، تمام توکنهای قبلی او حذف شوند. به این ترتیب امکان لاگین چندباره و یا یافتن دسترسی به منابع محافظت شدهی برنامه در سرور توسط چندین نفر (که به توکن شخص دسترسی یافتهاند یا حتی تقاضای توکن جدیدی کردهاند)، میسر نباشد. همینکه توکن جدیدی صادر شود، تمام لاگینهای دیگر این شخص غیرمعتبر خواهند شد.
ب) سرویس UsersService
از کلاس سرویس کاربران، برای یافتن شماره سریال یک کاربر استفاده میشود. در مورد شماره سریال پیشتر بحث کردیم و هدف آن مشخص سازی وضعیت تغییر این موجودیت است. اگر کاربری اطلاعاتش تغییر کرد، این فیلد باید با یک Guid جدید مقدار دهی شود.
همچنین متدهای دیگری برای یافتن یک کاربر بر اساس نام کاربری و کلمهی عبور او (جهت مدیریت مرحلهی لاگین)، یافتن کاربر بر اساس Id او (جهت استخراج اطلاعات کاربر) و همچنین یک متد اختیاری نیز برای به روز رسانی فیلد آخرین تاریخ فعالیت کاربر در اینجا پیش بینی شدهاند.
ج) سرویس SecurityService
در قسمتهای مختلف این برنامه، هش SHA256 مورد استفاده قرار گرفتهاست که با توجه به مباحث تزریق وابستگیها، کاملا قابل تعویض بوده و برنامه صرفا با اینترفیس آن کار میکند.
پیاده سازی قسمت لاگین و صدور access token
در کلاس AppOAuthProvider کار پیاده سازی قسمت لاگین برنامه انجام شدهاست. این کلاسی است که توسط کلاس AppOAuthOptions به OwinStartup معرفی میشود. قسمتهای مهم کلاس AppOAuthProvider به شرح زیر هستند:
برای درک عملکرد این کلاس، در ابتدای متدهای مختلف آن، یک break point قرار دهید. برنامه را اجرا کرده و سپس بر روی دکمهی login کلیک کنید. به این ترتیب جریان کاری این کلاس را بهتر میتوانید درک کنید. کار آن با فراخوانی متد ValidateClientAuthentication شروع میشود. چون با یک برنامهی وب در حال کار هستیم، ClientId آنرا نال درنظر میگیریم و برای ما مهم نیست. اگر کلاینت ویندوزی خاصی را تدارک دیدید، این کلاینت میتواند ClientId ویژهای را به سمت سرور ارسال کند که در اینجا مدنظر ما نیست.
مهمترین قسمت این کلاس، متد GrantResourceOwnerCredentials است که پس از ValidateClientAuthentication بلافاصله فراخوانی میشود. اگر به کدهای آن دقت کنید، خود owin دارای خاصیتهای user name و password نیز هست.
این اطلاعات را به نحو ذیل از کلاینت خود دریافت میکند. اگر به فایل index.html مراجعه کنید، یک چنین تعریفی را برای متد login میتوانید مشاهده کنید:
url آن به همان مسیری که در فایل web.config تنظیم کردیم، اشاره میکند. فرمت data ایی که به سرور ارسال میشود، دقیقا باید به همین نحو باشد و content type آن نیز مهم است و owin فقط حالت form-urlencoded را پردازش میکند. به این ترتیب است که user name و password توسط owin قابل شناسایی شده و grant_type آن است که سبب فراخوانی GrantResourceOwnerCredentials میشود و مقدار آن نیز دقیقا باید password باشد (در حین لاگین).
در متد GrantResourceOwnerCredentials کار بررسی نام کاربری و کلمهی عبور کاربر صورت گرفته و در صورت یافت شدن کاربر (صحیح بودن اطلاعات)، نقشهای او نیز به عنوان Claim جدید به توکن اضافه میشوند.
در اینجا یک Claim سفارشی هم اضافه شدهاست:
کار آن ذخیره سازی userId کاربر، در توکن صادر شدهاست. به این صورت هربار که توکن به سمت سرور ارسال میشود، نیازی نیست تا یکبار از بانک اطلاعاتی بر اساس نام او، کوئری گرفت و سپس id او را یافت. این id در توکن امضاء شده، موجود است. نمونهی نحوهی کار با آنرا در کنترلرهای این API میتوانید مشاهده کنید. برای مثال در MyProtectedAdminApiController این اطلاعات از توکن استخراج شدهاند (جهت نمایش مفهوم).
در انتهای این کلاس، از متد 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 صادر شدهی در زمان لاگین، به صورت زیر عمل میشود:
در اینجا نحوهی عملکرد، همانند متد login است. با این تفاوت که grant_type آن اینبار بجای password مساوی refresh_token است و مقداری که به عنوان refresh_token به سمت سرور ارسال میکند، همان مقداری است که در عملیات لاگین از سمت سرور دریافت کردهاست. آدرس ارسال این درخواست نیز همان tokenPath تنظیم شدهی در فایل web.config است. بنابراین مراحلی که در اینجا طی خواهند شد، به ترتیب زیر است:
- فرخوانی متد 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 در برنامه استفاده خواهیم کرد:
نحوهی ارسال درخواستهای Ajax ایی به همراه توکن صادر شده
تا اینجا کار صدور توکنهای برنامه به پایان میرسد. برای استفادهی از این توکنها در سمت کاربر، به فایل index.html دقت کنید. در متد doLogin، پس از موفقیت عملیات دو متغیر جدید مقدار دهی میشوند:
از متغیر jwtToken به ازای هربار درخواستی به یکی از کنترلرهای سایت، استفاده میکنیم و از متغیر refreshToken در متد doRefreshToken برای درخواست یک access token جدید. برای مثال اینبار برای اعتبارسنجی درخواست ارسالی به سمت سرور، نیاز است خاصیت headers را به نحو ذیل مقدار دهی کرد:
بنابراین هر درخواست ارسالی به سرور باید دارای هدر ویژهی Bearer فوق باشد؛ در غیراینصورت با پیام خطای 401، از دسترسی به منابع سرور منع میشود.
پیاده سازی logout سمت سرور و کلاینت
پیاده سازی سمت سرور logout را در کنترلر UserController مشاهده میکنید. در اینجا در اکشن متد Logout، کار حذف توکنهای کاربر از بانک اطلاعاتی انجام میشود. به این ترتیب دیگر مهم نیست که توکن او هنوز منقضی شدهاست یا خیر. چون هش آن دیگر در جدول توکنها وجود ندارد، از فیلتر JwtAuthorizeAttribute رد نخواهد شد.
سمت کلاینت آن نیز در فایل index.html ذکر شدهاست:
تنها کاری که در اینجا انجام شده، فراخوانی اکشن متد logout سمت سرور است. البته ذکر jwtToken نیز در اینجا الزامی است. زیرا این توکن است که حاوی اطلاعاتی مانند userId کاربر فعلی است و بر این اساس است که میتوان رکوردهای او را در جدول توکنها یافت و حذف کرد.
بررسی تنظیمات IoC Container برنامه
تنظیمات IoC Container برنامه را در پوشهی IoCConfig میتوانید ملاحظه کنید. از کلاس SmWebApiControllerActivator برای فعال سازی تزریق وابستگیها در کنترلرهای Web API استفاده میشود و از کلاس SmWebApiFilterProvider برای فعال سازی تزریق وابستگیها در فیلتر سفارشی که ایجاد کردیم، کمک گرفته خواهد شد.
هر دوی این تنظیمات نیز در کلاس WebApiConfig ثبت و معرفی شدهاند.
به علاوه در کلاس SmObjectFactory، کار معرفی وهلههای مورد استفاده و تنظیم طول عمر آنها انجام شدهاست. برای مثال طول عمر IOAuthAuthorizationServerProvider از نوع Singleton است؛ چون تنها یک وهله از AppOAuthProvider در طول عمر برنامه توسط Owin استفاده میشود و Owin هربار آنرا وهله سازی نمیکند. همین مساله سبب شدهاست که معرفی وابستگیها در سازندهی کلاس AppOAuthProvider کمی با حالات متداول، متفاوت باشند:
در کلاسی که طول عمر singleton دارد، وابستگیهای تعریف شدهی در سازندهی آن هم تنها یکبار در طول عمر برنامه نمونه سازی میشوند. برای رفع این مشکل و transient کردن آنها، میتوان از Func استفاده کرد. به این ترتیب هر بار ارجاهی به usersService، سبب وهله سازی مجدد آن میشود و این مساله برای کار با سرویسهایی که قرار است با بانک اطلاعاتی کار کنند ضروری است؛ چون باید طول عمر کوتاهی داشته باشند.
در اینجا سرویس IAppJwtConfiguration با Func معرفی نشدهاست؛ چون طول عمر تنظیمات خوانده شدهی از Web.config نیز Singleton هستند و معرفی آن به همین نحو صحیح است.
اگر علاقمند بودید که بررسی کنید یک سرویس چندبار وهله سازی میشود، یک سازندهی خالی را به آن اضافه کنید و سپس یک break point را بر روی آن قرار دهید و برنامه را اجرا و در این حالت چندبار Login کنید.
پیاده سازیهای زیادی را در مورد 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 کنید.
مطالب دورهها
نگاهی به اجزای سیستم راهبری بوت استرپ 3
در مطلب «نگاهی به اجزای تعاملی Twitter Bootstrap» مباحث سیستم راهبری بوت استرپ 2 بررسی شدند. در ادامه قصد داریم، نکات جدیدی را در اینباره خصوصا جهت ارتقاء به بوت استرپ 3، بررسی کنیم.
تعیین موقعیت کاربر در صفحه به کمک breadcrumbs در Bootstrap
در مورد قسمت breadcrumb، مطالب مانند قبل و پیشنیاز ذکر شده است. با این تفاوت که بهتر است بجای ul از ol استفاده شود؛ چون ترتیب این عناصر مهم است. ol به معنای ordered list میباشد:
یک سؤال: اگر نخواهیم این خطوط مورب ظاهر شوند و برای مثال علاقمند باشیم تا از گلیف آیکنهای معرفی شده در قسمت قبل استفاده کنیم، چه باید کرد؟
برای این منظور نیاز است با نحوه رندر خطوط مورب در بوت استرپ آشنا شویم. بنابراین فایل bootstrap-rtl.css را گشوده و چند سطر ذیل را جستجو کنید:
همانطور که ملاحظه میکنید، تفسیر عبارت ذکر شده در قسمت content سبب نمایش این خط مورب است. برای حذف آن، به فایل custom.css پروژه مراجعه و تعاریف ذیل را اضافه خواهیم کرد (این فایل همانطور که در قسمت اول ذکر شد، باید پس از ذکر لینک فایل CSS اصلی بوت استرپ، تعریف شود):
به این ترتیب خطوط مورب breadcrumb حذف میشوند. اکنون برای افزودن گلیف آیکنها به صورت زیر میتوان عمل کرد:
اگر از رنگ گلیف آیکنهای نمایش داده شده راضی نیستید، آنها را نیز میتوانید در فایل CSS سفارشی خود تغییر دهید. برای مثال:
تعریف برگهها در Twitter Bootstrap
در مورد تعریف برگهها، بوت استرپ 3 با نگارش 2 آن، تفاوتی ندارد و تمام نکات مطلب «نگاهی به اجزای تعاملی Twitter Bootstrap» در اینجا نیز صادق هستند. یک ul باید تعریف شود و سپس برای نمونه کلاسهای nav nav-tabs را به آنها اضافه خواهیم کرد تا به شکل tab به نظر برسند. برگه فعال نیز با کلاس active مشخص میشود.
یک نکته جدید: در بوت استرپ 3 میتوان یک برگه را کاملا در عرض صفحه کشید و امتداد داد:
با اضافه کردن nav-justified، بجای شکل زیر
به تصویر ذیل خواهیم رسید که در آن tab، در امتداد صفحه کشیده شده است:
تعریف navbar در بوت استرپ 3
اصول کلی navbar بوت استرپ 3 همانند بوت استرپ 2 است؛ با چند تفاوت کوچک:
- کلاس btn-navbar بوت استرپ 2 به کلاس navbar-btn در بوت استرپ 3 تغییر نام یافته است.
- کلاس navbar-inner بوت استرپ 2 کلا حذف شده است.
- کلاسهای nav-list به کلاسهای list-group تغییر نام یافتهاند.
- کلاس brand با navbar-brand جایگزین شده است.
- کلاسهای navbar-brand و navbar-toggle باید داخل المانی با کلاس navbar-header محصور شوند.
- کلاس nav باید به همراه navbar-nav باشد.
- کلاسهای جدید navbar-default navbar-text navbar-btn navbar-header اضافه شدهاند.
یک مثال:
به همراه CSS سفارشی:
توضیحات:
- CSS سفارشی، سبب خواهد شد تا تگ h4 بالای صفحه، زیر navbar مخفی نشده و قابل مشاهده باشد.
- کار با المان nav دارای کلاس navbar navbar-default شروع میشود. همچنین role=navigation نیز به این المان اضافه شدهاست. این مورد کمکی خواهد بود به افرادی با قدرت بینایی کم که از screen readers استفاده میکنند.
- داخل navbar-default یک div دیگر با کلاس navbar-header اضافه شده است. این مورد قابلیتهای واکنشگرای navbar را فراهم میسازد. به این ترتیب زمانیکه عرض صفحه کمتر میشود، مانند تصویر دوم، یک دکمه ویژه نمایش داده خواهد شد.
- هرجایی در بوت استرپ ویژگیهای data- را ملاحظه کردید، یعنی قرار است اطلاعاتی در اختیار اجزای جاوا اسکریپتی آن قرار گیرند. برای نمونه data-target به یک div با آی دی مساوی collapse اشاره میکند. به این معنا که اگر در زمان کم بودن عرض صفحه، دکمه ویژه واکنشگرای navbar ظاهر شد، با کلیک بر روی آن دکمه، div یاد شده را نمایش بده.
- span تعریف شده با کلاس sr-only به معنای اطلاعاتی است که صرفا جهت screen readers تدارک دیده شدهاند.
فایلهای نهایی این قسمت را از اینجا نیز میتوانید دریافت کنید:
bs3-sample03.zip
تعیین موقعیت کاربر در صفحه به کمک breadcrumbs در Bootstrap
در مورد قسمت breadcrumb، مطالب مانند قبل و پیشنیاز ذکر شده است. با این تفاوت که بهتر است بجای ul از ol استفاده شود؛ چون ترتیب این عناصر مهم است. ol به معنای ordered list میباشد:
<ol class="breadcrumb"> <li><a href="#">خانه</a></li> <li><a href="#">خدمات</a></li> <li class="active">محصولات</li> </ol>
یک سؤال: اگر نخواهیم این خطوط مورب ظاهر شوند و برای مثال علاقمند باشیم تا از گلیف آیکنهای معرفی شده در قسمت قبل استفاده کنیم، چه باید کرد؟
برای این منظور نیاز است با نحوه رندر خطوط مورب در بوت استرپ آشنا شویم. بنابراین فایل bootstrap-rtl.css را گشوده و چند سطر ذیل را جستجو کنید:
.breadcrumb > li + li:before { content: "/\00a0"; padding: 0 5px; color: #cccccc; }
.breadcrumb > li + li:before { content: none; }
<ol class="breadcrumb"> <li><a href="#">خانه</a> <span class="glyphicon glyphicon-circle-arrow-left"></span></li> <li><a href="#">خدمات</a> <span class="glyphicon glyphicon-circle-arrow-left"></span></li> <li class="active">محصولات</li> </ol>
اگر از رنگ گلیف آیکنهای نمایش داده شده راضی نیستید، آنها را نیز میتوانید در فایل CSS سفارشی خود تغییر دهید. برای مثال:
.glyphicon { color: #cdae51; }
تعریف برگهها در Twitter Bootstrap
در مورد تعریف برگهها، بوت استرپ 3 با نگارش 2 آن، تفاوتی ندارد و تمام نکات مطلب «نگاهی به اجزای تعاملی Twitter Bootstrap» در اینجا نیز صادق هستند. یک ul باید تعریف شود و سپس برای نمونه کلاسهای nav nav-tabs را به آنها اضافه خواهیم کرد تا به شکل tab به نظر برسند. برگه فعال نیز با کلاس active مشخص میشود.
یک نکته جدید: در بوت استرپ 3 میتوان یک برگه را کاملا در عرض صفحه کشید و امتداد داد:
<ul class="nav nav-pills nav-justified"> <li><a href="#">Home</a></li> <li><a href="#">About</a></li> <li class="active"><a href="#">Services</a></li> <li><a href="#">Photo Gallery</a></li> <li><a href="#">Contact</a></li> </ul>
به تصویر ذیل خواهیم رسید که در آن tab، در امتداد صفحه کشیده شده است:
تعریف navbar در بوت استرپ 3
اصول کلی navbar بوت استرپ 3 همانند بوت استرپ 2 است؛ با چند تفاوت کوچک:
- کلاس btn-navbar بوت استرپ 2 به کلاس navbar-btn در بوت استرپ 3 تغییر نام یافته است.
- کلاس navbar-inner بوت استرپ 2 کلا حذف شده است.
- کلاسهای nav-list به کلاسهای list-group تغییر نام یافتهاند.
- کلاس brand با navbar-brand جایگزین شده است.
- کلاسهای navbar-brand و navbar-toggle باید داخل المانی با کلاس navbar-header محصور شوند.
- کلاس nav باید به همراه navbar-nav باشد.
- کلاسهای جدید navbar-default navbar-text navbar-btn navbar-header اضافه شدهاند.
یک مثال:
<div class="container"> <h4 class="alert alert-info"> nav</h4> <div class="row"> <nav class="navbar navbar-default navbar-inverse navbar-fixed-top" role="navigation"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span><span class="icon-bar"></span><span class="icon-bar"></span> </button> </div> <div class="collapse navbar-collapse" id="collapse"> <ul class="nav navbar-nav"> <li><a href="#">Home</a></li> <li><a href="#">About</a></li> <li class="active"><a href="#">Services</a></li> <li><a href="#">Photo Gallery</a></li> <li><a href="#">Contact</a></li> </ul> </div> </nav> </div> <!-- end row --> </div> <!-- /container -->
body { padding-top: 70px; }
توضیحات:
- CSS سفارشی، سبب خواهد شد تا تگ h4 بالای صفحه، زیر navbar مخفی نشده و قابل مشاهده باشد.
- کار با المان nav دارای کلاس navbar navbar-default شروع میشود. همچنین role=navigation نیز به این المان اضافه شدهاست. این مورد کمکی خواهد بود به افرادی با قدرت بینایی کم که از screen readers استفاده میکنند.
- داخل navbar-default یک div دیگر با کلاس navbar-header اضافه شده است. این مورد قابلیتهای واکنشگرای navbar را فراهم میسازد. به این ترتیب زمانیکه عرض صفحه کمتر میشود، مانند تصویر دوم، یک دکمه ویژه نمایش داده خواهد شد.
- هرجایی در بوت استرپ ویژگیهای data- را ملاحظه کردید، یعنی قرار است اطلاعاتی در اختیار اجزای جاوا اسکریپتی آن قرار گیرند. برای نمونه data-target به یک div با آی دی مساوی collapse اشاره میکند. به این معنا که اگر در زمان کم بودن عرض صفحه، دکمه ویژه واکنشگرای navbar ظاهر شد، با کلیک بر روی آن دکمه، div یاد شده را نمایش بده.
- span تعریف شده با کلاس sr-only به معنای اطلاعاتی است که صرفا جهت screen readers تدارک دیده شدهاند.
فایلهای نهایی این قسمت را از اینجا نیز میتوانید دریافت کنید:
bs3-sample03.zip