本文旨在简单介绍mvc的权限验证。
1、首先是建一个asp.net web应用程序
为了实现身份验证,所以必须要添加一个登陆页面,同时还需要有对应的用户类型,所以添加了以下代码
IUserDb接口。其实这个接口可以不用,但是在这里我用的是MEF组件来实现依赖注入。所以有了这个接口
namespace QuanXianYanZheng.Interface{ interface IUserDb { bool ValidateUser(string userName, string password); string[] GetRoles(string userName); Models.User GetByNameAndPassword(string name, string password); }}
UserDb,这个类实际上是模拟数据库操作的。export是MEF组件实现依赖注入的,可以看看前一篇随笔了解。
namespace QuanXianYanZheng.DB{ [Export("UserDb",typeof(IUserDb))] public class UserDb: IUserDb { //模拟数据库,用户表 private static User[] usersForTest = new[]{ new User{ ID = 1, Name = "bob", Password = "bob", Roles = new []{ "employee"}}, new User{ ID = 2, Name = "tom", Password = "tom", Roles = new []{ "manager"}}, new User{ ID = 3, Name = "admin", Password = "admin", Roles = new[]{ "admin"}}, }; ////// 验证用户密码 /// /// /// ///public bool ValidateUser(string userName, string password) { return usersForTest .Any(u => u.Name == userName && u.Password == password); } /// /// 获取用户的角色 /// /// ///public string[] GetRoles(string userName) { return usersForTest .Where(u => u.Name == userName) .Select(u => u.Roles) .FirstOrDefault(); } /// /// 获取用户 /// /// /// ///public User GetByNameAndPassword(string name, string password) { return usersForTest .FirstOrDefault(u => u.Name == name && u.Password == password); } }}
User,用户模型
namespace QuanXianYanZheng.Models{ public class User { public int ID { get; set; } public string Name { get; set; } public string Password { get; set; } public string[] Roles { get; set; } }}
AccountController 登陆页面的控制器
namespace QuanXianYanZheng.Controllers{ public class AccountController : Controller { [Import("UserDb")] private IUserDb Repository { set; get; } public ActionResult LogOn() { return View(); } [HttpPost] public ActionResult LogOn(LogOnModel model, string returnUrl) { if (ModelState.IsValid) { if(null==Repository) CustomTool.Compose(this); if (Repository.ValidateUser(model.UserName, model.Password)) { //将用户信息保存到cookie,如果不能使用cookie则添加到URL FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe); if (!String.IsNullOrEmpty(returnUrl)) return Redirect(returnUrl); else return RedirectToAction("Index", "Home"); } else ModelState.AddModelError("", "用户名或密码不正确!"); } return View(model); } }}
Compose,这个是为了实现依赖注入来实例化对象专门写的一段公共代码,如果不用这种方式来实例化对象,可以不要这段。
namespace QuanXianYanZheng{ public class CustomTool { ////// 这个是为了实现依赖注入的方式来实例化对象而必须要执行的一段代码 /// /// public static void Compose(object o) { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); CompositionContainer container = new CompositionContainer(catalog); container.ComposeParts(o); } }}
LogOnModel类太简单了,就不贴代码了。然后创建登陆页面,就两个文本框一个登陆按钮,也没什么难度了。然后就是Global.asax,在类MvcApplication中添加一个构造函数,代码如下:
[Import("UserDb")] private IUserDb Repository { set; get; } ////// 添加构造函数 /// public MvcApplication() { AuthorizeRequest += new EventHandler(MvcApplication_AuthorizeRequest); } void MvcApplication_AuthorizeRequest(object sender, EventArgs e) { IIdentity id = Context.User.Identity; if(null== Repository) CustomTool.Compose(this);//这个是为了实现依赖注入的方式而调用的 if (id.IsAuthenticated)//这里判断访问者是否成功进行了身份验证 { var roles = Repository.GetRoles(id.Name); Context.User = new GenericPrincipal(id, roles); } }
至此,代码基本写好了,然后运行程序,将断点打在此处:
会发现程序每次进入后台控制器之前,都会经过这里,不管你有没有进行成功的登录,这个if语句的判断条件都是false。这很显然不符合我的要求,我要的是在成功登录后,这个判断是true。之所以出现现在的情况,是因为配置文件还没有相关的配置:
有了这段配置之后,再次运行程序。默认还是进入了home/index页面,这与我的要求是有出入的,我要求在用户没登陆的情况下,默认进入登陆页面。这个问题先放着,后面会解决。这里之所以进入的是home/index页面,是因为路由的配置是这样的,没什么可说的。
运行程序后,在没有登录的情况下,上面的断点一直是false,但如果登录成功后,就会变成true了。有这样的变化,是因为执行了下面这条语句
现在来处理刚才提到的在没有登录的时候默认进入了home/index页面的问题,解决这个问题不需要去修改路由的配置,只需要在HomeController的index方法上添加特性 [Authorize]即可。
再次运行程序之前记得清空浏览器缓存,因为刚才已经成功登陆过了,使得上面断点的地方判断的是true,默认的是已经验证过了,因此还是会进入home/index页面,所以必须清空缓存。这样就可以看到再次默认进入的是登陆页面了。
与Forms Authentication相关的配置
在web.config文件中,<system.web>/<authentication>配置节用于对验证进行配置。为<authentication>节点提供mode="Forms"属性可以启用Forms Authentication。一个典型的<authentication>配置节如下所示:
<authentication mode="Forms">
<forms
name=".ASPXAUTH"
loginUrl="login.aspx"
defaultUrl="default.aspx"
protection="All"
timeout="30"
path="/"
requireSSL="false"
slidingExpiration="false"
enableCrossAppRedirects="false"
cookieless="UseDeviceProfile"
domain=""
/>
</authentication>
以上代码使用的均是默认设置,换言之,如果你的哪项配置属性与上述代码一致,则可以省略该属性例如<forms name="MyAppAuth" />。下面依次介绍一下各种属性:
name——Cookie的名字。Forms Authentication可能会在验证后将用户凭证放在Cookie中,name属性决定了该Cookie的名字。通过FormsAuthentication.FormsCookieName属性可以得到该配置值(稍后介绍FromsAuthentication类)。
loginUrl——登录页的URL。通过FormsAuthentication.LoginUrl属性可以得到该配置值。当调用FormsAuthentication.RedirectToLoginPage()方法时,客户端请求将被重定向到该属性所指定的页面。loginUrl的默认值为“login.aspx”,这表明即便不提供该属性值,ASP.NET也会尝试到站点根目录下寻找名为login.aspx的页面。
defaultUrl——默认页的URL。通过FormsAuthentication.DefaultUrl属性得到该配置值。
protection——Cookie的保护模式,可取值包括All(同时进行加密和数据验证)、Encryption(仅加密)、Validation(仅进行数据验证)和None。为了安全,该属性通常从不设置为None。
timeout——Cookie的过期时间。通过FormsAuthentication.Timeout获取,默认是2天
path——Cookie的路径。可以通过FormsAuthentication.FormsCookiePath属性得到该配置值。 默认情况下得到的是:“/”
requireSSL——在进行Forms Authentication时,与服务器交互是否要求使用SSL。可以通过FormsAuthentication.RequireSSL属性得到该配置值。
slidingExpiration——是否启用“弹性过期时间”,如果该属性设置为false,从首次验证之后过timeout时间后Cookie即过期;如果该属性为true,则从上次请求该开始过timeout时间才过期,这意味着,在首次验证后,如果保证每timeout时间内至少发送一个请求,则Cookie将永远不会过期。通过FormsAuthentication.SlidingExpiration属性可以得到该配置值。
enableCrossAppRedirects——是否可以将已进行了身份验证的用户重定向到其他应用程序中。通过FormsAuthentication.EnableCrossAppRedirects属性可以得到该配置值。为了安全考虑,通常总是将该属性设置为false。
cookieless——定义是否使用Cookie以及Cookie的行为。Forms Authentication可以采用两种方式在会话中保存用户凭据信息,一种是使用Cookie,即将用户凭据记录到Cookie中,每次发送请求时浏览器都会将该Cookie提供给服务器。另一种方式是使用URI,即将用户凭据当作URL中额外的查询字符串传递给服务器。该属性有四种取值——UseCookies(无论何时都使用Cookie)、UseUri(从不使用Cookie,仅使用URI)、AutoDetect(检测设备和浏览器,只有当设备支持Cookie并且在浏览器中启用了Cookie时才使用Cookie)和UseDeviceProfile(只检测设备,只要设备支持Cookie不管浏览器是否支持,都是用Cookie)。通过FormsAuthentication.CookieMode属性可以得到该配置值。通过FormsAuthentication.CookiesSupported属性可以得到对于当前请求是否使用Cookie传递用户凭证。
domain——Cookie的域。通过FormsAuthentication.CookieDomain属性可以得到该配置值。
FormsAuthentication类
FormsAuthentication类用于辅助我们完成窗体验证,并进一步完成用户登录等功能。该类位于system.web.dll程序集的System.Web.Security命名空间中。通常在Web站点项目中可以直接使用这个类,如果是在类库项目中使用这个类,请确保引用了system.web.dll。
RedirectToLoginPage方法用于从任何页面重定向到登录页,该方法有两种重载方式:
public static void RedirectToLoginPage ()
public static void RedirectToLoginPage (string extraQueryString)
两种方式均会使浏览器重定向到登录页(登录页的URL由<forms>节点的loginUrl属性指出)。第二种重载方式还能够提供额外的查询字符串。
RedirectToLoginPage通常在任何非登录页的页面中调用。该方法除了进行重定向之外,还会向URL中附加一个ReturnUrl参数,该参数即为调用该方法时所在的函数的URL地址。这是为了方便登录后能够自动回到登录前所在的页面。
RedirectFromLoginPage方法用于从登录页跳转回登录前页面。这个“登录前”页面即由访问登录页时提供的ReturnUrl参数指定。如果没有提供ReturnUrl参数(例如,不是使用RedirectToLoginPage方法而是用其他手段重定向到或直接
访问登录页时),则该方法会自动跳转到由<forms>节点的defaultUrl属性所指定的默认页。
此外,如果<forms>节点的enableCrossAppRedirects属性被设置为false,ReturnUrl参数所指定的路径必须是当前Web应用程序中的路径,否则(如提供其他站点下的路径)也将返回到默认页。
RedirectFromLoginPage方法有两种重载形式:
public static void RedirectFromLoginPage (string userName, bool createPersistentCookie)
public static void RedirectFromLoginPage (string userName, bool createPersistentCookie, string strCookiePath)
userName参数表示用户的标识(如用户名、用户ID等);createPersistentCookie参数表示是否“记住我”;strCookiePath参数表示Cookie路径。
RedirectFromLoginPage方法除了完成重定向之外,还会将经过加密(是否加密取决于<forms>节点的protection属性)的用户凭据存放到Cookie或Uri中。在后续访问中,只要Cookie没有过期,则将可以通过HttpContext.User.Identity.Name
属性得到这里传入的userName属性。
此外,FormsAuthentication还有一个SignOut方法,用于完成用户注销。其原理是从Cookie或Uri中移除用户凭据。
现在我们再来看看登录控制的代码,这里看到有个returnUrl,这个参数是用于设置登录成功后返回到登录前的页面的。按照这种方式,在从登录前的页面跳转到登录页面的时候,是需要将登陆前的页面URL作为参数传递到登录页面里面保存起来,然后在登录的
时候再传入下面的函数中。这种方式肯定可行,但是比较麻烦。
接下来要通过上面介绍的方式来实现在登录成功后返回登录前页面。
为了简单化,我们在Home/Index页面上添加一个去登录的按钮,跳转到登录页面,然后在登录成功后又返回Home/Index页面。
1、去掉之前在home控制器中添加的[Authorize],然后在页面中添加一个按钮,代码如下:
运行代码,点击登录按钮,进入了登录页面了,可以看到URL中,在后面添加了一个ReturnUrl,但是,请注意这个ReturnUrl,看出问题了吗。问题在于这个ReturnUrl,当我们在登陆成功后,调用RedirectFromLoginPage的时候,会执行Home/GoToLogin。
所以,对RedirectToLoginPage的调用方式需要重新处理,如下:
登录按钮的访问地址多了个参数page。
在跳转到登录页面的函数中,通过Request.IsAuthenticated来判断是否已经登陆过了,然后下面根据page参数决定登录成功后返回的页面,当然代码怎么写根据自己的需要,如果登录之前的页面不是初始状态,这就需要在点击登录按钮的时候,将状态传递进来,然后根据情况选择是否用有参数的RedirectToLoginPage函数,这里要注意的问题是这个ReturnUrl,它不是登录页面之前的页面的URL,而是调用函数RedirectToLoginPage所在的函数的URL。运行程序,点击登录后,可以看到,系统自动的在URL后面添加了一个returnURL,而且这个page=Index也出现在了里面。
点击登录后执行的是下面的代码(代码中的参数returnUrl其实已经没用了,可以去掉,当然如果保留的话,可以通过这个参数获取到RedirectToLoginPage在URL中附加的ReturnUrl参数):
[HttpPost] public ActionResult LogOn(LogOnModel model, string returnUrl) { if (ModelState.IsValid) { if(null==Repository) CustomTool.Compose(this); if (Repository.ValidateUser(model.UserName, model.Password)) { //将用户信息保存到cookie,如果不能使用cookie则添加到URL //FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe); //if (!String.IsNullOrEmpty(returnUrl)) return Redirect(returnUrl); //else return RedirectToAction("Index", "Home"); FormsAuthentication.RedirectFromLoginPage(model.UserName, model.RememberMe); } else ModelState.AddModelError("", "用户名或密码不正确!"); } return View(model); }
现在,假如我们希望“关于”页面必须在登录后才能访问,那么在上面的代码写好的前提下,再做如下处理即可:
当登录成功后,程序会再次执行这个about函数,然后因为Request.IsAuthenticated=true,而跳过这个if语句,直接打开about页面。除了这种方式,还可以这样:
假如我希望角色为admin和manager的用户可以访问联系方式这个页面,那么按照如下方式处理:
这样一来,就只有角色为admin和manager的用户可以访问联系方式了。
特性Authorize不仅可以放在action上面,也可以放在control上面,这样控制的是整个控制器里面所有的函数。
注意:
当特性里面用users来控制的时候,记住,这个users不是下图中的name的值(但是用角色的时候,却是对应的角色的值)
而是下图中红框里面的值。假如下面的代码里面在model.UserName后面加上一个字符串“123”,那么在特性里面的users的值也必须是“123”结尾的字符串,如果有多个,那用逗号隔开。
现在有这样一个问题,我已经登录成功了,但是我没有权限去访问联系方式页面。按照上面的代码,当我没权限的时候,我去访问联系方式页面,那么系统会跳转到登陆页面去,这显然不对的,我希望的是,在我登录后,没有权限的情况下,访问联系方式页面,
这时给我一个提示,然后停留在原来的页面;如果我没登陆,那么就跳转到登陆页面。实现这样的功能,需要自定义权限特性。
代码如下:
MyAuthorize是我自定义的一个权限验证特性。
namespace QuanXianYanZheng.Attributes{ public class MyAuthorizeAttribute: AuthorizeAttribute { /* 下面3个函数执行的顺序是OnAuthorization--->AuthorizeCore--->HandleUnauthorizedRequest 在父类的OnAuthorization方法中会自动调用AuthorizeCore函数,因此如果重写OnAuthorization 函数的时候,不执行父类的OnAuthorization,那么将不会执行AuthorizeCore和HandleUnauthorizedRequest。 当AuthorizeCore返回false时,才会执行HandleUnauthorizedRequest */ //public override void OnAuthorization(AuthorizationContext filterContext) //{ // base.OnAuthorization(filterContext); //} ////// 这个方法主要是实现授权验证逻辑的地方 /// /// ///protected override bool AuthorizeCore(HttpContextBase httpContext) { bool result = false; if (httpContext == null) { throw new ArgumentNullException("httpContext"); } string[] roles = Roles.Split(','); if (!httpContext.User.Identity.IsAuthenticated)//判断用户是否成功登录 return false; foreach(string r in roles) { if(httpContext.User.IsInRole(r)) { result = true; break; } } if (!result) { httpContext.Response.StatusCode = 403;//表示无权限访问 } return result; } /// /// 这个函数处理在无权限的情况下的逻辑 /// /// protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { base.HandleUnauthorizedRequest(filterContext); if (filterContext == null) { throw new ArgumentNullException("filterContext"); } else if (filterContext.HttpContext.Response.StatusCode == 403) { filterContext.HttpContext.Response.Write(""); filterContext.HttpContext.Response.End(); //下面这一句的作用是让程序在无权访问的时候,不要再继续去执行被MyAuthorize修饰的函数。 //比如我们在访问联系方式页面的时候,如果没有权限,就不要继续去执行Home/Contact了。 filterContext.Result = new EmptyResult(); } } }}
注意这个流程,当AuthorizeCore返回false且httpContext.Response.StatusCode=403的时候,表示无权限,那么在HandleUnauthorizedRequest中就会给出提示。
当AuthorizeCore返回false且httpContext.Response.StatusCode!=403的时候,表示用户未登录,那么在HandleUnauthorizedRequest中将没有执行代码,系统会自动跳转到配置文件中authentication里面配置的loginUrl页面。
当AuthorizeCore返回true,表示有权限了,程序不会执行HandleUnauthorizedRequest。会直接跳转到目标页面。