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

时间:2014-06-26 16:16:57

标签: angularjs cors owin asp.net-web-api2

我正在使用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方法获得一个null请求。这是错误堆栈跟踪的开始:

{"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

有人可以解释一下吗?

1 个答案:

答案 0 :(得分:0)

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

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

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

  2. 添加覆盖,它只是通过允许所有内容响应OPTIONS请求(在我的测试环境中这没问题):

    protected void Application_BeginRequest(object sender,EventArgs e) {   if((Request.Headers.AllKeys.Contains(“Origin”))&amp;&amp;       (Request.HttpMethod ==“OPTIONS”))   {       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();
    

    } }

  3. 装饰帐户控制器方法的属性只是RouteAttribute

    〔路线( “的UserRole”)] public string [] GetUserRoles() {   string id = User.Identity.GetUserId();   Debug.Assert(id!= null);   string [] aRoles = UserManager.GetRoles(id).ToArray();   返回aRoles; }

  4. 这样OPTIONS请求得到了正确的响应,连续的GET成功。

    <强> ADDITION

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

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

    我的CorsAllowOriginHandlerDelegatingHandler,只是确保在请求包含*标头的每个响应中都包含值为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;
        }
    }