在 ASP.NET MVC 中重定向未授权的控制器

77

我有一个ASP.NET MVC中的控制器,我已经将其限制为管理员角色:

[Authorize(Roles = "Admin")]
public class TestController : Controller
{
   ...

如果一个非管理员角色的用户导航到这个控制器,他们会看到一个空白屏幕。

我想要做的是将其重定向到一个视图,上面写着“您需要成为管理员才能访问此资源。”

我想到的一种实现方式是在每个动作方法中检查IsUserInRole()方法,如果不在该角色中,则返回此信息视图。但是,这会违反DRY原则,显然很难维护。

8个回答

71
创建一个自定义的授权属性,基于AuthorizeAttribute并重写OnAuthorization方法来执行所需的检查。通常情况下,如果授权检查失败,AuthorizeAttribute将会将过滤器结果设置为HttpUnauthorizedResult,而你可以让它改为设置为ViewResult(例如你的Error视图)。

编辑:我有几篇关于此主题更详细的博客文章:

示例:

    [AttributeUsage( AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false )]
    public class MasterEventAuthorizationAttribute : AuthorizeAttribute
    {
        /// <summary>
        /// The name of the master page or view to use when rendering the view on authorization failure.  Default
        /// is null, indicating to use the master page of the specified view.
        /// </summary>
        public virtual string MasterName { get; set; }

        /// <summary>
        /// The name of the view to render on authorization failure.  Default is "Error".
        /// </summary>
        public virtual string ViewName { get; set; }

        public MasterEventAuthorizationAttribute()
            : base()
        {
            this.ViewName = "Error";
        }

        protected void CacheValidateHandler( HttpContext context, object data, ref HttpValidationStatus validationStatus )
        {
            validationStatus = OnCacheAuthorization( new HttpContextWrapper( context ) );
        }

        public override void OnAuthorization( AuthorizationContext filterContext )
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException( "filterContext" );
            }

            if (AuthorizeCore( filterContext.HttpContext ))
            {
                SetCachePolicy( filterContext );
            }
            else if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
            {
                // auth failed, redirect to login page
                filterContext.Result = new HttpUnauthorizedResult();
            }
            else if (filterContext.HttpContext.User.IsInRole( "SuperUser" ))
            {
                // is authenticated and is in the SuperUser role
                SetCachePolicy( filterContext );
            }
            else
            {
                ViewDataDictionary viewData = new ViewDataDictionary();
                viewData.Add( "Message", "You do not have sufficient privileges for this operation." );
                filterContext.Result = new ViewResult { MasterName = this.MasterName, ViewName = this.ViewName, ViewData = viewData };
            }

        }

        protected void SetCachePolicy( AuthorizationContext filterContext )
        {
            // ** IMPORTANT **
            // Since we're performing authorization at the action level, the authorization code runs
            // after the output caching module. In the worst case this could allow an authorized user
            // to cause the page to be cached, then an unauthorized user would later be served the
            // cached page. We work around this by telling proxies not to cache the sensitive page,
            // then we hook our custom authorization code into the caching mechanism so that we have
            // the final say on whether a page should be served from the cache.
            HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
            cachePolicy.SetProxyMaxAge( new TimeSpan( 0 ) );
            cachePolicy.AddValidationCallback( CacheValidateHandler, null /* data */);
        }


    }

1
我不确定是否有一个链接可以将这个问题分解成更容易理解的原因? - Maslow
1
有什么不清楚的吗?它首先使用AuthorizeCore检查用户是否已授权并处于允许的角色中。如果是,则检查是否在“SuperUser”(默认角色,未在属性中指定)的附加角色中。如果不是,则返回一个错误消息,表示虽然已经授权,但用户没有执行该操作所需的有效角色。如果用户已经授权且处于有效角色(或超级用户)中,则设置缓存策略以防止下游缓存。如果用户未经身份验证,则通过在过滤器的上下文中设置结果,返回未经授权的响应。 - tvanfosson
我在这里找到了更好的答案:https://dev59.com/gnI_5IYBdhLWcg3wK_s6 - bluee
需要提到的是,使用这种解决方案,您将不得不为您想要控制的类或方法“装饰”此属性:[MasterEventAuthorizationAttribute]。 - netfed
@netfed 你也可以将它添加为全局属性,不过你需要添加对于AllowAnonymousAttribute的处理(当我写这段代码时它还不存在)。 - tvanfosson

28

你可以在自定义的 AuthorizeAttribute 中使用可重写的 HandleUnauthorizedRequest

像这样:

protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
    // Returns HTTP 401 by default - see HttpUnauthorizedResult.cs.
    filterContext.Result = new RedirectToRouteResult(
    new RouteValueDictionary 
    {
        { "action", "YourActionName" },
        { "controller", "YourControllerName" },
        { "parameterName", "YourParameterValue" }
    });
}

你也可以像这样做:

private class RedirectController : Controller
{
    public ActionResult RedirectToSomewhere()
    {
        return RedirectToAction("Action", "Controller");
    }
}

现在你可以这样在你的HandleUnauthorizedRequest方法中使用它:

filterContext.Result = (new RedirectController()).RedirectToSomewhere();

10

"tvanfosson"的代码给了我“Error executing Child Request”错误。 我已将OnAuthorization更改为:

public override void OnAuthorization(AuthorizationContext filterContext)
    {
        base.OnAuthorization(filterContext);

        if (!_isAuthorized)
        {
            filterContext.Result = new HttpUnauthorizedResult();
        }
        else if (filterContext.HttpContext.User.IsInRole("Administrator") || filterContext.HttpContext.User.IsInRole("User") ||  filterContext.HttpContext.User.IsInRole("Manager"))
        {
            // is authenticated and is in one of the roles 
            SetCachePolicy(filterContext);
        }
        else
        {
            filterContext.Controller.TempData.Add("RedirectReason", "You are not authorized to access this page.");
            filterContext.Result = new RedirectResult("~/Error");
        }
    }

这很有效,我可以在错误页面上显示TempData。感谢“tvanfosson”提供的代码片段。我正在使用Windows身份验证,而_isAuthorized不过是HttpContext.User.Identity.IsAuthenticated的别名...


这会在用户没有权限的URL上返回401吗? - DevDave

6

我有同样的问题。与其弄清楚MVC代码,我选择了一个似乎可以起作用的便宜小技巧。在我的Global.asax类中:

member x.Application_EndRequest() =
  if x.Response.StatusCode = 401 then 
      let redir = "?redirectUrl=" + Uri.EscapeDataString x.Request.Url.PathAndQuery
      if x.Request.Url.LocalPath.ToLowerInvariant().Contains("admin") then
          x.Response.Redirect("/Login/Admin/" + redir)
      else
          x.Response.Redirect("/Login/Login/" + redir)

2
这个问题困扰了我好几天,所以当我发现这个答案可以肯定地解决tvanfosson的答案时,我认为强调答案的核心部分并解决一些相关的问题是值得的。
核心答案非常简单明了:
filterContext.Result = new HttpUnauthorizedResult();

在我的情况下,我继承自一个基础控制器,所以在每个继承它的控制器中,我都要重写OnAuthorize:

protected override void OnAuthorization(AuthorizationContext filterContext)
{
    base.OnAuthorization(filterContext);
    YourAuth(filterContext); // do your own authorization logic here
}

问题出在'YourAuth'中,我尝试了两种方法,认为这不仅可以奏效,还能立即终止请求。然而,事实并非如此。因此,首先介绍两种行不通的方法:

filterContext.RequestContext.HttpContext.Response.Redirect("/Login"); // doesn't work!
FormsAuthentication.RedirectToLoginPage(); // doesn't work!

不仅这些方法无效,而且它们也不能结束请求。这意味着以下内容:

if (!success) {
    filterContext.Result = new HttpUnauthorizedResult();
}
DoMoreStuffNowThatYouThinkYourAuthorized();

即使上面的答案是正确的,逻辑流仍将继续!因此请记住,在OnAuthorize中仍会触发DoMoreStuff...。因此,要把它放在else中。

但是,使用正确的答案后,尽管OnAuthorize的逻辑流仍将一直持续到结束,但在那之后,你确实会得到你期望的结果:重定向到登录页面(如果你在webconfig的Forms auth中设置了一个登录页面)。

但出乎意料的是, 1)Response.Redirect("/Login")不起作用:Action方法仍然被调用,而 2)FormsAuthentication.RedirectToLoginPage();也是同样的情况:Action方法仍然被调用!

这对我来说似乎完全错误,特别是后者:谁会想到FormsAuthentication.RedirectToLoginPage不会结束请求或执行filterContext.Result = new HttpUnauthorizedResult()的等效操作?


1
也许当你使用Windows身份验证(之前的主题)在Visual Studio下运行开发服务器时,会得到一个空白页面。
如果你部署到IIS,你可以为特定状态码配置自定义错误页面,例如401。在system.webServer下添加httpErrors即可:
<httpErrors>
  <remove statusCode="401" />
  <error statusCode="401" path="/yourapp/error/unauthorized" responseMode="Redirect" />
</httpErrors>

然后创建 ErrorController.Unauthorized 方法和相应的自定义视图。

1

你应该构建自己的授权过滤器属性。

这是我的供你学习 ;)

Public Class RequiresRoleAttribute : Inherits ActionFilterAttribute
    Private _role As String

    Public Property Role() As String
        Get
            Return Me._role
        End Get
        Set(ByVal value As String)
            Me._role = value
        End Set
    End Property

    Public Overrides Sub OnActionExecuting(ByVal filterContext As System.Web.Mvc.ActionExecutingContext)
        If Not String.IsNullOrEmpty(Me.Role) Then
            If Not filterContext.HttpContext.User.Identity.IsAuthenticated Then
                Dim redirectOnSuccess As String = filterContext.HttpContext.Request.Url.AbsolutePath
                Dim redirectUrl As String = String.Format("?ReturnUrl={0}", redirectOnSuccess)
                Dim loginUrl As String = FormsAuthentication.LoginUrl + redirectUrl

                filterContext.HttpContext.Response.Redirect(loginUrl, True)
            Else
                Dim hasAccess As Boolean = filterContext.HttpContext.User.IsInRole(Me.Role)
                If Not hasAccess Then
                    Throw New UnauthorizedAccessException("You don't have access to this page. Only " & Me.Role & " can view this page.")
                End If
            End If
        Else
            Throw New InvalidOperationException("No Role Specified")
        End If

    End Sub
End Class

这似乎是一个重定向,但它似乎也首先运行原始操作方法的全部内容。 - Mike Cole
你应该使用 filterContext.Result = new RedirectResult(loginUrl),而不是进行重定向。 - Mike Cole

1

我原本想将这个留言作为评论,但是我需要更多的声望。无论如何,我只是想提醒Nicholas Peterson,也许将第二个参数传递给Redirect调用以告诉它结束响应可能会起作用。虽然这不是最优雅的处理方式,但确实可以解决问题。

所以

filterContext.RequestContext.HttpContext.Response.Redirect("/Login", true);

代替

filterContext.RequestContext.HttpContext.Response.Redirect("/Login);

在你的控制器中会有这样的代码:

So you'd have this in your controller:

 protected override void OnAuthorization(AuthorizationContext filterContext)
 {
      if(!User.IsInRole("Admin")
      {
          base.OnAuthorization(filterContext);
          filterContext.RequestContext.HttpContext.Response.Redirect("/Login", true);
      }
 }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接