详解ASP.NET MVC Form表单验证
一、前言
关于表单验证,已经有不少的文章,相信Web开发人员也都基本写过,最近在一个个人项目中刚好用到,在这里与大家分享一下。本来想从用户注册开始写起,但发现东西比较多,涉及到界面、前端验证、前端加密、后台解密、用户密码Hash、权限验证等等,文章写起来可能会很长,所以这里主要介绍的是登录验证和权限控制部分,有兴趣的朋友欢迎一起交流。
一般验证方式有Windows验证和表单验证,web项目用得更多的是表单验证。原理很简单,简单地说就是利用浏览器的cookie,将验证令牌存储在客户端浏览器上,cookie每次会随请求发送到服务器,服务器验证这个令牌。通常一个系统的用户会分为多种角色:匿名用户、普通用户和管理员;这里面又可以再细分,例如用户可以是普通用户或Vip用户,管理员可以是普通管理员或超级管理员等。在项目中,我们有的页面可能只允许管理员查看,有的只允许登录用户查看,这就是角色区分(Roles);某些特别情况下,有些页面可能只允许叫“张三”名字的人查看,这就是用户区分(Users)。
我们先看一下最后要实现的效果:
1.这是在Action级别的控制。
publicclassHome1Controller:Controller
{
//匿名访问
publicActionResultIndex()
{
returnView();
}
//登录用户访问
[RequestAuthorize]
publicActionResultIndex2()
{
returnView();
}
//登录用户,张三才能访问
[RequestAuthorize(Users="张三")]
publicActionResultIndex3()
{
returnView();
}
//管理员访问
[RequestAuthorize(Roles="Admin")]
publicActionResultIndex4()
{
returnView();
}
}
2.这是在Controller级别的控制。当然,如果某个Action需要匿名访问,也是允许的,因为控制级别上,Action优先级大于Controller。
//Controller级别的权限控制
[RequestAuthorize(User="张三")]
publicclassHome2Controller:Controller
{
//登录用户访问
publicActionResultIndex()
{
returnView();
}
//允许匿名访问
[AllowAnonymous]
publicActionResultIndex2()
{
returnView();
}
}
3.Area级别的控制。有时候我们会把一些模块做成分区,当然这里也可以在Area的Controller和Action进行标记。
从上面可以看到,我们需要在各个地方进行标记权限,如果把Roles和Users硬写在程序中,不是很好的做法。我希望能更简单一点,在配置文件进行说明。例如如下配置:
<?xmlversion="1.0"encoding="utf-8"?> <!-- 1.这里可以把权限控制转移到配置文件,这样就不用在程序中写roles和users了 2.如果程序也写了,那么将覆盖配置文件的。 3.action级别的优先级>controller级别>Area级别 --> <root> <!--area级别--> <areaname="Admin"> <roles>Admin</roles> </area> <!--controller级别--> <controllername="Home2"> <user>张三</user> </controller> <!--action级别--> <controllername="Home1"> <actionname="Inde3"> <users>张三</users> </action> <actionname="Index4"> <roles>Admin</roles> </action> </controller> </root>
写在配置文件里,是为了方便管理,如果程序里也写了,将覆盖配置文件的。ok,下面进入正题。
二、主要接口
先看两个主要用到的接口。
IPrincipal定义了用户对象的基本功能,接口定义如下:
publicinterfaceIPrincipal
{
//标识对象
IIdentityIdentity{get;}
//判断当前角色是否属于指定的角色
boolIsInRole(stringrole);
}
它有两个主要成员,IsInRole用于判断当前对象是否属于指定角色的,IIdentity定义了标识对象信息。HttpContext的User属性就是IPrincipal类型的。
IIdentity定义了标识对象的基本功能,接口定义如下:
publicinterfaceIIdentity
{
//身份验证类型
stringAuthenticationType{get;}
//是否验证通过
boolIsAuthenticated{get;}
//用户名
stringName{get;}
}
IIdentity包含了一些用户信息,但有时候我们需要存储更多信息,例如用户ID、用户角色等,这些信息会被序列到cookie中加密保存,验证通过时可以解码再反序列化获得,状态得以保存。例如定义一个UserData。
publicclassUserData:IUserData
{
publiclongUserID{get;set;}
publicstringUserName{get;set;}
publicstringUserRole{get;set;}
publicboolIsInRole(stringrole)
{
if(string.IsNullOrEmpty(role))
{
returntrue;
}
returnrole.Split(',').Any(item=>item.Equals(this.UserRole,StringComparison.OrdinalIgnoreCase));
}
publicboolIsInUser(stringuser)
{
if(string.IsNullOrEmpty(user))
{
returntrue;
}
returnuser.Split(',').Any(item=>item.Equals(this.UserName,StringComparison.OrdinalIgnoreCase));
}
}
UserData实现了IUserData接口,该接口定义了两个方法:IsInRole和IsInUser,分别用于判断当前用户角色和用户名是否符合要求。该接口定义如下:
publicinterfaceIUserData
{
boolIsInRole(stringrole);
boolIsInUser(stringuser);
}
接下来定义一个Principal实现IPrincipal接口,如下:
publicclassPrincipal:IPrincipal
{
publicIIdentityIdentity{get;privateset;}
publicIUserDataUserData{get;set;}
publicPrincipal(FormsAuthenticationTicketticket,IUserDatauserData)
{
EnsureHelper.EnsureNotNull(ticket,"ticket");
EnsureHelper.EnsureNotNull(userData,"userData");
this.Identity=newFormsIdentity(ticket);
this.UserData=userData;
}
publicboolIsInRole(stringrole)
{
returnthis.UserData.IsInRole(role);
}
publicboolIsInUser(stringuser)
{
returnthis.UserData.IsInUser(user);
}
}
Principal包含IUserData,而不是具体的UserData,这样很容易更换一个UserData而不影响其它代码。Principal的IsInRole和IsInUser间接调用了IUserData的同名方法。
三、写入cookie和读取cookie
接下来,需要做的就是用户登录成功后,创建UserData,序列化,再利用FormsAuthentication加密,写到cookie中;而请求到来时,需要尝试将cookie解密并反序列化。如下:
publicclassHttpFormsAuthentication
{
publicstaticvoidSetAuthenticationCookie(stringuserName,IUserDatauserData,doublerememberDays=0)
{
EnsureHelper.EnsureNotNullOrEmpty(userName,"userName");
EnsureHelper.EnsureNotNull(userData,"userData");
EnsureHelper.EnsureRange(rememberDays,"rememberDays",0);
//保存在cookie中的信息
stringuserJson=JsonConvert.SerializeObject(userData);
//创建用户票据
doubletickekDays=rememberDays==0?7:rememberDays;
varticket=newFormsAuthenticationTicket(2,userName,
DateTime.Now,DateTime.Now.AddDays(tickekDays),false,userJson);
//FormsAuthentication提供webforms身份验证服务
//加密
stringencryptValue=FormsAuthentication.Encrypt(ticket);
//创建cookie
HttpCookiecookie=newHttpCookie(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);
}
publicstaticPrincipalTryParsePrincipal<TUserData>(HttpContextcontext)
whereTUserData:IUserData
{
EnsureHelper.EnsureNotNull(context,"context");
HttpRequestrequest=context.Request;
HttpCookiecookie=request.Cookies[FormsAuthentication.FormsCookieName];
if(cookie==null||string.IsNullOrEmpty(cookie.Value))
{
returnnull;
}
//解密cookie值
FormsAuthenticationTicketticket=FormsAuthentication.Decrypt(cookie.Value);
if(ticket==null||string.IsNullOrEmpty(ticket.UserData))
{
returnnull;
}
IUserDatauserData=JsonConvert.DeserializeObject<TUserData>(ticket.UserData);
returnnewPrincipal(ticket,userData);
}
}
在登录时,我们可以类似这样处理:
publicActionResultLogin(stringuserName,stringpassword)
{
//验证用户名和密码等一些逻辑...
UserDatauserData=newUserData()
{
UserName=userName,
UserID=userID,
UserRole="Admin"
};
HttpFormsAuthentication.SetAuthenticationCookie(userName,userData,7);
//验证通过...
}
登录成功后,就会把信息写入cookie,可以通过浏览器观察请求,就会有一个名称为"Form"的Cookie(还需要简单配置一下配置文件),它的值是一个加密后的字符串,后续的请求根据此cookie请求进行验证。具体做法是在HttpApplication的AuthenticateRequest验证事件中调用上面的TryParsePrincipal,如:
protectedvoidApplication_AuthenticateRequest(objectsender,EventArgse)
{
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)]
publicclassRequestAuthorizeAttribute:AuthorizeAttribute
{
//验证
publicoverridevoidOnAuthorization(AuthorizationContextcontext)
{
EnsureHelper.EnsureNotNull(context,"httpContent");
//是否允许匿名访问
if(context.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute),false))
{
return;
}
//登录验证
Principalprincipal=context.HttpContext.UserasPrincipal;
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;
}
}
//验证不通过时
privatevoidSetUnAuthorizedResult(AuthorizationContextcontext)
{
HttpRequestBaserequest=context.HttpContext.Request;
if(request.IsAjaxRequest())
{
//处理ajax请求
stringresult=JsonConvert.SerializeObject(JsonModel.Error(403));
context.Result=newContentResult(){Content=result};
}
else
{
//跳转到登录页面
stringloginUrl=FormsAuthentication.LoginUrl+"?ReturnUrl="+preUrl;
context.Result=newRedirectResult(loginUrl);
}
}
//override
protectedoverridevoidHandleUnauthorizedRequest(AuthorizationContextfilterContext)
{
if(filterContext.Result!=null)
{
return;
}
base.HandleUnauthorizedRequest(filterContext);
}
}
注:这里的代码摘自个人项目中的,简写了部分代码,有些是辅助类,代码没有贴出,但应该不影响阅读。
1.如果我们在HttpApplication的AuthenticateRequest事件中获得的IPrincipal为null,那么验证不通过。
2.如果验证通过,程序会进行验证AuthorizeAttribute的Roles和User属性。
3.如果验证通过,程序会验证配置文件中对应的Roles和Users属性。
验证配置文件的方法如下:
privateboolValidateAuthorizeConfig(Principalprincipal,AuthorizationContextcontext)
{
//action可能有重载,重载时应该标记ActionName区分
ActionNameAttributeactionNameAttr=context.ActionDescriptor
.GetCustomAttributes(typeof(ActionNameAttribute),false)
.OfType<ActionNameAttribute>().FirstOrDefault();
stringactionName=actionNameAttr==null?null:actionNameAttr.Name;
AuthorizationConfigac=ParseAuthorizeConfig(actionName,context.RouteData);
if(ac!=null)
{
if(!principal.IsInRole(ac.Roles))
{
returnfalse;
}
if(!principal.IsInUser(ac.Users))
{
returnfalse;
}
}
returntrue;
}
privateAuthorizationConfigParseAuthorizeConfig(stringactionName,RouteDatarouteData)
{
stringareaName=routeData.DataTokens["area"]asstring;
stringcontrollerName=null;
objectcontroller,action;
if(string.IsNullOrEmpty(actionName))
{
if(routeData.Values.TryGetValue("action",outaction))
{
actionName=action.ToString();
}
}
if(routeData.Values.TryGetValue("controller",outcontroller))
{
controllerName=controller.ToString();
}
if(!string.IsNullOrEmpty(controllerName)&&!string.IsNullOrEmpty(actionName))
{
returnAuthorizationConfig.ParseAuthorizationConfig(
areaName,controllerName,actionName);
}
returnnull;
}
}
可以看到,它会根据当前请求的area、controller和action名称,通过一个AuthorizationConfig类进行验证,该类的定义如下:
publicclassAuthorizationConfig
{
publicstringRoles{get;set;}
publicstringUsers{get;set;}
privatestaticXDocument_doc;
//配置文件路径
privatestaticstring_path="~/Identity/Authorization.xml";
//首次使用加载配置文件
staticAuthorizationConfig()
{
stringabsPath=HttpContext.Current.Server.MapPath(_path);
if(File.Exists(absPath))
{
_doc=XDocument.Load(absPath);
}
}
//解析配置文件,获得包含Roles和Users的信息
publicstaticAuthorizationConfigParseAuthorizationConfig(stringareaName,stringcontrollerName,stringactionName)
{
EnsureHelper.EnsureNotNullOrEmpty(controllerName,"controllerName");
EnsureHelper.EnsureNotNullOrEmpty(actionName,"actionName");
if(_doc==null)
{
returnnull;
}
XElementrootElement=_doc.Element("root");
if(rootElement==null)
{
returnnull;
}
AuthorizationConfiginfo=newAuthorizationConfig();
XElementrolesElement=null;
XElementusersElement=null;
XElementareaElement=rootElement.Elements("area")
.Where(e=>CompareName(e,areaName)).FirstOrDefault();
XElementtargetElement=areaElement??rootElement;
XElementcontrollerElement=targetElement.Elements("controller")
.Where(e=>CompareName(e,controllerName)).FirstOrDefault();
//如果没有area节点和controller节点则返回null
if(areaElement==null&&controllerElement==null)
{
returnnull;
}
//此时获取标记的area
if(controllerElement==null)
{
rootElement=areaElement.Element("roles");
usersElement=areaElement.Element("users");
}
else
{
XElementactionElement=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;
returninfo;
}
privatestaticboolCompareName(XElemente,stringvalue)
{
XAttributeattribute=e.Attribute("name");
if(attribute==null||string.IsNullOrEmpty(attribute.Value))
{
returnfalse;
}
returnattribute.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。
以上就是本文的全部内容,希望对大家的学习有所帮助。