当前位置:Gxlcms > asp.net > 详解ASP.NET MVC Form表单验证

详解ASP.NET MVC Form表单验证

时间:2021-07-01 10:21:17 帮助过:10人阅读

一、前言

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

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

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

1.这是在Action级别的控制。

  1. public class Home1Controller : Controller
  2. {
  3. //匿名访问
  4. public ActionResult Index()
  5. {
  6. return View();
  7. }
  8. //登录用户访问
  9. [RequestAuthorize]
  10. public ActionResult Index2()
  11. {
  12. return View();
  13. }
  14. //登录用户,张三才能访问
  15. [RequestAuthorize(Users="张三")]
  16. public ActionResult Index3()
  17. {
  18. return View();
  19. }
  20. //管理员访问
  21. [RequestAuthorize(Roles="Admin")]
  22. public ActionResult Index4()
  23. {
  24. return View();
  25. }
  26. }

2.这是在Controller级别的控制。当然,如果某个Action需要匿名访问,也是允许的,因为控制级别上,Action优先级大于Controller。

  1. //Controller级别的权限控制
  2. [RequestAuthorize(User="张三")]
  3. public class Home2Controller : Controller
  4. {
  5. //登录用户访问
  6. public ActionResult Index()
  7. {
  8. return View();
  9. }
  10. //允许匿名访问
  11. [AllowAnonymous]
  12. public ActionResult Index2()
  13. {
  14. return View();
  15. }
  16. }

3.Area级别的控制。有时候我们会把一些模块做成分区,当然这里也可以在Area的Controller和Action进行标记。

  从上面可以看到,我们需要在各个地方进行标记权限,如果把Roles和Users硬写在程序中,不是很好的做法。我希望能更简单一点,在配置文件进行说明。例如如下配置:

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <!--
  3. 1.这里可以把权限控制转移到配置文件,这样就不用在程序中写roles和users了
  4. 2.如果程序也写了,那么将覆盖配置文件的。
  5. 3.action级别的优先级 > controller级别 > Area级别
  6. -->
  7. <root>
  8. <!--area级别-->
  9. <area name="Admin">
  10. <roles>Admin</roles>
  11. </area>
  12. <!--controller级别-->
  13. <controller name="Home2">
  14. <user>张三</user>
  15. </controller>
  16. <!--action级别-->
  17. <controller name="Home1">
  18. <action name="Inde3">
  19. <users>张三</users>
  20. </action>
  21. <action name="Index4">
  22. <roles>Admin</roles>
  23. </action>
  24. </controller>
  25. </root>

写在配置文件里,是为了方便管理,如果程序里也写了,将覆盖配置文件的。ok,下面进入正题。

二、主要接口

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

IPrincipal 定义了用户对象的基本功能,接口定义如下:

  1. public interface IPrincipal
  2. {
  3. //标识对象
  4. IIdentity Identity { get; }
  5. //判断当前角色是否属于指定的角色
  6. bool IsInRole(string role);
  7. }

它有两个主要成员,IsInRole用于判断当前对象是否属于指定角色的,IIdentity定义了标识对象信息。HttpContext的User属性就是IPrincipal类型的。

IIdentity 定义了标识对象的基本功能,接口定义如下:

  1. public interface IIdentity
  2. {
  3. //身份验证类型
  4. string AuthenticationType { get; }
  5. //是否验证通过
  6. bool IsAuthenticated { get; }
  7. //用户名
  8. string Name { get; }
  9. }

IIdentity包含了一些用户信息,但有时候我们需要存储更多信息,例如用户ID、用户角色等,这些信息会被序列到cookie中加密保存,验证通过时可以解码再反序列化获得,状态得以保存。例如定义一个UserData。

  1. public class UserData : IUserData
  2. {
  3. public long UserID { get; set; }
  4. public string UserName { get; set; }
  5. public string UserRole { get; set; }
  6. public bool IsInRole(string role)
  7. {
  8. if (string.IsNullOrEmpty(role))
  9. {
  10. return true;
  11. }
  12. return role.Split(',').Any(item => item.Equals(this.UserRole, StringComparison.OrdinalIgnoreCase));
  13. }
  14. public bool IsInUser(string user)
  15. {
  16. if (string.IsNullOrEmpty(user))
  17. {
  18. return true;
  19. }
  20. return user.Split(',').Any(item => item.Equals(this.UserName, StringComparison.OrdinalIgnoreCase));
  21. }
  22. }

  UserData实现了IUserData接口,该接口定义了两个方法:IsInRole和IsInUser,分别用于判断当前用户角色和用户名是否符合要求。该接口定义如下:

  1. public interface IUserData
  2. {
  3. bool IsInRole(string role);
  4. bool IsInUser(string user);
  5. }
  6.   接下来定义一个Principal实现IPrincipal接口,如下:
  7. public class Principal : IPrincipal
  8. {
  9. public IIdentity Identity{get;private set;}
  10. public IUserData UserData{get;set;}
  11. public Principal(FormsAuthenticationTicket ticket, IUserData userData)
  12. {
  13. EnsureHelper.EnsureNotNull(ticket, "ticket");
  14. EnsureHelper.EnsureNotNull(userData, "userData");
  15. this.Identity = new FormsIdentity(ticket);
  16. this.UserData = userData;
  17. }
  18. public bool IsInRole(string role)
  19. {
  20. return this.UserData.IsInRole(role);
  21. }
  22. public bool IsInUser(string user)
  23. {
  24. return this.UserData.IsInUser(user);
  25. }
  26. }

  Principal包含IUserData,而不是具体的UserData,这样很容易更换一个UserData而不影响其它代码。Principal的IsInRole和IsInUser间接调用了IUserData的同名方法。

三、写入cookie和读取cookie

  接下来,需要做的就是用户登录成功后,创建UserData,序列化,再利用FormsAuthentication加密,写到cookie中;而请求到来时,需要尝试将cookie解密并反序列化。如下:

  1. public class HttpFormsAuthentication
  2. {
  3. public static void SetAuthenticationCookie(string userName, IUserData userData, double rememberDays = 0)
  4. {
  5. EnsureHelper.EnsureNotNullOrEmpty(userName, "userName");
  6. EnsureHelper.EnsureNotNull(userData, "userData");
  7. EnsureHelper.EnsureRange(rememberDays, "rememberDays", 0);
  8. //保存在cookie中的信息
  9. string userJson = JsonConvert.SerializeObject(userData);
  10. //创建用户票据
  11. double tickekDays = rememberDays == 0 ? 7 : rememberDays;
  12. var ticket = new FormsAuthenticationTicket(2, userName,
  13. DateTime.Now, DateTime.Now.AddDays(tickekDays), false, userJson);
  14. //FormsAuthentication提供web forms身份验证服务
  15. //加密
  16. string encryptValue = FormsAuthentication.Encrypt(ticket);
  17. //创建cookie
  18. HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptValue);
  19. cookie.HttpOnly = true;
  20. cookie.Domain = FormsAuthentication.CookieDomain;
  21. if (rememberDays > 0)
  22. {
  23. cookie.Expires = DateTime.Now.AddDays(rememberDays);
  24. }
  25. HttpContext.Current.Response.Cookies.Remove(cookie.Name);
  26. HttpContext.Current.Response.Cookies.Add(cookie);
  27. }
  28. public static Principal TryParsePrincipal<TUserData>(HttpContext context)
  29. where TUserData : IUserData
  30. {
  31. EnsureHelper.EnsureNotNull(context, "context");
  32. HttpRequest request = context.Request;
  33. HttpCookie cookie = request.Cookies[FormsAuthentication.FormsCookieName];
  34. if(cookie == null || string.IsNullOrEmpty(cookie.Value))
  35. {
  36. return null;
  37. }
  38. //解密cookie值
  39. FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
  40. if(ticket == null || string.IsNullOrEmpty(ticket.UserData))
  41. {
  42. return null;
  43. }
  44. IUserData userData = JsonConvert.DeserializeObject<TUserData>(ticket.UserData);
  45. return new Principal(ticket, userData);
  46. }
  47. }

  在登录时,我们可以类似这样处理:

  1. public ActionResult Login(string userName,string password)
  2. {
  3. //验证用户名和密码等一些逻辑...
  4. UserData userData = new UserData()
  5. {
  6. UserName = userName,
  7. UserID = userID,
  8. UserRole = "Admin"
  9. };
  10. HttpFormsAuthentication.SetAuthenticationCookie(userName, userData, 7);
  11. //验证通过...
  12. }

  登录成功后,就会把信息写入cookie,可以通过浏览器观察请求,就会有一个名称为"Form"的Cookie(还需要简单配置一下配置文件),它的值是一个加密后的字符串,后续的请求根据此cookie请求进行验证。具体做法是在HttpApplication的AuthenticateRequest验证事件中调用上面的TryParsePrincipal,如:

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

  这里如果验证不通过,HttpContext.Current.User就是null,表示当前用户未标识。但在这里还不能做任何关于权限的处理,因为上面说到的,有些页面是允许匿名访问的。

