未经授权的webapi调用返回登录页面而不是401


180

如何配置我的mvc / webapi项目,以使从剃刀视图调用的webapi方法在未经授权时不返回登录页面?

它是一个MVC5应用程序,还具有用于通过javascript进行调用的WebApi控制器。

下面的两种方法

[Route("api/home/LatestProblems")]      
[HttpGet()]
public List<vmLatestProblems> LatestProblems()
{
    // Something here
}

[Route("api/home/myLatestProblems")]
[HttpGet()]
[Authorize(Roles = "Member")]
public List<vmLatestProblems> mylatestproblems()
{
   // Something there
}

通过以下角度代码调用:

angular.module('appWorship').controller('latest', 
    ['$scope', '$http', function ($scope,$http) {         
        var urlBase = baseurl + '/api/home/LatestProblems';
        $http.get(urlBase).success(function (data) {
            $scope.data = data;
        }).error(function (data) {
            console.log(data);
        });
        $http.get(baseurl + '/api/home/mylatestproblems')
          .success(function (data) {
            $scope.data2 = data;
        }).error(function (data) {
            console.log(data);
        });  
    }]
);

因此,我尚未登录,第一个方法成功返回了数据。第二种方法返回(在成功函数中)包含登录页面等效项的数据。例如,如果您请求带有[Authorize]标记的控制器操作并且尚未登录,则在mvc中会得到什么。

我希望它返回未经授权的401,以便我可以根据用户是否登录来为用户显示不同的数据。理想情况下,如果用户已登录,则我希望能够访问Controller的User属性,以便我可以返回特定于该Member的数据。

更新:由于以下任何建议似乎都不再起作用(对Identity或WebAPI的更改),我在github上创建了一个原始示例,该示例可以说明问题。

Answers:


78

有两种AuthorizeAttribute实现,您需要确保为Web API引用了正确的实现。有System.Web.Http.AuthorizeAttribute这是用于Web API的,并且System.Web.Mvc.AuthorizeAttribute这是用来与视图控制器。 如果授权失败,则Http.AuthorizeAttribute将返回401错误,并且Mvc.AuthorizeAttribute将重定向到登录页面。

更新于11/26/2013

因此,正如布洛克·艾伦(Brock Allen)在他的文章中指出的那样随着MVC 5的出现,情况发生了巨大变化。我猜想OWIN管道会接管并引入一些新行为。现在,当未授权用户时,将在HTTP标头中返回状态200和以下信息。

X-Responded-JSON: {"status":401,"headers":{"location":"http:\/\/localhost:59540\/Account\/Login?ReturnUrl=%2Fapi%2FTestBasic"}}

您可以在客户端更改逻辑,以检查标头中的此信息以确定如何处理此信息,而不是在错误分支上查找401状态。

我试图通过在OnAuthorizationHandleUnauthorizedRequest方法的响应中设置状态来覆盖自定义AuthorizeAttribute中的此行为。

actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);

但这没有用。新管道必须稍后再获取此响应,并将其修改为我之前获得的响应。抛出HttpException也不起作用,因为它只是更改为500错误状态。

我测试了Brock Allen的解决方案,当我使用jQuery ajax调用时它确实起作用。如果它对您不起作用,我猜是因为您使用的是角度。用Fiddler运行测试,看看标题中是否包含以下内容。

X-Requested-With: XMLHttpRequest

如果不是,那就是问题所在。我不熟悉angular,但是如果它允许您插入自己的标头值,然后将其添加到ajax请求中,则它可能会开始工作。


我认为我正在使用System.web.http.authorizeattribute,至少此webapicontroller没有使用system.web.mvc,并且要转到authorize属性的定义将我发送到system.web.http
Tim

您好@ kevin-junghans在这里完全感到困惑。上面来自shiva的示例使用了mvc授权属性,该属性肯定不应该应用于webapi动作。来自Brock allen的示例似乎不起作用,或者当我逐步执行时,它也不认为其ajax请求。
蒂姆(Tim)

1
只是发现了这个答案(想想stackoverflow不发送通知),我添加了一个github示例来说明问题,现在将您的修复程序添加到有角度的标题中。谢谢。但是似乎不正确,我可以检查的authorize属性中没有属性,或者您提到的原始功能不再起作用。
蒂姆(Tim)

