HttpModule چیست؟

تصویر مهدی

مقدمه:

چند وقت قبل به چند نفر از دوستان قول داده بودم که نحوه نوشتن و استفاده از HttpModule در ASP.NET را با یک مثال آموزش دهم. فکر کردم شاید این مطلب برای دوستان دیگر هم مناسب باشد و بنابراین مطلب را در سایت قرار دادم. این مقاله برای دوستانی نوشته شده است که اطلاعاتی درباره HttpModule ندارند و یا آشنایی نسبی با این موضوع دارند ولی به دنبال چند مثال از نحوه استفاده از این امکان جالب در ASP.NET هستند.

HttpModule چیست:

در ASP.NET پردازش هر درخواست دارای مراحل یا فازهای مختلف است و در هر فاز رویدادهای مشخصی وجود دارد که با استفاده از HttpModuleها میتوانیم در این فرآیند تغییراتی ایجاد کنیم. به عنوان مثال می توانیم در اینجا عملیات لاگ کردن درخواستها، اعتبار سنجی درخواستها، بازنویسی Urlها و ... را به یک برنامه ASP.NET اضافه کنیم. برای مشاهده لیست این رویدادها و توضیحات بیشتر در این مورد اینجا را ببینید. در این مقاله برای سادگی بیشتر من فقط به دو تا از مهمترین رویدادها اشاره میکنم. این دو رویداد، BeginRequest (در لحظه دریافت درخواست توسط سرور) و EndRequest (پس از خاتمه پردازش درخواست توسط سرور) هستند و من در چند مثال موجود در این مقاله فقط از همین دو رویداد استفاده کرده ام. اگر به کدی که قبلا برای تصحیح حروف ی و ک عربی نوشته بودم دقت کنید، در آنجا نیز تنها از رویداد BeginRequest استفاده کرده ام.

شیوه نوشتن یک HttpModule:

یک HttpModule کلاسی است که از IHttpModule ارثبری میکنید و دو متد Init و Dispose را پیاده سازی میکند. همانطور که از اسم این دو متد مشخص است، در متد Init کارهای اولیه و آماده سازی HttpModule انجام میشود و در متد Dispose هرگونه آزاد سازی منابع که مورد نیاز است انجام میشود.
بسیاری از اوقات در متد Dispose کد خاصی نوشته نمیشود اما در متد Init باید رویدادهایی که میخواهیم از آنها استفاده کنیم را مشخص کنیم. پارامتر ورودی متد Init یک شئ از نوع HttpApplication است که اطلاعات Application جاری را در خود دارد و اتصال رویدادها از طریق آن انجام میشود. به عنوان مثال به کد نمونه زیر توجه کنید:

class SampleHttpModule : IHttpModule
{
    public void Dispose(){}

    public void Init(HttpApplication context)
    {
        context.BeginRequest += new EventHandler(context_BeginRequest);
        context.EndRequest += new EventHandler(context_EndRequest);
    }

	void context_BeginRequest(object sender, EventArgs e)
	{
		//handle BeginRequest here...
	}

	void context_EndRequest(object sender, EventArgs e)
	{
		//handle EndRequest here...
	}
}

همانطور که در کد بالا مشاهده میشود، هر کدام از رویدادهای BeginRequest و EndRequest دارای دو پارامتر ورودی هستند که پارامتر اول (در اینجا sender) همان شئ HttpApplication است که برای دسترسی به اطلاعات درخواست، تغییر پاسخ سرور، دسترسی به Cache، دسترسی به Session و ... از این شئ استفاده میکنیم.
به عنوان مثال فرض کنیم که میخواهیم در انتهای هرکدام از صفحات سایت یک پیغام اضافه کنیم. همانطور که حدس میزنید، برای این کار از رویداد EndRequest میتوانیم استفاده کنیم. البته دقت کنید که یک HttpModule برای تمام درخواستها فراخوانی خواهد شد، پس در کد مورد نظر، باید حتما بررسی کنیم که در صورتی که درخواست صورت گرفته برای یک صفحه (Page) بوده این متن را به انتهای درخواست اضافه کنیم. بنابراین برای این مثال به شئ Response (برای نوشتن اطلاعات در آن) و شئ Context (برای بررسی Handler آن که نشاندهنده Handler استفاده شده برای درخواست جاری است) نیاز داریم.
نکته: در صورتی که با بحث HttpHandler آشنایی ندارید، پیشنهاد میکنم در این مورد نیز مطالعه کنید ولی برای الان فقط این را بدانید که اگر درخواست یک صفحه عادی دریافت شده باشد، Handler از نوع System.Web.UI.Page خواهد بود.
بنابراین کد کامل مورد نظر برای این ماژول به صورت زیر خواهد بود:

