详解ASP.NET MVC Form表单验证
来源: 阅读:784 次 日期:2016-08-10 15:41:48
温馨提示: 小编为您整理了“详解ASP.NET MVC Form表单验证”,方便广大网友查阅!

这篇文章主要为大家详细介绍了ASP.NET MVC Form表单验证,一般验证方式有Windows验证和表单验证,web项目用得更多的是表单验证,感兴趣的小伙伴们可以参考一下

一、前言

关于表单验证,已经有不少的文章,相信Web开发人员也都基本写过,最近在一个个人项目中刚好用到,在这里与大家分享一下。本来想从用户注册开始写起,但发现东西比较多,涉及到界面、前端验证、前端加密、后台解密、用户密码Hash、权限验证等等,文章写起来可能会很长,所以这里主要介绍的是登录验证和权限控制部分,有兴趣的朋友欢迎一起交流。

一般验证方式有Windows验证和表单验证,web项目用得更多的是表单验证。原理很简单,简单地说就是利用浏览器的cookie,将验证令牌存储在客户端浏览器上,cookie每次会随请求发送到服务器,服务器验证这个令牌。通常一个系统的用户会分为多种角色:匿名用户、普通用户和管理员;这里面又可以再细分,例如用户可以是普通用户或Vip用户,管理员可以是普通管理员或超级管理员等。在项目中,我们有的页面可能只允许管理员查看,有的只允许登录用户查看,这就是角色区分(Roles);某些特别情况下,有些页面可能只允许叫“张三”名字的人查看,这就是用户区分(Users)。

我们先看一下最后要实现的效果:

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,下面进入正题。

二、主要接口

先看两个主要用到的接口。

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的同名方法。

三、写入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,表示当前用户未标识。但在这里还不能做任何关于权限的处理,因为上面说到的,有些页面是允许匿名访问的。

三、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方法中进行验证和权限逻辑处理。

四、总结

上面就是整个登录认证的核心实现过程,只需要简单配置一下就可以实现了。但实际项目中从用户注册到用户管理整个过程是比较复杂的,而且涉及到前后端验证、加解密问题。关于安全问题,FormsAuthentication在加密的时候,会根据服务器的MachineKey等一些信息进行加密,所以相对安全。当然,如果说请求被恶意拦截,然后被伪造登录还是有可能的,这是后面要考虑的问题了,例如使用安全的http协议https。

以上就是本文的全部内容,希望对大家的学习有所帮助。

更多信息请查看网络编程
由于各方面情况的不断调整与变化, 提供的所有考试信息和咨询回复仅供参考,敬请考生以权威部门公布的正式信息和咨询为准!
关于我们 | 联系我们 | 人才招聘 | 网站声明 | 网站帮助 | 非正式的简要咨询 | 简要咨询须知 | 加入群交流 | 手机站点 | 投诉建议
工业和信息化部备案号:滇ICP备2023014141号-1 云南省教育厅备案号:云教ICP备0901021 滇公网安备53010202001879号 人力资源服务许可证:(云)人服证字(2023)第0102001523号
云南网警备案专用图标
联系电话:0871-65317125(9:00—18:00) 获取招聘考试信息及咨询关注公众号:hfpxwx
咨询QQ:526150442(9:00—18:00)版权所有:
云南网警报警专用图标
Baidu
map