MVC Controllers和Forms验证
想想下,一组从Forms验证抽象出来的登入/登出Controllers行为特有的ActionResult实现。。
我已经使用ASP.NETMVC重新实现了我的个人网站(projects.nikhilk.net)的一部分。其中之一就是登入/登出功能的实现。当你创建一个新的ASP.NETMVC应用程序,默认的就会包含一个简单的AccountController。这个Controller中包含了登入/登出,并实现了IFormsAuthentication接口。其默认实现基于System.Web.Security.FormsAuthentication APIs(SetAuthCookie and SignOut)。可以仿照给这个接口的实现以便用于United测试。
尽管如此,当我在自己的Controller中实现登入/登出时,我发现一个奇怪现象,行为(actions)直接的访问FormsAuthentication(甚至是通过接口)。我以为行为应该简单地完成用户验证,而后返回一个处理生成响应的匹配的ActionResult,这该实例中就是设置或清除cookie,并重定向Url。为此,我创建了FormsLoginResult和FormsLogoutResult。很明显的他们是成对的出现的,but I thought I'd go ahead and share anyway。
首先,我将显示AccountController中修改过的Login和Logout方法。
[AcceptVerbs(HttpVerbs.Post)] public ActionResult Login(string username, string password, bool rememberMe) { ... if (ViewData.ModelState.IsValid) { // Attempt to login bool loginSuccessful = Provider.ValidateUser(username, password); if (loginSuccessful) { return new FormsLoginResult(username, rememberMe); } else { ... } } // If we got this far, something failed, redisplay form ... return View(); } public ActionResult Logout() { return new FormsLogoutResult(); }
现在Login和Logout行为不再和authentication是否通过cookie跟踪以及需要重定位到哪里(默认主页、参数关联的url等)相关联。
其次、建立这两个函数的测试单元。我使用Moq实现测试需要的MembershipProvider的模拟,当是不再需要IFormsAuthentication的实现。
[TestClass] public class AccountControllerTest { [TestMethod] public void LoginSuccessful() { string testUserName = "TestUser"; string testPassword = "TestPassword"; Mock<MembershipProvider> mockMembership = new Mock<MembershipProvider>(); mockMembership.Expect<bool>(m => m.ValidateUser(testUserName, testPassword)) .Returns(true).AtMostOnce().Verifiable(); AccountController controller = new AccountController(mockMembership.Object); ActionResult result = controller.Login(testUserName, testPassword, false); Assert.IsInstanceOfType(result, typeof(FormsLoginResult)); FormsLoginResult loginResult = (FormsLoginResult)result; Assert.AreEqual<string>(loginResult.UserName, testUserName); Assert.AreEqual<bool>(loginResult.PersistentCookie, false); } [TestMethod] public void LoginFailure() { string testUserName = "TestUser"; string testPassword = "TestPassword"; Mock<MembershipProvider> mockMembership = new Mock<MembershipProvider>(); mockMembership.Expect<bool>(m => m.ValidateUser(testUserName, testPassword)) .Returns(true).AtMostOnce().Verifiable(); AccountController controller = new AccountController(mockMembership.Object); ActionResult result = controller.Login(testUserName, "badPassword", false); Assert.IsInstanceOfType(result, typeof(ViewResult)); } }
下面是FormsLoginResult 和FormsLogoutResult的实现。这里使用了MVC提供的构建自定义action results可扩展性较好的实现。
public class FormsLoginResult : ActionResult { private string _userName; private string _userData; private bool _persistentCookie; public FormsLoginResult(string userName) : this(userName, /* persistentCookie */ false) { } public FormsLoginResult(string userName, bool persistentCookie) { if (String.IsNullOrEmpty(userName)) { throw new ArgumentNullException("userName"); } _userName = userName; _persistentCookie = persistentCookie; } public bool PersistentCookie { get { return _persistentCookie; } } public string UserData { get { return _userData; } set { _userData = value; } } public string UserName { get { return _userName; } } public override void ExecuteResult(ControllerContext context) { HttpResponseBase response = context.HttpContext.Response; if (String.IsNullOrEmpty(_userData)) { FormsAuthentication.SetAuthCookie(_userName, _persistentCookie); } else { FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, _userName, DateTime.Now, DateTime.Now.AddMinutes(30), _persistentCookie, _userData, FormsAuthentication.FormsCookiePath); string encryptedTicket = FormsAuthentication.Encrypt(ticket); HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket); cookie.HttpOnly = true; cookie.Secure = FormsAuthentication.RequireSSL; cookie.Path = FormsAuthentication.FormsCookiePath; if (FormsAuthentication.CookieDomain != null) { cookie.Domain = FormsAuthentication.CookieDomain; } response.Cookies.Add(cookie); } response.Redirect(FormsAuthentication.GetRedirectUrl(_userName, _persistentCookie)); } }
令人庆幸的是,FormsLoginResult同样的在FormsAuth cookie内封装了一些自定义用户数据,这些数据可以通过HttpApplication的Authenticate事件获取,并可以使用FormsAuthentication.Decrypt将AuthCookie内数据转换为一个FormsAuthenticationTicket实例,以及从Ticket中获取的用户数据。我在我的网站中使用这个跟踪一些元数据以便于在Requests后重新创建代理实例。
第二个action result FormsLogoutResult非常简单。
public class FormsLogoutResult : ActionResult { private string _url; public FormsLogoutResult() : this(FormsAuthentication.DefaultUrl) { } public FormsLogoutResult(string url) { if (String.IsNullOrEmpty(url)) { throw new ArgumentNullException("url"); } _url = url; } public string Url { get { return _url; } } public override void ExecuteResult(ControllerContext context) { FormsAuthentication.SignOut(); context.HttpContext.Response.Redirect(_url); } } 源:http://www.nikhilk.net/Default.aspx