namespace MyApplication
{
    class SampleHttpModule : IHttpModule
    {
        public void Dispose() { }

			public void Init(HttpApplication context)
			{
				context.EndRequest += new EventHandler(context_EndRequest);
			}

			void context_EndRequest(object sender, EventArgs e)
			{
				HttpApplication application = (HttpApplication)sender;
				if (application.Context.Handler is System.Web.UI.Page)
				{
					application.Response.Write("
This works!"); } } } }

شیوه استفاده از یک HttpModule:

تا اینجا نحوه نوشتن یک HttpModule را دیدیم. بعد از نوشتن یک HttpModule برای استفاده از آن باید این HttpModule را در فایل web.config معرفی کنیم تا از این به بعد سرور در هنگام اجرای رویدادهای مورد نظر، HttpModule ما را نیز دخالت دهد! تنظیمات مورد نیاز برای این کار بسته به نسخه IIS که استفاده میکنیم متفاوت است.
در IIS 6 و قبل از آن، (ویندوز 2003 و قبل از آن) برای استفاده از یک HttpModule آن را در بخش httpModules (در قسمت system.web) قرار میدهیم و در IIS 7 به بعد (ویندوز 2008 به بعد) برای این کار از بخش modules (در قسمت system.webServer) استفاده میکنیم. به عنوان مثال برای اضافه کردن ماژول مثال بالا در IIS 6 تنظیمات فایل web.config را به این صورت انجام میدهیم:

<configuration>
	<system.web>
		<httpModules>
			<add name="SampleHttpModule" type="MyApplication.SampleHttpModule"/>
		</httpModules>
	</system.web>
</configuration>

برای IIS 7 تنظیمات به این صورت انجام میشود:

<configuration>
	<system.webServer>
		<modules>
			<add name="SampleHttpModule" type="MyApplication.SampleHttpModule"/>
		</modules>
	</system.webServer>
</configuration>

نکته: اگر هر دو قسمت ذکر شده را در فایل web.config اضافه کنیم (به عنوان مثال در حالتی که بخواهیم یک فایل web.config در هر دو حالت IIS 6 و IIS 7 کار کند) در IIS 7 با خطا روبرو میشویم که برای رفع این خطا میتوانیم از تگ validation (در قسمت system.webServer) با ویژگی validateIntegratedModeConfiguration=false استفاده کنیم. به عنوان مثال فایل web.config مورد نیاز برای مثال قبلی در این حالت به این صورت خواهد شد:

<configuration>
	<system.web>
		<httpModules>
			<add name="SampleHttpModule" type="MyApplication.SampleHttpModule"/>
		</httpModules>
	</system.web>
	<system.webServer>
		<validation validateIntegratedModeConfiguration="false" />
		<modules>
			<add name="SampleHttpModule" type="MyApplication.SampleHttpModule"/>
		</modules>
	</system.webServer>
</configuration>

چند مثال

در این قسمت چند نمونه ساده برای استفاده از HttpModule آورده ام که علاوه بر کمک در درک بیشتر موضوع، میتواند به عنوان نقطه شروعی برای نوشتن HttpModuleهای کامل و کاربردی تر محسوب شود. در پایان این مقاله، در یک پروژه نمونه کد کامل همه این مثالها آمده است ولی در اینجا برای سادگی بیشتر پیاده سازی بعضی قسمتهایی که به مبحث HttpModule ارتباط چندانی نداشت را نیاورده ام. ضمنا دقت کنید که این هدف این مثالها بیشتر آموزش نحوه استفاده از HttpModule بوده و در بعضی موارد راه حلهای استفاده شده، چندان بی نقص نیست!

مثال اول: LongRequestTracker

فرض کنید که میخواهیم پردازشهایی که زمان محاسبه آنها در سرور بیشتر از حد معینی است را پیدا کنیم. برای این منظور در شروع درخواست، زمان سرور را در Context.Items ذخیره میکنیم (این مجموعه از لحظه شروع یک درخواست تا پایان همان درخواست قابل استفاده است) و در پایان درخواست زمان سپری شده را محاسبه میکنیم. اگر زمان درخواست بیشتر از حد معین بود، اطلاعات درخواست را در یک فایل ذخیره میکنیم.
بنابراین کد BeginRequest در این ماژول به این صورت خواهد بود:

void context_BeginRequest(object sender, EventArgs e)
{
	var application = sender as HttpApplication;
	application.Context.Items["_StartTime"] = DateTime.Now;
}

و کد EndRequest برای این منظور به این صورت نوشته خواهد شد:

void context_EndRequest(object sender, EventArgs e)
{
	var application = sender as HttpApplication;
	var totalTime = DateTime.Now - (DateTime)application.Context.Items["_StartTime"];
	if (totalTime > TimeSpan.FromSeconds(2))
	{
		// log application.Request
	}
}

مثال دوم: SimpleUrlRewriter

در این مثال میخواهیم یک UrlRewriter خیلی ساده طراحی کنیم به این صورت که هرگاه درخواستی برای صفحه Fake.aspx توسط سرور درخواست شد، بجای آن، صفحه Default.aspx با یک QueryString خاص به کاربر تحویل داده شود. نحوه کار بسیار ساده است. در رویداد BeginRequest با بررسی شئ Request متوجه میشویم که درخواست برای چه صفحه ای است و در صورت نیاز به ارسال صفحه دیگری به کاربر با استفاده از شئ Server، کاربر را به صفحه دیگر هدایت میکنیم.

void context_BeginRequest(object sender, EventArgs e)
{
	var application = sender as HttpApplication;
	if (application.Request.Path.Equals("/fake.aspx", StringComparison.InvariantCultureIgnoreCase))
	{
		application.Server.Transfer("Default.aspx?msg=This+is+Fake.aspx!");
	}
}

مثال سوم: CookieVerifier

در این مثال میخواهیم مکانیزم امنیتی پیاده سازی کنیم که از تغییر کوکی های توسط کاربر جلوگیری کند. به این منظور در تابع EndRequest به مقدار value هر کوکی یک کد احراز هویت پیام (MAC) اضافه میکنیم و در تابع BeginReuqest هر کوکی که دارای MAC مناسب نبود از لیست کوکی ها حذف میکنیم. از آنجا که تهیه MAC فقط با استفاده از کلیدی که در سمت سرور موجود است قابل انجام است، کاربر دیگر قادر نخواهد بود کوکی های سایت را تغییر دهد.
کد استفاده شده برای اضافه کردن MAC به کوکی ها در تابع EndRequest مشابه کد زیر خواهد بود:

void context_EndRequest(object sender, EventArgs e)
{
	var application = sender as HttpApplication;

	for (int i = application.Response.Cookies.Count - 1; i >= 0; i--)
	{
		HttpCookie cookie = application.Response.Cookies[i];
		cookie.Value = CalculateSignature(cookie.Value) + cookie.Value;
	}
}

همچنین کد استفاده شده در تابع BeginRequest برای بررسی صحت MAC به این صورت است:

void context_BeginRequest(object sender, EventArgs e)
{
	var application = sender as HttpApplication;

	for (int i = application.Request.Cookies.Count - 1; i >= 0; i--)
	{
		HttpCookie cookie = application.Request.Cookies[i];

		bool isValid = false;
		if (cookie.Value.Length >= SignatureLengh)
		{
			string value = cookie.Value.Substring(SignatureLength);
			string signature = cookie.Value.Substring(0, SignatureLength);
			isValid = CalculateSignature(value) == signature;
			cookie.Value = value;
		}

		if (!isValid) application.Request.Cookies.Remove(cookie.Name);
	}
}

استفاده از HttpModule در مقابل استفاده از Global.asax

احتمالا برای بسیاری از شما این سوال پیش آمده است که ظاهرا کارهایی که توسط HttpModule قابل انجام است، توسط Global.asax نیز میتوان انجام داد، پس استفاده از HttpModule واقعا چه لزومی دارد؟ جواب کوتاه برای این سوال این است که شما میتوانید یک HttpModule را به صورت یک فایل dll جدا تهیه کنید و بدون کد نویسی مجدد، در تمام سایتهای خود مورد استفاده قرار دهید ولی اگر از Global.asax استفاده کنید، مجبور خواهید شد برای اضافه کردن هرکدام از قابلیتهای ذکر شده، کد سایت خود را تغییر دهید و احتمالا دوباره آن را کامپایل کنید. البته تفاوتهای دیگری نیز وجود دارد که برای این مبحث کوتاه در همین حد کافی است.

ضمیمهاندازه
پروژه نمونه شامل پیاده سازی کامل تمام مثالهای ذکر شده در مقاله19.58 کیلو بایت