为Web Api 2和OWIN令牌认证启用CORS

33

我有一个ASP.NET MVC 5网站项目(localhost:81),通过Knockoutjs调用我的WebApi 2项目(localhost:82)中的函数,为了让这两个项目之间通信,我启用了CORS。到目前为止一切正常,直到我尝试实现OWIN令牌身份验证到WebApi。

要使用WebApi上的/token端点,我还需要在该端点上启用CORS,但是经过数小时的尝试和寻找解决方案,它仍然无法工作,api/token仍然会导致以下结果:

XMLHttpRequest cannot load http://localhost:82/token. No 'Access-Control-Allow-Origin' header is present on the requested resource. 
public void Configuration(IAppBuilder app)
{
    app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
    TokenConfig.ConfigureOAuth(app);
    ...
}

令牌配置

public static void ConfigureOAuth(IAppBuilder app)
{
    app.CreatePerOwinContext(ApplicationDbContext.Create);
    app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);

    OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
    {
        AllowInsecureHttp = true,
        TokenEndpointPath = new PathString("/token"),
        AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
        Provider = new SimpleAuthorizationServerProvider()
    };

    app.UseOAuthAuthorizationServer(OAuthServerOptions);
    app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}

授权提供程序

public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
    context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });

    var appUserManager = context.OwinContext.GetUserManager<AppUserManager>();
    IdentityUser user = await appUserManager.FindAsync(context.UserName, context.Password);

    if (user == null)
    {
        context.SetError("invalid_grant", "The user name or password is incorrect.");
        return;
    }
    ... claims
}

身份验证配置

public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context)
{
    // Tried to enable it again without success. 
    //context.Response.Headers.Add("Access-Control-Allow-Origin", new[] {"*"});
    
    var manager = new AppUserManager(new UserStore<AppUser>(context.Get<ApplicationDbContect>()));
        
    ...

    var dataProtectionProvider = options.DataProtectionProvider;
    if (dataProtectionProvider != null)
    {
        manager.UserTokenProvider =
                new DataProtectorTokenProvider<AppUser>(dataProtectionProvider.Create("ASP.NET Identity"));
    }
    return manager;
}

编辑:

1. 重要提示是可以直接打开端点 (localhost:82/token)。

2. 从 web 项目调用 API (localhost:82/api/..) 也可以正常工作,因此 WebApi 启用了 CORS。


请查看此链接:http://www.asp.net/web-api/overview/security/enabling-cross-origin-requests-in-web-api - rashfmnb
2
我读了那篇文章几遍,它帮助我为Web Api启用CORS,问题是还需要为令牌端点启用它。 - Sam
谢谢,但是是已经在那里了,如果你看一下我的示例代码,AuthorizationProvider.cs(在教程中为ApplicationOAuthProvider)已经包含了那行代码。 - Sam
1
谢谢,但我在另一个答案中读到,当使用OWIN时,app.UseCors(CorsOptions.AllowAll)会替换EnableCors():https://dev59.com/T18d5IYBdhLWcg3wiSjI#32294445 - Sam
4
请尝试将 app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll); 放在 TokenConfig.ConfigureOAuth(app); 之后。TokenConfig 对上下文做了很多不好的事情。 - Marcus Höglund
显示剩余4条评论
3个回答

53

我知道你的问题已经在评论中得到解决,但我认为了解是什么导致这个问题以及如何解决整个类似的问题是非常重要的。

从你的代码中可以看出,在Token终端点上,你设置了Access-Control-Allow-Origin头超过一次:

app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);

GrantResourceOwnerCredentials方法内部:

context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" }); 

查看CORS规范后发现,这本身就是个问题,因为:

如果响应包含零个或多个Access-Control-Allow-Origin头值,则返回失败并终止此算法。

在您的场景中,框架设置了此标头两次,根据CORS必须实现的方式,这将导致在某些情况下(可能与客户端相关)删除该标头。

