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

时间:2013-11-22 16:28:23

标签: c# asp.net-web-api asp.net-mvc-5 asp.net-identity owin

如何配置我的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);
        });  
    }]
);

所以我没有登录,第一个方法成功返回数据。第二种方法返回(在成功函数中)包含等效登录页面的数据。也就是说,如果你请求一个标有[授权]的控制器动作并且你没有登录,你会在mvc中获得什么。

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

更新:由于下面的建议似乎都不再有效(对身份或WebAPI的更改)我在github上创建了一个原始示例,该示例应说明问题。

15 个答案:

答案 0 :(得分:113)

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

答案 1 :(得分:80)

如果您在asp.net MVC网站中添加asp.net WebApi,您可能希望对某些请求进行未经授权的响应。但是随后ASP.NET基础设施发挥作用,当您尝试将响应状态代码设置为HttpStatusCode.Unauthorized时,您将获得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);
}

答案 2 :(得分:79)

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

2013年11月26日更新

因为Brock Allen指出in his article,MVC 5似乎发生了翻天覆地的变化。我猜OWIN管道接管并引入了一些新的行为。现在,当用户未获得授权时,将返回状态200,并在HTTP标头中包含以下信息。

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

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

我尝试通过在 OnAuthorization HandleUnauthorizedRequest 方法中的响应中设置状态,在自定义 AuthorizeAttribute 中覆盖此行为。

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

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

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

X-Requested-With: XMLHttpRequest

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

答案 3 :(得分:24)

当OWIN总是从WebApi重定向401对Login页面的响应时,我遇到了同样的情况。我们的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;
}

答案 4 :(得分:15)

在以前的ASP.NET版本中,您必须do a whole bunch of stuff才能使其正常工作。

好消息是,因为您使用的是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的at this article,它有更精致的方式来完成您想要做的事情。

下面粘贴了相关的代码段和作者旁白。代码和旁白的原作者 - Sergey Zwezdin

首先 - 让我们确定当前的HTTP请求是否是AJAX请求。如果是,我们应该禁用用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);
    }
}

第二 - 让我们添加条件::如果用户通过身份验证,那么我们将发送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请求。

答案 5 :(得分:9)

如果您在Web API项目中运行MVC,则需要创建自定义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);
    }

答案 6 :(得分: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,通知用户该应用程序没有更多访问权限,需要重新加载才能再次登录。

答案 7 :(得分:3)

我还有一个带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行为不变。

答案 8 :(得分: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;

    }

问候!!

答案 9 :(得分:1)

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

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

答案 10 :(得分:1)

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

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

答案 11 :(得分:1)

只需安装以下NeGet Package

即可

安装包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"));
    }
}

答案 12 :(得分: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);
                }
            }
        }
    });

答案 13 :(得分:0)

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

答案 14 :(得分:-1)

在使用Dot Net Framework 4.5.2的MVC 5中,我们得到了 &#34; application / json,plaint text ..&#34;在&#34;接受&#34;头 如下使用会很好:

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