简体   繁体   English

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

[英]Unauthorised webapi call returning login page rather than 401

How do I configure my mvc/webapi project so that a webapi method called from a razor view doesn't return the loginpage when its unauthorised? 如何配置我的mvc / webapi项目,以便从剃刀视图调用的webapi方法在未经授权时不返回登录页面?

Its a MVC5 application which also has WebApi controllers for calls via javascript. 它是一个MVC5应用程序,它也有通过javascript调用的WebApi控制器。

The two methods below 以下两种方法

[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
}

are called via the following angular code: 通过以下角度代码调用:

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);
        });  
    }]
);

So I'm not logged in and the first method successfully returns data. 所以我没有登录,第一个方法成功返回数据。 the second method returns (in the success function) data which contains the equivalent of a login page. 第二种方法返回(在成功函数中)包含等效登录页面的数据。 ie what you would get in mvc if you requested a controller action which was stamped with [Authorize] and you weren't logged in. 也就是说,如果你请求一个标有[授权]并且你没有登录的控制器动作,你会在mvc中获得什么。

I want it to return a 401 unauthorized, so that i can display different data for users based on if they are logged in or not. 我希望它返回401未授权,以便我可以根据用户是否登录显示不同的数据。 Ideally if the user is logged in i want to be able to access the Controller's User property so i can return data specific to that Member. 理想情况下,如果用户已登录,我希望能够访问Controller的用户属性,以便我可以返回特定于该成员的数据。

UPDATE: Since none of the suggestions below seem to work anymore (changes to Identity or WebAPI) ive created a raw example on github which should illustrate the problem. 更新:由于下面的建议似乎都不再适用(对Identity或WebAPI的更改)我在github上创建了一个原始示例,它应该说明问题。

Brock Allen has a nice blog post on how to return 401 for ajax calls when using Cookie authentication and OWIN. Brock Allen有一篇关于如何在使用Cookie身份验证和OWIN时为ajax调用返回401的博文。 http://brockallen.com/2013/10/27/using-cookie-authentication-middleware-with-web-api-and-401-response-codes/ http://brockallen.com/2013/10/27/using-cookie-authentication-middleware-with-web-api-and-401-response-codes/

Put this in ConfigureAuth method in the Startup.Auth.cs file: 将它放在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"));
}

If you are adding asp.net WebApi inside asp.net MVC web site you probably want to respond unauthorized to some requests. 如果您在asp.net MVC网站中添加asp.net WebApi,您可能希望对某些请求进行未经授权的响应。 But then ASP.NET infrastructure come into play and when you try to set response status code to HttpStatusCode.Unauthorized you will get 302 redirect to login page. 但是当ASP.NET基础结构发挥作用时,当您尝试将响应状态代码设置为HttpStatusCode.Unauthorized时,您将获得302重定向到登录页面。

If you are using asp.net identity and owin based authentication here a code that can help to solve that issue: 如果你在这里使用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);
}

There are two AuthorizeAttribute implementations and you need to make sure you are referencing the correct one for Web API's. 有两个AuthorizeAttribute实现,您需要确保引用Web API的正确实现。 There is System.Web.Http.AuthorizeAttribute which is used for Web API's, and System.Web.Mvc.AuthorizeAttribute which is used for controllers with views. System.Web.Http.AuthorizeAttribute用于Web API, System.Web.Mvc.AuthorizeAttribute用于带视图的控制器。 Http.AuthorizeAttribute will return a 401 error if authorization fails and Mvc.AuthorizeAttribute will redirect to the login page. 如果授权失败, Http.AuthorizeAttribute将返回401错误,并且Mvc.AuthorizeAttribute将重定向到登录页面。

Updated 11/26/2013 更新于2013年11月26日

So it appears things have drastically changed with MVC 5 as Brock Allen pointed out in his article . 因此,正如Brock Allen 在他的文章中指出的那样 MVC 5似乎已经发生了巨大的变化。 I guess the OWIN pipeline takes over and introduces some new behavior. 我猜OWIN管道接管并引入了一些新的行为。 Now when the user is not authorized a status of 200 is returned with the following information in the HTTP header. 现在,当用户未被授权时,将返回状态200,并在HTTP标头中包含以下信息。

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

You could change your logic on the client side to check this information in the header to determine how to handle this, instead of looking for a 401 status on the error branch. 您可以在客户端更改逻辑以检查标头中的此信息以确定如何处理此信息,而不是在错误分支上查找401状态。

I tried to override this behavior in a custom AuthorizeAttribute by setting the status in the response in the OnAuthorization and HandleUnauthorizedRequest methods. 我试图通过在OnAuthorizationHandleUnauthorizedRequest方法中设置响应中的状态来在自定义AuthorizeAttribute中覆盖此行为。

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

But this did not work. 但这没效果。 The new pipeline must grab this response later and modify it to the same response I was getting before. 新管道必须稍后获取此响应并将其修改为我之前获得的相同响应。 Throwing an HttpException did not work either as it is just changed into a 500 error status. 抛出HttpException也不起作用,因为它只是变为500错误状态。

I tested Brock Allen's solution and it did work when I was using a jQuery ajax call. 我测试了Brock Allen的解决方案,当我使用jQuery ajax调用时它确实有效。 If it is not working for you my guess is that it is because you are using angular. 如果它不适合你,我的猜测是因为你正在使用角度。 Run your test with Fiddler and see if the following is in your header. 使用Fiddler运行测试,看看标题中是否包含以下内容。

X-Requested-With: XMLHttpRequest

If it is not then that is the problem. 如果不是那么那就是问题。 I am not familiar with angular but if it lets you insert your own header values then add this to your ajax requests and it will probably start working. 我不熟悉angular,但是如果它允许你插入自己的头值,那么将它添加到你的ajax请求中它可能会开始工作。

I got the same situation when OWIN always redirects 401 response to Login page from WebApi.Our Web API supports not only ajax calls from Angular but also Mobile, Win Form calls. 当OWIN总是从WebApi重定向401对Login页面的响应时,我得到了相同的情况。我们的Web API不仅支持来自Angular的ajax调用,还支持Mobile,Win Form调用。 Therefore, the solution to check whether the request is ajax request is not really sorted for our case. 因此,检查请求是否是ajax请求的解决方案实际上并没有针对我们的情况进行排序。

I have opted another approach is to inject new header response: Suppress-Redirect if responses come from webApi. 我选择了另一种方法是注入新的标头响应:如果响应来自webApi,则Suppress-Redirect The implementation is on handler: 实现是在处理程序上:

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);
    }
}

And register this handler in global level of WebApi: 并在WebApi的全局级别注册此处理程序:

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

So, on OWIN startup you are able to check whether response header has Suppress-Redirect : 因此,在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;
}

In previous versions of ASP.NET, you had to do a whole bunch of stuff to get this working. 在以前的ASP.NET版本中,您必须完成大量工作才能实现此功能。

The good news is, since you are using ASP.NET 4.5. 好消息是,因为您使用的是ASP.NET 4.5。 you can disable forms authentication redirect using the new HttpResponse.SuppressFormsAuthenticationRedirect property. 您可以使用新的HttpResponse.SuppressFormsAuthenticationRedirect属性禁用表单身份验证重定向。

In Global.asax : Global.asax

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

EDIT : You might also want to take a look at this article by Sergey Zwezdin which has a more refined way of accomplishing what you are trying to do. 编辑 :您可能还想看看Sergey Zwezdin的这篇文章 ,它有一种更精致的方式来完成你想要做的事情。

Relevant code snippets and author narration pasted below. 粘贴在下面的相关代码片段和作者旁白。 Original Author of code and narration -- Sergey Zwezdin . 代码和旁白的原作者 - 谢尔盖兹韦兹丁

First – let's determine whether current HTTP-request is AJAX-request. 首先 - 让我们确定当前的HTTP请求是否是AJAX请求。 If yes, we should disable replacing HTTP 401 with HTTP 302: 如果是,我们应该禁用用HTTP 302替换HTTP 401:

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);
    }
}

Second – let's add a condition:: if user authenticated, then we will send HTTP 403; 第二 - 让我们添加一个条件::如果用户通过身份验证,那么我们将发送HTTP 403; and HTTP 401 otherwise. 否则,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);
    }
}

Well done. 做得好。 Now we should replace all usings of standard AuthorizeAttribute with this new filter. 现在我们应该用这个新过滤器替换标准AuthorizeAttribute的所有使用。 It may be not applicable for sime guys, who is aesthete of code. 它可能不适用于sime的人,他们是代码的美学家。 But I don't know any other way. 但我不知道其他任何方式。 If you have, let's go to comments, please. 如果你有,请让我们去评论。

The last, what we should to do – to add HTTP 401/403 handling on a client-side. 最后一点,我们应该做什么 - 在客户端添加HTTP 401/403处理。 We can use ajaxError at jQuery to avoid code duplication: 我们可以在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.");
});