3
使用POSTMAN和标头参数X-Requested-With:XMLHttpRequest对我
有用

那么,如果您打算做一个纯Web API项目呢?我正在其他人设置的项目上进行工作,并且如此处所述重定向Authorize,但是我有一个工作正常的其他API项目。一定有一些东西使它认为它是MVC应用程序而不是API应用程序,但是我找不到可能使它成为垃圾的东西。
德里克·格里尔

122

Brock Allen有一篇不错的博客文章,介绍了在使用Cookie身份验证和OWIN时如何为ajax调用返回401。 http://brockallen.com/2013/10/27/using-cookie-authentication-middleware-with-web-api-and-401-response-codes/

将其放在Startup.Auth.cs文件中的ConfigureAuth方法中:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
  AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
  LoginPath = new PathString("/Account/Login"),
  Provider = new CookieAuthenticationProvider
  {
    OnApplyRedirect = ctx =>
    {
      if (!IsAjaxRequest(ctx.Request))
      {
        ctx.Response.Redirect(ctx.RedirectUri);
      }
    }
  }
});

private static bool IsAjaxRequest(IOwinRequest request)
{
  IReadableStringCollection query = request.Query;
  if ((query != null) && (query["X-Requested-With"] == "XMLHttpRequest"))
  {
     return true;
  }
  IHeaderDictionary headers = request.Headers;
  return ((headers != null) && (headers["X-Requested-With"] == "XMLHttpRequest"));
}

68
对此的一种变化:如果您所有的Web API调用都经过某个路径(例如)/api,则可以使用该路径来确定是否重定向。如果您的客户端使用其他格式(例如JSON),则特别有用。更换呼叫IsAjaxRequestif (!context.Request.Path.StartsWithSegments(new PathString("/api")))
爱德华·布雷

晚了晚些,但是这种方法是唯一对我有用的方法,而且似乎更“准确”。
Stephen Collins 2014年

甚至晚到聚会,但这已被证明非常有用...这让我感到困惑,因为默认生成的代码以如此令人沮丧的难以调试的方式犯了如此错误。
尼克

如果您正在寻求WebApi解决方案,那么Manik的答案可以替代此处备受好评的评论。
2015年

5
使用C#6,这是IsAjaxRequest的较小版本: private static bool IsAjaxRequest(IOwinRequest request) { return request.Query?["X-Requested-With"] == "XMLHttpRequest" || request.Headers?["X-Requested-With"] == "XMLHttpRequest"; }
PeterÖrneholm,2016年

85

如果要在asp.net MVC网站中添加asp.net WebApi,则可能要对某些请求进行未经授权的响应。但是随后ASP.NET基础结构开始起作用,当您尝试将响应状态代码设置为HttpStatusCode时。未经授权,您将获得302重定向到登录页面。

如果您在此处使用asp.net身份和基于owin的身份验证,则可以使用以下代码来解决该问题:

public void ConfigureAuth(IAppBuilder app)
{
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/Account/Login"),
        Provider = new CookieAuthenticationProvider()
        {
            OnApplyRedirect = ctx =>
            {
                if (!IsApiRequest(ctx.Request))
                {
                    ctx.Response.Redirect(ctx.RedirectUri);
                }
            }
        }
    });

    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
}


private static bool IsApiRequest(IOwinRequest request)
{
    string apiPath = VirtualPathUtility.ToAbsolute("~/api/");
    return request.Uri.LocalPath.StartsWith(apiPath);
}

1
我更改了判别式,以检查请求是否接受text / html或application / xhtml作为响应,如果不是,我认为这是一个“自动”客户端请求,例如ajax请求
L.Trabacchin

4
我也更喜欢这种方法。我唯一做的就是在他们请求“ / API”之类的时候转换LocalPath .ToLower()。
FirstDivision '16

1
非常感谢。它挽救了我的一天。:)
阿米特·库玛

有人有运气吗?从aspnet core 1.1开始,CookieAuthenticationOptions不再具有Provider属性。
杰里米

27

