• 详解ASP.NET MVC中的身份验证
  • 林烨 发表于 2016/3/28 12:37:00 | 分类标签: 身份验证 表单验证 MVC验证
  •  一、前言

      关于表单验证,园子里已经有不少的文章,相信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 { getset; }
        public string UserName { getset; }
        public string UserRole { getset; }
     
        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))
            {