Need recommendation for global, targeted redirects

2019-08-05 07:33发布

问题:

I'm working on an ASP.NET MVC application where administrators can add new users and flag them to complete additional information before they can use other features of the site. For example, we have a "ForcePasswordReset" bool, and a requirement to complete Security Questions. We're not using Active Directory for these users.

Ultimately this is the behavior I'd like to implement:

  1. Direct any logged in user who is required to change password to the ChangePassword view. And if that user clicks on other links, funnel him back to the ChangePassword view.
  2. Same scenario for users who must change their security questions.

Initially I placed the checks directly into a Login Controller ActionResult. But this only accounts for the Login action. An abbreviated code sample is below.

public ActionResult Login(LoginModel model, string returnUrl)
    {
        if (ModelState.IsValid)
        {    
            // ...

            // Does the user need to complete some missing information?
            if (externalUser.IsSecurityQuestionInfoComplete == false)
                return RedirectToAction("ChangeSecurityQuestions", "MyInfo");
            if (externalUser.ForcePasswordReset)
                return RedirectToAction("ChangePassword", "MyInfo");

            // Login was successful
            return RedirectToLocal(returnUrl);
        }
    }

One problem with this approach is that there are other hyperlinks presented in those targeted views, where the user could navigate away from the targeted interface. So for example, a user directed to the ChangeSecurityQuestions view could just click away from it.

Logged-in users can change those settings at any time. I could create duplicate views for changing passwords and security questions that are fashioned just for this scenario, where the user is being forced to update these values. In the interest of staying DRY and reducing maintenance, I'd like to use the same views for both scenarios (users who just want to edit that info, and users who are being forced to edit that info). But, trying to stay DRY in this respect may be wrongheaded if the alternative is worse.

I started to write a method within a helper class to divert these users, trying something like this.

    /// <summary>
    /// Check scenarios where the ExternalUser needs to complete some missing information, and divert as necessary.
    /// </summary>
    public static void divertExternalUserAsRequired(Controller controller, ExternalUser externalUser)
    {
        if (externalUser.IsSecurityQuestionInfoComplete == false)
            return controller.RedirectToAction("ChangeSecurityQuestions", "MyInfo");

        if (externalUser.ForcePasswordReset)
            return controller.RedirectToAction("ChangePassword", "MyInfo");
    }

But that RedirectToAction is inaccessible due to protection level. Moreover, that doesn't appear to be recommended (Is it possible to use RedirectToAction() inside a custom AuthorizeAttribute class?). And I don't like the idea of junking up my controllers by pasting this check all over the place.

UtilityHelpers.divertExternalUserAsRequired(this, externalUser);

What is the recommended approach to handling this scenario? I would perfer something that's more globally implemented, where the check can run when any relevant view loads.

Thanks for your help.

回答1:

If I'm understanding your question correctly then you've got a few options available to you.

One option is to check the necessary conditions within Application_BeginRequest in your Global.asax.cs class. This method is called at the very beginning of every request and if the condition fails then you can load a different controller action like so:

    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        if (!externalUser.IsSecurityQuestionInfoComplete)
        {
            var routeData = new RouteData();
            routeData.Values["action"] = "MyInfo";
            routeData.Values["controller"] = "ChangeSecurityQuestions";

            RequestContext requestContext = new RequestContext(new HttpContextWrapper(Context), routeData);

            IController errorController = new ChangeSecurityQuestionsController();
            errorController.Execute(requestContext);

            requestContext.HttpContext.Response.End();
        }
    }

Another option available to you is to create and register a global action filter. As you mentioned in your question, you don't like the idea of littering your controllers with these condition checks. By registering a global action filter your controllers can remain completely unaware of the action filter being performed against it. All you need to do is register your action filter within Global.asax.cs like so:

protected void Application_Start()
{
    ...

    GlobalFilters.Filters.Add(new SecurityQuestionCompleteFilter());

    ...
}

I hope this helps.