一、前言

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

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

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

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

    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。

    //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硬写在程序中,不是很好的做法。我希望能更简单一点,在配置文件进行说明。例如如下配置:

<?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 定义了用户对象的基本功能,接口定义如下:

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

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

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

public interface IIdentity
{
//身份验证类型
string AuthenticationType { get; }
//是否验证通过
bool IsAuthenticated { get; }
//用户名
string Name { get; }
}

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

    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,分别用于判断当前用户角色和用户名是否符合要求。该接口定义如下:

    public interface IUserData
{
bool IsInRole(string role);
bool IsInUser(string user);
}

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

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解密并反序列化。如下:

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);
}
}

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

        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,如:

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

[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属性。

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

        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类进行验证,该类的定义如下:

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。

ASP.NET MVC Form验证的更多相关文章

  1. ASP.NET MVC 5 - 验证编辑方法(Edit method)和编辑视图(Edit view)

    在本节中,您将验证电影控制器生成的编辑方法(Edit action methods)和视图.但是首先将修改点代码,使得发布日期属性(ReleaseDate)看上去更好.打开Models \ Movie ...

  2. Asp.Net MVC 身份验证-Forms

    Asp.Net MVC 身份验证-Forms 在MVC中对于需要登录才可以访问的页面,只需要在对应的Controller或Action上添加特性[Authorize]就可以限制非登录用户访问该页面.那 ...

  3. ASP.NET MVC异步验证是如何工作的03,jquery.validate.unobtrusive.js是如何工作的

    在上一篇"ASP.NET MVC异步验证是如何工作的02,异步验证表单元素的创建"中了解了ASP.NET异步验证是如何创建表单元素的,本篇体验jquery.validate.uno ...

  4. ASP.NET MVC异步验证是如何工作的02,异步验证表单元素的创建

    在上一篇"ASP.NET MVC异步验证是如何工作的01,jQuery的验证方式.错误信息提示.validate方法的背后"中,了解了jQuery如何验证,如何显示错误信息,本篇要 ...

  5. ASP.NET MVC异步验证是如何工作的01,jQuery的验证方式、错误信息提示、validate方法的背后

    ASP.NET MVC借助jQuery的验证机制,提供了一套从客户端到服务端异步验证的解决方案,通常情况下,用起来相当方便.但面对一些相对特殊的情况,可能会遇到验证失效的场景,比如在使用ajax动态异 ...

  6. [转]ASP.NET MVC 5 - 验证编辑方法(Edit method)和编辑视图(Edit view)

    在本节中,您将验证电影控制器生成的编辑方法(Edit action methods)和视图.但是首先将修改点代码,使得发布日期属性(ReleaseDate)看上去更好.打开Models \ Movie ...

  7. ASP.NET MVC Model验证(五)

    ASP.NET MVC Model验证(五) 前言 上篇主要讲解ModelValidatorProvider 和ModelValidator两种类型的自定义实现, 然而在MVC框架中还给我们提供了其它 ...

  8. ASP.NET MVC Model验证(四)

    ASP.NET MVC Model验证(四) 前言 本篇主要讲解ModelValidatorProvider 和ModelValidator两种类型的自定义实现,前者是Model验证提供程序,而Mod ...

  9. ASP.NET MVC Model验证(三)

    ASP.NET MVC Model验证(三) 前言 上篇中说到在MVC框架中默认的Model验证是在哪里验证的,还讲到DefaultModelBinder类型的内部执行的示意图,让大家可以看到默认的M ...

随机推荐

  1. CSS学习笔记——视觉格式化模型 visual formatting model

    CSS 视觉格式化模型(visual formatting model)是用来处理文档并将它显示在视觉媒体上的机制.他有一套既定的规则(也就是W3C规范),规定了浏览器该怎么处理每一个盒子.以下内容翻 ...

  2. Visual studio 通用开发环境配置:SDL,FFMPEG为例

    引言 每一个C++库的使用都是从开发环境的配置开始的,其实每个库的配置过程都是大同小异,总结下来有下面几个步骤: 下载库文件,这里假定是已经预先编译完成的. 配置库文件的包含目录(include)和库 ...

  3. 自己封装了一个EF的上下文类.,分享一下,顺便求大神指点

    using System; using System.Collections.Generic; using System.Configuration; using System.Data; using ...

  4. ASP.NET Core 中文文档 第二章 指南(3)用 Visual Studio 发布一个 Azure 云 Web 应用程序

    原文:Getting Started 作者:Rick Anderson 翻译:谢炀(Kiler) 校对:孟帅洋(书缘).刘怡(AlexLEWIS).何镇汐 设置开发环境 安装最新版本的 Azure S ...

  5. golang 使用 iota

    iota是golang语言的常量计数器,只能在常量的表达式中使用. iota在const关键字出现时将被重置为0(const内部的第一行之前),const中每新增一行常量声明将使iota计数一次(io ...

  6. Asp.NET MVC 使用 SignalR 实现推送功能二(Hubs 在线聊天室 获取保存用户信息)

    简单介绍 关于SignalR的简单实用 请参考 Asp.NET MVC 使用 SignalR 实现推送功能一(Hubs 在线聊天室) 在上一篇中,我们只是介绍了简单的消息推送,今天我们来修改一下,实现 ...

  7. Delphi_03_Delphi_Object_Pascal_基本语法_01

    这次是一个基本语法的第一部分,包括变量.变量初始化.常量.运算符.字符串等内容. { 本程序演示 Delphi Pascal 的基本语法 1.变量及变量的初始化 2.常量 3.运算符 3. 4. } ...

  8. 记录一次bug解决过程:eclipse集成lombok插件

    一 总结 eclipse集成插件lombok: 启动Spring Boot项目: sublime全局搜索关键字:ctrl + shift + F JDK8中的lambda表达式使用 二 BUG描述:集 ...

  9. 来玩Play框架02 响应

    作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 我上一章总结了Play框架的基本使用.这一章里,我将修改和增加响应. HTTP协议 ...

  10. 《连载 | 物联网框架ServerSuperIO教程》- 6.并发通讯模式开发及注意事项

    1.C#跨平台物联网通讯框架ServerSuperIO(SSIO)介绍 <连载 | 物联网框架ServerSuperIO教程>1.4种通讯模式机制. <连载 | 物联网框架Serve ...