欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

详解ASP.NET MVC Form表单验证

程序员文章站 2023-12-17 13:25:28
一、前言   关于表单验证,已经有不少的文章,相信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。

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

上一篇:

下一篇: