一、前言

  关于表单验证,园子里已经有不少的文章,相信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. java代码的初始化过程研究

        刚刚在ITeye上看到一篇关于java代码初始化的文章,看到代码我试着推理了下结果,虽然是大学时代学的知识了,没想到还能做对.(看来自己大学时掌握的基础还算不错,(*^__^*) 嘻嘻……)但 ...

  2. jQuery中怎样阻止后绑定事件

    你的代码在页面载入过程中已经完成事件绑定了,没有阻止后绑定的事件的办法了,不过可以删除当前指定节点的事件绑定.方法如下:$("#btn").click(function(){if( ...

  3. Python VS PHP 基础语法

    这几天在学习Python,鄙人平时学习中为了方便记忆和更好的比较与理解语言二者之间在某些情况的优劣性,所以花了点时间,整理了一下 Python 和 PHP 常用语法的一些区别. 一.大小写 PHP: ...

  4. spider RPC框架的需求来源与特性介绍(一)

    spider RPC 特性介绍 spider RPC 性能测试 spider RPC 入门指南 spider RPC 配置文件参考 spider RPC 开发指南 spider RPC 安全性 spi ...

  5. atitit.http原理与概论attilax总结

    atitit.http原理与概论attilax总结 1. 图解HTTP 作者:[日]上野宣 著1 2. HTTP权威指南(国内首本HTTP及其相关核心Web技术权威著作)1 3. TCP/IP详解(中 ...

  6. 关于docker

    摘要: 最近很多阿里内部的同学和客户私信来咨询如何学习 Docker 技术.为此,我们列了一个路线图供大家学习Docker和阿里云容器服务.这个列表包含了一些社区的优秀资料和我们的原创文章.我们会随着 ...

  7. 【原】低版本MyEclipse整合高版本Tomcat

    [使用工具] 1.MyEclipse_6.0.1GA_E3.3.1_FullStackInstaller 2.Tomcat 7.0 [问题描述] 直接在MyEclipse中整合,因为这个版本的MyEc ...

  8. Canvas——使用定时器模拟动态加载动画!

    <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8&quo ...

  9. HTML5 Canvas绘制转盘抽奖

    新项目:完整的Canvas转盘抽奖代码 https://github.com/givebest/GB-canvas-turntable 演示 http://blog.givebest.cn/GB-ca ...

  10. js类型转换

    1.js中有六种基本类型,分别是object.number.string.Boolean.null.undefined,其中number.string.Boolean为基本类型,有时使用会强制转换成对 ...