详解ASP.NET MVC Form表单验证
一、前言
关于表单验证,已经有不少的文章,相信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。
以上就是本文的全部内容,希望对大家的学习有所帮助。