为什么AuthorizeAttribute在身份验证和授权失败时会重定向到登录页面?

273
在ASP.NET MVC中,您可以使用AuthorizeAttribute标记控制器方法,如下所示:
[Authorize(Roles = "CanDeleteTags")]
public void Delete(string tagName)
{
    // ...
}

这意味着,如果当前登录的用户不属于“CanDeleteTags”角色,则控制器方法将永远不会被调用。

不幸的是,对于失败情况,AuthorizeAttribute返回HttpUnauthorizedResult,这总是返回HTTP状态代码401。这会导致重定向到登录页面。

如果用户没有登录,这很合理。然而,如果用户已经登录,但不在所需角色中,则将其发送回登录页面会让人感到困惑。

似乎AuthorizeAttribute混淆了身份验证和授权。

这在ASP.NET MVC中似乎有点疏忽,或者我漏掉了什么?

我不得不想出一个DemandRoleAttribute来分离它们两个。当用户未通过身份验证时,它返回HTTP 401,将其发送到登录页面。当用户已经登录但不具备所需的角色时,它会创建一个NotAuthorizedResult。目前,这将重定向到一个错误页面。

难道我真的不得不这样做吗?


10
好的问题,我同意,应该返回一个 HTTP 未授权状态。 - Pure.Krome
3
我喜欢你的解决方案,罗杰。即使你不喜欢它。 - Jon Davis
1
我的登录页面有一个检查,如果用户已经通过身份验证,则简单地将其重定向到ReturnUrl。所以我成功创建了一个无限的302重定向循环:D woot。 - juhan_h
1
请查看这个链接 - Jogi
Roger,你的解决方案很好,这篇文章写得不错--https://www.red-gate.com/simple-talk/dotnet/asp-net/thoughts-on-asp-net-mvc-authorization-and-security/。看起来你的解决方案是唯一一个干净利落的方法。 - Craig
7个回答

314

当初开发的时候,System.Web.Mvc.AuthorizeAttribute是正确的——早期版本的HTTP规范将状态码401用于“未经授权”和“未经身份验证”。

从最初的规范来看:

如果请求已经包括授权凭据,则401响应表示对这些凭据已拒绝授权。

事实上,你可以在这里看到混淆的地方——它在表示“身份验证”时使用了“授权”一词。然而,在日常实践中,当用户经过身份验证但未经授权时,返回403 Forbidden更有意义。用户不太可能有第二组凭据来获取访问权限——用户体验非常糟糕。

考虑大多数操作系统——当您尝试读取无权访问的文件时,您不会看到登录屏幕!

值得庆幸的是,HTTP规范已更新(2014年6月)以消除歧义。

从“超文本传输协议(HTTP/1.1):身份验证”(RFC 7235)中:

401(未经授权)状态码表示请求未被应用程序执行,因为它缺少目标资源的有效身份验证凭据。

从“超文本传输协议(HTTP/1.1):语义和内容”(RFC 7231)中:

403 Forbidden状态码表示服务器理解了请求,但拒绝授权。

有趣的是,在发布ASP.NET MVC 1时,AuthorizeAttribute的行为是正确的。现在,该行为是不正确的——HTTP/1.1规范已被修复。

与其试图更改ASP.NET的登录页重定向,不如从源头上解决问题更容易。您可以在网站的默认命名空间中创建一个具有相同名称(AuthorizeAttribute)的新属性(这非常重要),然后编译器将自动选择它,而不是MVC的标准属性。当然,如果您愿意采取这种方法,您也可以给该属性命名一个新名称。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class AuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAuthenticated)
        {
            filterContext.Result = new System.Web.Mvc.HttpStatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
        }
        else
        {
            base.HandleUnauthorizedRequest(filterContext);
        }
    }
}

53
非常好的方法。一个小建议:不要检查filterContext.HttpContext.User.Identity.IsAuthenticated,而是可以只检查filterContext.HttpContext.Request.IsAuthenticated,因为它内置了空值检查。请参见https://dev59.com/HnM_5IYBdhLWcg3wcCrc#1379601 - Daniel Liuzzi
2
@DePeter,规范中从未提到重定向,那么为什么重定向是更好的解决方案?这会导致Ajax请求失败,除非采取某种方法来解决它。 - Adam Tuliper
1
由于它显然是一个行为错误,因此应将其记录在MS Connect上。谢谢。 - Tony Wall
实际上,根据我在Windows和Ubuntu上有限的账户使用经验,我记得当我尝试做一些不被允许的事情时,会看到输入凭证的窗口。但这仍然与Simple Membership不同。在这些操作系统中,您可以通过输入凭证来授权单个操作,但在这里,您的身份验证将完全更改(简而言之,您将重新登录为不同的用户)。 - jahu
顺便提一下,现在不需要强制转换(有一个采用HttpStatusCode的构造函数可以使用来创建一个 HttpStatusCodeResult)。 - Mark Sowul
显示剩余6条评论

24
在你的登录页面的Page_Load函数中添加以下内容:
// User was redirected here because of authorization section
if (User.Identity != null && User.Identity.IsAuthenticated)
    Response.Redirect("Unauthorized.aspx");

当用户重定向到该页面,但已经登录时,会显示未授权页面。如果他们没有登录,则会继续并显示登录页面。


18
Page_Load是WebForms中的重要函数。 - Chance
2
@Chance - 然后在默认的 ActionMethod 中执行这个操作,该方法是在已设置 FormsAuthentication 调用的控制器中调用的。 - Pure.Krome
实际上,这个非常好用,但对于MVC应该是类似于if (User.Identity != null && User.Identity.IsAuthenticated) return RedirectToRoute("Unauthorized");的东西,其中Unauthorized是一个定义好的路由名称。 - Moses Machua
所以你请求一个资源,然后被重定向到登录页面,接着又被重定向到403页面?对我来说看起来很糟糕。我甚至无法容忍任何一次重定向。在我看来,这个东西构建得非常糟糕。 - SandRock
3
根据你的解决方案,如果你已经登录并通过输入URL进入登录页面...这将使你跳转到未授权页面,这是不正确的。 - Rajshekar Reddy
显示剩余2条评论

4
很遗憾,您正在处理ASP.NET表单身份验证的默认行为。这里讨论了一种解决方法(我没有尝试过):http://www.codeproject.com/KB/aspnet/Custon401Page.aspx(它不仅适用于MVC)。我认为,在大多数情况下,最好的解决方案是在用户尝试访问未经授权的资源之前限制其访问权限。通过删除/灰化可能将他们带到此未经授权页面的链接或按钮。也许在属性上增加一个附加参数以指定未经授权的用户重定向位置会很好。但与此同时,我把AuthorizeAttribute看作是一种安全网。

我计划根据授权删除链接(我在这里看到过一个相关的问题),所以稍后我会编写一个HtmlHelper扩展方法。 - Roger Lipscombe
1
我仍然需要防止用户直接访问URL,这就是此属性的作用。我对自定义401解决方案不太满意(似乎有点全局),因此我将尝试将我的NotAuthorizedResult建模为RedirectToRouteResult... - Roger Lipscombe

4
我一直认为这是有道理的。如果您已登录并尝试访问需要您没有的角色的页面,则会被重定向到登录屏幕,要求您使用具有该角色的用户进行登录。
您可以在登录页面上添加逻辑以检查用户是否已经通过验证。您可以添加友好的消息来解释为什么他们又被踢回到了那里。

4
我认为大多数人在使用一个网络应用时不会有多个身份。如果他们有多个身份,那么他们通常会足够聪明地想到“我的当前身份没有作用了,我会重新登录另一个身份”。 - Roger Lipscombe
1
虽然你提到在登录页面上显示某些内容的观点很好。谢谢。 - Roger Lipscombe

0
在您的Global.ascx文件的Application_EndRequest处理程序中尝试此操作。
if (HttpContext.Current.Response.Status.StartsWith("302") && HttpContext.Current.Request.Url.ToString().Contains("/<restricted_path>/"))
{
    HttpContext.Current.Response.ClearContent();
    Response.Redirect("~/AccessDenied.aspx");
}

0
如果您正在使用aspnetcore 2.0,请使用以下内容:
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Core
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
    public class AuthorizeApiAttribute : Microsoft.AspNetCore.Authorization.AuthorizeAttribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            var user = context.HttpContext.User;

            if (!user.Identity.IsAuthenticated)
            {
                context.Result = new UnauthorizedResult();
                return;
            }
        }
    }
}

0
在我的情况下,问题是“HTTP规范将状态码401用于未经授权和未经身份验证”。正如ShadowChaser所说。
这个解决方案对我有用:
if (User != null &&  User.Identity.IsAuthenticated && Response.StatusCode == 401)
{
    //Do whatever

    //In my case redirect to error page
    Response.RedirectToRoute("Default", new { controller = "Home", action = "ErrorUnauthorized" });
}

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