当OWIN总是将401响应从WebApi重定向到登录页面时,我遇到了同样的情况。我们的Web API不仅支持来自Angular的ajax调用,还支持Mobile,Win Form调用。因此,对于我们的情况,检查请求是否为ajax请求的解决方案并未真正排序。

我选择了另一种方法是注入新的标头响应:Suppress-Redirect如果响应来自webApi。实现在处理程序上:

public class SuppressRedirectHandler : DelegatingHandler
{
    /// <summary>
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken).ContinueWith(task =>
        {
            var response = task.Result;
            response.Headers.Add("Suppress-Redirect", "True");
            return response;
        }, cancellationToken);
    }
}

并在WebApi的全局级别注册此处理程序:

config.MessageHandlers.Add(new SuppressRedirectHandler());

因此,在OWIN启动时,您可以检查响应标头是否具有Suppress-Redirect

public void Configuration(IAppBuilder app)
{
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationMode = AuthenticationMode.Active,
        AuthenticationType = DefaultApplicationTypes.ApplicationCookie,
        ExpireTimeSpan = TimeSpan.FromMinutes(48),

        LoginPath = new PathString("/NewAccount/LogOn"),

        Provider = new CookieAuthenticationProvider()
        {
            OnApplyRedirect = ctx =>
            {
                var response = ctx.Response;
                if (!IsApiResponse(ctx.Response))
                {
                    response.Redirect(ctx.RedirectUri);
                }
            }
        }
    });
}

private static bool IsApiResponse(IOwinResponse response)
{
    var responseHeader = response.Headers;

    if (responseHeader == null) 
        return false;

    if (!responseHeader.ContainsKey("Suppress-Redirect"))
        return false;

    if (!bool.TryParse(responseHeader["Suppress-Redirect"], out bool suppressRedirect))
        return false;

    return suppressRedirect;
}

谢谢 !除了Xamarin / Android,我们的API均可在所有平台上使用。将使用此解决方案
Jurion

17

在早期版本的ASP.NET中,您必须做很多工作才能使此工作正常进行。

好消息是,因为您正在使用ASP.NET 4.5。您可以使用新的HttpResponse.SuppressFormsAuthenticationRedirect属性禁用表单身份验证重定向。

Global.asax

protected void Application_EndRequest(Object sender, EventArgs e)
{
        HttpApplication context = (HttpApplication)sender;
        context.Response.SuppressFormsAuthenticationRedirect = true;
}

编辑:您可能还想看一下Sergey Zwezdin的这篇文章,它有一种更完善的方式来完成您想做的事情。

下面粘贴了相关的代码片段和作者旁白。代码和叙述的最初作者- 谢尔盖•兹韦兹丁Sergey Zwezdin)

首先–让我们确定当前的HTTP请求是否为AJAX请求。如果是,我们应该禁用将HTTP 401替换为HTTP 302:

public class ApplicationAuthorizeAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        var httpContext = filterContext.HttpContext;
        var request = httpContext.Request;
        var response = httpContext.Response;

        if (request.IsAjaxRequest())
            response.SuppressFormsAuthenticationRedirect = true;

        base.HandleUnauthorizedRequest(filterContext);
    }
}

其次–让我们添加一个条件::如果用户通过了身份验证,则我们将发送HTTP 403;否则为HTTP 401。

public class ApplicationAuthorizeAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        var httpContext = filterContext.HttpContext;
        var request = httpContext.Request;
        var response = httpContext.Response;
        var user = httpContext.User;

        if (request.IsAjaxRequest())
        {
            if (user.Identity.IsAuthenticated == false)
                response.StatusCode = (int)HttpStatusCode.Unauthorized;
            else
                response.StatusCode = (int)HttpStatusCode.Forbidden;

            response.SuppressFormsAuthenticationRedirect = true;
            response.End();
        }

        base.HandleUnauthorizedRequest(filterContext);
    }
}

做得好。现在我们应该用这个新的过滤器替换所有标准AuthorizeAttribute的用法。它可能不适用于具有美感的Sime小伙。但是我没有其他办法。如果有的话,请让我们发表评论。

最后,我们应该做的–在客户端添加HTTP 401/403处理。我们可以在jQuery上使用ajaxError来避免代码重复:

