• Form表单认证


    一、概述

    1、理解Http的无状态特性

    HTTP是一个无状态的协议,WEB服务器在处理所有传入HTTP请求时,根本就不知道某个请求是否是一个用户的第一次请求与后续请求,或者是另一个用户的请求。 WEB服务器每次在处理请求时,都会按照用户所访问的资源所对应的处理代码,从头到尾执行一遍,然后输出响应内容,WEB服务器根本不会记住已处理了哪些用户的请求,因此,我们通常说HTTP协议是无状态的。

    2、为什么需要认证

    虽然HTTP协议与WEB服务器是无状态,但我们的业务需求却要求有状态,典型的就是用户登录, 在这种业务需求中,要求WEB服务器端能区分某个请求是不是一个已登录用户发起的,或者当前请求是哪个用户发出的。 在开发WEB应用程序时,我们通常会使用Cookie来保存一些简单的数据供服务端维持必要的状态。总的来说,加入认证的根本原因就是确保请求的合法性以及资源的安全性,如下图:

    二、Form表单认证

    登录的操作通常会检查用户提供的用户名和密码,因此登录状态也必须具有足够高的安全性。 在Forms身份认证中,由于登录状态是保存在Cookie中,而Cookie又会保存到客户端,因此,为了保证登录状态不被恶意用户伪造, ASP.NET采用了加密的方式保存登录状态。 为了实现安全性,ASP.NET采用【Forms身份验证凭据】(即FormsAuthenticationTicket对象)来表示一个Forms登录用户, 加密与解密由FormsAuthentication的Encrypt与Decrypt的方法来实现。

    下面通过一张图详细的了解Form表单认证的过程:

    三、Form表单认证的示例

    1、创建mvc项目

    2、mvc项目结构

    3、Action加入认证

     HomeController中默认提供了几个action,我们加入[Authorize]标识,如下:

    using System.Web.Mvc;
    
    namespace FormAuthentication.Controllers
    {
        public class HomeController : Controller
        {
            [Authorize]
            public ActionResult Index()
            {
                return View();
            }
            [Authorize]
            public ActionResult About()
            {
                ViewBag.Message = "Your application description page.";
    
                return View();
            }
            [Authorize]
            public ActionResult Contact()
            {
                ViewBag.Message = "Your contact page.";
    
                return View();
            }
        }
    }

    因为路由默认启动Home/Index,所以启动下项目,看下效果:

    提示没有授权请求home/index。Authorize做了什么,为什么加入[Authorize],action就无权访问了?我们先来分析下Authorize的定义:

    namespace System.Web.Mvc
    {
        //
        // 摘要:
        //     指定对控制器或操作方法的访问只限于满足授权要求的用户。
        [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
        public class AuthorizeAttribute : FilterAttribute, IAuthorizationFilter
        {
            //
            // 摘要:
            //     初始化 System.Web.Mvc.AuthorizeAttribute 类的新实例。
            public AuthorizeAttribute();
    
            //
            // 摘要:
            //     获取或设置有权访问控制器或操作方法的用户角色。
            //
            // 返回结果:
            //     有权访问控制器或操作方法的用户角色。
            public string Roles { get; set; }
            //
            // 摘要:
            //     获取此特性的唯一标识符。
            //
            // 返回结果:
            //     此特性的唯一标识符。
            public override object TypeId { get; }
            //
            // 摘要:
            //     获取或设置有权访问控制器或操作方法的用户。
            //
            // 返回结果:
            //     有权访问控制器或操作方法的用户。
            public string Users { get; set; }
    
            //
            // 摘要:
            //     在过程请求授权时调用。
            //
            // 参数:
            //   filterContext:
            //     筛选器上下文,它封装有关使用 System.Web.Mvc.AuthorizeAttribute 的信息。
            //
            // 异常:
            //   T:System.ArgumentNullException:
            //     filterContext 参数为 null。
            public virtual void OnAuthorization(AuthorizationContext filterContext);
            //
            // 摘要:
            //     重写时,提供一个入口点用于进行自定义授权检查。
            //
            // 参数:
            //   httpContext:
            //     HTTP 上下文,它封装有关单个 HTTP 请求的所有 HTTP 特定的信息。
            //
            // 返回结果:
            //     如果用户已经过授权,则为 true;否则为 false。
            //
            // 异常:
            //   T:System.ArgumentNullException:
            //     httpContext 参数为 null。
            protected virtual bool AuthorizeCore(HttpContextBase httpContext);
            //
            // 摘要:
            //     处理未能授权的 HTTP 请求。
            //
            // 参数:
            //   filterContext:
            //     封装有关使用 System.Web.Mvc.AuthorizeAttribute 的信息。filterContext 对象包括控制器、HTTP 上下文、请求上下文、操作结果和路由数据。
            protected virtual void HandleUnauthorizedRequest(AuthorizationContext filterContext);
            //
            // 摘要:
            //     在缓存模块请求授权时调用。
            //
            // 参数:
            //   httpContext:
            //     HTTP 上下文,它封装有关单个 HTTP 请求的所有 HTTP 特定的信息。
            //
            // 返回结果:
            //     对验证状态的引用。
            //
            // 异常:
            //   T:System.ArgumentNullException:
            //     httpContext 参数为 null。
            protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext);
        }
    }

    可以看出Authorize是应用于类或者方法的特性,AuthorizeAttribute实现了IAuthorizationFilter接口和FilterAttribute抽象类,接口中的OnAuthorization(AuthorizationContext filterContext)方法是最终验证授权的逻辑(其中AuthorizationContext是继承了ControllerContext类),AuthorizeCore方法是最终OnAuthorization()方法调用的最终逻辑。

    • bool AuthorizeCore(HttpContextBase httpContext):授权验证的逻辑处理,返回true则是通过授权,返回false则不是。若验证不通过时,OnAuthorization方法内部会调用HandleUnauthorizedRequest
    • void HandleUnauthorizedRequest(AuthorizationContext filterContext):这个方法是处理授权失败的事情。

    我们看下AuthorizeCore核心代码如下:

        protected virtual bool AuthorizeCore(HttpContextBase httpContext)
            {
                if (httpContext == null)
                {
                    throw new ArgumentNullException("httpContext");
                }
     
                IPrincipal user = httpContext.User;
                if (!user.Identity.IsAuthenticated)
                {
                    return false;
                }
     
                if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase))
                {
                    return false;
                }
     
                if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole))
                {
                    return false;
                }
     
                return true;
            }

    AuthorizeAttribute提供了四个虚方法,我们可以不使用默认的认证逻辑,可以根据自己的项目情况进行重写。想了解更多的过滤器特性可以看另一篇文章:MVC过滤器特性

    现在了解了Authorize的原理,那么我们希望认证失败可以弹出登录(认证)页面,而不是401页面。下面先创建登录相关的页面。

    4、新建LoginController

    新建LoginController截图如下:

    LoginController中代码如下:

    • 检查用户提交的登录名和密码是否正确。

    • 根据登录名创建一个FormsAuthenticationTicket对象。

    • 调用FormsAuthentication.Encrypt()加密。

    • 根据加密结果创建登录Cookie,并写入Response。在登录验证结束后,一般会产生重定向操作, 那么后面的每次请求将带上前面产生的加密Cookie,供服务器来验证每次请求的登录状态。

    using System;
    using System.Web;
    using System.Web.Mvc;
    using System.Web.Security;
    
    namespace FormAuthentication.Controllers
    {
        public class LoginController : Controller
        {
            // GET: Login
            public ActionResult Index()
            {
                string returnUrl = Request["ReturnUrl"];
                if (Request.HttpMethod=="POST")
                {
                    string userId = Request["userid"];
                    string password = Request["password"];
                    if (userId=="admin"&&password=="123")
                    {
                        var ticket = new FormsAuthenticationTicket (
                            1,//version
                            userId,//name
                            DateTime.Now,//issueDate
                            DateTime.Now.AddMinutes(5),//expiration
                            true,//isPersistent 持久性保存在cookie中
                            "role1,role2,role3,role4",//userData 用户数据
                            "/"//cookiePath
                            );
                        var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket));
                        cookie.HttpOnly = true;
                        HttpContext.Response.Cookies.Add(cookie);
                        return Redirect(returnUrl);
                    }
                }
                return View();
            }
        }
    }

    5、新建LoginController/Index视图

    新建LoginController/Index视图,截图如下:

    代码如下:

    @{
        ViewBag.Title = "Index";
    }
    
    <h2>Index</h2>
    <form method="post">
        <input type="text" name="userid" />
        <input type="password" name="password" />
        <input type="submit" value="认证" />
    </form>

    6、重定向登录页面 

    上边通过4/5步完成了登录相关的页面,那么请求home/index如何重定向到登录页面呢?这一步可以通过配置文件进行处理,在<system.web>节点中加入如下信息:

    <authentication mode="Forms">
        <forms loginUrl="~/Login/Index" timeout="2880"/>
    </authentication>

    我们在执行下项目,看下效果:

    可以看到,请求home/index的时候,认证失败,会重定向login/index页面,我们输入admin和123,点击认证:

    认证成功后,生成一个加密的ticket放到cookie中,并且重定向原来的地址home/index:

    可以看到认证成功了,刷新页面,再次请求home/index,请求头中会携带cookie中的ticket票据,当票据没有过期或者被删除的情况下,就不需要再次认证:

    7、重写Authorize过滤器

    在步骤3中介绍了Authorize的原理,现在准备重写下Authorize中的虚方法,新建一个类MyAuthorizeAttribute.cs

    using System.Web;
    using System.Web.Mvc;
    using System.Web.Security;
    
    namespace FormAuthentication
    {
        public class MyAuthorizeAttribute: AuthorizeAttribute
        {
            protected override bool AuthorizeCore(HttpContextBase httpContext)
            {
                var cookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];
                if (cookie == null) return false;
                var ticket = FormsAuthentication.Decrypt(cookie.Value);
                var roles = ticket.UserData;
                var inRoles = false;
                foreach (var role in roles?.Split(','))
                {
                    if (Roles.Contains(role))
                    {
                        inRoles = true;
                        break;
                    }
                }
                return inRoles;
            }
        }
    }

    在home控制器中的action采用重写的MyAuthorizeAttribute进行认证:

    using System.Web.Mvc;
    
    namespace FormAuthentication.Controllers
    {
        public class HomeController : Controller
        {
            /// <summary>
            /// 角色为role1的用户可以请求
            /// </summary>
            /// <returns></returns>
            [MyAuthorize(Roles = "role1")]
            public ActionResult Index()
            {
                return View();
            }
            /// <summary>
            /// 角色为role4的用户可以请求
            /// </summary>
            /// <returns></returns>
            [MyAuthorize(Roles = "role4")]
            public ActionResult About()
            {
                ViewBag.Message = "Your application description page.";
    
                return View();
            }
            [Authorize]
            public ActionResult Contact()
            {
                ViewBag.Message = "Your contact page.";
    
                return View();
            }
        }
    }

    看下效果:

    8、不进行重定向,返回自定义错误信息

    上边介绍了使用Authorize和MyAuthorize方式进行认证,认证失败都会重定向登录页面进行授权,之所以会重定向是因为我们在配置文件中添加了如下代码:

    <authentication mode="Forms">
            <forms loginUrl="~/Login/Index" timeout="2880"/>
        </authentication>

    但是有时候我们不需要进行重定向登录页面,而是直接返回自定义的错误信息,怎么处理呢?方法很简单,我们在自定义验证特性MyAuthorize的时候,除了重写AuthorizeCore方法外,还要重写下HandleUnauthorizedRequest方法,当AuthorizeCore返回false,验证不通过时,OnAuthorization方法内部会调用HandleUnauthorizedRequest方法,而不受配置文件的影响。如下:

    using System.Web;
    using System.Web.Mvc;
    using System.Web.Security;
    
    namespace FormAuthentication
    {
        public class MyAuthorizeAttribute: AuthorizeAttribute
        {
            protected override bool AuthorizeCore(HttpContextBase httpContext)
            {
                var cookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];
                if (cookie == null) return false;
                var ticket = FormsAuthentication.Decrypt(cookie.Value);
                var roles = ticket.UserData;
                var inRoles = false;
                foreach (var role in roles?.Split(','))
                {
                    if (Roles.Contains(role))
                    {
                        inRoles = true;
                        break;
                    }
                }
                return inRoles;
            }
            protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
            {
                ActionResult result = new ContentResult
                {
                    Content = "没有页面访问权限!",
                    ContentType = filterContext.HttpContext.Response.ContentType
                };
                filterContext.Result = result ?? new HttpUnauthorizedResult();
            }
        }
    }

    效果如下:

    四、源码下载

    源码:https://github.com/qiuxianhu/AuthenticationAndAuthorization

  • 相关阅读:
    Failed to convert from type [java.lang.String] to type [java.util.Date] for value '2020-02-06'; nested exception is java.lang.IllegalArgumentException]解决
    idea常用快捷键
    java中list集合怎么判断是否为空
    jsp页面中怎么利用a标签的href进行传递参数以及需要注意的地方
    jsp页面重定向后地址栏controller名重复而导致报404错误
    面试前都需要做些什么准备?
    spring抽象父类注入
    java打包jar反编译
    activiti--安装
    分布式事务解决方案
  • 原文地址:https://www.cnblogs.com/qtiger/p/14866727.html
Copyright © 2020-2023  润新知