Cross origin SignalR connection stops after negoti

2019-03-28 06:28发布

问题:

I have an MVC 5 app serving up views, and a Web API 2 app as the service layer (.NET 4.5). The Web API app uses SignalR 2.1.2 to return progress as it's processing POSTs to the service API. The two are deployed to different domains, so I've set up cross origin support as per the asp.net tutorial article.

[assembly: OwinStartup(typeof (Startup))]
namespace MyApp.Service
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.Map("/signalr", map =>
            {
                //worry about locking it down to specific origin later
                map.UseCors(CorsOptions.AllowAll);
                map.RunSignalR(new HubConfiguration());
            });
            //now start the WebAPI app
            GlobalConfiguration.Configure(WebApiConfig.Register);
        }
    }
}

WebApiConfig.cs also contains its own CORS declaration.

namespace MyApp.Service
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            //controller invocations will come from the MVC project which is deployed to a
            //different domain, so must enable cross origin resource sharing
            config.EnableCors();
            // Web API routes
            config.MapHttpAttributeRoutes();

            //Snip other controller dependency initialisation
        }
    }
}

I've defined a simple hub class with no server-side API (it's only to allow the server to push to the clients, not for the clients to call into).

namespace MyApp.Service.Hubs
{
    [HubName("testresult")]
    public class TestResultHub : Hub
    {
    }
}

Since I'm going cross-domain AND the hub is not exposing any server side API, I'm not bothering to use a generated JS proxy.

The relevant bits of the JS that set up the signalr hub connection is: (remember this is being served up from the MVC app, which does not have any signalr support (except jquery-signalr-{version}.js of course))

function TestScenarioHandler(signalrHubUrl) {
    var self = this;
//Snip irrelevant bits (mostly Knockout initialisation)

    self.signalrConnectionId = ko.observable();

    var hubConnection = $.hubConnection(signalrHubUrl, { useDefaultPath: false });

    var hubProxy = hubConnection.createHubProxy("testresult");
    hubProxy.on("progress", function(value) {
        console.log("Hooray! Got a new value from the server: " + value);
    });

    hubConnection.start()
        .done(function() {
            self.signalrConnectionId(hubConnection.id);
            console.log("Connected to signalr hub with connection id " + hubConnection.id);
        })
        .fail(function() {
            console.log("Failed to connect to signalr hub at " + hubConnection.url);
        });
}

Going cross-origin like this, Firefox network traffic shows (and I've confirmed Chrome shows the same thing) a GET to

http://****service.azurewebsites.net/signalr/negotiate?clientProtocol=1.5&connectionData=[{"name":"testresult"}]&_=1424419288550

Notice that the name matches the value of the HubName attribute on my hub class.

This GET returns HTTP 200, the response gives me a JSON payload containing a ConnectionId, ConnectionToken, and a bunch of other fields that suggests everything's ok. The HTTP response also has the Access-Control-Allow-Origin: header set to the domain that the GET originated from. All up it looks good, except that's where the traffic stops.

But the JS console prints "Failed to connect to signalr hub at http://****service.azurewebsites.net/signalr"

To verify I'm not doing anything too stupid, I've added signalr support and a basic hub to the MVC app (so no cross origin required), and changed the $.hubConnection() and hubConnection.createProxy() calls accordingly. When I do that, browser traffic shows the same /signalr/negotiate?... GET (obviously not cross origin any more), but then also GETs to /signalr/connect?... and /signalr/start?.... The JS console also prints a success message.

So in summary;

  • CORS is enabled on the service layer, and the signalr /negotiate GET returns 200, what appears to be a valid connection id, and the expected Access-Control-Allow-Origin: header. This suggests to me that the server-side CORS support is behaving itself correctly, but the signalr connection does not succeed.
  • When I reconfigure so the signalr connection is NOT cross origin, everything works as expected.

WTF am I missing or doing wrong?! Some conflict between HttpConfiguration.EnableCors() and IAppBuilder.UseCors(CorsOption) perhaps?

回答1:

Solved it. I had changed the map.UseCors(CorsOptions.AllowAll) to pass in a CorsPolicy object instead, and set SupportsCredentials to false, having read elsewhere that Access-Control-Allow-Origin: * is incompatible with access-control-allow-credentials: true.

private static readonly Lazy<CorsOptions> SignalrCorsOptions = new Lazy<CorsOptions>(() =>
{
    return new CorsOptions
    {
        PolicyProvider = new CorsPolicyProvider
        {
            PolicyResolver = context =>
            {
                var policy = new CorsPolicy();
                policy.AllowAnyOrigin = true;
                policy.AllowAnyMethod = true;
                policy.AllowAnyHeader = true;
                policy.SupportsCredentials = false;
                return Task.FromResult(policy);
            }
        }
    };
});

public void Configuration(IAppBuilder app)
{
    app.Map("/signalr", map =>
    {
        map.UseCors(SignalrCorsOptions.Value);
        map.RunSignalR(new HubConfiguration());
     });
     //now start the WebAPI app
     GlobalConfiguration.Configure(WebApiConfig.Register);
}

Setting SupportCredentials to true results in the Access-Control-Allow-Origin header being rewritten with the actual origin (not *) and access-control-allow-credentials: true in the response.

And now it works.