这也得到了以下问题的确认:Duplicate Access-Control-Allow-Origin: * causing COR error?

因此,将调用app.UseCors移动到ConfigureOAuth调用之后允许设置CORS标头仅一次(因为OAuth中间件中断了owin管道,并且永远不会到达Token端点的Microsoft CORS中间件),并使您的Ajax调用正常工作。

为了更好和全局的解决方案,您可以尝试在OAuth中间件调用之前再次放置app.UseCors,并删除GrantResourceOwnerCredentials中的第二个Access-Control-Allow-Origin插入。


我尝试了一下,可以确认你的全局解决方案有效(即在GrantResourceOwnerCredentials中删除Access-Control-Allow-Origin)。感谢您提供这个解释清晰的答案。 - Sam

11

按照以下步骤进行操作,您就可以让API正常工作:

  1. Remove any code like config.EnableCors(), [EnableCors(header:"*"....)] from your API.
  2. Go to startup.cs and add below line

    app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
    

之前

    ConfigureAuth(app);

您还需要安装 Microsoft.Owin.Cors 包来使用此功能。

如果有人对“before ConfigureAuth(app)”感到困惑,因为你正在ConfigureAuth内设置Cors(虽然这可能是错误的),真正的重点是它需要在设置令牌端点之前进行设置(例如app.UseOAuthAuthorizationServer和/或app.UseOAuthBearerTokens等)。 - bech

2

解决问题,不使用app.UseCors()

我曾经遇到同样的问题。我使用了 Vue.Js 客户端axois 访问我的 REST-API,还需要进行跨域访问。但是在我的 Owin-Api-Server 中,由于与其他第三方组件的版本冲突,我无法添加 Microsoft.Owin.Cors nuget 包。因此,我无法使用 app.UseCors() 方法,但是通过使用中间件管道,我解决了这个问题。

private IDisposable _webServer = null;

public void Start(ClientCredentials credentials)
{
    ...
    _webServer = WebApp.Start(BaseAddress, (x) => Configuration(x));
    ...
}

public void Configuration(IAppBuilder app)
{
    ...
    // added middleware insted of app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
    app.Use<MyOwinMiddleware>();
    app.UseWebApi(config);
    ...
}

public class MyOwinMiddleware : OwinMiddleware
{
    public MyOwinMiddleware(OwinMiddleware next) :
        base(next)
    { }

    public override async Task Invoke(IOwinContext context)
    {
        var request = context.Request;
        var response = context.Response;

        response.OnSendingHeaders(state =>
        {
            var resp = (IOwinResponse)state;

            // without this headers -> client apps will be blocked to consume data from this api
            if (!resp.Headers.ContainsKey("Access-Control-Allow-Origin"))
                resp.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
            if (!resp.Headers.ContainsKey("Access-Control-Allow-Headers"))
                resp.Headers.Add("Access-Control-Allow-Headers", new[] { "*" });
            if (!resp.Headers.ContainsKey("Access-Control-Allow-Methods"))
                resp.Headers.Add("Access-Control-Allow-Methods", new[] { "*" });

            // by default owin is blocking options not from same origin with MethodNotAllowed
            if (resp.StatusCode == (int)HttpStatusCode.MethodNotAllowed &&
                HttpMethod.Options == new HttpMethod(request.Method))
            {
                resp.StatusCode = (int)HttpStatusCode.OK;
                resp.ReasonPhrase = HttpStatusCode.OK.ToString();
            }

        }, response);

        await Next.Invoke(context);
    }
}

我创建了自己的中间件并操纵了响应。对于GET调用,只需要Access-Control-Allow头文件,而对于OPTIONS调用,我还需要操纵StatusCode,因为axois.post()在发送POST之前首先调用OPTIONS方法。如果OPTIONS返回StatusCode 405,则POST将永远不会被发送。
这解决了我的问题。也许这能帮助别人。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接