一、概述
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