ASP.NET MVC Form驗證

1、前言html

  關於表單驗證,園子裏已經有很多的文章,相信Web開發人員也都基本寫過,最近在一個我的項目中恰好用到,在這裏與你們分享一下。原本想從用戶註冊開始寫起,但發現東西比較多,涉及到界面、前端驗證、前端加密、後臺解密、用戶密碼Hash、權限驗證等等,文章寫起來可能會很長,因此這裏主要介紹的是登陸驗證和權限控制部分,有興趣的朋友歡迎一塊兒交流。前端

  通常驗證方式有Windows驗證和表單驗證,web項目用得更多的是表單驗證。原理很簡單,簡單地說就是利用瀏覽器的cookie,將驗證令牌存儲在客戶端瀏覽器上,cookie每次會隨請求發送到服務器,服務器驗證這個令牌。一般一個系統的用戶會分爲多種角色:匿名用戶、普通用戶和管理員;這裏面又能夠再細分,例如用戶能夠是普通用戶或Vip用戶,管理員能夠是普通管理員或超級管理員等。在項目中,咱們有的頁面可能只容許管理員查看,有的只容許登陸用戶查看,這就是角色區分(Roles);某些特別狀況下,有些頁面可能只容許叫「張三」名字的人查看,這就是用戶區分(Users)。web

  咱們先看一下最後要實現的效果:ajax

  1.這是在Action級別的控制。後端

public class Home1Controller : Controller
{
    //匿名訪問
    public ActionResult Index()
    {
        return View();
    }
    //登陸用戶訪問
    [RequestAuthorize]
    public ActionResult Index2()
    {
        return View();
    }
    //登陸用戶,張三才能訪問
    [RequestAuthorize(Users="張三")]
    public ActionResult Index3()
    {
        return View();
    }
    //管理員訪問
    [RequestAuthorize(Roles="Admin")]
    public ActionResult Index4()
    {
        return View();
    }
}

  2.這是在Controller級別的控制。固然,若是某個Action須要匿名訪問,也是容許的,由於控制級別上,Action優先級大於Controller。瀏覽器

//Controller級別的權限控制
[RequestAuthorize(User="張三")]
public class Home2Controller : Controller
{
    //登陸用戶訪問
    public ActionResult Index()
    {
        return View();
    }
    //容許匿名訪問
    [AllowAnonymous]
    public ActionResult Index2()
    {
        return View();
    }
}

  3.Area級別的控制。有時候咱們會把一些模塊作成分區,固然這裏也能夠在Area的Controller和Action進行標記。安全

  從上面能夠看到,咱們須要在各個地方進行標記權限,若是把Roles和Users硬寫在程序中,不是很好的作法。我但願能更簡單一點,在配置文件進行說明。例如以下配置:服務器

<?xml version="1.0" encoding="utf-8" ?>
<!--
    1.這裏能夠把權限控制轉移到配置文件,這樣就不用在程序中寫roles和users了
    2.若是程序也寫了,那麼將覆蓋配置文件的。
    3.action級別的優先級 > controller級別 > Area級別   
-->
<root>
  <!--area級別-->
  <area name="Admin">
    <roles>Admin</roles>
  </area>
   
  <!--controller級別-->
  <controller name="Home2">
    <user>張三</user>
  </controller>
   
  <!--action級別-->
  <controller name="Home1">
    <action name="Inde3">
      <users>張三</users>
    </action>
    <action name="Index4">
      <roles>Admin</roles>
    </action>
  </controller>
</root>

  寫在配置文件裏,是爲了方便管理,若是程序裏也寫了,將覆蓋配置文件的。ok,下面進入正題。cookie

2、主要接口ide

  先看兩個主要用到的接口。

  IPrincipal 定義了用戶對象的基本功能,接口定義以下:

public interface IPrincipal
{
    //標識對象
    IIdentity Identity { get; }
    //判斷當前角色是否屬於指定的角色
    bool IsInRole(string role);
}

  它有兩個主要成員,IsInRole用於判斷當前對象是否屬於指定角色的,IIdentity定義了標識對象信息。HttpContext的User屬性就是IPrincipal類型的。

  IIdentity 定義了標識對象的基本功能,接口定義以下:

public interface IIdentity
{   
    //身份驗證類型
    string AuthenticationType { get; }
    //是否驗證經過
    bool IsAuthenticated { get; }  
    //用戶名
    string Name { get; }
}

  IIdentity包含了一些用戶信息,但有時候咱們須要存儲更多信息,例如用戶ID、用戶角色等,這些信息會被序列到cookie中加密保存,驗證經過時能夠解碼再反序列化得到,狀態得以保存。例如定義一個UserData。

public class UserData : IUserData
{
    public long UserID { get; set; }
    public string UserName { get; set; }
    public string UserRole { get; set; }
 
    public bool IsInRole(string role)
    {
        if (string.IsNullOrEmpty(role))
        {
            return true;
        }
        return role.Split(',').Any(item => item.Equals(this.UserRole, StringComparison.OrdinalIgnoreCase));           
    }
 
    public bool IsInUser(string user)
    {
        if (string.IsNullOrEmpty(user))
        {
            return true;
        }
        return user.Split(',').Any(item => item.Equals(this.UserName, StringComparison.OrdinalIgnoreCase));
    }
}

  UserData實現了IUserData接口,該接口定義了兩個方法:IsInRole和IsInUser,分別用於判斷當前用戶角色和用戶名是否符合要求。該接口定義以下:

public interface IUserData
{
    bool IsInRole(string role);
    bool IsInUser(string user);
}

  接下來定義一個Principal實現IPrincipal接口,以下:

public class Principal : IPrincipal       
{
    public IIdentity Identity{get;private set;}
    public IUserData UserData{get;set;}
 
    public Principal(FormsAuthenticationTicket ticket, IUserData userData)
    {
        EnsureHelper.EnsureNotNull(ticket, "ticket");
        EnsureHelper.EnsureNotNull(userData, "userData");
        this.Identity = new FormsIdentity(ticket);
        this.UserData = userData;
    }
 
    public bool IsInRole(string role)
    {
        return this.UserData.IsInRole(role);           
    }      
 
    public bool IsInUser(string user)
    {
        return this.UserData.IsInUser(user);
    }
}

  Principal包含IUserData,而不是具體的UserData,這樣很容易更換一個UserData而不影響其它代碼。Principal的IsInRole和IsInUser間接調用了IUserData的同名方法。

3、寫入cookie和讀取cookie

  接下來,須要作的就是用戶登陸成功後,建立UserData,序列化,再利用FormsAuthentication加密,寫到cookie中;而請求到來時,須要嘗試將cookie解密並反序列化。以下:

public class HttpFormsAuthentication
{       
    public static void SetAuthenticationCookie(string userName, IUserData userData, double rememberDays = 0)                        
    {
        EnsureHelper.EnsureNotNullOrEmpty(userName, "userName");
        EnsureHelper.EnsureNotNull(userData, "userData");
        EnsureHelper.EnsureRange(rememberDays, "rememberDays", 0);
 
        //保存在cookie中的信息
        string userJson = JsonConvert.SerializeObject(userData);
 
        //建立用戶票據
        double tickekDays = rememberDays == 0 ? 7 : rememberDays;
        var ticket = new FormsAuthenticationTicket(2, userName,
            DateTime.Now, DateTime.Now.AddDays(tickekDays), false, userJson);
 
        //FormsAuthentication提供web forms身份驗證服務
        //加密
        string encryptValue = FormsAuthentication.Encrypt(ticket);
 
        //建立cookie
        HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptValue);
        cookie.HttpOnly = true;
        cookie.Domain = FormsAuthentication.CookieDomain;
 
        if (rememberDays > 0)
        {
            cookie.Expires = DateTime.Now.AddDays(rememberDays);
        }           
        HttpContext.Current.Response.Cookies.Remove(cookie.Name);
        HttpContext.Current.Response.Cookies.Add(cookie);
    }
 
    public static Principal TryParsePrincipal<TUserData>(HttpContext context)                            
        where TUserData : IUserData
    {
        EnsureHelper.EnsureNotNull(context, "context");
 
        HttpRequest request = context.Request;
        HttpCookie cookie = request.Cookies[FormsAuthentication.FormsCookieName];
        if(cookie == null || string.IsNullOrEmpty(cookie.Value))
        {
            return null;
        }
        //解密cookie值
        FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
        if(ticket == null || string.IsNullOrEmpty(ticket.UserData))                   
        {
            return null;                       
        }
        IUserData userData = JsonConvert.DeserializeObject<TUserData>(ticket.UserData);             
        return new Principal(ticket, userData);
    }
}

  在登陸時,咱們能夠相似這樣處理:

public ActionResult Login(string userName,string password)
{
    //驗證用戶名和密碼等一些邏輯...  
  
    UserData userData = new UserData()
    {
        UserName = userName,
        UserID = userID,
        UserRole = "Admin"
    };
    HttpFormsAuthentication.SetAuthenticationCookie(userName, userData, 7);
     
    //驗證經過...
}

  登陸成功後,就會把信息寫入cookie,能夠經過瀏覽器觀察請求,就會有一個名稱爲"Form"的Cookie(還須要簡單配置一下配置文件),它的值是一個加密後的字符串,後續的請求根據此cookie請求進行驗證。具體作法是在HttpApplication的AuthenticateRequest驗證事件中調用上面的TryParsePrincipal,如:

protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
    HttpContext.Current.User = HttpFormsAuthentication.TryParsePrincipal<UserData>(HttpContext.Current);
}

  這裏若是驗證不經過,HttpContext.Current.User就是null,表示當前用戶未標識。但在這裏還不能作任何關於權限的處理,由於上面說到的,有些頁面是容許匿名訪問的。

3、AuthorizeAttribute

  這是一個Filter,在Action執行前執行,它實現了IActionFilter接口。關於Filter,能夠看我以前的這篇文章,這裏就很少介紹了。咱們定義一個RequestAuthorizeAttribute繼承AuthorizeAttribute,並重寫它的OnAuthorization方法,若是一個Controller或者Action標記了該特性,那麼該方法就會在Action執行前被執行,在這裏判斷是否已經登陸和是否有權限,若是沒有則作出相應處理。具體代碼以下:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequestAuthorizeAttribute : AuthorizeAttribute
{
    //驗證
    public override void OnAuthorization(AuthorizationContext context)
    {
        EnsureHelper.EnsureNotNull(context, "httpContent");           
        //是否容許匿名訪問
        if (context.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), false))
        {
            return;
        }
        //登陸驗證
        Principal principal = context.HttpContext.User as Principal;
        if (principal == null)
        {
            SetUnAuthorizedResult(context);
            HandleUnauthorizedRequest(context);
            return;
        }
        //權限驗證
        if (!principal.IsInRole(base.Roles) || !principal.IsInUser(base.Users))
        {
            SetUnAuthorizedResult(context);
            HandleUnauthorizedRequest(context);
            return;
        }
        //驗證配置文件
        if(!ValidateAuthorizeConfig(principal, context))
        {
            SetUnAuthorizedResult(context);
            HandleUnauthorizedRequest(context);
            return;
        }           
    }
 
    //驗證不經過時
    private void SetUnAuthorizedResult(AuthorizationContext context)
    {
        HttpRequestBase request = context.HttpContext.Request;
        if (request.IsAjaxRequest())
        {
            //處理ajax請求
            string result = JsonConvert.SerializeObject(JsonModel.Error(403));               
            context.Result = new ContentResult() { Content = result };
        }
        else
        {
            //跳轉到登陸頁面
            string loginUrl = FormsAuthentication.LoginUrl + "?ReturnUrl=" + preUrl;
            context.Result = new RedirectResult(loginUrl);
        }
    }
 
  //override
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        if(filterContext.Result != null)
        {
            return;
        }
        base.HandleUnauthorizedRequest(filterContext);
    }
}

  注:這裏的代碼摘自我的項目中的,簡寫了部分代碼,有些是輔助類,代碼沒有貼出,但應該不影響閱讀。

  1. 若是咱們在HttpApplication的AuthenticateRequest事件中得到的IPrincipal爲null,那麼驗證不經過。

  2. 若是驗證經過,程序會進行驗證AuthorizeAttribute的Roles和User屬性。

  3. 若是驗證經過,程序會驗證配置文件中對應的Roles和Users屬性。

  驗證配置文件的方法以下:

    private bool ValidateAuthorizeConfig(Principal principal, AuthorizationContext context)
    {
        //action可能有重載,重載時應該標記ActionName區分
        ActionNameAttribute actionNameAttr = context.ActionDescriptor
            .GetCustomAttributes(typeof(ActionNameAttribute), false)
            .OfType<ActionNameAttribute>().FirstOrDefault();
        string actionName = actionNameAttr == null ? null : actionNameAttr.Name;
        AuthorizationConfig ac = ParseAuthorizeConfig(actionName, context.RouteData);
        if (ac != null)
        {
            if (!principal.IsInRole(ac.Roles))
            {
                return false;
            }
            if (!principal.IsInUser(ac.Users))
            {
                return false;
            }
        }
        return true;
    }
 
    private AuthorizationConfig ParseAuthorizeConfig(string actionName, RouteData routeData)
    {
        string areaName = routeData.DataTokens["area"] as string;
        string controllerName = null;
        object controller, action;
        if(string.IsNullOrEmpty(actionName))
        {
            if(routeData.Values.TryGetValue("action", out action))
            {
                actionName = action.ToString();
            }
        }
        if (routeData.Values.TryGetValue("controller", out controller))
        {
            controllerName = controller.ToString();
        }
        if(!string.IsNullOrEmpty(controllerName) && !string.IsNullOrEmpty(actionName))
        {
            return AuthorizationConfig.ParseAuthorizationConfig(
                areaName, controllerName, actionName);
        }
        return null;
    }
}

  能夠看到,它會根據當前請求的area、controller和action名稱,經過一個AuthorizationConfig類進行驗證,該類的定義以下:

public class AuthorizationConfig
{
    public string Roles { get; set; }
    public string Users { get; set; }
 
    private static XDocument _doc;
 
    //配置文件路徑
    private static string _path = "~/Identity/Authorization.xml";
 
    //首次使用加載配置文件
    static AuthorizationConfig()
    {
        string absPath = HttpContext.Current.Server.MapPath(_path);
        if (File.Exists(absPath))
        {
            _doc = XDocument.Load(absPath);
        }
    }
 
    //解析配置文件,得到包含Roles和Users的信息
    public static AuthorizationConfig ParseAuthorizationConfig(string areaName, string controllerName, string actionName)
    {
        EnsureHelper.EnsureNotNullOrEmpty(controllerName, "controllerName");
        EnsureHelper.EnsureNotNullOrEmpty(actionName, "actionName");
 
        if (_doc == null)
        {
            return null;
        }
        XElement rootElement = _doc.Element("root");
        if (rootElement == null)
        {
            return null;
        }
        AuthorizationConfig info = new AuthorizationConfig();
        XElement rolesElement = null;
        XElement usersElement = null;
        XElement areaElement = rootElement.Elements("area")
            .Where(e => CompareName(e, areaName)).FirstOrDefault();
        XElement targetElement = areaElement ?? rootElement;
        XElement controllerElement = targetElement.Elements("controller")
            .Where(e => CompareName(e, controllerName)).FirstOrDefault();
 
        //若是沒有area節點和controller節點則返回null
        if (areaElement == null && controllerElement == null)
        {
            return null;
        }
        //此時獲取標記的area
        if (controllerElement == null)
        {
            rootElement = areaElement.Element("roles");
            usersElement = areaElement.Element("users");
        }
        else
        {
            XElement actionElement = controllerElement.Elements("action")
                .Where(e => CompareName(e, actionName)).FirstOrDefault();
            if (actionElement != null)
            {
                //此時獲取標記action的
                rolesElement = actionElement.Element("roles");
                usersElement = actionElement.Element("users");
            }
            else
            {
                //此時獲取標記controller的
                rolesElement = controllerElement.Element("roles");
                usersElement = controllerElement.Element("users");
            }
        }
        info.Roles = rolesElement == null ? null : rolesElement.Value;
        info.Users = usersElement == null ? null : usersElement.Value;
        return info;
    }
 
    private static bool CompareName(XElement e, string value)
    {
        XAttribute attribute = e.Attribute("name");
        if (attribute == null || string.IsNullOrEmpty(attribute.Value))
        {
            return false;
        }
        return attribute.Value.Equals(value, StringComparison.OrdinalIgnoreCase);
    }
}

  這裏的代碼比較長,但主要邏輯就是解析文章開頭的配置信息。

  簡單總結一下程序實現的步驟:

  1. 校對用戶名和密碼正確後,調用SetAuthenticationCookie將一些狀態信息寫入cookie。

  2. 在HttpApplication的Authentication事件中,調用TryParsePrincipal得到狀態信息。

  3. 在須要驗證的Action(或Controller)標記 RequestAuthorizeAttribute特性,並設置Roles和Users;Roles和Users也能夠在配置文件中配置。

  4. 在RequestAuthorizeAttribute的OnAuthorization方法中進行驗證和權限邏輯處理。

4、總結

  上面就是整個登陸認證的核心實現過程,只須要簡單配置一下就能夠實現了。但實際項目中從用戶註冊到用戶管理整個過程是比較複雜的,並且涉及到先後端驗證、加解密問題。關於安全問題,FormsAuthentication在加密的時候,會根據服務器的MachineKey等一些信息進行加密,因此相對安全。固然,若是說請求被惡意攔截,而後被僞造登陸仍是有可能的,這是後面要考慮的問題了,例如使用安全的http協議https。

 

本文非原創,原文連接

相關文章
相關標籤/搜索