[ASP.NET MVC]使用IAuthenticationFilter,IAuthorizationFilter實作Form表單登入認證&授權

實務上大多情境並不採用ASP.NET Identity處理身分認證授權,而是選擇使用Form表單認證輕量且易於快速實現自訂邏輯,網路上也有許多教學如何在ASP.NET中使用FormIdentity。

此篇將提供的是在ASP.NET MVC 5 使用IAuthenticationFilter,IAuthorizationFilter實現Form表單認證的方法

啟用Form表單認證

修改Web.config將authentication mode改為Forms表單認證(預設是None)

<system.web>
  <!--<authentication mode="None"/>-->
  <authentication mode="Forms">
    <forms loginUrl="~/Account/Login">
    </forms>
  </authentication>
</system.web>

註解remove,啟用FormsAuthentication模組(ASP.NET 5 專案範本預設移除的)

<system.webServer>
  <modules>
    <!--<remove name="FormsAuthentication" />-->
  </modules>
</system.webServer>

 

使用FormIdentity實作登入邏輯

通過自訂邏輯後,將使用者資訊放入FormsAuthenticationTicketuserData參數,經由加密並建立cookie加入Response回傳給Client

public ActionResult Login(LoginViewModel model, string returnUrl)
{
    if (!ModelState.IsValid) {
        return View();
    }
    var user = _db.Users
        .FirstOrDefault(x => x.Email == model.Email);
    if (user == null) {
        ModelState.AddModelError("", "請輸入正確的帳號或密碼!");
        return View();
    }
    if (user.Password.Equals(model.Password)) {
        //string roles = string.Join(",", user.Roles.Select(x => x.Name).ToArray());
        var now = DateTime.Now;
        var ticket = new FormsAuthenticationTicket(
            version: 1,
            name: user.Email,
            issueDate: now,
            expiration: now.AddMinutes(30),
            isPersistent: model.RememberMe,
            userData: user.Id.ToString(),//userData:roles,
            cookiePath: FormsAuthentication.FormsCookiePath);

        var encryptedTicket = FormsAuthentication.Encrypt(ticket);
        var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
        Response.Cookies.Add(cookie);

        return RedirectToAction("Index", "Home");
    }
    else {
        ModelState.AddModelError("", "請輸入正確的帳號或密碼!");
        return View();
    }
}

 

IAuthenticationFilter認證過濾器-提供自定義登入認證邏輯

Authentication filter是ASP.NET MVC 5的新過濾器,可以在Action方法或Controller或全域設置至全部Controller的驗證邏輯。

在ASP.NET MVC 5處理管線中會先執行認證過濾器OnAuthentication(提供AuthenticationContext包含認證主體IPrincipal),進行認證檢核。

另外提供OnAuthenticationChallenge將未經授權的請求進行額外處裡(如回應自訂ChallengeResult)再回應。

Lifecycle of an ASP.NET MVC 5 Application

public class CustomAuthenticationFilter : IAuthenticationFilter
{
    private readonly Database _db = new Database();
    public void OnAuthentication(AuthenticationContext filterContext)
    {
        if (filterContext.ActionDescriptor.IsDefined(typeof (AllowAnonymousAttribute), inherit: true)
            || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof (AllowAnonymousAttribute), inherit: true)) {
            return;
        }

        if (filterContext.Principal.Identity.IsAuthenticated && filterContext.Principal.Identity is FormsIdentity)
        {
            var identity = (FormsIdentity)filterContext.Principal.Identity;
            var ticket = identity.Ticket;

            if (!string.IsNullOrEmpty(ticket.UserData)) 
            {
                //var roles = ticket.UserData.Split(',');
                //filterContext.Principal = new GenericPrincipal(identity,roles);
                var user = _db.Users.FirstOrDefault(u => u.Id.ToString() == ticket.UserData);
                if (user != null) {
                    var roles = user.Roles.Select(r => r.Name).ToArray();
                    filterContext.Principal = new GenericPrincipal(identity, roles);

                }
            }
        }
        else
        {
            filterContext.Result = new HttpUnauthorizedResult();
        }
    }

    public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
    {
        if (filterContext.Result == null || filterContext.Result is HttpUnauthorizedResult)
        {
            filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary
            {
                {"controller","Account"},
                {"action","Login"},
                {"returnUrl",filterContext.HttpContext.Request.RawUrl }
            });
        }
        //or do something , add challenge to response 

    }
}

 

IAuthorizationFilter授權過濾器-提供自定義身分(角色)授權邏輯

Authorization filter驗證使用者是否擁有權限或角色身分執行Action,ASP.NET MVC 已內建提供實作AuthorizeAttribute只需繼承並覆寫 AuthorizeCore()方法提供自訂邏輯。

直接繼承IAuthorizationFilter介面實作需要考量更多網路安全性的問題,為了避免發生漏洞或是特性不完全,所以Framwork已經提供經由廣泛測試且功能完善的良好實作類別,只需複用即可。白話:避免明明是自己寫不好,卻要怪是微軟的漏洞xDD

Github:AuthorizeAttribute.cs 有興趣或有需要自製可以前往察看原始碼


public class CustomAuthorizationFilter : AuthorizeAttribute
{
    private readonly Database _db = new Database();
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        if (httpContext.Request.IsLocal) {
            return true;
        }
        var identity = httpContext.User.Identity as FormsIdentity;
        if (identity ?.Ticket != null)
        {
            //var userRoles = identity.Ticket.UserData.Split(',');
            var userRoles = _db.Users.Find(identity.Ticket.UserData) ?.Roles.Select(r => r.Name);
            
            if (userRoles ?.Intersect(Roles.Split(',')).Any() ?? false)//交集->具有某一角色->有權限
            {
                return true;
            }
            //or get current action's roles from db and check the authorization
        }


        return false;
    }
}

補充運作流程圖:

轉載自:一张图看懂ASP.NET MVC5认证和授权过滤器的执行顺序

FilterConfig.cs全域註冊過濾器

註冊後即可達到全站套用過濾器,搭配[AllowAnonymous] 屬性控制各別Action功能是否需要檢查授權。

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
        filters.Add(new CustomAuthenticationFilter());
        //filters.Add(new CustomAuthorizationFilter());
    }
}

額外補充:

  • 登出

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult LogOff()
    {
        FormsAuthentication.SignOut();
        return RedirectToAction("Index", "Home");
    }
  • 3A(AAA) - Authentication、Authorization、Accounting

參考&延伸閱讀