ASP.NET MVC Form验证

一、前言

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//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硬写在程序中,不是很好的做法。我希望能更简单一点,在配置文件进行说明。例如如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<? 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 定义了用户对象的基本功能,接口定义如下:

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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,分别用于判断当前用户角色和用户名是否符合要求。该接口定义如下:

1
2
3
4
5
public interface IUserData
{
     bool IsInRole( string role);
     bool IsInUser( string user);
}

  接下来定义一个Principal实现IPrincipal接口,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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解密并反序列化。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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);
     }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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,如:

1
2
3
4
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执行前被执行,在这里判 断是否已经登录和是否有权限,如果没有则做出相应处理。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
[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属性。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
     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类进行验证,该类的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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。

转载于:https://www.cnblogs.com/hs8888/p/5520595.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值