一、概述
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票据,当票据没有过期或者被删除的情况下,就不需要再次认证: