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

Posted

技术标签:

【中文标题】未经授权的 webapi 调用返回登录页面而不是 401【英文标题】:Unauthorised webapi call returning login page rather than 401 【发布时间】:2013-12-07 14:55:33 【问题描述】:

如何配置我的 mvc/webapi 项目,以便从 razor 视图调用的 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,以便我可以根据用户是否登录为他们显示不同的数据。理想情况下,如果用户已登录,我希望能够访问控制器的用户属性,以便返回特定于该成员的数据。

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

【问题讨论】:

【参考方案1】:

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

【讨论】:

对此的一种变体:如果您的所有 Web API 调用都通过某个路径,例如/api,可以通过路径来判断是否重定向。如果您有使用其他格式(如 JSON)的客户端,这将特别有用。将对IsAjaxRequest 的调用替换为if (!context.Request.Path.StartsWithSegments(new PathString("/api"))) 聚会迟到了,但这种方法是唯一对我有用的方法,而且似乎更“准确”。 即使晚了(r) 参加聚会,但这已被证明非常有用……令我惊讶的是,默认生成的代码如此错误,调试起来非常困难。 如果您追求 WebApi 解决方案,Manik 的回答是此处高票数评论的一个很好的替代方案。 使用 C# 6,这里是 IsAjaxRequest 的小版本:private static bool IsAjaxRequest(IOwinRequest request) return request.Query?["X-Requested-With"] == "XMLHttpRequest" || request.Headers?["X-Requested-With"] == "XMLHttpRequest"; 【参考方案2】:

如果您在 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);

【讨论】:

我更改了判别式以检查请求是否接受 text/html 或 application/xhtml 作为响应,如果不接受,我认为它是“自动”客户端请求,例如 ajax 请求 我也更喜欢这种方法。我所做的唯一补充是转换 LocalPath .ToLower() 以防他们请求“/API”或其他内容。 非常感谢。它拯救了我的一天。 :) 有人对此有好运吗?自 aspnet core 1.1 起,CookieAuthenticationOptions 不再具有 Provider 属性。 非常感谢你!正在寻找如何实现这一目标的小时数。【参考方案3】:

有两个 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 状态。

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

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

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

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

X-Requested-With: XMLHttpRequest

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

【讨论】:

我认为我正在使用 System.web.http.authorizeattribute,至少这个 webapicontroller 没有用于 system.web.mvc,并且去授权属性的定义将我发送到 system.web。 http 嗨@kevin-junghans 在这里彻底搞糊涂了。上面来自 shiva 的示例使用了一个 mvc 授权属性,我肯定不应该将其应用于 webapi 操作,来自 Brock allen 的示例似乎不起作用,或者当我单步执行时它认为它不是 ajax 请求。 只是发现了这个答案(想想 *** 不发送通知)我添加了一个 github 示例来说明问题,现在将您的修复添加到角度标题中。谢谢。但是,我可以检查的 authorize 属性中没有属性,或者您提到的原始功能不再起作用,这似乎不对。 使用 POSTMAN 和标头参数 X-Requested-With: XMLHttpRequest 对我有用...谢谢 那么,如果您打算做一个纯 Web API 项目怎么办?我正在处理其他人设置的项目,并且授权正在按照此处所述进行重定向,但是我有一个可以正常工作的不同 API 项目。一定有什么东西让它认为它是一个 MVC 应用程序而不是一个 API 应用程序,但我找不到可能是什么原因造成的。【参考方案4】:

当 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;

【讨论】:

谢谢!我们的 API 适用于除 Xamarin/android 之外的所有平台。将使用此解决方案【参考方案5】:

在以前的 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,它有一种更精致的方式来完成您正在尝试做的事情。

下面贴上相关代码sn-ps和作者旁白。代码和旁白的原作者——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 的所有使用。它可能不适用于喜欢代码的人。但我不知道其他方法。如果你有,我们去 cmets,拜托。

最后,我们应该做的——在客户端添加 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 调用之后的页面。 如果用户通过了身份验证,但没有足够的权限,那么他将看到用户友好的错误消息。 如果用户通过身份验证并拥有足够的权限,则不会出现任何错误,HTTP 请求将照常进行。

【讨论】:

我正在使用新的身份框架通过 mvc 进行身份验证。这个设置不会阻止 mvc 登录以及 webapi 调用吗? 当我检查这个例子时,似乎正在使用的授权属性是 MVC 版本而不是 WebApi 版本。但是 webapi 版本没有用于抑制表单身份验证的选项。 我的请求没有 IsAjaxRequest 方法。 Tim 看看这个 IsAjaxRequest:brockallen.com/2013/10/27/… 如果您使用 AngularJs 而不编辑标头,您将没有“XMLHttpRequest”并添加它或检查其他内容。【参考方案6】:

我自己使用 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,通知用户该应用程序没有更多访问权限,需要重新加载才能再次登录。

【讨论】:

这与针对相关问题提出的解决方案非常相似:***.com/questions/34997674/…【参考方案7】:

如果您在 MVC 项目中运行 Web API,则需要创建自定义 AuthorizeAttribute 以应用于您的 API 方法。在IsAuthorizedoverride 中,您需要获取当前的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】:

我还有一个带有 WebApi(使用 OWIN)的 MVC5 应用程序 (System.Web),只是想防止来自 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。【参考方案9】:

只需按照 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【参考方案10】:

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

【讨论】:

【参考方案11】:

如果你想捕获 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;

    

问候!!

【讨论】:

【参考方案12】:

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

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

【讨论】:

【参考方案13】:

谢谢大家!

就我而言,我结合了 cuongle 和 Shiva 的答案,得到了这样的结果:

在控制器的 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);
                
            
        
    );

【讨论】:

我认为为这个特定场景引入了“X-Requested-With: XMLHttpRequest”。进行 ajx 调用时,只需将其添加到您的标头中,服务器应该返回正确的响应。至少这是在abp.io 中处理的【参考方案14】:

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

public class HideFromAnonymousUsersAttribute : AuthorizeAttribute

    protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
    
         actionContext.Response = ActionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, "Access Restricted");
    

【讨论】:

【参考方案15】:

在带有 Dot Net Framework 4.5.2 的 MVC 5 中,我们得到了 “接受”标题下的“应用程序/json,纯文本..” 像下面这样使用会很好:

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

【讨论】:

以上是关于未经授权的 webapi 调用返回登录页面而不是 401的主要内容,如果未能解决你的问题,请参考以下文章

使用 API 路由时,在未授权时返回 Http Response 401 而不是重定向到登录页面

如何在 Ocelot 和 .Net WebApi 中覆盖 AuthorizationMiddleware 时返回未经授权的响应

如何使授权属性返回自定义 403 错误页面而不是重定向到登录页面

通过 api.php 的路由返回未经授权,而通过 web.php 的相同路由有效

当用户未经身份验证时,ASP.NET Core 5 Web API 返回 404 代码而不是 401

从角度访问webapi时出现401未经授权的错误