一、前言

  关于表单验证,园子里已经有不少的文章,相信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。

ASP.NET MVC Form验证的更多相关文章

  1. ASP.NET MVC 5 - 验证编辑方法(Edit method)和编辑视图(Edit view)

    在本节中,您将验证电影控制器生成的编辑方法(Edit action methods)和视图.但是首先将修改点代码,使得发布日期属性(ReleaseDate)看上去更好.打开Models \ Movie ...

  2. Asp.Net MVC 身份验证-Forms

    Asp.Net MVC 身份验证-Forms 在MVC中对于需要登录才可以访问的页面,只需要在对应的Controller或Action上添加特性[Authorize]就可以限制非登录用户访问该页面.那 ...

  3. ASP.NET MVC异步验证是如何工作的03,jquery.validate.unobtrusive.js是如何工作的

    在上一篇"ASP.NET MVC异步验证是如何工作的02,异步验证表单元素的创建"中了解了ASP.NET异步验证是如何创建表单元素的,本篇体验jquery.validate.uno ...

  4. ASP.NET MVC异步验证是如何工作的02,异步验证表单元素的创建

    在上一篇"ASP.NET MVC异步验证是如何工作的01,jQuery的验证方式.错误信息提示.validate方法的背后"中,了解了jQuery如何验证,如何显示错误信息,本篇要 ...

  5. ASP.NET MVC异步验证是如何工作的01,jQuery的验证方式、错误信息提示、validate方法的背后

    ASP.NET MVC借助jQuery的验证机制,提供了一套从客户端到服务端异步验证的解决方案,通常情况下,用起来相当方便.但面对一些相对特殊的情况,可能会遇到验证失效的场景,比如在使用ajax动态异 ...

  6. [转]ASP.NET MVC 5 - 验证编辑方法(Edit method)和编辑视图(Edit view)

    在本节中,您将验证电影控制器生成的编辑方法(Edit action methods)和视图.但是首先将修改点代码,使得发布日期属性(ReleaseDate)看上去更好.打开Models \ Movie ...

  7. ASP.NET MVC Model验证(五)

    ASP.NET MVC Model验证(五) 前言 上篇主要讲解ModelValidatorProvider 和ModelValidator两种类型的自定义实现, 然而在MVC框架中还给我们提供了其它 ...

  8. ASP.NET MVC Model验证(四)

    ASP.NET MVC Model验证(四) 前言 本篇主要讲解ModelValidatorProvider 和ModelValidator两种类型的自定义实现,前者是Model验证提供程序,而Mod ...

  9. ASP.NET MVC Model验证(三)

    ASP.NET MVC Model验证(三) 前言 上篇中说到在MVC框架中默认的Model验证是在哪里验证的,还讲到DefaultModelBinder类型的内部执行的示意图,让大家可以看到默认的M ...

随机推荐

  1. 外边距塌陷之clearance

    在一个BFC中,垂直方向上相邻的块级盒子产生外边距塌陷,本文要说一个特殊的外边距塌陷情况,即当垂直方向上,两个块级盒子之间有个浮动元素相隔时,这个时候会产生什么样的效果呢? .outer{ overf ...

  2. ASP.NET Core 静态文件及JS包管理器(npm, Bower)的使用

    在 ASP.NET Core 中添加静态文件 虽然ASP.NET主要大都做着后端的事情,但前端的一些静态文件也是很重要的.在ASP.NET Core中要启用静态文件,需要Microsoft.AspNe ...

  3. java设计模式之简单工厂模式

    简单工厂: 简单工厂的优点: 1.去除客户端与具体产品的耦合,在客户端与具体的产品中增加一个工厂类,增加客户端与工厂类的耦合 2.封装工厂类,实现代码平台的复用性,创建对象的过程被封装成工厂类,可以多 ...

  4. Sql常用语句(3)

    --显示sql server现有的所有数据库 exec sp_helpdb --查看数据表设置的约束 exec sp_helpconstraint SubjectType --update selec ...

  5. QML 从无到有 3 (自动更新)

    新的需求出来啦,需要自动更新功能,不怕程序升级了. 自动更新,QML不好写,需要c++来辅助,这里就涉及QML中调用c++功能(这里就不写了,百度一下,很多). 思路:获取版本>下载程序> ...

  6. spring源码:核心组件(li)

    一.AOP实现 Spring代理对象的产生:代理的目的是调用目标方法时我们可以转而执行InvocationHandler类的invoke方法,所以如何在InvocationHandler上做文章就是S ...

  7. 同比 VS 环比

    同比(YoY=year on year):与历史同时期比较,例如2014年7月份与2013年7月份相比,叫同比 环比(MoM=month on month):是本期统计数据与上期比较,例如2014年7 ...

  8. Console.In.ReadToEnd() 控制台 输入完毕

    输入完数据后 按回车(另起一行) ctrl+z enter .......百度了半天 没百度到..最后还是google 强大..解决了问题 ..

  9. php的http_build_query使用

    http_build_query生成 url-encoded 之后的请求字符串 1.使用键值对,关联数组: <?php $data = array('foo'=>'bar', 'baz'= ...

  10. Position属性四个值:static、fixed、relative、absolute的区别和用法

    1.static(静态定位):默认值.没有定位,元素出现在正常的文档流中(如果设置 top, bottom, left, right, z-index这些属性就不起做作了). 2.relative(相 ...