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
posted @ 2009-03-15 09:18  俩醒叁醉  阅读(882)  评论(0编辑  收藏  举报