۱۳۹۱/۰۱/۳۱

ASP.NET MVC #18


اعتبار سنجي كاربران در ASP.NET MVC

دو مكانيزم اعتبارسنجي كاربران به صورت توكار در ASP.NET MVC در دسترس هستند: Forms authentication و Windows authentication.
در حالت Forms authentication، برنامه موظف به نمايش فرم لاگين به كاربر‌ها و سپس بررسي اطلاعات وارده توسط آن‌ها است. برخلاف آن، Windows authentication حالت يكپارچه با اعتبار سنجي ويندوز است. براي مثال زمانيكه كاربري به يك دومين ويندوزي وارد مي‌شود، از همان اطلاعات ورود او به شبكه داخلي، به صورت خودكار و يكپارچه جهت استفاده از برنامه كمك گرفته خواهد شد و بيشترين كاربرد آن در برنامه‌هاي نوشته شده براي اينترانت‌هاي داخلي شركت‌ها است. به اين ترتيب كاربران يك بار به دومين وارد شده و سپس براي استفاده از برنامه‌هاي مختلف ASP.NET، نيازي به ارائه نام كاربري و كلمه عبور نخواهند داشت. Forms authentication بيشتر براي برنامه‌هايي كه از طريق اينترنت به صورت عمومي و از طريق انواع و اقسام سيستم عامل‌ها قابل دسترسي هستند، توصيه مي‌شود (و البته منعي هم براي استفاده در حالت اينترانت ندارد).
ضمنا بايد به معناي اين دو كلمه هم دقت داشت: هدف از Authentication اين است كه مشخص گردد هم اكنون چه كاربري به سايت وارد شده است. Authorization، سطح دسترسي كاربر وارد شده به سيستم و اعمالي را كه مجاز است انجام دهد، مشخص مي‌كند.


فيلتر Authorize در ASP.NET MVC

يكي ديگر از فيلترهاي امنيتي ASP.NET MVC به نام Authorize، كار محدود ساختن دسترسي به متدهاي كنترلرها را انجام مي‌دهد. زمانيكه اكشن متدي به اين فيلتر يا ويژگي مزين مي‌شود، به اين معنا است كه كاربران اعتبارسنجي نشده، امكان دسترسي به آن‌را نخواهند داشت. فيلتر Authorize همواره قبل از تمامي فيلترهاي تعريف شده ديگر اجرا مي‌شود.
فيلتر Authorize با پياده سازي اينترفيس System.Web.Mvc.IAuthorizationFilter توسط كلاس System.Web.Mvc.AuthorizeAttribute در دسترس مي‌باشد. اين كلاس علاوه بر پياده سازي اينترفيس ياد شده، داراي دو خاصيت مهم زير نيز مي‌باشد:

public string Roles { get; set; } // comma-separated list of role names
public string Users { get; set; } // comma-separated list of usernames

زمانيكه فيلتر Authorize به تنهايي بكارگرفته مي‌شود، هر كاربر اعتبار سنجي شده‌اي در سيستم قادر خواهد بود به اكشن متد مورد نظر دسترسي پيدا كند. اما اگر همانند مثال زير، از خواص Roles و يا Users نيز استفاده گردد، تنها كاربران اعتبار سنجي شده مشخصي قادر به دسترسي به يك كنترلر يا متدي در آن خواهند شد:

[Authorize(Roles="Admins")]
public class AdminController : Controller
{
    [Authorize(Users="Vahid")]
    public ActionResult DoSomethingSecure()
    {
    }
}

در اين مثال، تنها كاربراني با نقش Admins قادر به دسترسي به كنترلر جاري Admin خواهند بود. همچنين در بين اين كاربران ويژه، تنها كاربري به نام Vahid قادر است متد DoSomethingSecure را فراخواني و اجرا كند.

اكنون سؤال اينجا است كه فيلتر Authorize چگونه از دو مكانيزم اعتبار سنجي ياد شده استفاده مي‌كند؟ براي پاسخ به اين سؤال، فايل web.config برنامه را باز نموده و به قسمت authentication آن دقت كنيد:

<authentication mode="Forms">
      <forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>

به صورت پيش فرض، برنامه‌هاي ايجاد شده توسط VS.NET جهت استفاده از حالت Forms يا همان Forms authentication تنظيم شده‌اند. در اينجا كليه كاربران اعتبار سنجي نشده، به كنترلري به نام Account و متد LogOn در آن هدايت مي‌شوند.
براي تغيير آن به حالت اعتبار سنجي يكپارچه با ويندوز، فقط كافي است مقدار mode را به Windows تغيير داد و تنظيمات forms آن‌را نيز حذف كرد.


يك نكته: اعمال تنظيمات اعتبار سنجي اجباري به تمام صفحات سايت
تنظيم زير نيز در فايل وب كانفيگ برنامه، همان كار افزودن ويژگي Authorize را انجام مي‌دهد با اين تفاوت كه تمام صفحات سايت را به صورت خودكار تحت پوشش قرار خواهد داد (البته منهاي loginUrl ايي كه در تنظيمات فوق مشاهده نموديد):

<authorization>
     <deny users="?" />
</authorization>

در اين حالت دسترسي به تمام آدرس‌هاي سايت تحت تاثير قرار مي‌گيرند، منجمله دسترسي به تصاوير و فايل‌هاي CSS و غيره. براي اينكه اين موارد را براي مثال در حين نمايش صفحه لاگين نيز نمايش دهيم، بايد تنظيم زير را پيش از تگ system.web به فايل وب كانفيگ برنامه اضافه كرد:

<!-- we don't want to stop anyone seeing the css and images -->
<location path="Content">
    <system.web>
      <authorization>
        <allow users="*" />
      </authorization>
    </system.web>
</location>

در اينجا پوشه Content از سيستم اعتبارسنجي اجباري خارج مي‌شود و تمام كاربران به آن دسترسي خواهند داشت.
به علاوه امكان امن ساختن تنها قسمتي از سايت نيز ميسر است؛ براي مثال:

<location path="secure">
  <system.web>
    <authorization>
      <allow roles="Administrators" />
      <deny users="*" />
    </authorization>
  </system.web>
</location>

در اينجا مسيري به نام secure، نياز به اعتبارسنجي اجباري دارد. به علاوه تنها كاربراني در نقش Administrators به آن دسترسي خواهند داشت.


نكته: به تنظيمات انجام شده در فايل Web.Config دقت داشته باشيد
همانطور كه مي‌شود دسترسي به يك مسير را توسط تگ location بازگذاشت، امكان بستن آن هم فراهم است (بجاي allow از deny استفاده شود). همچنين در ASP.NET MVC به سادگي مي‌توان تنظيمات مسيريابي را در فايل global.asax.cs تغيير داد. براي مثال اينبار مسير دسترسي به صفحات امن سايت، Admin خواهد بود نه Secure. در اين حالت چون از فيلتر Authorize استفاده نشده و همچنين فايل web.config نيز تغيير نكرده، اين صفحات بدون محافظت رها خواهند شد.
بنابراين اگر از تگ location براي امن سازي قسمتي از سايت استفاده مي‌كنيد، حتما بايد پس از تغييرات مسيريابي، فايل web.config را هم به روز كرد تا به مسير جديد اشاره كند.
به همين جهت در ASP.NET MVC بهتر است كه صريحا از فيلتر Authorize بر روي كنترلرها (جهت اعمال به تمام متدهاي آن) يا بر روي متدهاي خاصي از كنترلرها استفاده كرد.
امكان تعريف AuthorizeAttribute در فايل global.asax.cs و متد RegisterGlobalFilters آن به صورت سراسري نيز وجود دارد. اما در اين حالت حتي صفحه لاگين سايت هم ديگر در دسترس نخواهد بود. براي رفع اين مشكل در ASP.NET MVC 4 فيلتر ديگري به نام AllowAnonymousAttribute معرفي شده است تا بتوان قسمت‌هايي از سايت را مانند صفحه لاگين، از سيستم اعتبارسنجي اجباري خارج كرد تا حداقل كاربر بتواند نام كاربري و كلمه عبور خودش را وارد نمايد:

[System.Web.Mvc.AllowAnonymous]
public ActionResult Login()
{
    return View();
}

بنابراين در ASP.NET MVC 4.0، فيلتر AuthorizeAttribute را سراسري تعريف كنيد. سپس در كنترلر لاگين برنامه از فيلتر AllowAnonymous استفاده نمائيد.
البته نوشتن فيلتر سفارشي AllowAnonymousAttribute در ASP.NET MVC 3.0 نيز ميسر است. براي مثال:

public class LogonAuthorize : AuthorizeAttribute {
     public override void OnAuthorization(AuthorizationContext filterContext) {
         if (!(filterContext.Controller is AccountController))
             base.OnAuthorization(filterContext);
     }
}

در اين فيلتر سفارشي، اگر كنترلر جاري از نوع AccountController باشد، از سيستم اعتبار سنجي اجباري خارج خواهد شد. مابقي كنترلرها همانند سابق پردازش مي‌شوند. به اين معنا كه اكنون مي‌توان LogonAuthorize را به صورت يك فيلتر سراسري در فايل global.asax.cs معرفي كرد تا به تمام كنترلرها، منهاي كنترلر Account اعمال شود.



مثالي جهت بررسي حالت Windows Authentication

يك پروژه جديد خالي ASP.NET MVC را آغاز كنيد. سپس يك كنترلر جديد را به نام Home نيز به آن اضافه كنيد. در ادامه متد Index آن‌را با ويژگي Authorize، مزين نمائيد. همچنين بر روي نام اين متد كليك راست كرده و يك View خالي را براي آن ايجاد كنيد:

using System.Web.Mvc;

namespace MvcApplication15.Controllers
{
    public class HomeController : Controller
    {
        [Authorize]
        public ActionResult Index()
        {
            return View();
        }
    }
}

محتواي View متناظر با متد Index را هم به شكل زير تغيير دهيد تا نام كاربر وارد شده به سيستم را نمايش دهد:

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>
Current user: @User.Identity.Name

به علاوه در فايل Web.config برنامه، حالت اعتبار سنجي را به ويندوز تغيير دهيد:

<authentication mode="Windows" />

اكنون اگر برنامه را اجرا كنيد و وب سرور آزمايشي انتخابي هم IIS Express باشد، پيغام HTTP Error 401.0 - Unauthorized نمايش داده مي‌شود. علت هم اينجا است كه Windows Authentication به صورت پيش فرض در اين وب سرور غيرفعال است. براي فعال سازي آن به مسير My Documents\IISExpress\config مراجعه كرده و فايل applicationhost.config را باز نمائيد. تگ windowsAuthentication را يافته و ويژگي enabled آن‌را كه false است به true تنظيم نمائيد. اكنون اگر برنامه را مجددا اجرا كنيم، در محل نمايش User.Identity.Name، نام كاربر وارد شده به سيستم نمايش داده خواهد شد.
همانطور كه مشاهده مي‌كنيد در اينجا همه چيز يكپارچه است و حتي نيازي نيست صفحه لاگين خاصي را به كاربر نمايش داد. همينقدر كه كاربر توانسته به سيستم ويندوزي وارد شود، بر اين اساس هم مي‌تواند از برنامه‌هاي وب موجود در شبكه استفاده كند.



بررسي حالت Forms Authentication

براي كار با Forms Authentication نياز به محلي براي ذخيره سازي اطلاعات كاربران است. اكثر مقالات را كه مطالعه كنيد شما را به مباحث membership مطرح شده در زمان ASP.NET 2.0 ارجاع مي‌دهند. اين روش در ASP.NET MVC هم كار مي‌كند؛ اما الزامي به استفاده از آن نيست.

براي بررسي حالت اعتبار سنجي مبتني بر فرم‌ها، يك برنامه خالي ASP.NET MVC جديد را آغاز كنيد. يك كنترلر Home ساده را نيز به آن اضافه نمائيد.
سپس نياز است نكته «تنظيمات اعتبار سنجي اجباري تمام صفحات سايت» را به فايل وب كانفيگ برنامه اعمال نمائيد تا نيازي نباشد فيلتر Authorize را در همه جا معرفي كرد. سپس نحوه معرفي پيش فرض Forms authentication تعريف شده در فايل web.config نيز نياز به اندكي اصلاح دارد:

<authentication mode="Forms">
      <!--one month ticket-->
      <forms name=".403MyApp" 
             cookieless="UseCookies" 
             loginUrl="~/Account/LogOn" 
             defaultUrl="~/Home" 
             slidingExpiration="true" 
             protection="All" 
             path="/" 
             timeout="43200"/>
</authentication>

در اينجا استفاده از كوكي‌ها اجباري شده است. loginUrl به كنترلر و متد لاگين برنامه اشاره مي‌كند. defaultUrl مسيري است كه كاربر پس از لاگين به صورت خودكار به آن هدايت خواهد شد. همچنين نكته‌ي مهم ديگري را كه بايد رعايت كرد، name ايي است كه در اين فايل config عنوان مي‌‌كنيد. اگر بر روي يك وب سرور، چندين برنامه وب ASP.Net را در حال اجرا داريد، بايد براي هر كدام از اين‌ها نامي جداگانه و منحصربفرد انتخاب كنيد، در غيراينصورت تداخل رخ داده و گزينه مرا به خاطر بسپار شما كار نخواهد كرد.
كار slidingExpiration كه در اينجا تنظيم شده است نيز به صورت زير مي‌باشد:
اگر لاگين موفقيت آميزي ساعت 5 عصر صورت گيرد و timeout شما به عدد 10 تنظيم شده باشد، اين لاگين به صورت خودكار در 5:10‌ منقضي خواهد شد. اما اگر در اين حين در ساعت 5:05 ، كاربر، يكي از صفحات سايت شما را مرور كند، زمان منقضي شدن كوكي ذكر شده به 5:15 تنظيم خواهد شد(مفهوم تنظيم slidingExpiration). لازم به ذكر است كه اگر كاربر پيش از نصف زمان منقضي شدن كوكي (مثلا در 5:04)، يكي از صفحات را مرور كند، تغييري در اين زمان نهايي منقضي شدن رخ نخواهد داد.
اگر timeout ذكر نشود، زمان منقضي شدن كوكي ماندگار (persistent) مساوي زمان جاري + زمان منقضي شدن سشن كاربر كه پيش فرض آن 30 دقيقه است، خواهد بود.

سپس يك مدل را به نام Account به پوشه مدل‌هاي برنامه با محتواي زير اضافه نمائيد:

using System.ComponentModel.DataAnnotations;

namespace MvcApplication15.Models
{
    public class Account
    {
        [Required(ErrorMessage = "Username is required to login.")]
        [StringLength(20)]
        public string Username { get; set; }

        [Required(ErrorMessage = "Password is required to login.")]
        [DataType(DataType.Password)]
        public string Password { get; set; }

        public bool RememberMe { get; set; }
    }
}

همچنين مطابق تنظيمات اعتبار سنجي مبتني بر فرم‌هاي فايل وب كانفيگ، نياز به يك AccountController نيز هست:

using System.Web.Mvc;
using MvcApplication15.Models;

namespace MvcApplication15.Controllers
{
    public class AccountController : Controller
    {
        [HttpGet]
        public ActionResult LogOn()
        {
            return View();
        }

        [HttpPost]
        public ActionResult LogOn(Account loginInfo, string returnUrl)
        {
            return View();
        }
    }
}

در اينجا در حالت HttpGet فرم لاگين نمايش داده خواهد شد. بنابراين بر روي اين متد كليك راست كرده و گزينه Add view را انتخاب كنيد. سپس در صفحه باز شده گزينه Create a strongly typed view را انتخاب كرده و مدل را هم بر روي كلاس Account قرار دهيد. قالب scaffolding را هم Create انتخاب كنيد. به اين ترتيب فرم لاگين برنامه ساخته خواهد شد.
اگر به متد HttpPost فوق دقت كرده باشيد، علاوه بر دريافت وهله‌اي از شيء Account، يك رشته را به نام returnUrl نيز تعريف كرده است. علت هم اينجا است كه سيستم Forms authentication، صفحه بازگشت را به صورت خودكار به شكل يك كوئري استرينگ به انتهاي Url جاري اضافه مي‌كند. مثلا:

http://localhost/Account/LogOn?ReturnUrl=something

بنابراين اگر يكي از پارامترهاي متد تعريف شده به نام returnUrl باشد، به صورت خودكار مقدار دهي خواهد شد.

تا اينجا زمانيكه برنامه را اجرا كنيم، ابتدا بر اساس تعاريف مسيريابي پيش فرض برنامه، آدرس كنترلر Home و متد Index آن فراخواني مي‌گردد. اما چون در وب كانفيگ برنامه authorization را فعال كرده‌ايم، برنامه به صورت خودكار به آدرس مشخص شده در loginUrl قسمت تعاريف اعتبارسنجي مبتني بر فرم‌ها هدايت خواهد شد. يعني آدرس كنترلر Account و متد LogOn آن درخواست مي‌گردد. در اين حالت صفحه لاگين نمايان خواهد شد.

مرحله بعد، اعتبار سنجي اطلاعات وارد شده كاربر است. بنابراين نياز است كنترلر Account را به نحو زير بازنويسي كرد:

using System.Web.Mvc;
using System.Web.Security;
using MvcApplication15.Models;

namespace MvcApplication15.Controllers
{
    public class AccountController : Controller
    {
        [HttpGet]
        public ActionResult LogOn(string returnUrl)
        {
            if (User.Identity.IsAuthenticated) //remember me
            {
                if (shouldRedirect(returnUrl))
                {
                    return Redirect(returnUrl);
                }
                return Redirect(FormsAuthentication.DefaultUrl);
            }

            return View(); // show the login page
        }

        [HttpGet]
        public void LogOut()
        {
            FormsAuthentication.SignOut();
        }

        private bool shouldRedirect(string returnUrl)
        {
            // it's a security check
            return !string.IsNullOrWhiteSpace(returnUrl) &&
                                Url.IsLocalUrl(returnUrl) &&
                                returnUrl.Length > 1 &&
                                returnUrl.StartsWith("/") &&
                                !returnUrl.StartsWith("//") &&
                                !returnUrl.StartsWith("/\\");
        }

        [HttpPost]
        public ActionResult LogOn(Account loginInfo, string returnUrl)
        {
            if (this.ModelState.IsValid)
            {
                if (loginInfo.Username == "Vahid" && loginInfo.Password == "123")
                {
                    FormsAuthentication.SetAuthCookie(loginInfo.Username, loginInfo.RememberMe);
                    if (shouldRedirect(returnUrl))
                    {
                        return Redirect(returnUrl);
                    }
                    FormsAuthentication.RedirectFromLoginPage(loginInfo.Username, loginInfo.RememberMe);
                }
            }
            this.ModelState.AddModelError("", "The user name or password provided is incorrect.");
            ViewBag.Error = "Login faild! Make sure you have entered the right user name and password!";
            return View(loginInfo);
        }
    }
}

در اينجا با توجه به گزينه «مرا به خاطر بسپار»، اگر كاربري پيشتر لاگين كرده و كوكي خودكار حاصل از اعتبار سنجي مبتني بر فرم‌هاي او نيز معتبر باشد، مقدار User.Identity.IsAuthenticated مساوي true خواهد بود. بنابراين نياز است در متد LogOn از نوع HttpGet به اين مساله دقت داشت و كاربر اعتبار سنجي شده را به صفحه پيش‌فرض تعيين شده در فايل web.config برنامه يا returnUrl هدايت كرد.
در متد LogOn از نوع HttpPost، كار اعتبارسنجي اطلاعات ارسالي به سرور انجام مي‌شود. در اينجا فرصت خواهد بود تا اطلاعات دريافتي، با بانك اطلاعاتي مقايسه شوند. اگر اطلاعات مطابقت داشتند، ابتدا كوكي خودكار FormsAuthentication تنظيم شده و سپس به كمك متد RedirectFromLoginPage كاربر را به صفحه پيش فرض سيستم هدايت مي‌كنيم. يا اگر returnUrl ايي وجود داشت، آن‌را پردازش خواهيم كرد.
براي پياده سازي خروج از سيستم هم تنها كافي است متد FormsAuthentication.SignOut فراخواني شود تا تمام اطلاعات سشن و كوكي‌هاي مرتبط، به صورت خودكار حذف گردند.

تا اينجا فيلتر Authorize بدون پارامتر و همچنين در حالت مشخص سازي صريح كاربران به نحو زير را پوشش داديم:

[Authorize(Users="Vahid")]

اما هنوز حالت استفاده از Roles در فيلتر Authorize باقي مانده است. براي فعال سازي خودكار بررسي نقش‌هاي كاربران نياز است يك Role provider سفارشي را با پياده سازي كلاس RoleProvider، طراحي كنيم. براي مثال:

using System;
using System.Web.Security;

namespace MvcApplication15.Helper
{
    public class CustomRoleProvider : RoleProvider
    {
        public override bool IsUserInRole(string username, string roleName)
        {
            if (username.ToLowerInvariant() == "ali" && roleName.ToLowerInvariant() == "user")
                return true;
            // blabla ...
            return false;
        }

        public override string[] GetRolesForUser(string username)
        {
            if (username.ToLowerInvariant() == "ali")
            {
                return new[] { "User", "Helpdesk" };
            }

            if(username.ToLowerInvariant()=="vahid")
            {
                return new [] { "Admin" };
            }

            return new string[] { };
        }

        public override void AddUsersToRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }

        public override string ApplicationName
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        public override void CreateRole(string roleName)
        {
            throw new NotImplementedException();
        }

        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            throw new NotImplementedException();
        }

        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            throw new NotImplementedException();
        }

        public override string[] GetAllRoles()
        {
            throw new NotImplementedException();
        }        

        public override string[] GetUsersInRole(string roleName)
        {
            throw new NotImplementedException();
        }        

        public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }

        public override bool RoleExists(string roleName)
        {
            throw new NotImplementedException();
        }
    }
}

در اينجا حداقل دو متد IsUserInRole و GetRolesForUser بايد پياده سازي شوند و مابقي اختياري هستند.
بديهي است در يك برنامه واقعي اين اطلاعات بايد از يك بانك اطلاعاتي خوانده شوند؛ براي نمونه به ازاي هر كاربر تعدادي نقش وجود دارد. به ازاي هر نقش نيز تعدادي كاربر تعريف شده است (يك رابطه many-to-many بايد تعريف شود).
در مرحله بعد بايد اين Role provider سفارشي را در فايل وب كانفيگ برنامه در قسمت system.web آن تعريف و ثبت كنيم:

<roleManager>
      <providers>
        <clear />
        <add name="CustomRoleProvider" type="MvcApplication15.Helper.CustomRoleProvider"/>
      </providers>
    </roleManager>


همين مقدار براي راه اندازي بررسي نقش‌ها در ASP.NET MVC كفايت مي‌كند. اكنون امكان تعريف نقش‌ها، حين بكارگيري فيلتر Authorize ميسر است:

[Authorize(Roles = "Admin")]
public class HomeController : Controller