三、AuthorizeAttribute

  这是一个Filter,在Action执行前执行,它实现了IActionFilter接口。关于Filter,可以看我之前的这篇文章,这里就不多介绍了。我们定义一个RequestAuthorizeAttribute继承AuthorizeAttribute,并重写它的OnAuthorization方法,如果一个Controller或者Action标记了该特性,那么该方法就会在Action执行前被执行,在这里判断是否已经登录和是否有权限,如果没有则做出相应处理。具体代码如下:

  1. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
  2. public class RequestAuthorizeAttribute : AuthorizeAttribute
  3. {
  4. //验证
  5. public override void OnAuthorization(AuthorizationContext context)
  6. {
  7. EnsureHelper.EnsureNotNull(context, "httpContent");
  8. //是否允许匿名访问
  9. if (context.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), false))
  10. {
  11. return;
  12. }
  13. //登录验证
  14. Principal principal = context.HttpContext.User as Principal;
  15. if (principal == null)
  16. {
  17. SetUnAuthorizedResult(context);
  18. HandleUnauthorizedRequest(context);
  19. return;
  20. }
  21. //权限验证
  22. if (!principal.IsInRole(base.Roles) || !principal.IsInUser(base.Users))
  23. {
  24. SetUnAuthorizedResult(context);
  25. HandleUnauthorizedRequest(context);
  26. return;
  27. }
  28. //验证配置文件
  29. if(!ValidateAuthorizeConfig(principal, context))
  30. {
  31. SetUnAuthorizedResult(context);
  32. HandleUnauthorizedRequest(context);
  33. return;
  34. }
  35. }
  36. //验证不通过时
  37. private void SetUnAuthorizedResult(AuthorizationContext context)
  38. {
  39. HttpRequestBase request = context.HttpContext.Request;
  40. if (request.IsAjaxRequest())
  41. {
  42. //处理ajax请求
  43. string result = JsonConvert.SerializeObject(JsonModel.Error(403));
  44. context.Result = new ContentResult() { Content = result };
  45. }
  46. else
  47. {
  48. //跳转到登录页面
  49. string loginUrl = FormsAuthentication.LoginUrl + "?ReturnUrl=" + preUrl;
  50. context.Result = new RedirectResult(loginUrl);
  51. }
  52. }
  53.   //override
  54. protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
  55. {
  56. if(filterContext.Result != null)
  57. {
  58. return;
  59. }
  60. base.HandleUnauthorizedRequest(filterContext);
  61. }
  62. }

  注:这里的代码摘自个人项目中的,简写了部分代码,有些是辅助类,代码没有贴出,但应该不影响阅读。

  1. 如果我们在HttpApplication的AuthenticateRequest事件中获得的IPrincipal为null,那么验证不通过。

  2. 如果验证通过,程序会进行验证AuthorizeAttribute的Roles和User属性。

  3. 如果验证通过,程序会验证配置文件中对应的Roles和Users属性。

  验证配置文件的方法如下:

  1. private bool ValidateAuthorizeConfig(Principal principal, AuthorizationContext context)
  2. {
  3. //action可能有重载,重载时应该标记ActionName区分
  4. ActionNameAttribute actionNameAttr = context.ActionDescriptor
  5. .GetCustomAttributes(typeof(ActionNameAttribute), false)
  6. .OfType<ActionNameAttribute>().FirstOrDefault();
  7. string actionName = actionNameAttr == null ? null : actionNameAttr.Name;
  8. AuthorizationConfig ac = ParseAuthorizeConfig(actionName, context.RouteData);
  9. if (ac != null)
  10. {
  11. if (!principal.IsInRole(ac.Roles))
  12. {
  13. return false;
  14. }
  15. if (!principal.IsInUser(ac.Users))
  16. {
  17. return false;
  18. }
  19. }
  20. return true;
  21. }
  22. private AuthorizationConfig ParseAuthorizeConfig(string actionName, RouteData routeData)
  23. {
  24. string areaName = routeData.DataTokens["area"] as string;
  25. string controllerName = null;
  26. object controller, action;
  27. if(string.IsNullOrEmpty(actionName))
  28. {
  29. if(routeData.Values.TryGetValue("action", out action))
  30. {
  31. actionName = action.ToString();
  32. }
  33. }
  34. if (routeData.Values.TryGetValue("controller", out controller))
  35. {
  36. controllerName = controller.ToString();
  37. }
  38. if(!string.IsNullOrEmpty(controllerName) && !string.IsNullOrEmpty(actionName))
  39. {
  40. return AuthorizationConfig.ParseAuthorizationConfig(
  41. areaName, controllerName, actionName);
  42. }
  43. return null;
  44. }
  45. }

  可以看到,它会根据当前请求的area、controller和action名称,通过一个AuthorizationConfig类进行验证,该类的定义如下:

  1. public class AuthorizationConfig
  2. {
  3. public string Roles { get; set; }
  4. public string Users { get; set; }
  5. private static XDocument _doc;
  6. //配置文件路径
  7. private static string _path = "~/Identity/Authorization.xml";
  8. //首次使用加载配置文件
  9. static AuthorizationConfig()
  10. {
  11. string absPath = HttpContext.Current.Server.MapPath(_path);
  12. if (File.Exists(absPath))
  13. {
  14. _doc = XDocument.Load(absPath);
  15. }
  16. }
  17. //解析配置文件,获得包含Roles和Users的信息
  18. public static AuthorizationConfig ParseAuthorizationConfig(string areaName, string controllerName, string actionName)
  19. {
  20. EnsureHelper.EnsureNotNullOrEmpty(controllerName, "controllerName");
  21. EnsureHelper.EnsureNotNullOrEmpty(actionName, "actionName");
  22. if (_doc == null)
  23. {
  24. return null;
  25. }
  26. XElement rootElement = _doc.Element("root");
  27. if (rootElement == null)
  28. {
  29. return null;
  30. }
  31. AuthorizationConfig info = new AuthorizationConfig();
  32. XElement rolesElement = null;
  33. XElement usersElement = null;
  34. XElement areaElement = rootElement.Elements("area")
  35. .Where(e => CompareName(e, areaName)).FirstOrDefault();
  36. XElement targetElement = areaElement ?? rootElement;
  37. XElement controllerElement = targetElement.Elements("controller")
  38. .Where(e => CompareName(e, controllerName)).FirstOrDefault();
  39. //如果没有area节点和controller节点则返回null
  40. if (areaElement == null && controllerElement == null)
  41. {
  42. return null;
  43. }
  44. //此时获取标记的area
  45. if (controllerElement == null)
  46. {
  47. rootElement = areaElement.Element("roles");
  48. usersElement = areaElement.Element("users");
  49. }
  50. else
  51. {
  52. XElement actionElement = controllerElement.Elements("action")
  53. .Where(e => CompareName(e, actionName)).FirstOrDefault();
  54. if (actionElement != null)
  55. {
  56. //此时获取标记action的
  57. rolesElement = actionElement.Element("roles");
  58. usersElement = actionElement.Element("users");
  59. }
  60. else
  61. {
  62. //此时获取标记controller的
  63. rolesElement = controllerElement.Element("roles");
  64. usersElement = controllerElement.Element("users");
  65. }
  66. }
  67. info.Roles = rolesElement == null ? null : rolesElement.Value;
  68. info.Users = usersElement == null ? null : usersElement.Value;
  69. return info;
  70. }
  71. private static bool CompareName(XElement e, string value)
  72. {
  73. XAttribute attribute = e.Attribute("name");
  74. if (attribute == null || string.IsNullOrEmpty(attribute.Value))
  75. {
  76. return false;
  77. }
  78. return attribute.Value.Equals(value, StringComparison.OrdinalIgnoreCase);
  79. }
  80. }

这里的代码比较长,但主要逻辑就是解析文章开头的配置信息。

简单总结一下程序实现的步骤:

  1. 校对用户名和密码正确后,调用SetAuthenticationCookie将一些状态信息写入cookie。

  2. 在HttpApplication的Authentication事件中,调用TryParsePrincipal获得状态信息。

  3. 在需要验证的Action(或Controller)标记 RequestAuthorizeAttribute特性,并设置Roles和Users;Roles和Users也可以在配置文件中配置。

  4. 在RequestAuthorizeAttribute的OnAuthorization方法中进行验证和权限逻辑处理。

四、总结

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

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

人气教程排行