public class Customer { public string Email { get; private set; } public string Name { get; private set; } private Customer(email, name) { Email = email; Name = name; } public static Result<Customer> New(string email, string name, INewCustomerPolicy policy) { var isUnique = policy.IsUnique(email); if (!isUnique) { return Result.Fail<Customer>("Customer with this email already exists."); } var customer = new Customer(email, name); //customer.AddDomainEvent(new CustomerRegistered(customer)); return Result.Ok(customer); } }
پیشنیازها
در اینجا فرض بر این است که موارد ذیل را نصب کردهاید:
- آخرین نگارش مرورگر Chrome
- افزونهی Debugger for Chrome که از آن برای دیباگ کدهای جاوا اسکریپتی و تایپاسکریپتی یکپارچهی با VSCode میتوان استفاده کرد.
تنظیمات VSCode جهت دیباگ برنامههای مبتنی بر Angular CLI
کدهای مطلب «فرمهای مبتنی بر قالبها در Angular - قسمت پنجم - ارسال اطلاعات به سرور» از انتهای بحث آن قابل دریافت هستند. این کدها را دریافت کرده و سپس پوشهی اصلی آنرا در VSCode باز کنید. اگر در ویندوز هستید با کلیک راست بر روی پوشه، گزینهی Open With Code ظاهر میشود و یا حتی میتوان از طریق خط فرمان به پوشهی اصلی برنامه مراجعه کرده و سپس دستور . code را صادر نمود.
در ادامه نیاز است دیباگر VSCode را تنظیم کنیم. اگر پیشتر هیچ تنظیمی را نداشته باشید، پس از مراجعهی به برگهی debug آن، بر روی دکمهی سبز رنگ Start Debugging آن کلیک کنید. در ادامه در منوی باز شده، گزینهی Chrome را انتخاب کنید:
اینکار سبب میشود تا فایل جدید vscode\launch.json. به پوشهی جاری اضافه شده و سپس تنظیمات دیباگر کروم در آن قرار گیرند.
حالت دوم شبیه به حالتی است که برنامهی مورد بحث جاری دارد: پیشتر دیباگری مانند NET Core. در آن تنظیم شدهاست. در این حالت، منوی drop down مربوط به دیباگرهای تنظیم شده را گشوده و سپس گزینهی آخر آن یعنی Add configuration را انتخاب کنید:
در اینجا نیز منوی Intellisense آن ظاهر شده و امکان انتخاب گزینهی Chrome را میدهد:
در نهایت هر دو حالت به ایجاد فایل جدید vscode\launch.json. و یا ویرایش آن منتهی میشوند. در اینجا تنها کاری را که باید انجام داد، تغییر پورت پیش فرض آن است:
- اگر از دستور ng serve استفاده میکنید، این پورت را به 4200 تغییر دهید (پورت پیش فرض این دستور است؛ برای تغییر آن، از پارامتر port-- در دستور ng serve استفاده کنید):
{ "version": "0.2.0", "configurations": [ { "type": "chrome", "request": "launch", "name": "Launch Chrome with ng serve", "url": "http://localhost:4200/#", "webRoot": "${workspaceRoot}" } ] }
- اگر از دستورات dotnet watch run و سپس ng build -watch استفاده میکنید (اولی وب سرور آزمایشی NET Core. را راه اندازی میکند و دومی کار ساخت پیوستهی برنامهی Angular را انجام میدهد)، این پورت را بر روی 5000 تنظیم کنید:
{ "version": "0.2.0", "configurations": [ { "type": "chrome", "request": "launch", "name": "Launch Chrome for dotnet build & ng build", "url": "http://localhost:5000/#", "webRoot": "${workspaceRoot}" } ] }
آزمایش یک break point و بررسی مقادیر دریافتی از سرور
تا اینجا کاری را که انجام دادیم، به افزودن یک قطعه تنظیم جدید به فایل vscode\launch.json. خلاصه میشود.
در ادامه به کامپوننت employee-register.component.ts مراجعه کرده و سطر this.languages = data را تبدیل به یک سطر مستقل درون {} میکنیم تا بتوانیم بر روی آن break point قرار دهیم:
ngOnInit() { this.formPoster.getLanguages().subscribe( data => { this.languages = data; }, err => console.log("get error: ", err) ); }
پس از آن از طریق خط فرمان به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات
>npm install >ng build --watch
>dotnet restore >dotnet watch run
سپس به برگهی دیباگ مراجعه کرده و گزینهی جدید Launch Chrome for dotnet build & ng build را انتخاب و سپس بر روی دکمهی سبز رنگ اجرای آن کلیک کنید:
اکنون اگر صفحهی مشاهدهی فرم را در مرورگر کروم باز شده درخواست کنیم، به این break point خواهیم رسید؛ اما ... حاوی اطلاعات data نیست. برای رفع این مشکل نیاز است تنظیمات پیشفرض را به صورت ذیل بهبود بخشید:
{ "version": "0.2.0", "configurations": [ { "type": "chrome", "request": "launch", "name": "Launch Chrome for dotnet build & ng build", "url": "http://localhost:5000", "runtimeArgs": [ "--user-data-dir", "--remote-debugging-port=9222", "--disable-session-crashed-bubble" ], "sourceMaps": true, "trace": true, "webRoot": "${workspaceRoot}/wwwroot/", "userDataDir": "${workspaceRoot}/.vscode/chrome" } ] }
- تنظیم sourceMaps و همچنین مشخص سازی محل دقیق پوشهی قرارگیری فایلهای نهایی ng build که همان پوشهی wwwroot است در webRoot سبب خواهند شد تا اینبار break point ما حاوی اطلاعات واقعی data دریافتی از سرور باشند (با نزدیک کردن اشارهگر ماوس به data، اطلاعات تکمیلی آن ظاهر میشود):
- اگر از دستور ng serve استفاده میکنید، در این تنظیمات پورت را به 4200 و محل پوشهی wwwroot را به dist تغییر دهید (مطابق تنظیمات فایل angular-cli.json.).
@inherits OwningComponentBase ... private IUserInfoService userInfoService { get; set; } ... userInfoService = ScopedServices.GetRequiredService<IUserInfoService>();
@inject IUserInfoService UserInfoService
public class ManualLog: IManualLog { private readonly IUserInfoService _userInfoService; public ManualLog(IUserInfoService userInfoService) { _userInfoService = userInfoService ?? throw new ArgumentNullException(nameof(userInfoService)); } public async task Log() { int userId = await _userInfoService.GetUserIdAsync(); } }
GetAuthenticationStateAsync was called before SetAuthenticationState
class HandlerChain
{
setNextObj(nextObjInChain){}
processMultiple(req){
console.log("No multiple for: " + req.getMultiple());
}
}
class Multiple
{
constructor(multiple){
this.multiple = multiple;
}
getMultiple(){
return this.multiple;
}
}
class MultipleofTwoHandler extends HandlerChain
{
constructor(){
super()
this.nextObjInChain = new HandlerChain()
}
setNextObj(nextObj){
this.nextObjInChain = nextObj;
}
processMultiple(req) {
if ((req.getMultiple() % 2) == 0) {
console.log("Multiple of 2: " + req.getMultiple());
}else{
this.nextObjInChain.processMultiple(req);
}
}
}
class MultipleofThreeHandler extends HandlerChain
{
constructor(){
super()
this.nextObjInChain = new HandlerChain()
}
setNextObj(nextObj){
this.nextObjInChain = nextObj;
}
processMultiple(req)
{
if ((req.getMultiple() % 3) == 0) {
console.log("Multiple of 3: " + req.getMultiple());
}else{
this.nextObjInChain.processMultiple(req);
}
}
}
class MultipleofFiveHandler extends HandlerChain
{
constructor(){
super()
this.nextObjInChain = new HandlerChain()
}
setNextObj(nextObj){
this.nextObjInChain = nextObj;
}
processMultiple(req) {
if ((req.getMultiple() % 5) == 0) {
console.log("Multiple of 5: " + req.getMultiple());
}else{
this.nextObjInChain.processMultiple(req);
}
}
}
//configuring the chain of handler objects
var c1 = new MultipleofTwoHandler();
var c2 = new MultipleofThreeHandler();
var c3 = new MultipleofFiveHandler();
c1.setNextObj(c2);
c2.setNextObj(c3);
//the chain handling different cases
c1.processMultiple(new Multiple(95)); // Multiple of 5: 95
c1.processMultiple(new Multiple(50)); // Multiple of 2: 50
c1.processMultiple(new Multiple(9)); // Multiple of 3: 9
c1.processMultiple(new Multiple(4)); // Multiple of 2: 4
c1.processMultiple(new Multiple(21)); // Multiple of 3: 21
c1.processMultiple(new Multiple(23)); // No multiple for: 23
- MultipleofTwoHandler: بررسی میکند که آیا عدد وارد شده، مضربی از 2 است یا نه.
- MultipleofThreeHandler: بررسی میکند که آیا عدد وارد شده، مضربی از 3 است یا نه
- MultipleofFiveHandler: بررسی میکند که آیا عدد وارد شده، مضربی از 5 است یا نه.
class HandlerChain { setNextObj(nextObjInChain){} processMultiple(req){ console.log("No multiple for: " + req.getMultiple()); } }
- تنظیم کردن handler بعدی در زنجیره
- پردازش عدد وارد شده به این منظور که آیا در مضرب مورد نظر قرار دارد یا نه.
class MultipleofTwoHandler extends HandlerChain { constructor(){/*code*/} setNextObj(nextObj){/*code*/} processMultiple(req){/*code*/} } class MultipleofThreeHandler extends HandlerChain { constructor(){/*code*/} setNextObj(nextObj){/*code*/} processMultiple(req){/*code*/} } class MultipleofFiveHandler extends HandlerChain { constructor(){/*code*/} setNextObj(nextObj){/*code*/} processMultiple(req){/*code*/} }
constructor(){ super() this.nextObjInChain = new HandlerChain() }
مقدار دهی اولیه میشود که تعیین کنندهی شیء بعدی در زنجیره میباشد.
setNextObj(nextObj){ this.nextObjInChain = nextObj; }
var c1 = new MultipleofTwoHandler(); var c2 = new MultipleofThreeHandler(); var c3 = new MultipleofFiveHandler(); c1.setNextObj(c2); c2.setNextObj(c3);
class Multiple { constructor(multiple){ this.multiple = multiple } getMultiple(){ return this.multiple; } }
processMultiple(req) { if ((req.getMultiple() % 2) == 0) { console.log("Multiple of 2: " + req.getMultiple()); }else{ this.nextObjInChain.processMultiple(req); } }
c1.processMultiple(new Multiple(95)) // Multiple of 5: 95
مشخص سازی رشتههای متفاوت اتصالی
فرض کنید برنامهی جاری شما قرار است از دو بانک اطلاعاتی مشخص استفاده کند که تعاریف رشتههای اتصالی آنها در وب کانفیگ به صورت ذیل مشخص شدهاند:
<connectionStrings> <clear /> <add name="Sample07Context" connectionString="Data Source=(local);Initial Catalog=TestDbIoC;Integrated Security = true" providerName="System.Data.SqlClient" /> <add name="Database2012" connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true" providerName="System.Data.SqlClient" /> </connectionStrings>
تغییر Context برنامه جهت پذیرش رشتههای اتصالی پویای قابل تغییر در زمان اجرا
اکنون که قرار است کاربران در حین ورود به برنامه، بانک اطلاعاتی مدنظر خود را انتخاب کنند و یا سیستم قرار است به ازای کاربری خاص، رشتهی اتصالی خاص او را به Context ارسال کند، نیاز است Context برنامه را به صورت ذیل تغییر دهیم:
using System.Collections.Generic; using System.Data.Entity; using System.Linq; using EF_Sample07.DomainClasses; namespace EF_Sample07.DataLayer.Context { public class Sample07Context : DbContext, IUnitOfWork { public DbSet<Category> Categories { set; get; } public DbSet<Product> Products { set; get; } /// <summary> /// It looks for a connection string named Sample07Context in the web.config file. /// </summary> public Sample07Context() : base("Sample07Context") { } /// <summary> /// To change the connection string at runtime. See the SmObjectFactory class for more info. /// </summary> public Sample07Context(string connectionString) : base(connectionString) { //Note: defaultConnectionFactory in the web.config file should be set. } public void SetConnectionString(string connectionString) { this.Database.Connection.ConnectionString = connectionString; } } }
یک متد دیگر هم در اینجا در انتهای کلاس به نام SetConnectionString تعریف شدهاست. از این متد در حین ورود کاربر به سایت میتوان استفاده کرد. برای مثال حداقل دو نوع طراحی را میتوان درنظر گرفت:
الف) کاربر با برنامهای کار میکند که به ازای سالهای مختلف، بانکهای اطلاعاتی مختلفی دارد و در ابتدای ورود، یک drop down انتخاب سال کاری برای او درنظر گرفته شدهاست (علاوه بر سایر ورودیهای استانداردی مانند نام کاربری و کلمهی عبور). در این حالت بهتر است متد SetConnectionString نام رشتهی اتصالی را بر اساس سال انتخابی، در حین لاگین دریافت کند که اطلاعات آن در فایل کانفیگ سایت پیشتر مشخص شدهاست.
ب) کاربر یا مشتری پس از ورود به سایت، نیاز است صرفا از بانک اطلاعاتی خاص خودش استفاده کند. بنابراین اطلاعات تعریف کاربران و مشتریها در یک بانک اطلاعاتی مجزا قرار دارند و پس از لاگین، نیاز است رشتهی اتصالی او به صورت پویا از بانک اطلاعاتی خوانده شده و سپس توسط متد SetConnectionString تنظیم گردد.
مدیریت سشنهای رشتهی اتصالی جاری
پس از اینکه کاربر، در حین ورود مشخص کرد که از چه بانک اطلاعاتی قرار است استفاده کند و یا اینکه برنامه بر اساس اطلاعات ثبت شدهی او تصمیمگیری کرد که باید از کدام رشتهی اتصالی استفاده کند، نگهداری این رشتهی اتصالی نیاز به سشن دارد تا به ازای هر کاربر متصل به سایت منحصربفرد باشد. در مورد مدیریت سشنها در برنامههای وب، از نکات مطرح شدهی در مطلب «مدیریت سشنها در برنامههای وب به کمک تزریق وابستگیها» استفاده خواهیم کرد:
using System; using System.Threading; using System.Web; using EF_Sample07.DataLayer.Context; using EF_Sample07.ServiceLayer; using StructureMap; using StructureMap.Web; using StructureMap.Web.Pipeline; namespace EF_Sample07.IoCConfig { public static class SmObjectFactory { private static readonly Lazy<Container> _containerBuilder = new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication); public static IContainer Container { get { return _containerBuilder.Value; } } public static void HttpContextDisposeAndClearAll() { HttpContextLifecycle.DisposeAndClearAll(); } private static Container defaultContainer() { return new Container(ioc => { // session manager setup ioc.For<ISessionProvider>().Use<DefaultWebSessionProvider>(); ioc.For<HttpSessionStateBase>() .Use(ctx => new HttpSessionStateWrapper(HttpContext.Current.Session)); ioc.For<IUnitOfWork>() .HybridHttpOrThreadLocalScoped() .Use<Sample07Context>() // Remove these 2 lines if you want to use a connection string named Sample07Context, defined in the web.config file. .Ctor<string>("connectionString") .Is(ctx => getCurrentConnectionString(ctx)); ioc.For<ICategoryService>().Use<EfCategoryService>(); ioc.For<IProductService>().Use<EfProductService>(); ioc.For<ICategoryService>().Use<EfCategoryService>(); ioc.For<IProductService>().Use<EfProductService>(); ioc.Policies.SetAllProperties(properties => { properties.OfType<IUnitOfWork>(); properties.OfType<ICategoryService>(); properties.OfType<IProductService>(); properties.OfType<ISessionProvider>(); }); }); } private static string getCurrentConnectionString(IContext ctx) { if (HttpContext.Current != null) { // this is a web application var sessionProvider = ctx.GetInstance<ISessionProvider>(); var connectionString = sessionProvider.Get<string>("CurrentConnectionString"); if (string.IsNullOrWhiteSpace(connectionString)) { // It's a default connectionString. connectionString = "Database2012"; // this session value should be set during the login phase sessionProvider.Store("CurrentConnectionStringName", connectionString); } return connectionString; } else { // this is a desktop application, so you can store this value in a global static variable. return "Database2012"; } } } }
نکتهی مهم این تنظیمات، قسمت مقدار دهی سازندهی کلاس Context برنامه به صورت پویا توسط IoC Container جاری است. در اینجا هر زمانیکه قرار است وهلهای از Sample07Context ساخته شود، از سازندهی دوم آن که دارای پارامتری به نام connectionString است، استفاده خواهد شد. همچنین مقدار آن به صورت پویا از متد getCurrentConnectionString که در انتهای کلاس تعریف شدهاست، دریافت میگردد.
در این متد ابتدا مقدار HttpContext.Current بررسی شدهاست. این مقدار اگر نال باشد، یعنی برنامهی جاری یک برنامهی دسکتاپ است و مدیریت رشتهی اتصالی جاری آنرا توسط یک خاصیت Static یا Singleton تعریف شدهی در برنامه نیز میتوان تامین کرد. از این جهت که در هر زمان، تنها یک کاربر در App Domain جاری برنامهی دسکتاپ میتواند وجود داشته باشد و Singleton یا Static تعریف شدن اطلاعات رشتهی اتصالی، مشکلی را ایجاد نمیکند. اما در برنامههای وب، چندین کاربر در یک App Domain به سیستم وارد میشوند. به همین جهت است که مشاهده میکنید در اینجا از تامین کنندهی سشن، برای نگهداری اطلاعات رشتهی اتصالی جاری کمک گرفته شدهاست.
کلید این سشن نیز در این مثال مساوی CurrentConnectionStringName تعریف شدهاست. بنابراین در حین لاگین موفقیت آمیز کاربر، دو مرحلهی زیر باید طی شوند:
sessionProvider.Store("CurrentConnectionString", "Sample07Context"); uow.SetConnectionString(WebConfigurationManager.ConnectionStrings[_sessionProvider.Get<string>("CurrentConnectionString")].ConnectionString);
سپس از متد SetConnectionString برای خواندن مقدار نام مشخص شده در سشن CurrentConnectionStringName کمک گرفتهایم. هرچند سازندهی کلاس Context برنامه، هر دو حالت استفاده از نام رشتهی اتصالی و یا مقدار کامل رشتهی اتصالی را پشتیبانی میکند، اما خاصیت this.Database.Connection.ConnectionString تنها رشتهی کامل اتصالی را میپذیرد (بکار رفته در متد SetConnectionString).
تا اینجا کار پویا سازی انتخاب و استفاده از رشتهی اتصالی برنامه به پایان میرسد. هر زمانیکه قرار است Context برنامه توسط IoC Container نمونه سازی شود، به متد getCurrentConnectionString رجوع کرده و مقدار رشتهی اتصالی را از سشن تنظیم شدهای به نام CurrentConnectionStringName دریافت میکند. سپس از مقدار آن جهت مقدار دهی سازندهی دوم کلاس Context استفاده خواهد کرد.
مدیریت migrations خودکار برنامه در حالت استفاده از چندین بانک اطلاعاتی
یکی از مشکلات کار با برنامههای چند دیتابیسی، به روز رسانی ساختار تمام بانکهای اطلاعاتی مورد استفاده، پس از تغییری در ساختار مدلهای برنامه است. از این جهت که اگر تمام بانکهای اطلاعاتی به روز نشوند، کوئریهای جدید برنامه که از خواص و فیلدهای جدید استفاده میکنند، دیگر کار نخواهند کرد. پویا سازی اعمال این تغییرات را میتوان به صورت ذیل انجام داد:
using System; using System.Data.Entity; using System.Web; using EF_Sample07.DataLayer.Context; using EF_Sample07.IoCConfig; namespace EF_Sample07.WebFormsAppSample { public class Global : HttpApplication { void Application_Start(object sender, EventArgs e) { initDatabases(); } private static void initDatabases() { // defined in web.config string[] connectionStringNames = { "Sample07Context", "Database2012" }; foreach (var connectionStringName in connectionStringNames) { Database.SetInitializer( new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>(connectionStringName)); using (var ctx = new Sample07Context(connectionStringName)) { ctx.Database.Initialize(force: true); } } } void Application_EndRequest(object sender, EventArgs e) { SmObjectFactory.HttpContextDisposeAndClearAll(); } } }
در این مثال خاص، متد initDatabases در حین آغاز برنامه فراخوانی شدهاست. منظور این است که اینکار در طول عمر برنامه تنها کافی است یکبار انجام شود و پس از آن است که EF Code first میتواند از رشتههای اتصالی متفاوتی که به آن ارسال میشود، بدون مشکل استفاده کند. زیرا اطلاعات نگاشت کلاسهای مدل برنامه به جداول بانک اطلاعاتی به این ترتیب است که کش میشوند و یا بر اساس کلاس Configuration به صورت خودکار به بانک اطلاعاتی اعمال میگردند.
کدهای کامل این مثال را که در حقیقت نمونهی بهبود یافتهی مطلب «EF Code First #12» است، از اینجا میتوانید دریافت کنید:
UoW-Sample
No Authentication
اگر گزینه No Authentication را انتخاب کنید، پروژه ایجاد شده صفحاتی را برای ورود به سایت نخواهد ساخت. همچنین رابط کاربری ای برای نمایش کاربر فعلی، کلاسهای موجودیتها برای یک دیتابیس عضویت و رشتههای اتصالی نیز وجود نخواهند داشت.
- سیستم عضویت جدید بجای استفاده از ماژول ASP.NET Forms Authentication بر پایه OWIN نوشته شده است. این بدین معنا است که از یک مکانیزم احراز هویت واحد میتوانید در اپلیکیشنهای ASP.NET Web Forms, MVC, Web API و SignalR استفاده کنید.
- سیستم عضویت جدید توسط Entity Framework Code First مدیریت میشود و شامل تمامی کلاسهایی است که نماینده جداول و موجودیتها هستند. این بدین معنا است که روی الگوی دیتابیس کنترل کامل دارید. سفارشی سازی و تغییر اطلاعات کاربران و پروفایل هایشان بسیار سادهتر است، تنها لازم است برای اعمال تغییرات از Code First Migrations استفاده کنید.
- asp.net/identity
- Create an ASP.NET MVC 5 App with Facebook and Google OAuth2 and OpenID Sign-on
- Web API - External Authentication Services
- Adding External Logins to your ASP.NET application in Visual Studio 2013
اگر گزینه Organizational Accounts را انتخاب کنید پروژه ایجاد شده برای استفاده از (Windows Identity Foundation (WIF پیکربندی خواهد شد. این فریم ورک برای احراز هویت کاربران از (Windows Azure Active Directory (WAAD استفاده میکند که شامل Office 365 نیز میشود.
از این گزینه برای احراز هویت کاربرانی استفاده کنید که در قالب یک OWIN Tenant تعریف میشوند. برای مثال سایتی با نام Company.com داریم که برای کارمندان این سازمان از طریق company.onmicrosoft.com قابل دسترسی خواهد بود. نمیتوانید WAAD را طوری پیکربندی کنید که کاربران tenantهای دیگر نیز به اپلیکیشن شما دسترسی داشته باشند.
Domain
نام دامنهای در WAAD که میخواهید اپلیکیشن را برای آن پیکربندی کنید، مثلا company.onmicrosoft.com. اگر از custom domain ها استفاده میکنید مانند company.com بجای company.onmicrosoft.com میتوانید این اطلاعات را اینجا وارد کنید.
سطح دسترسی
اگر اپلیکیشن نیاز به کوئری گرفتن یا بروز رسانی اطلاعات پوشهها (directory information) را توسط Graph API دارد، از گزینههای Single Sign-On, Read Directory Data و یا Single Sign-On, Read and Write Directory Data استفاده کنید. در غیر اینصورت گزینه Single Sign-On را رها کنید. برای اطلاعات بیشتر به Application Access Levels و Using the Graph API to Query Windows Azure AD مراجعه کنید.
Application ID URI
بصورت پیش فرض، قالب پروژه یک شناسه application ID URI برای شما تولید میکند، که این کار با الحاق نام پروژه شما به نام دامنه WAAD صورت میگیرد. برای مثال، اگر نام پروژه Example باشد و نام دامنه contoso.onmicrosoft.com، شناسه خروجی https://contoso.onmicrosoft.com/Example میشود. اگر میخواهید بصورت دستی این فیلد را مقدار دهی کنید، گزینه More Options را انتخاب کنید. این شناسه باید با //:https شروع شود.
بصورت پیش فرض، اگر اپلیکیشنی که در WAAD تهیه و تدارک دیده شده است، شناسهای یکسان با شناسه موجود در پروژه Visual Studio داشته باشد، پروژه شما به اپلیکیشن موجود در WAAD متصل خواهد شد. اگر میخواهید تدارکات جدیدی ببینید تیک گزینه Overwrite the application entry if one with the same ID already exists را حذف کنید.
اگر تیک این گزینه حذف شده باشد، و ویژوال استودیو اپلیکیشنی با شناسهای یکسان را پیدا کند، عددی به آخر URI اضافه خواهد شد. مثلا فرض کنید نام پروژه Example است و اپلیکیشنی نیز با شناسه https://contoso.onmicrosoft.com/Example در WAAD وجود دارد. در این صورت اپلیکیشن جدیدی با شناسه ای مانند https://contoso.onmicrosoft.com/Example_ 20130619330903 ایجاد میشود.
تهیه و تدارک اپلیکیشن در WAAD
برای آنکه یک اپلیکیشن WAAD ایجاد کنید و یا پروژه را به یک اپلیکیشن موجود متصل کنید، ویژوال استودیو به اطلاعات ورود یک مدیر کل برای دامنه مورد نظر، نیاز دارد. هنگامی که در دیالوگ Configure Authentication روی OK کلیک میکنید، اطلاعات ورود یک مدیر کل از شما درخواست میشود و نهایتا هنگامیکه روی Create Project کلیک میکنید، ویژوال استودیو اپلیکیشن شما را در WAAD پیکربندی میکند.
برای اطلاعات بیشتر درباره نحوه استفاده از مدل احراز هویت Cloud - Single Organization به لینکهای زیر مراجعه فرمایید:
- Windows Azure Authentication
- Adding Sign-On to Your Web Application Using Windows Azure AD
- Developing ASP.NET Apps with Windows Azure Active Directory
از این گزینه برای احراز هویت کاربرانی استفاده کنید که در WAAD tenantهای متعددی تعریف شدهاند. برای مثال، نام سایت contoso.com است و برای کارمندان دو سازمان از طریق آدرسهای contoso.onmicrosoft.com و fabrikam.onmicrosoft.com قابل دسترسی خواهد بود. نحوه پیکربندی این مدل نیز مانند قسمت قبلی است.
برای اطلاعات بیشتر درباره احراز هویت Cloud - Multi Organization به لینکهای زیر مراجعه کنید:
- Easy Web App Integration with Windows Azure Active Directory, ASP.NET & Visual Studio
- Developing Multi-Tenant Web Applications with Windows Azure AD
این گزینه را هنگامی انتخاب کنید که کاربران در (Windows Server Active Directory (AD تعریف شده اند و نمیخواهید از WAAD استفاده کنید. از این مدل برای ایجاد وب سایتهای اینترنت و اینترانت میتوانید استفاده کنید. برای یک وب سایت اینترنتی از (Active Directory Federation Services (ADFS استفاده کنید.
برای یک وب سایت اینترانتی، میتوانید کلا این گزینه را رها کنید و از Windows Authentication استفاده کنید. در صورت استفاده از گزینه Windows Authentication لازم نیست تا آدرس سند متادیتا (metadata document URL) را فراهم کنید، همچنین توجه داشته باشید که Windows Authentication امکان کوئری گرفتن از پوشهها و کنترل سطوح دسترسی در Active Directory را ندارد.
On-Premises Authority
آدرس سند متادیتا. این سند اطلاعاتی درباره مختصات Authority دارد که اپلیکیشن از آنها برای به پیش بردن روند احراز هویت و ورود به سایت استفاده میکند.
Application ID URI
یک شناسه منحصر به فرد که AD از آن برای شناسایی اپلیکیشن استفاده میکند. میتوانید این فیلد را خالی رها کنید تا ویژوال استودیو بصورت خودکار اپلیکیشنی بدین منظور بسازد.
قدمهای بعدی
در این مقاله با مدلهای مختلف احراز هویت در اپلیکیشنهای Visual Studio 2013 آشنا شدید و برخی تغییرات و امکانات جدید نیز بررسی شدند. برای اطلاعات تکمیلی به ASP.NET and Web Tools for Visual Studio 2013 Release Notes مراجعه کنید.
خیلی از ما با کابوس پروژه ای که هیچ تجربه ای در انجام آن نداریم روبرو شده ایم. نبودن تجربه موثر منجر به خطاهای تکراری و غیر قابل پیش بینی شده و تلاش و وقت ما را به هدر میدهد. مشتریان از کیفیت پایین، هزینه بالا و تحویل دیر هنگام محصول ناراضی هستند و توسعه دهندگان از اضافه کارهای بیشتر که منجر به نرم افزار ضعیتتر میگردد، ناخشنود.
همین که با شکستی مواجه میشویم از تکرار چنین پروژه هایی اجتناب میکنیم. ترس ما باعث میشود تا فرآیندی بسازیم که فعالیتهای ما را محدود نموده و ایجاد آرتیفکتها[۱] را الزامی کند. در پروژه جدید از چیزهایی که در پروژههای قبلی به خوبی کار کردهاند، استفاده میکنیم. انتظار ما این است که آنها برای پروژه جدید نیز به همان خوبی کار کند.
اما پروژهها آنقدر ساده نیستند که تعدادی محدودیت و آرتیفکت ما را از خطاها ایمن سازند. با بروز خطاهای جدید ما آنها را شناسایی و رفع میکنیم. برای اینکه در آینده با این خطاها روبرو نشویم آنها را در محدودیتها و آرتیفکتهای جدیدی قرار میدهیم. بعد از انجام پروژههای زیاد با فرآیندهای حجیم و پر زحمتی روبرو هستیم که توانایی تیم را کم کرده و باعث کاهش کیفیت تولید میشوند.
فرآیندهای بزرگ و حجیم میتواند مشکلات زیادی را ایجاد کند. متاسفانه این مشکلات باعث میشود که خیلی از افراد فکر کنند که علت مشکلات، نبود فرآیندهای کافی است. بنابراین فرآیندها را حجیمتر و پیچیدهتر میکنند. این مسئله منجر به تورم فرآیندها میگردد که در محدوده سال ۲۰۰۰ گریبان بسیاری از شرکتهای نرم افزاری را گرفت.
اتحاد چابک
در وضعیتی که تیمهای نرم افزاری در بسیاری از شرکتها خود را در مردابی از فرآیندهای زیاد شونده میدیدند، تعدادی از خبرههای این صنعت که خود را اتحاد چابک[۲] نامیدند در اویل سال ۲۰۰۱ یکدیگر را ملاقات کرده و ارزش هایی را معرفی کردند تا تیمهای نرم افزاری سریعتر نرم افزار را توسعه داده و زودتر به تغییرات پاسخ دهند. چند ماه بعد، این گروه ارزشهایی تعریف شده را تحت مانیفست اتحاد چابک در سایت http://agilemanifesto.org منتشر کردند.
مانیفست اتحاد چابک
ما با توسعه نرم افزار و کمک به دیگران در انجام آن، در حال کشف راههای بهتری برای توسعه نرم افزار هستیم. از این کار به ارزشهای زیر میرسیم :
۱- افراد و تعاملات بالاتر از فرآیندها و ابزارها
۲- نرم افزار کار کننده بالاتر از مستندات جامع
۳- مشارکت مشتری بالاتر از قرارداد کاری
۴- پاسخگویی به تغییرات بالاتر از پیروی از یک برنامه
با آنکه موارد سمت چپ ارزشمند هستند ولی ما برای موارد سمت راست ارزش بیشتری قائل هستیم.
افراد و تعاملات بالاتر از فرآیندها و ابزارها
افراد مهمترین نقش را در پیروزی یک پروژه دارند. یک فرآیند عالی بدون نیروی مناسب منجر به شکست میگردد و بر عکس افراد قوی تحت فرآیند ضعیت ناکارآمد خواهند بود.
یک نیروی قوی لازم نیست که برنامه نویسی عالی باشد، بلکه کافیست که یک برنامه نویسی معمولی با قابلیت همکاری مناسب با سایر اعضای تیم باشد. کار کردن با دیگران، تعامل درست و سازنده با سایر اعضای تیم خیلی مهمتر از این که یک برنامه نویس با هوش باشد. برنامه نویسان معمولی که تعامل درستی با یکدیگر دارند به مراتب موفقتر هستند از تعداد برنامه نویسی عالی که قدرت تعامل مناسب با یکدیگر را ندارند.
در انتخاب ابزارها آنقدر وقت نگذارید که کار اصلی و تیم را فراموش کنید. به عنوان مثال میتوانید در شروع به جای بانک اطلاعاتی از فایل استفاده کنید، به جای ابزار کنترل کد گرانقیمت از برنامه رایگان کد باز استفاده کنید. باید به هیچ ابزاری عادت نکنید و صرفا به آنها به عنوان امکانی جهت تسهیل فرآیندها نگاه کنید.
نرم افزار کار کننده بالاتر از مستندات جامع
نرم افزار بدون مستندات، فاجعه است. کد برنامه ابزار مناسبی برای تشریح سیستم نرم افزاری نیست. تیم باید مستندات قابل فهم مشتری بسازد تا ابعاد سیستم از تجزیه تحلیل تا طراحی و پیاده سازی آن را تشریح نماید.
با این حال، مستندات زیاد از مستندات کم بدتر است. ساخت مستندات زیاد نیاز به وقت زیادی دارد و وقت بیشتری را میگیرد تا آن را با کد برنامه به روز نمایید. اگر آنها با یکدیگر به روز نباشند باعث درک اشتباه از سیستم میشوند.
بهتر است که همیشه مستندات کم حجمی از منطق و ساختار برنامه داشته باشید و آن را به روز نماید. البته آنها باید کوتاه و برجسته باشند. کوتاه به این معنی که ۱۰ تا ۲۰ صفحه بیشتر نباشد و برجسته به این معنی که طراحی کلی و ساختار سطح بالای سیستم را بیان نماید.
اگر فقط مستندات کوتاه از ساختار و منطق سیستم داشته باشیم چگونه میتوانیم اعضای جدید تیم را آموزش دهیم؟ پاسخ کار نزدیک شدن به آنها است. ما دانش خود را با نشستن در کنار آنها و کمک کردن به آنها انتقال میدهیم. ما آنها را بخشی از تیم میکنیم و با تعامل نزدیک و رو در رو به آنها آموزش میدهیم.
مشارکت مشتری بالاتر از قرارداد کاری
نرم افزار نمیتواند مثل یک جنس سفارش داده شود. شما نمیتوانید یک توصیف از نرم افزاری که میخواهید را بنویسید و آنگاه فردی آن را بسازد و در یک زمان معین با قیمت مشخص به شما تحویل دهد. بارها و بارها این شیوه با شکست مواجه شده است.
این قابل تصور است که مدیران شرکت به اعضای تیم توسعه بگویند که نیازهای آنها چیست، سپس اعضای تیم بروند و بعد از مدتی برگردند و یک سیستمی که نیازهای آنها را برآورده میکند، بسازند. اما این تعامل به کیفیت پایین نرم افزار و در نهایت شکست آن میانجامد. پروژههای موفق بر اساس دریافت بازخورد مشتری در بازههای زمانی کوتاه و مداوم است. به جای وابستگی به قرارداد یا دستور کار، مشتری به طور تنگاتنگ با تیم توسعه کار کرده و مرتبا اعمال نظر میکند.
قراردادی که مشخص کننده نیازمندیها، زمانبندی و قیمت پروژه است، اساسا نقص دارد. بهترین قرارداد این است که تیم توسعه و مشتری با یکدیگر کار کنند.
پاسخگویی به تغییرات بالاتر از پیروی از یک برنامه
توانایی پاسخ به تغییرات اغلب تعیین کننده موفقیت یا شکست یک پروژه نرم افزاری است. وقتی که طرحی را میریزیم باید مطمئن شویم که به اندازه کافی انعطاف پذیر است و آمادگی پذیرش تغییرات در سطح بیزنس و تکنولوژی را دارد.
مسیر یک پروژه نرم افزاری نمیتواند برای بازه زمانی طولانی برنامه ریزی شود. اولا احتمالا محیط تغییر میکند و باعث تغییر در نیازمندیها میشود. ثانیا همین که سیستم شروع به کار کند مشتریان نیازمندیهای خود را تغییر میدهند. بنابراین اگر بدانیم که نیازها چیست و مطمئن شویم که تغییر نمیکنند، قادر به برآورد مناسب خواهیم بود، که این شرایط بعید است.
یک استراتژی خوب برای برنامه ریزی این است که یک برنامه ریزی دقیق برای یک هفته بعد داشته باشیم و یک برنامه ریزی کلی برای سه ماه بعد.
اصول چابک
۱- بالاترین اولویت ما عبارت است از راضی کردن مشتری با تحویل سریع و مداوم نرم افزار با ارزش. تحویل نرم افزار با کارکردهای کم در زود هنگام بسیار مهم است چون هم مشتری چشم اندازی از محصول نهایی خواهد داشت و هم مسیر کمتر به بیراهه میرود.
۲- خوش آمدگویی به تغییرات حتی در انتهای توسعه. اعضای تیم چابک، تغییرات را چیز خوبی میبینند زیرا تغییرات به این معنی است که تیم بیشتر یاد گرفته است که چه چیزی مشتری را راضی میکند.
۳- تحویل نرم افزار قابل استفاده از چند هفته تا چند ماه با تقدم بر تحویل در دوره زمانی کوتاهتر. ما مجموعه از مستندات و طرحها را به مشتری نمیدهیم.
۴- افراد مسلط به بیزنس و توسعه دهندگان باید روزانه با یکدیگر روی پروژه کار کنند. یک پروژه نرم افزاری نیاز به هدایت مداوم دارد.
۵- ساخت پروژه را بر توان افراد با انگیزه بگذارید و به آنها محیط و ابزار را داده و اعتماد کنید. مهمترین فاکتور موفقیت افراد هستند، هر چیز دیگر مانند فرآیند، محیط و مدیریت فاکتورهای بعدی محسوب میشوند که اگر تاثیر بدی روی افراد میگذارند، باید تغییر کنند.
۶- بهترین و موثرترین روش کسب اطلاعات در تیم توسعه، ارتباط چهره به چهره است. در تیم چابک افراد با یکدیگر صحبت میکنند. نامه نگاری و مستند سازی فقط زمانی که نیاز است باید صورت گیرد.
۷- نرم افزار کار کننده معیار اصلی پیشرفت است. پروژههای چابک با نرم افزاری که در حال حاضر نیازهای مشتری را پاسخ میدهد، سنجیده میشوند. میزان مستندات، حجم کدهای زیر ساخت و هر چیز دیگری غیره از نرم افزار کار کننده معیار پیشرفت نرم افزار نیستند.
۸- فرآیندهای چابک توسعه با آهنگ ثابت را ترویج میدهد. حامیان، توسعه دهندگان و کاربران باید یک آهنگ توسعه ثابت را حفظ کنند که بیشتر شبیه به دو ماراتون است یا دوی ۱۰۰ متر. آنها با سرعتی کار میکنند که بالاترین کیفیت را ارائه دهند.
۹- توجه مداوم به برتری تکنیکی و طراحی خوب منجر به چابکی میگردد. کیفیت بالاتر کلیدی برای سرعت بالا است. راه سریعتر رفتن این است که نرم افزار تا جایی که ممکن است پاک و قوی نگهداریم. بنابراین همه اعضای تیم چابک تلاش میکنند که با کیفیتترین کار ممکن را انجام دهند. آنها هر آشفتگی را به محض ایجاد برطرف میکنند.
۱۰- سادگی هنر بیشینه کردن مقدار کاری که لازم نیست انجام شود، است. تیم چابک همیشه سادهترین مسیر که با هدف آنها سازگار است را در پیش میگیرند. آنها وقت زیادی روی مشکلاتی که ممکن است فردا رخ دهد، نمیگذارند. آنها کار امروز را با کیفیت انجام داده و مطمئن میشوند که تغییر آن در صورت بروز مشکلات در فردا، آسان خواهد بود.
۱۱- بهترین معماری و طراحی از تیمهای خود سازمان ده بیرون میآید. مدیران، مسئولیتها را به یک فردی خاصی در تیم نمیدهند بلکه بر عکس با تیم به صورت یک نیروی واحد برخورد میکنند. خود تیم تصمیم میگیرد که هر مسئولیت را چه کسی انجام دهد. تیم چابک با هم روی کل جنبههای پروژه کار میکنند. یعنی یک فرد خاص مسئول معماری، برنامه نویسی، تست و غیره نیستند. تیم، مسئولیتها را به اشتراک گذاشته و هر فرد بر کل کار تاثیر دارد.
۱۲- در بازهای زمانی مناسب تیم در مییابد که چگونه میتواند کاراتر باشد و رفتار خود را متناسب با آن تغییر دهد. تیم میداند که محیط دائما در حال تغییر است، بنابراین خود را با محیط تغییر میدهد تا چابک بماند.
ضرورت توسعه چابک
امروزه صنعت نرم افزار دارای سابقه بدی در تحویل به موقع و با کیفیت نرم افزار است. گزارشات بسیاری تایید میکنند که بیش از ۸۰ درصد از پروژههای نرم افزاری با شکست مواجه میشوند؛ در سال ۲۰۰۵ موسسه IEEE برآورد زده است که بیش از ۶۰ بیلیون دلار صرف پروژههای نرم افزاری شکست خورده شده است. عجب فاجعهای؟
شش دلیل اصلی شکست پروژههای نرم افزاری
وقتی که از مدیران و کارکنان سوال میشود که چرا پروژههای نرم افزاری با شکست مواجه میشوند، آنها به موضوعات گسترده ای اشاره میکنند. اما شش دلیل زیر بارها و بارها تکرار شده است که به عنوان دلایل اصلی شکست نرم افزار معرفی میشوند:
۱- درگیر نشدن مشتری
۲- عدم درک درست نیازمندها
۳- زمان بندی غیر واقعی
۴- عدم پذیریش و مدیریت تغییرات
۵- کمبود تست نرم افزار
۶- فرآیندهای غیر منعطف و باد دار
چگونه چابکی این مشکلات را رفع میکند؟
با آنکه Agile برای هر مشکلی راه حل ندارد ولی برای مسائل فوق بدین صورت کمک میکند:
مشتری پادشاه است!
برای رفع مشکل عدم همکاری کاربر نهایی یا مشتری، Agile مشتری را عضوی از تیم توسعه میکند. به عنوان عضوی از تیم، مشتری با تیم توسعه کار میکند تا مطمئن شود که نیازمندها به درستی برآورده میشوند. مشتری همکاری میکند در شناسایی نیازمندیها، تایید میکند نتیجه نهایی را و حرف آخر را در اینکه کدام ویژگی به نرم افزار اضافه شود، حذف شود و یا تغییر کند، را میزند.
نیازمندیها به صورت تستهای پذیرش[۳] نوشته میشوند
برای مقابله با مشکل عدم درک درست نیازمندیها، Agile تاکید دارد که نیازمندیهای کسب شده باید به صورت ویژگیهایی تعریف شوند که بر اساس معیارهای مشخصی قابل پذیرش باشند. این معیارهای پذیرش برای نوشتن تستهای پذیرش به کار میروند. به این ترتیب قبل از اینکه کدی نوشته شود، ابتدا تست پذیرش نوشته میشود. این بدین معنی است که هر کسی باید اول فکر کند که چه میخواهد، قبل از اینکه از کسی بخواهد آن را انجام دهد. این راهکار فرایند کسب نیازمندیها را از بنیاد تغییر میدهد و به صورت چشم گیری کیفیت برآورد و زمان بندی را بهبود میدهد.
زمانبندی با مذاکره بین تیم توسعه و سفارش دهنده تنظیم میشود
برای حل مشکل زمان بندی غیر واقعی، Agile زمان بندی را به صورت یک فرآیند مشترک بین تیم توسعه و سفارش دهنده تعریف میکند. در شروع هر نسخه از نرم افزار، سفارش دهنده ویژگیهای مورد انتظار را به تیم توسعه میگوید. تیم توسعه تاریخ تحویل را بر اساس ویژگیها برآورد میزد و در اختیار سفارش دهنده قرار میدهد. این تعامل تا رسیدن به یک دیدگاه مشترک ادامه مییابد.
هیچ چیزی روی سنگ حک نشده است، مگر تاریخ تحویل
برای رفع مشکل ضعف در مدیریت تغییرات، Agile اصرار دارد که هر کسی باید تغییرات را بپذیرد و نسبت به آنها واقع بین باشد. یک اصل مهم Agile میگوید که هر چیزی میتواند تغییر کند مگر تاریخ تحویل! به عبارت دیگر همین که محصول به سمت تولید شدن حرکت میکند، مشتری (در تیم محصول) میتواند بر اساس اولویتها و ارزشهای خود ویژگیهای محصول را کم یا زیاد کرده و یا تغییر دهد. به هر حال او باید واقع بین باشد. اگر او یک ویژگی جدید اضافه کنید، باید تاریخ تحویل را تغییر دهد. به این ترتیب همیشه تاریخ تحویل رعایت میگردد.
تستها قبل از کد نوشته میشوند و کاملا خودکار هستند
برای رفع مشکل کمبود تست، Agile تاکید میکند که ابتدا باید تستها نوشته شوند و همواره ارزیابی گردند. هر برنامه نویس باید اول تست را بنویسد، سپس کد لازم برای پاس شدن آن را. همین که کد تغییر میکند باید تستها دوباره اجرا شوند. در این راهکار، هر برنامه نویس مسئول تستهای خود است تا درستی برنامه از ابتدا تضمین گردد.
مدیریت پروژه یک فعالیت جداگانه نیست
برای رفع مشکل فرآیندهای غیر منعطف و باددار، Agile مدیریت پروژه را درون فرآیند توسعه میگنجاند. وظایف مدیریت پروژه بین اعضای تیم توسعه تقسیم میشود. برای مثال هر ۷ نفر در تیم توسعه نرم افزار (متدلوژی اسکرام) زمان تحویل را با مذاکره تعیین میکنند. همچنین کد برنامه به صورت خودکار اطلاعات وضعیت پروژه را تولید میکند. به عنوان مثال نمودار burndown ، تستهای انجام نشده، پاس شده و رد شده به صورت خودکار تولید میشوند.
به کار گیری توسعه چابک
یکی از مشکلات توسعه چابک این است که شما اول باید به خوبی آن را درک کنید تا قادر به پیاده سازی درست آن باشید. این درک هم باید کلی باشد (مانند Scrum و XP) و هم جزئی (مانند TDD و جلسات روازنه). اما چگونه باید به این درک برسیم؟ کتابها و مقالات انگلیسی زیادی برای یادگیری توسعه چابک و پیاده سازی آن در سازمان وجود دارند، ولی متاسفانه منابع فارسی کمی در این زمینه است. هدف این کتاب رفع این کمبود و آموزش عملی توسعه چابک و ابزارهای پیاده سازی آن است.
برای این یک توسعه دهنده چابک شوید، باید به مهارتهای فردی و تیمی چابک برسید. در ادامه این مهارتها معرفی میشوند.
مهارتهای فردی
قبل از هر چیز شما باید یک برنامه نویس باشید و مقدمات برنامه نویسی مانند الگوریتم و فلوچارت، دستورات برنامه نویسی، کار با متغیرها، توابع و آرایهها را بلد باشید. پس از تسلط به مقدمات برنامه نویسی میتوانید مهارتهای برنامه نویسی چابک را فرا بگیرید که عبارتند از:
- برنامه نویسی شیءگرا
- توسعه تست محور
- الگوهای طراحی
در ادامه نحوه کسب این مهارتها بیان میشوند.
برنامه نویسی شیءگرا
اساس طراحی چابک بر تفکر شیءگرا استوار است. بنابراین تسلط به مفاهیم و طراحی شیءگرا ضروری است.
توسعه تست محور
مهمترین و انقلابیترین سبک برنامه نویسی از دهه گذشته تا به امروز، توسعه یا برنامه نویسی تست محور است. این سبک بسیاری از ارزشهای توسعه چابک را فراهم میکند و یادگیری آن برای هر توسعه دهنده چابک ضروری است.
الگوهای طراحی
الگوهای طراحی راه حلهای انتزاعی سطح بالا هستند. این الگوها بهترین تکنیکهای[۴] طراحی نرم افزار هستند و بسیاری از مشکلاتی که در طراحی نرم افزار رخ میدهند با استفاده از این الگوها قابل حل هستند.
مهارتهای تیمی
انجام پروژه نرم افزاری یک کار تیمی است. شما پس از یادگیری مهارتهای فردی باید خود را آماده حضور در تیم توسعه چابک کنید. برای این منظور باید با مهارت تیمی مانند آشنایی با گردشکار تولید نرم افزار، حضور موثر در جلسات، قبول مسئولیتها و غیره آشنا شوید.
اسکرام
تمامی مهارتهای تیمی توسعه چابک توسط اسکرام آموزش داده میشوند. اسکرام فریم ورکی برای توسعه چابک است که با تعریف فرآیندها، نقشها و آرتیفکتهای مشخص به تیمهای نرم افزاری کمک میکند تا چابک شوند.
[۱] Artifact : خروجی یک فرآیند است. مثلا خروجی طراحی شیءگرا، نمودارهای UML است.
[۲] Agile Alliance
[3] Acceptance Tests
AngularJS #3
<!DOCTYPE html> <html ng-app> <head> <title>Sample2</title> </head> <body> <div> <input type="text" ng-model="greeting.text" /> <p>{{greeting.text}}, World!</p> </div> <script src="../Scripts/angular.js"></script> </body> </html>
<!DOCTYPE html> <html ng-app> <head> <title>Sample2</title> </head> <body> <div ng-controller="GreetingController"> <input type="text" ng-model="greeting.text" /> <p>{{greeting.text}}, World!</p> <button ng-click="showData()">Show</button> </div> <script src="../Scripts/angular.js"></script> <script> var GreetingController = function ($scope, $window) { $scope.greeting = { text: "Hello" }; $scope.showData = function () { $window.alert($scope.greeting.text); }; }; </script> </body> </html>
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
امن سازی برنامههای ASP.NET Core توسط IdentityServer 4x - قسمت چهاردهم- آماده شدن برای انتشار برنامه
تنظیم مجوز امضای توکنهای IDP
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddDeveloperSigningCredential() .AddCustomUserStore() .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients());
مرحلهی بعد، تغییر AddDeveloperSigningCredential به یک نمونهی واقعی است. استفادهی از روش فعلی آن چنین مشکلاتی را ایجاد میکند:
- اگر برنامهی IDP را در سرورهای مختلفی توزیع کنیم و این سرورها توسط یک Load balancer مدیریت شوند، هر درخواست رسیده، به سروری متفاوت هدایت خواهد شد. در این حالت هر برنامه نیز مجوز امضای توکن متفاوتی را پیدا میکند. برای مثال اگر یک توکن دسترسی توسط سرور A امضاء شود، اما در درخواست بعدی رسیده، توسط مجوز سرور B تعیین اعتبار شود، این اعتبارسنجی با شکست مواجه خواهد شد.
- حتی اگر از یک Load balancer استفاده نکنیم، به طور قطع Application pool برنامه در سرور، در زمانی خاص Recycle خواهد شد. این مورد DeveloperSigningCredential تنظیم شده را نیز ریست میکند. یعنی با ریاستارت شدن Application pool، کلیدهای مجوز امضای توکنها تغییر میکنند که در نهایت سبب شکست اعتبارسنجی توکنهای صادر شدهی توسط IDP میشوند.
بنابراین برای انتشار نهایی برنامه نمیتوان از DeveloperSigningCredential فعلی استفاده کرد و نیاز است یک signing certificate را تولید و تنظیم کنیم. برای این منظور از برنامهی makecert.exe مایکروسافت که جزئی از SDK ویندوز است، استفاده میکنیم. این فایل را از پوشهی src\IDP\DNT.IDP\MakeCert نیز میتوانید دریافت کنید.
سپس دستور زیر را با دسترسی admin اجرا کنید:
makecert.exe -r -pe -n "CN=DntIdpSigningCert" -b 01/01/2018 -e 01/01/2025 -eku 1.3.6.1.5.5.7.3.3 -sky signature -a sha256 -len 2048 -ss my -sr LocalMachine
پس از آن در قسمت run ویندوز، دستور mmc را وارد کرده و enter کنید. سپس از منوی File گزینهی Add remove span-in را انتخاب کنید. در اینجا certificate را add کنید. در صفحهی باز شده Computer Account و سپس Local Computer را انتخاب کنید و در نهایت OK. اکنون میتوانید این مجوز جدید را در قسمت «Personal/Certificates»، مشاهده کنید:
در اینجا Thumbprint این مجوز را در حافظه کپی کنید؛ از این جهت که در ادامه از آن استفاده خواهیم کرد.
چون این مجوز از نوع self signed است، در قسمت Trusted Root Certification Authorities قرار نگرفتهاست که باید این انتقال را انجام داد. در غیراینصورت میتوان توسط آن توکنهای صادر شده را امضاء کرد اما به عنوان یک توکن معتبر به نظر نخواهند رسید.
در ادامه این مجوز جدید را انتخاب کرده و بر روی آن کلیک راست کنید. سپس گزینهی All tasks -> export را انتخاب کنید. نکتهی مهمی را که در اینجا باید رعایت کنید، انتخاب گزینهی «yes, export the private key» است. کپی و paste این مجوز از اینجا به جایی دیگر، این private key را export نمیکند. در پایان این عملیات، یک فایل pfx را خواهید داشت.
- در آخر نیاز است این فایل pfx را در مسیر «Trusted Root Certification Authorities/Certificates» قرار دهید. برای اینکار بر روی نود certificate آن کلیک راست کرده و گزینهی All tasks -> import را انتخاب کنید. سپس مسیر فایل pfx خود را داده و این مجوز را import نمائید.
پس از ایجاد مجوز امضای توکنها و انتقال آن به Trusted Root Certification Authorities، نحوهی معرفی آن به IDP به صورت زیر است:
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddSigningCredential(loadCertificateFromStore()) .AddCustomUserStore() .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients()); } private X509Certificate2 loadCertificateFromStore() { var thumbPrint = Configuration["CertificateThumbPrint"]; using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine)) { store.Open(OpenFlags.ReadOnly); var certCollection = store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, true); if (certCollection.Count == 0) { throw new Exception("The specified certificate wasn't found."); } return certCollection[0]; } } private X509Certificate2 loadCertificateFromFile() { // NOTE: // You should check out the identity of your application pool and make sure // that the `Load user profile` option is turned on, otherwise the crypto susbsystem won't work. var certificate = new X509Certificate2( fileName: Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data", Configuration["X509Certificate:FileName"]), password: Configuration["X509Certificate:Password"], keyStorageFlags: X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); return certificate; } } }
پس از این تغییرات، برنامه را اجرا کنید. سپس مسیر discovery document را طی کرده و آدرس jwks_uri آنرا در مرورگر باز کنید. در اینجا خاصیت kid نمایش داده شده با thumbPrint مجوز یکی است.
https://localhost:6001/.well-known/openid-configuration https://localhost:6001/.well-known/openid-configuration/jwks
انتقال سایر قسمتهای فایل Config برنامهی IDP به بانک اطلاعاتی
قسمت آخر آماده سازی برنامه برای انتشار آن، انتقال سایر دادههای فایل Config، مانند Resources و Clients برنامهی IDP، به بانک اطلاعاتی است. البته هیچ الزامی هم به انجام اینکار نیست. چون اگر تعداد برنامههای متفاوتی که در سازمان قرار است از IDP استفاده کنند، کم است، تعریف مستقیم آنها داخل فایل Config برنامهی IDP، مشکلی را ایجاد نمیکند و این تعداد رکورد الزاما نیازی به بانک اطلاعاتی ندارند. اما اگر بخواهیم امکان به روز رسانی این اطلاعات را بدون نیاز به کامپایل مجدد برنامهی IDP توسط یک صفحهی مدیریتی داشته باشیم، نیاز است آنها را به بانک اطلاعاتی منتقل کنیم. این مورد مزیت به اشتراک گذاری یک چنین اطلاعاتی را توسط Load balancers نیز میسر میکند.
البته باید درنظر داشت قسمت دیگر اطلاعات IdentityServer شامل refresh tokens و reference tokens هستند. تمام اینها اکنون در حافظه ذخیره میشوند که با ریاستارت شدن Application pool برنامه از بین خواهند رفت. بنابراین حداقل در این مورد استفادهی از بانک اطلاعاتی اجباری است.
خوشبختانه قسمت عمدهی این کار توسط خود تیم IdentityServer توسط بستهی IdentityServer4.EntityFramework انجام شدهاست که در اینجا از آن استفاده خواهیم کرد. البته در اینجا این بستهی نیوگت را مستقیما مورد استفاده قرار نمیدهیم. از این جهت که نیاز به 2 رشتهی اتصالی جداگانه و دو Context جداگانه را دارد که داخل خود این بسته تعریف شدهاست و ترجیح میدهیم که اطلاعات آنرا با ApplicationContext خود یکی کنیم.
برای این منظور آخرین سورس کد پایدار آنرا از این آدرس دریافت کنید:
https://github.com/IdentityServer/IdentityServer4.EntityFramework/releases
انتقال موجودیتها به پروژهی DNT.IDP.DomainClasses
در این بستهی دریافتی، در پوشهی src\IdentityServer4.EntityFramework\Entities آن، کلاسهای تعاریف موجودیتهای متناظر با منابع IdentityServer قرار دارند. بنابراین همین فایلها را از این پروژه استخراج کرده و به پروژهی DNT.IDP.DomainClasses در پوشهی جدید IdentityServer4Entities اضافه میکنیم.
البته در این حالت پروژهی DNT.IDP.DomainClasses نیاز به این وابستگیها را خواهد داشت:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="System.ComponentModel.Annotations" Version="4.3.0" /> <PackageReference Include="IdentityServer4" Version="2.2.0" /> </ItemGroup> </Project>
انتقال تنظیمات روابط بین موجودیتها، به پروژهی DNT.IDP.DataLayer
در فایل src\IdentityServer4.EntityFramework\Extensions\ModelBuilderExtensions.cs بستهی دریافتی، تعاریف تنظیمات این موجودیتها به همراه نحوهی برقراری ارتباطات بین آنها قرار دارد. بنابراین این اطلاعات را نیز از این فایل استخراج و به پروژهی DNT.IDP.DataLayer اضافه میکنیم. البته در اینجا از روش IEntityTypeConfiguration برای قرار هر کدام از تعاریف یک در کلاس مجزا استفاده کردهایم.
پس از این انتقال، به کلاس Context برنامه مراجعه کرده و توسط متد builder.ApplyConfiguration، این فایلهای IEntityTypeConfiguration را معرفی میکنیم.
تعاریف DbSetهای متناظر با موجودیتهای منتقل و تنظیم شده در پروژهی DNT.IDP.DataLayer
پس از انتقال موجودیتها و روابط بین آنها، دو فایل DbContext را در این بستهی دریافتی خواهید یافت:
الف) فایل src\IdentityServer4.EntityFramework\DbContexts\ConfigurationDbContext.cs
این فایل، موجودیتهای تنظیمات برنامه مانند Resources و Clients را در معرض دید EF Core قرار میدهد.
سپس فایل src\IdentityServer4.EntityFramework\Interfaces\IConfigurationDbContext.cs نیز جهت استفادهی از این DbContext در سرویسهای این بستهی دریافتی تعریف شدهاست.
ب) فایل src\IdentityServer4.EntityFramework\DbContexts\PersistedGrantDbContext.cs
این فایل، موجودیتهای ذخیره سازی اطلاعات مخصوص IDP را مانند refresh tokens و reference tokens، در معرض دید EF Core قرار میدهد.
همچنین فایل src\IdentityServer4.EntityFramework\Interfaces\IPersistedGrantDbContext.cs نیز جهت استفادهی از این DbContext در سرویسهای این بستهی دریافتی تعریف شدهاست.
ما در اینجا DbSetهای هر دوی این DbContextها را در ApplicationDbContext خود، خلاصه و ادغام میکنیم.
انتقال نگاشتهای AutoMapper بستهی دریافتی به پروژهی جدید DNT.IDP.Mappings
در پوشهی src\IdentityServer4.EntityFramework\Mappers، تعاریف نگاشتهای AutoMapper، برای تبدیلات بین موجودیتهای برنامه و IdentityServer4.Models انجام شدهاست. کل محتویات این پوشه را به یک پروژهی Class library جدید به نام DNT.IDP.Mappings منتقل و فضاهای نام آنرا نیز اصلاح میکنیم.
انتقال src\IdentityServer4.EntityFramework\Options به پروژهی DNT.IDP.Models
در پوشهی Options بستهی دریافتی سه فایل موجود هستند:
الف) Options\ConfigurationStoreOptions.cs
این فایل، به همراه تنظیمات نام جداول متناظر با ذخیره سازی اطلاعات کلاینتها است. نیازی به آن نداریم؛ چون زمانیکه موجودیتها و تنظیمات آنها را به صورت مستقیم در اختیار داریم، نیازی به فایل تنظیمات ثالثی برای انجام اینکار نیست.
ب) Options\OperationalStoreOptions.cs
این فایل، تنظیمات نام جداول مرتبط با ذخیره سازی توکنها را به همراه دارد. به این نام جداول نیز نیازی نداریم. اما این فایل به همراه سه تنظیم زیر جهت پاکسازی دورهای توکنهای قدیمی نیز هست:
namespace IdentityServer4.EntityFramework.Options { public class OperationalStoreOptions { public bool EnableTokenCleanup { get; set; } = false; public int TokenCleanupInterval { get; set; } = 3600; public int TokenCleanupBatchSize { get; set; } = 100; } }
public TokenCleanup( IServiceProvider serviceProvider, ILogger<TokenCleanup> logger, IOptions<OperationalStoreOptions> options)
کلاسی است به همراه خواص نام اسکیمای جداول که در دو کلاس تنظیمات قبلی بکار رفتهاست. نیازی به آن نداریم.
انتقال سرویسهای IdentityServer4.EntityFramework به پروژهی DNT.IDP.Services
بستهی دریافتی، شامل دو پوشهی src\IdentityServer4.EntityFramework\Services و src\IdentityServer4.EntityFramework\Stores است که سرویسهای آنرا تشکیل میدهند (جمعا 5 سرویس TokenCleanup، CorsPolicyService، ClientStore، PersistedGrantStore و ResourceStore). بنابراین این سرویسها را نیز مستقیما از این پوشهها به پروژهی DNT.IDP.Services کپی خواهیم کرد.
همانطور که عنوان شد دو فایل Interfaces\IConfigurationDbContext.cs و Interfaces\IPersistedGrantDbContext.cs برای دسترسی به دو DbContext این بستهی دریافتی در سرویسهای آن، تعریف شدهاند و چون ما در اینجا صرفا یک ApplicationDbContext را داریم که از طریق IUnitOfWork، در دسترس لایهی سرویس قرار میگیرد، ارجاعات به دو اینترفیس یاد شده را با IUnitOfWork تعویض خواهیم کرد تا مجددا قابل استفاده شوند.
انتقال متدهای الحاقی معرفی سرویسهای IdentityServer4.EntityFramework به پروژهی DNT.IDP
پس از انتقال قسمتهای مختلف IdentityServer4.EntityFramework به لایههای مختلف برنامهی جاری، اکنون نیاز است سرویسهای آنرا به برنامه معرفی کرد که در نهایت جایگزین متدهای فعلی درون حافظهای کلاس آغازین برنامهی IDP میشوند. خود این بسته در فایل زیر، به همراه متدهایی الحاقی است که این معرفی را انجام میدهند:
src\IdentityServer4.EntityFramework\Extensions\IdentityServerEntityFrameworkBuilderExtensions.cs
به همین جهت این فایل را به پروژهی وب DNT.IDP ، منتقل خواهیم کرد؛ همانجایی که در قسمت دهم AddCustomUserStore را تعریف کردیم.
این کلاس پس از انتقال، نیاز به تغییرات ذیل را دارد:
الف) چون یکسری از کلاسهای تنظیمات را حذف کردیم و نیازی به آنها نداریم، آنها را نیز به طور کامل از این فایل حذف میکنیم. تنها تنظیم مورد نیاز آن، OperationalStoreOptions است که اینبار آنرا از فایل appsettings.json دریافت میکنیم. بنابراین ذکر این مورد نیز در اینجا اضافی است.
ب) در آن، دو Context موجود در بستهی اصلی IdentityServer4.EntityFramework مورد استفاده قرار گرفتهاند. ما در اینجا آنها را نیز با تک Context برنامهی خود تعویض میکنیم.
ج) در آن سرویسی به نام TokenCleanupHost تعریف شدهاست. آنرا نیز به لایهی سرویسها منتقل میکنیم. همچنین در امضای سازندهی آن بجای تزریق مستقیم OperationalStoreOptions از <IOptions<OperationalStoreOptions استفاده خواهیم کرد.
نگارش نهایی و تمیز شدهی IdentityServerEntityFrameworkBuilderExtensions را در اینجا مشاهده میکنید.
افزودن متدهای الحاقی جدید به فایل آغازین برنامهی IDP
پس از انتقال IdentityServerEntityFrameworkBuilderExtensions به پروژهی DNT.IDP، اکنون نوبت به استفادهی از آن است:
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddSigningCredential(loadCertificateFromStore()) .AddCustomUserStore() .AddConfigurationStore() .AddOperationalStore();
اجرای Migrations در پروژهی DNT.IDP.DataLayer
پس از پایان این نقل و انتقالات، اکنون نیاز است ساختار بانک اطلاعاتی برنامه را بر اساس موجودیتها و روابط جدید بین آنها، به روز رسانی کنیم. به همین جهت فایل add_migrations.cmd موجود در پوشهی src\IDP\DNT.IDP.DataLayer را اجرا میکنیم تا کلاسهای Migrations متناظر تولید شوند و سپس فایل update_db.cmd را اجرا میکنیم تا این تغییرات، به بانک اطلاعاتی برنامه نیز اعمال گردند.
انتقال اطلاعات فایل درون حافظهای Config، به بانک اطلاعاتی برنامه
تا اینجا اگر برنامه را اجرا کنیم، دیگر کار نمیکند. چون جداول کلاینتها و منابع آن خالی هستند. به همین جهت نیاز است اطلاعات فایل درون حافظهای Config را به بانک اطلاعاتی منتقل کنیم. برای این منظور سرویس ConfigSeedDataService را به برنامه اضافه کردهایم:
public interface IConfigSeedDataService { void EnsureSeedDataForContext( IEnumerable<IdentityServer4.Models.Client> clients, IEnumerable<IdentityServer4.Models.ApiResource> apiResources, IEnumerable<IdentityServer4.Models.IdentityResource> identityResources); }
برای استفادهی از آن، به کلاس آغازین برنامهی IDP مراجعه میکنیم:
namespace DNT.IDP { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... initializeDb(app); seedDb(app); // ... } private static void seedDb(IApplicationBuilder app) { var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); using (var scope = scopeFactory.CreateScope()) { var configSeedDataService = scope.ServiceProvider.GetService<IConfigSeedDataService>(); configSeedDataService.EnsureSeedDataForContext( Config.GetClients(), Config.GetApiResources(), Config.GetIdentityResources() ); } }
آزمایش برنامه
پس از اعمال این تغییرات، برنامهها را اجرا کنید. سپس به بانک اطلاعاتی آن مراجعه نمائید. در اینجا لیست جداول جدیدی را که اضافه شدهاند ملاحظه میکنید:
همچنین برای نمونه، در اینجا اطلاعات منابع منتقل شدهی به بانک اطلاعاتی، از فایل Config، قابل مشاهده هستند:
و یا قسمت ذخیره سازی خودکار توکنهای تولیدی توسط آن نیز به درستی کار میکند:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.
اگر شما یک فرم تماس با ما داشته باشید استفاده از کپچا یک مکانیزم امنیتی معقول میباشد و همچنین اگر فرمی جهت ارسال پست داشته باشید. اما در برخی مواقع مانند فرمهای ارسال کامنت، پاسخ، چت و ... امکان استفاده از این روش وجود ندارد و باید به فکر راه حلی مناسب برای مقابل با درخواستهای مخرب باشیم.
اگر شما هم به دنبال تامین امنیت سایت خود هستید و دوست ندارید که وب سایت شما (به دلیل کمبود پهنای باند یا ارسال مطالب نامربوط که گاهی اوقات به صدها هزار مورد میرسد) از دسترس خارج شود این آموزش را دنبال کنید.
برای این منظور ما از یک ActionFilter برای امضای ActionMethodهایی استفاده میکنیم که باید با ارسالهای متعدد از سوی یک کاربر مقابله کنند. این ActionFilter باید قابلیت تنظیم حداقل زمان بین درخواستها را داشته باشد و اگر درخواستی در زمانی کمتر از مدت مجاز تعیین شده برسد، به نحوی مطلوبی به آن رسیدگی کند.
پس از آن ما نیازمند مکانیزمی هستیم تا درخواستهای رسیدهی از سوی هرکاربر را به شکلی کاملا خاص و یکتا شناسایی کند. راه حلی که قرار است در این ActionFilter از آن استفاده کنم به شرح زیر است:
ما به دنبال آن هستیم که یک شناسهی منحصر به فرد را برای هر درخواست ایجاد کنیم. لذا از اطلاعات شیئ Request جاری برای این منظور استفاده میکنیم.
1) IP درخواست جاری (قابل بازیابی از هدر HTTP_X_FORWARDED_FOR یا REMOTE_ADDR)
2) مشخصات مرورگر کاربر (قابل بازیابی از هدر USER_AGENT)
3) آدرس درخواست جاری (برای اینکه شناسهی تولیدی کاملا یکتا باشد، هرچند میتوانید آن را حذف کنید)
اطلاعات فوق را در یک رشته قرار میدهیم و بعد Hash آن را حساب میکنیم. به این ترتیب ما یک شناسه منحصر فرد را از درخواست جاری ایجاد کردهایم.
مرحله بعد پیاده سازی مکانیزمی برای نگهداری این اطلاعات و بازیابی آنها در هر درخواست است. ما برای این منظور از سیستم Cache استفاده میکنیم؛ هرچند راه حلهای بهتری هم وجود دارند.
بنابراین پس از ایجاد شناسه یکتای درخواست، آن را در Cache قرار میدهیم و زمان انقضای آن را هم پارامتری که ابتدای کار گفتم قرار میدهیم. سپس در هر درخواست Cache را برای این مقدار یکتا جستجو میکنیم. اگر شناسه پیدا شود، یعنی در کمتر از زمان تعیین شده، درخواست مجددی از سوی کاربر صورت گرفته است و اگر شناسه در Cache موجود نباشد، یعنی درخواست رسیده در زمان معقولی صادر شده است.
باید توجه داشته باشید که تعیین زمان بین هر درخواست به ازای هر ActionMethod خواهد بود و نباید آنقدر زیاد باشد که عملا کاربر را محدود کنیم. برای مثال در یک سیستم چت، زمان معقول بین هر درخواست 5 ثانیه است و در یک سیستم ارسال نظر یا پاسخ، 10 ثانیه. در هر حال بسته به نظر شما این زمان میتواند قابل تغییر باشد. حتی میتوانید کاربر را مجبور کنید که در روز فقط یک دیدگاه ارسال کند!
قبل از پیاده سازی سناریوی فوق، در مورد نقش گزینهی سوم در شناسهی درخواست، لازم است توضیحاتی بدهم. با استفاده از این خصوصیت (یعنی آدرس درخواست جاری) شدت سختگیری ما کمتر میشود. زیرا به ازای هر آدرس، شناسهی تولیدی متفاوت خواهد بود. اگر فرد مهاجم، برنامهای را که با آن اسپم میکند، طوری طراحی کرده باشد که مرتبا درخواستها را به آدرسهای متفاوتی ارسال کند، مکانیزم ما کمتر با آن مقابله خواهد کرد.
برای مثال فرد مهاجم میتواند در یک حلقه، ابتدا درخواستی را به AddComment بدهد، بعد AddReply و بعد SendMessage. پس همانطور که میبینید اگر از پارامتر سوم استفاده کنید، عملا قدرت مکانیزم ما به یک سوم کاهش مییابد.
نکتهی دیگری که قابل ذکر است اینست که این روش راهی برای تشخیص زمان بین درخواستهای صورت گرفته از کاربر است و به تنهایی نمیتواند امنیت کامل را برای مقابله با اسپمها، مهیا کند و باید به فکر مکانیزم دیگری برای مقابله با کاربری که درخواستهای نامعقولی در مدت زمان کمی میفرستد پیاده کنیم (پیاده سازی مکانیزم تکمیلی را در آینده شرح خواهم داد).
اکنون نوبت پیاده سازی سناریوی ماست. ابتدا یک کلاس ایجاد کنید و آن را از ActionFilterAttribute مشتق کنید و کدهای زیر را وارد کنید:
using System; using System.Linq; using System.Web.Mvc; using System.Security.Cryptography; using System.Text; using System.Web.Caching; namespace Parsnet.Core { public class StopSpamAttribute : ActionFilterAttribute { // حداقل زمان مجاز بین درخواستها برحسب ثانیه public int DelayRequest = 10; // پیام خطایی که در صورت رسیدن درخواست غیرمجاز باید صادر کنیم public string ErrorMessage = "درخواستهای شما در مدت زمان معقولی صورت نگرفته است."; //خصوصیتی برای تعیین اینکه آدرس درخواست هم به شناسه یکتا افزوده شود یا خیر public bool AddAddress = true; public override void OnActionExecuting(ActionExecutingContext filterContext) { // درسترسی به شئی درخواست var request = filterContext.HttpContext.Request; // دسترسی به شیئ کش var cache = filterContext.HttpContext.Cache; // کاربر IP بدست آوردن var IP = request.ServerVariables["HTTP_X_FORWARDED_FOR"] ?? request.UserHostAddress; // مشخصات مرورگر var browser = request.UserAgent; // در اینجا آدرس درخواست جاری را تعیین میکنیم var targetInfo = (this.AddAddress) ? (request.RawUrl + request.QueryString) : ""; // شناسه یکتای درخواست var Uniquely = String.Concat(IP, browser, targetInfo); //در اینجا با کمک هش یک امضا از شناسهی درخواست ایجاد میکنیم var hashValue = string.Join("", MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(Uniquely)).Select(s => s.ToString("x2"))); // ابتدا چک میکنیم که آیا شناسهی یکتای درخواست در کش موجود نباشد if (cache[hashValue] != null) { // یک خطا اضافه میکنیم ModelState اگر موجود بود یعنی کمتر از زمان موردنظر درخواست مجددی صورت گرفته و به filterContext.Controller.ViewData.ModelState.AddModelError("ExcessiveRequests", ErrorMessage); } else { // اگر موجود نبود یعنی درخواست با زمانی بیشتر از مقداری که تعیین کردهایم انجام شده // پس شناسه درخواست جدید را با پارامتر زمانی که تعیین کرده بودیم به شیئ کش اضافه میکنیم cache.Add(hashValue, true, null, DateTime.Now.AddSeconds(DelayRequest), Cache.NoSlidingExpiration, CacheItemPriority.Default, null); } base.OnActionExecuting(filterContext); } } }
[HttpPost] [StopSpam(DelayRequest = 5)] [ValidateAntiForgeryToken] public virtual async Task<ActionResult> SendFile(HttpPostedFileBase file, int userid = 0) { } [HttpPost] [StopSpam(DelayRequest = 30, ErrorMessage = "زمان لازم بین ارسال هر مطلب 30 ثانیه است")] [ValidateAntiForgeryToken] public virtual async Task<ActionResult> InsertPost(NewPostModel model) { }
همانطور که گفتم این مکانیزم تنها تا حدودی با درخواستهای اسپم مقابله میکند و برای تکمیل آن نیاز به مکانیزم دیگری داریم تا بتوانیم از ارسالهای غیرمجاز بعد از زمان تعیین شده جلوگیری کنیم.
به توجه به دیدگاههای مطرح شده اصلاحاتی در کلاس صورت گرفت و قابلیتی به آن اضافه گردید که بتوان مکانیزم اعتبارسنجی را کنترل کرد.
برای این منظور خصوصیتی به این ActionFilter افزوده شد تا هنگامیکه دادههای فرم معتبر نباشند و در واقع هنوز چیزی ثبت نشده است این مکانیزم را بتوان کنترل کرد. خصوصیت CheckResult باعث میشود تا اگر دادههای مدل ما در اعتبارسنجی، معتبر نبودند کلید افزوده شده به کش را حذف تا کاربر بتواند مجدد فرم را ارسال کند. مقدار آن به طور پیش فرض true است و اگر برابر false قرار بگیرد تا اتمام زمان تعیین شده در مکانیزم ما، کاربر امکان ارسال مجدد فرم را ندارد.
همچنین باید بعد از اتمام عملیات در صورت عدم موفقیت آمیز بودن آن به ViewBag یک خصوصیت به نام ExecuteResult اضافه کنید و مقدار آن را برابر false قرار دهید. تا کلید از کش حذف گردد.
نحوه استفاده آن هم به شکل زیر میباشد:
[HttpPost] [StopSpam(AddAddress = true, DelayRequest = 20)] [ValidateAntiForgeryToken] public Task<ActionResult> InsertPost(NewPostModel model) { if (ModelState.IsValid) { var newPost = dbContext.InsertPost(model); if (newPost != null) { ViewBag.ExecuteResult = true; } } if (ModelState.IsValidField("ExcessiveRequests") == true)
{
ViewBag.ExecuteResult = false;
}
return View(); }
فایل ضمیمه را میتوانید از زیر دانلود کنید:
StopSpamAttribute.rar