The result – 结果 -

  • If user is not authenticated, then he will be redirected to a login page after any AJAX-call. 如果用户未经过身份验证,那么在任何AJAX调用之后,他将被重定向到登录页面。
  • If user is authenticated, but have no enough permissions, then he will see user-friendly erorr message. 如果用户已通过身份验证,但没有足够的权限,那么他将看到用户友好的erorr消息。
  • If user is authenticated and have enough permissions, the there is no any errors and HTTP-request will be proceeded as usual. 如果用户经过身份验证并具有足够的权限,则不会出现任何错误,并且将像往常一样继续执行HTTP请求。

If you are running your Web API from within your MVC project, you'll need to create a custom AuthorizeAttribute to apply to your API methods. 如果从MVC项目中运行Web API ,则需要创建自定义AuthorizeAttribute以应用于API方法。 Within the IsAuthorized override you need to grab the current HttpContext in order prevent the redirection, like this: 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);
    }

Using Azure Active Directory integration myself, the approach using the CookieAuthentication middleware didn't work for me. 自己使用Azure Active Directory集成,使用CookieAuthentication中间件的方法对我不起作用。 I had to do the following: 我必须做以下事情:

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

If the request comes from the browser itself (and not an AJAX call, for instance) then the Accept header will contain the string html in it somewhere. 如果请求来自浏览器本身(例如,而不是AJAX调用),那么Accept标头将在其中包含字符串html Only when the client accepts HTML I will consider a redirect something useful. 只有当客户端接受HTML时,我才会考虑重定向一些有用的东西。

My client application can handle the 401 informing the user that the app has no more access and needs to reload to login again. 我的客户端应用程序可以处理401,通知用户该应用程序没有更多访问权限,需要重新加载才能再次登录。

I also had an MVC5 application (System.Web) with WebApi (using OWIN) and just wanted to prevent 401 responses from WebApi being changed to 302 responses. 我还有一个带WebApi的MVC5应用程序(System.Web)(使用OWIN),只是想阻止来自WebApi的401响应被更改为302响应。

What worked for me was to create a customised version of the WebApi AuthorizeAttribute like this: 对我有用的是创建WebApi AuthorizeAttribute的自定义版本,如下所示:

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

And to use it in place of the standard WebApi AuthorizeAttribute. 并使用它代替标准WebApi AuthorizeAttribute。 I used the standard MVC AuthorizeAttribute to keep the MVC behaviour unchanged. 我使用标准MVC AuthorizeAttribute来保持MVC行为不变。

if you want to catch Content-Type == application/json you can use that code: 如果你想捕获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;

    }

regards!! 问候!!

After much fuss trying to avoid the redirections to the login page, I realised that this is actually quite appropriate for the Authorise attribute. 在试图避免重定向到登录页面之后,我意识到这实际上非常适合Authorize属性。 It is saying go and get Authorisation. 它说要去获得授权。 Instead for Api calls which are not authorised, I just wanted not to reveal any information to would be hackers. 相反,对于未经授权的Api呼叫,我只是想不透露任何信息是黑客。 This objective was easier to achieve directly by adding a new attribute derived off Authorize which instead hides the content as a 404 error: 通过添加从Authorize派生的新属性可以直接实现此目标,而该属性将内容隐藏为404错误:

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

I was having a hard time getting both the status code and a text response working in the OnAuthorization/HandleUnauthorizedRequest methods. 我很难在OnAuthorization / HandleUnauthorizedRequest方法中获取状态代码和文本响应。 This turned out to be the best solution for me: 这对我来说是最好的解决方案:

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

Just install following NeGet Package 只需安装以下NeGet包即可

Install-Package Microsoft.AspNet.WebApi.Owin 安装包Microsoft.AspNet.WebApi.Owin

Write following code in WebApiConfig file. 在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"));
    }
}

Thanks guys! 多谢你们!

In my case, I combined cuongle & Shiva 's answers, and got something like this: 在我的例子中,我结合了cuongleShiva的答案,得到了这样的结论

In Controller's OnException() handler for API Exceptions: 在Controller的API例外的OnException()处理程序中:

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

In App startup config code: 在App启动配置代码中:

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);
                }
            }
        }
    });

Mixing MVC and WebAPI, if the request is unauthorized then it will redirect to login page even in WebAPI request also. 混合使用MVC和WebAPI,如果请求未经授权,则即使在WebAPI请求中也会重定向到登录页面。 For that, we can add below code to send a response to mobile application 为此,我们可以添加以下代码来向移动应用程序发送响应

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();
}

在使用Dot Net Framework 4.5.2的MVC 5中,我们在“Accept”标题下获得“application / json,plaint text ..”这将是很好用的如下:

isJson = headers["Content-Type"] == "application/json" || headers["Accept"].IndexOf("application/json", System.StringComparison.CurrentCultureIgnoreCase) >= 0;

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM