ASP.NET WebAPI2 CORS:预检时 GetOwinContext 中的空请求

Posted

技术标签:

【中文标题】ASP.NET WebAPI2 CORS:预检时 GetOwinContext 中的空请求【英文标题】:ASP.NET WebAPI2 CORS: null request in GetOwinContext on preflight 【发布时间】:2014-08-17 14:28:07 【问题描述】:

我正在创建一个带有 WebAPI2 后端的 AngularJS (Typescript) SPA,需要来自 API 的身份验证和授权。 API 托管在不同的服务器上,因此我使用 CORS,主要遵循 http://www.codeproject.com/Articles/742532/Using-Web-API-Individual-User-Account-plus-CORS-En 上的指导,因为我是该领域的新手。

一切正常,我可以注册和登录,然后通过在客户端传递接收到的访问令牌,向受限访问控制器操作(这里是默认 VS WebAPI 2 模板中的虚拟“值”控制器)发出请求 -使用此相关代码的辅助服务:

private buildHeaders() 
    if (this.settings.token) 
        return  "Authorization": "Bearer " + this.settings.token ;
    
    return undefined;


public getValues(): ng.IPromise<string[]> 
    var deferred = this.$q.defer();
    this.$http(
        url: this.config.rootUrl + "api/values",
        method: "GET",
        headers: this.buildHeaders(),
    ).success((data: string[]) => 
        deferred.resolve(data);
    ).error((data: any, status: any) => 
        deferred.reject(status.toString() + " " +
            data.Message + ": " +
            data.ExceptionMessage);
    );
    return deferred.promise;

现在,我想在登录后检索用户的角色,以便 AngularJS 应用程序能够做出相应的行为。因此,我在我的帐户 API 中添加了此方法(在类级别具有属性 [Authorize][RoutePrefix("api/Account")][EnableCors(origins: "*", headers: "*", methods: "*")]* 用于测试目的):

[Route("UserRoles")]
public string[] GetUserRoles()

    return UserManager.GetRoles(User.Identity.GetUserId()).ToArray();

然后我将此代码添加到我的登录控制器:

private loadUserRoles() 
    this.accountService.getUserRoles()
        .then((data: string[]) => 
            // store roles in an app-settings service
            this.settings.roles = data;
        , (reason) => 
            this.settings.roles = [];
        );


public login() 
    if ((!this.$scope.name) || (!this.$scope.password)) return;

    this.accountService.loginUser(this.$scope.name,
            this.$scope.password)
        .then((data: ILoginResponseModel) => 
            this.settings.token = data.access_token;
            // LOAD ROLES HERE
            this.loadUserRoles();
        , (reason) => 
            this.settings.token = null;
            this.settings.roles = [];
        );

帐户控制器的方法在哪里:

public getUserRoles() : ng.IPromise<string[]> 
    var deferred = this.$q.defer();
    this.$http(
        url: this.config.rootUrl + "api/account/userroles",
        method: "GET",
        headers: this.buildHeaders()
    ).success((data: string[]) => 
        deferred.resolve(data);
    ).error((data: any, status: any) => 
        deferred.reject(status.toString() + ": " +
            data.error + ": " +
            data.error_description);
    );
    return deferred.promise;            

无论如何,这都会触发 OPTIONS 预检请求,进而导致 500 错误。如果我检查响应,我可以看到 GetOwinContext 方法收到一个空请求。这是错误堆栈跟踪的开头:

"message":"An error has occurred.","exceptionMessage":"Value cannot be null.\r\nParameter name: request","exceptionType":"System.ArgumentNullException","stackTrace":" at System.Net.Http.OwinHttpRequestMessageExtensions.GetOwinContext(HttpRequestMessage request)\r\n at Accounts.Web.Controllers.AccountController.get_UserManager() ...

然而,我用于获取角色的代码与我用于从 WebAPI 测试控制器获取虚拟“值”的代码没有什么不同。我不能完全理解为什么需要预检的原因,但无论如何我都会在 OWIN 代码中遇到这个讨厌的异常。

我的请求标头是(API 位于端口 49592):

OPTIONS /api/account/userroles HTTP/1.1 Host: localhost:49592 Connection: keep-alive Access-Control-Request-Method: GET Origin: http://localhost:64036 User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (Khtml, like Gecko) Chrome/35.0.1916.153 Safari/537.36 Access-Control-Request-Headers: accept, authorization Accept: */* Referer: http://localhost:64036/ Accept-Encoding: gzip,deflate,sdch Accept-Language: en-US,en;q=0.8,it;q=-5.4

谁能解释一下?

【问题讨论】:

你确定 User.Identity.GetUserId() 有值吗? 我无法在此处设置断点,异常在我的代码之外和之前由管道中的 OWIN 内容引发。我也这么认为,但后来我看到断点永远不会被命中,并且堆栈跟踪指向参数“request”为空的那个 OWIN 方法。 我必须补充一点,我尝试了以下操作:(a) 按照***.com/questions/13624386/… 中的建议,我添加了帖子中指定的 Application_BeginRequest,但这会触发另一个异常:System.Web.HttpException: Server cannot在发送 HTTP 标头后设置状态; (b)按照文档中的建议,我用 [AcceptVerbs(new[] "GET")] 修饰了控制器操作,没有 OPTION 以避免 MVC 与 OPTIONS 请求混淆,但这似乎没有任何效果。还有其他想法吗? 您是否确保在 web.config 中已将允许的动词设置为包含选项? 谢谢,我的 web.config 基本上没有受到 webapi 标准模板的影响。我能找到的对 OPTIONS 的唯一引用是在 system.webServer/handlers 下:。我尝试删除它,但没有任何改变。 【参考方案1】:

我想我找到了某种可行的解决方案,即使它看起来有些脏,但至少它有效。我将它发布在这里,以便其他人最终可以利用它,但我愿意接受建议。 (抱歉格式不好,但我尝试了几次,编辑器不允许我正确标记代码)。

基本上,解决方案是由这篇帖子的答案提出的:Handling CORS Preflight requests to ASP.NET MVC actions,但我更改了对我不起作用的代码(WebAPI 2 和 .NET 4.5.1)。就是这样:

    Global.asax,方法Application_Start,添加BeginRequest += Application_BeginRequest;

    添加覆盖,它只是通过允许一切来响应OPTIONS 请求(这在我的测试环境中是可以的):

    protected void Application_BeginRequest(object sender, EventArgs e) if ((Request.Headers.AllKeys.Contains("Origin")) && (Request.HttpMethod == "选项")) Response.StatusCode = 200; Response.Headers.Add("Access-Control-Allow-Origin", "*"); Response.Headers.Add("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE");

      string sRequestedHeaders = String.Join(", ",
          Request.Headers.GetValues("Access-Control-Request-Headers") ?? new string[0]);
      if (!String.IsNullOrEmpty(sRequestedHeaders))
          Response.Headers.Add("Access-Control-Allow-Headers", sRequestedHeaders);
    
      Response.End();
    

    装饰accounts控制器方法的属性就是RouteAttribute

    [路线(“用户角色”)] 公共字符串[] GetUserRoles() 字符串 id = User.Identity.GetUserId(); Debug.Assert(id != null); string[] aRoles = UserManager.GetRoles(id).ToArray(); 返回一个角色;

这样OPTIONS 请求会得到正确的响应,并且后续的 GET 会成功。

添加

我还必须补充一点,EnableCors 属性是不够的,因为我们不仅要处理 OPTIONS 动词,还要确保任何 CORS 请求都获得 Access-Control-Allow-Origin 标头。否则,您可能会观察到明显正确的响应(代码 200 等),但会看到 $http 调用失败。在我的情况下,我添加到 global.asax 这一行:

GlobalConfiguration.Configuration.MessageHandlers.Add(new CorsAllowOriginHandler());

我的CorsAllowOriginHandler 是一个DelegatingHandler,它只是确保这个值为* 的标头出现在请求包含Origin 标头的每个响应中:

public sealed class CorsAllowOriginHandler : DelegatingHandler

    protected async override Task<HttpResponseMessage> SendAsync
        (HttpRequestMessage request, CancellationToken cancellationToken)
    
        HttpResponseMessage response = await base.SendAsync(request, cancellationToken);

        // all CORS-related headers must contain the Access-Control-Allow-Origin header,
        // or the request will fail. The value may echo the Origin request header, 
        // or just be `*`.
        if ((request.Headers.Any(h => h.Key == "Origin")) &&
            (response.Headers.All(h => h.Key != "Access-Control-Allow-Origin")))
        
            response.Headers.Add("Access-Control-Allow-Origin", "*");
        
        return response;
    

【讨论】:

以上是关于ASP.NET WebAPI2 CORS:预检时 GetOwinContext 中的空请求的主要内容,如果未能解决你的问题,请参考以下文章

处理对 ASP.NET MVC 操作的 CORS 预检请求

ASP.NET Core:带有 OPTIONS 异常的 Windows 身份验证(CORS 预检)

来自 ASP NET Web API 的 CORS 阻止 Ajax POST 调用 - 对预检请求的响应未通过访问控制检查

使用 Windows 身份验证启用 CORS ASP.NET Web API 2 应用程序中的预检请求

React + ASP.Net Core 3:CORS 对预检请求的响应未通过访问控制检查:没有“Access-Control-Allow-Origin”标头

ASP.NET WebAPI2 和 AngularJS