$(document).ajaxError(function (e, xhr) {
    if (xhr.status == 401)
        window.location = "/Account/Login";
    else if (xhr.status == 403)
        alert("You have no enough permissions to request this resource.");
});

结果 -

  • 如果用户未通过身份验证,则在进行任何AJAX调用后,他都会被重定向到登录页面。
  • 如果用户已通过身份验证,但没有足够的权限,则他将看到用户友好的erorr消息。
  • 如果用户通过了身份验证并具有足够的权限,则不会有任何错误,并且HTTP请求将照常进行。

我正在通过mvc使用新的身份验证框架进行身份验证。此设置不会阻止mvc登录以及webapi调用吗?
蒂姆(Tim)

5
当我检查此示例时,似乎使用的Authorize Attribute是MVC版本而不是WebApi版本。但是,webapi版本没有用于禁止表单身份验证的选项。
蒂姆(Tim)

我的请求没有IsAjaxRequest方法。
蒂姆(Tim)

1
蒂姆(Tim)会在IsAjaxRequest上查看它:brockallen.com/2013/10/27/… 如果您使用的是AngularJs而未编辑标头,那么您将没有“ XMLHttpRequest”,而是添加它或检查其他内容。
蒂姆(Tim)

10

如果您是Web APIMVC项目中运行的,则需要创建一个自定义AuthorizeAttribute以应用于您的API方法。在其中,IsAuthorized override您需要获取电流HttpContext以防止重定向,如下所示:

    protected override bool IsAuthorized(HttpActionContext actionContext)
    {
        if (string.IsNullOrWhiteSpace(Thread.CurrentPrincipal.Identity.Name))
        {
            var response = HttpContext.Current.Response;
            response.SuppressFormsAuthenticationRedirect = true;
            response.StatusCode = (int)System.Net.HttpStatusCode.Forbidden;
            response.End();
        }

        return base.IsAuthorized(actionContext);
    }

8

我自己使用Azure Active Directory集成,使用CookieAuthentication中间件的方法对我不起作用。我必须执行以下操作:

app.UseOpenIdConnectAuthentication(
    new OpenIdConnectAuthenticationOptions
    {
        ...
        Notifications = new OpenIdConnectAuthenticationNotifications
        {   
            ...         
            RedirectToIdentityProvider = async context =>
            {
                if (!context.Request.Accept.Contains("html"))
                {
                    context.HandleResponse();
                }
            },
            ...
        }
    });

如果请求来自浏览器本身(例如,而不是AJAX调用),那么Accept标头将html在其中的某个位置包含字符串。只有当客户端接受HTML时,我才会考虑重定向有用的东西。

我的客户端应用程序可以处理401,通知用户该应用程序不再具有访问权限,需要重新加载才能再次登录。


这是非常相似,提出了一个相关的问题解决方案:stackoverflow.com/questions/34997674/...
纪尧姆·莱希

6

我也有一个带有WebApi的MVC5应用程序(System.Web)(使用OWIN),只是想防止WebApi的401响应更改为302响应。

对我有用的是创建一个定制的WebApi AuthorizeAttribute版本,如下所示:

public class MyAuthorizeAttribute : System.Web.Http.AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
    {
        base.HandleUnauthorizedRequest(actionContext);
        HttpContext.Current.Response.SuppressFormsAuthenticationRedirect = true;
    }
}

并使用它代替标准的WebApi AuthorizeAttribute。我使用了标准的MVC AuthorizeAttribute来保持MVC行为不变。


作品,但现在我认为客户端接收状态-1,而不是401有问题
塞巴斯蒂安·罗哈斯

@SebastiánRojas我不确定是什么引起的-设置SuppressFormsAuthenticationRedirect 标志会导致它只是为我返回现有的401。
乔诺·乔布

3

只需安装以下NeGet软件包

安装包Microsoft.AspNet.WebApi.Owin

在WebApiConfig文件中编写以下代码。

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        //Web API configuration and services
        //Configure Web API to use only bearer token authentication.
        config.SuppressDefaultHostAuthentication();
        config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
        config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
        config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));
    }
}

我需要做的就是添加此过滤器,config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));否则它的工作User.Identity.IsAuthenticated始终都是false
Ricardo Saracino

1

如果要捕获Content-Type == application / json,则可以使用该代码:

private static bool IsAjaxRequest(IOwinRequest request)
    {
        IReadableStringCollection queryXML = request.Query;
        if ((queryXML != null) && (queryXML["X-Requested-With"] == "XMLHttpRequest"))
        {
            return true;
        }

        IReadableStringCollection queryJSON = request.Query;
        if ((queryJSON != null) && (queryJSON["Content-Type"] == "application/json"))
        {
            return true;
        }

        IHeaderDictionary headersXML = request.Headers;
        var isAjax = ((headersXML != null) && (headersXML["X-Requested-With"] == "XMLHttpRequest"));

        IHeaderDictionary headers = request.Headers;
        var isJson = ((headers != null) && (headers["Content-Type"] == "application/json"));

        return isAjax || isJson;

    }

问候!!


1

我很难在OnAuthorization / HandleUnauthorizedRequest方法中同时获取状态代码和文本响应。事实证明这是对我最好的解决方案:

    actionContext.Response = new HttpResponseMessage()
    {
        StatusCode = HttpStatusCode.Forbidden,
        Content = new StringContent(unauthorizedMessage)
    };

1

在试图避免重定向到登录页面时大惊小怪之后,我意识到这实际上非常适合Authorize属性。就是说去获得授权。对于未经授权的Api呼叫,我只想不向黑客泄露任何信息。通过添加从Authorize派生的新属性更容易直接实现此目标,该属性将内容隐藏为404错误:

public class HideFromAnonymousUsersAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
    {
         actionContext.Response = ActionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, "Access Restricted");
    }
}

1

混合使用MVC和WebAPI,如果请求未经授权,那么即使在WebAPI请求中,它也会重定向到登录页面。为此,我们可以添加以下代码以将响应发送到移动应用程序

protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
{
    var httpContext = HttpContext.Current;
    if (httpContext == null)
    {
        base.HandleUnauthorizedRequest(actionContext);
        return;
    }

    actionContext.Response = httpContext.User.Identity.IsAuthenticated == false ?
        actionContext.Request.CreateErrorResponse(
      System.Net.HttpStatusCode.Unauthorized, "Unauthorized") :
       actionContext.Request.CreateErrorResponse(
      System.Net.HttpStatusCode.Forbidden, "Forbidden");

    httpContext.Response.SuppressFormsAuthenticationRedirect = true;
    httpContext.Response.End();
}

0

多谢你们!

就我而言,我将cuongleShiva的答案结合在一起,得到了以下内容:

在Controller的OnException()处理程序中,用于API异常:

filterContext.ExceptionHandled = true;
//...
var response = filterContext.HttpContext.Response;
response.Headers.Add("Suppress-Redirect", "true");
response.SuppressFormsAuthenticationRedirect = true;

在应用启动配置代码中:

app.UseCookieAuthentication(new CookieAuthenticationOptions {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/Account/Login"),
        Provider = new CookieAuthenticationProvider {
            OnValidateIdentity = ctx => {
                return validateFn.Invoke(ctx);
            },
            OnApplyRedirect = ctx =>
            {
                bool enableRedir = true;
                if (ctx.Response != null)
                {
                    string respType = ctx.Response.ContentType;
                    string suppress = ctx.Response.Headers["Suppress-Redirect"];
                    if (respType != null)
                    {
                        Regex rx = new Regex("^application\\/json(;(.*))?$",
                            RegexOptions.IgnoreCase);
                        if (rx.IsMatch(respType))
                        {
                            enableRedir = false;
                        }  
                    }
                    if ((!String.IsNullOrEmpty(suppress)) && (Boolean.Parse(suppress)))
                    {
                        enableRedir = false;
                    }
                }
                if (enableRedir)
                {
                    ctx.Response.Redirect(ctx.RedirectUri);
                }
            }
        }
    });

-1

在带有Dot Net Framework 4.5.2的MVC 5中,我们在“ Accept”标头下获得了“ application / json,纯文本..”,如下所示,使用起来很好:

isJson = headers["Content-Type"] == "application/json" || headers["Accept"].IndexOf("application/json", System.StringComparison.CurrentCultureIgnoreCase) >= 0;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.