3

是否可以在强制执行 Windows 身份验证时处理 WCF 服务的“跨源资源共享”请求?

我的场景:

  1. 我已经设置了一个通过 webHttpBinding 公开的自托管 WCF 服务。

  2. 应该使用 jQuery 从浏览器直接调用此服务。实际上,这将限制我使用 basicHttpBinding 或 webHttpBinding。在这种情况下,我使用 webHttpBinding 来调用服务操作。

  3. HTML 页面(将调用 WCF 服务)由同一台机器上的 Web 服务器提供,但端口与 WCF 服务不同。这意味着我需要 CORS 支持才能在 Firefox、Chrome、...

  4. 调用 WCF 服务时,用户必须使用 Windows 身份验证进行身份验证。为此,我已将 webHttpBinding 配置为使用传输安全模式“TransportCredentialsOnly”。

W3C 规定在这种情况下应使用 CORS。简单地说,这意味着浏览器会检测到我在做一个跨域请求。在实际向我的 WCF 服务发送请求之前,它会向我的 WCF 服务 URL 发送一个所谓的“预检”请求。此预检请求使用 HTTP 方法“OPTIONS”并询问是否允许原始 URL(= 为我的 HTML 提供服务的网络服务器)将请求发送到我的服务 URL。然后,浏览器在将实际请求发送到我的 WCF 服务之前需要一个 HTTP 200 响应(=“OK”)。我的服务的任何其他回复都将阻止发送实际请求。

CORS 目前还没有内置到 WCF 中,所以我使用了 WCF 扩展点来添加 CORS 兼容性。

我的自托管服务的 App.Config 的服务部分:

<system.serviceModel>
  <behaviors>
    <serviceBehaviors>
      <behavior name="MyApp.DefaultServiceBehavior">
        <serviceMetadata httpGetEnabled="True"/>
        <serviceDebug includeExceptionDetailInFaults="True"/>
      </behavior>
    </serviceBehaviors>
    <endpointBehaviors>
      <behavior name="MyApp.DefaultEndpointBehavior">
        <webHttp/>
      </behavior>
    </endpointBehaviors>
  </behaviors>
  <bindings>
    <webHttpBinding>
      <binding name="MyApp.DefaultWebHttpBinding">
        <security mode="TransportCredentialOnly">
          <transport clientCredentialType="Windows"/>
        </security>
      </binding>
    </webHttpBinding>
  </bindings>
  <services>
    <service 
      name="MyApp.FacadeLayer.LookupFacade"
      behaviorConfiguration="MyApp.DefaultServiceBehavior"
      >
      <endpoint
        contract="MyApp.Services.ILookupService"
        binding="webHttpBinding"
        bindingConfiguration="MyApp.DefaultWebHttpBinding"
        address=""
        behaviorConfiguration="MyApp.DefaultEndpointBehavior"
        >
      </endpoint>
      <host>
        <baseAddresses>
          <add baseAddress="http://localhost/Temporary_Listen_Addresses/myapp/LookupService"/>
        </baseAddresses>
      </host>
    </service>
  </services>
</system.serviceModel>

我已经实现了一个 IDispatchMessageInspector 来回复预检消息:

public class CORSSupport : IDispatchMessageInspector
{
    private Dictionary<string, string> requiredHeaders;

    public CORSSupport(Dictionary<string, string> requiredHeaders)
    {
        this.requiredHeaders = requiredHeaders ?? new Dictionary<string, string>();
    }

    public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        HttpRequestMessageProperty httpRequest = request.Properties["httpRequest"] as HttpRequestMessageProperty;

        if (httpRequest.Method.ToUpper() == "OPTIONS")
            instanceContext.Abort();

        return httpRequest;
    }

    public void BeforeSendReply(ref Message reply, object correlationState)
    {
        HttpRequestMessageProperty httpRequest = correlationState as HttpRequestMessageProperty;
        HttpResponseMessageProperty httpResponse = reply.Properties["httpResponse"] as HttpResponseMessageProperty;

        foreach (KeyValuePair<string, string> item in this.requiredHeaders)
            httpResponse.Headers.Add(item.Key, item.Value);

        string origin = httpRequest.Headers["origin"];
        if (origin != null)
            httpResponse.Headers.Add("Access-Control-Allow-Origin", origin);

        if (httpRequest.Method.ToUpper() == "OPTIONS")
            httpResponse.StatusCode = HttpStatusCode.NoContent;
    }
}

此 IDispatchMessageInspector 是通过自定义 IServiceBehavior 属性注册的。

我通过 jQuery 调用我的服务,如下所示:

$.ajax(
    {
        url: 'http://localhost/Temporary_Listen_Addresses/myapp/LookupService/SomeLookup',
        type: 'GET',
        xhrFields:
            {
                withCredentials: true
            }
    }
)
.done(function () { alert('Yay!'); })
.error(function () { alert('Nay!'); });

这适用于 IE10 和 Chrome(我收到一个消息框说“耶!”),但不适用于 Firefox。在 Firefox 中,我得到一个“不!” 和 HTTP 401(未经授权)错误。

这个 401 是由于我在服务配置中设置的“Windows 身份验证”。身份验证的工作方式是浏览器首先发送一个没有任何身份验证信息的请求。然后服务器回复 HTTP 401(未经授权),指示要使用的身份验证方法。然后,浏览器通常会重新提交包含用户凭据的请求(之后请求将正常进行)。

不幸的是,W3C 似乎已表明不应将凭据传递到 CORS 预检消息中。因此,WCF 以 HTTP 401 回复。Chrome 似乎确实在预检请求标头中发送了凭据(根据 W3C 规范,这实际上是不正确的),而 Firefox 则没有。此外,W3C 仅识别对预检请求的 HTTP 200 响应:任何其他响应(例如我收到的 HTTP 401)仅表示 CORS 请求失败并且可能未提交实际请求......

我不知道如何让这个(简单)场景发挥作用。任何人都可以帮忙吗?

4

2 回答 2

0

尤里卡(有点)。Firefox 似乎不喜欢我为我的服务指定的“协商”身份验证。当我将身份验证方案从“协商,匿名”更改为“Ntlm,匿名”时,它似乎有效:

<system.serviceModel>
  <behaviors>
    <serviceBehaviors>
      <behavior name="MyApp.DefaultServiceBehavior">
        <serviceMetadata httpGetEnabled="True"/>
        <serviceDebug includeExceptionDetailInFaults="True"/>
        <serviceAuthenticationManager authenticationSchemes="Ntlm, Anonymous"/>
      </behavior>
    </serviceBehaviors>
    <endpointBehaviors>
      <behavior name="MyApp.DefaultEndpointBehavior">
        <webHttp/>
      </behavior>
    </endpointBehaviors>
  </behaviors>
  <bindings>
    <webHttpBinding>
      <binding name="MyApp.DefaultWebHttpBinding">
        <security mode="TransportCredentialOnly">
          <transport clientCredentialType="InheritedFromHost"/>
        </security>
      </binding>
    </webHttpBinding>
  </bindings>
  <services>
    <service 
      name="MyApp.FacadeLayer.LookupFacade"
      behaviorConfiguration="MyApp.DefaultServiceBehavior"
      >
      <endpoint
        contract="MyApp.Services.ILookupService"
        binding="webHttpBinding"
        bindingConfiguration="MyApp.DefaultWebHttpBinding"
        address=""
        behaviorConfiguration="MyApp.DefaultEndpointBehavior"
        >
      </endpoint>
      <host>
        <baseAddresses>
          <add baseAddress="http://localhost/Temporary_Listen_Addresses/myapp/LookupService"/>
        </baseAddresses>
      </host>
    </service>
  </services>
</system.serviceModel>

我认为 Firefox 支持“协商”方案......有人知道为什么它不起作用吗?

于 2013-10-08T16:43:00.200 回答
0

走得更远了一点。

使用 .NET 4.5,可以为单个端点支持多个身份验证方案。这使我可以同时定义 Windows 身份验证和匿名身份验证:

<system.serviceModel>
  <behaviors>
    <serviceBehaviors>
      <behavior name="MyApp.DefaultServiceBehavior">
        <serviceMetadata httpGetEnabled="True"/>
        <serviceDebug includeExceptionDetailInFaults="True"/>
        <serviceAuthenticationManager authenticationSchemes="Negotiate, Anonymous"/>
      </behavior>
    </serviceBehaviors>
    <endpointBehaviors>
      <behavior name="MyApp.DefaultEndpointBehavior">
        <webHttp/>
      </behavior>
    </endpointBehaviors>
  </behaviors>
  <bindings>
    <webHttpBinding>
      <binding name="MyApp.DefaultWebHttpBinding">
        <security mode="TransportCredentialOnly">
          <transport clientCredentialType="InheritedFromHost"/>
        </security>
      </binding>
    </webHttpBinding>
  </bindings>
  <services>
    <service 
      name="MyApp.FacadeLayer.LookupFacade"
      behaviorConfiguration="MyApp.DefaultServiceBehavior"
      >
      <endpoint
        contract="MyApp.Services.ILookupService"
        binding="webHttpBinding"
        bindingConfiguration="MyApp.DefaultWebHttpBinding"
        address=""
        behaviorConfiguration="MyApp.DefaultEndpointBehavior"
        >
      </endpoint>
      <host>
        <baseAddresses>
          <add baseAddress="http://localhost/Temporary_Listen_Addresses/myapp/LookupService"/>
        </baseAddresses>
      </host>
    </service>
  </services>
</system.serviceModel>

这样,我的 IDispatchMessageInspector 会被调用,并且我可以正确处理所有浏览器的预检消息。

然后,我想调整我的 IDispatchMessageInspector 以强制对除预检之外的任何请求进行身份验证:

public class CrossOriginResourceSharingMessageInspector : IDispatchMessageInspector
{
    private Dictionary<string, string> requiredHeaders;

    public CrossOriginResourceSharingMessageInspector(Dictionary<string, string> requiredHeaders)
    {
        this.requiredHeaders = requiredHeaders ?? new Dictionary<string, string>();
    }

    public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        HttpRequestMessageProperty httpRequestHeader = request.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;

        if (httpRequestHeader.Method.ToUpper() == "OPTIONS")
            instanceContext.Abort();

        else if (httpRequestHeader.Headers[HttpRequestHeader.Authorization] == null)
            instanceContext.Abort();

        return httpRequestHeader;
    }

    public void BeforeSendReply(ref Message reply, object correlationState)
    {
        HttpRequestMessageProperty httpRequestHeader = correlationState as HttpRequestMessageProperty;
        HttpResponseMessageProperty httpResponseHeader = reply.Properties[HttpResponseMessageProperty.Name] as HttpResponseMessageProperty;

        foreach (KeyValuePair<string, string> item in this.requiredHeaders)
            httpResponseHeader.Headers.Add(item.Key, item.Value);

        string origin = httpRequestHeader.Headers["origin"];
        if (origin != null)
            httpResponseHeader.Headers.Add("Access-Control-Allow-Origin", origin);

        string method = httpRequestHeader.Method;
        if (method.ToUpper() == "OPTIONS")
        {
            httpResponseHeader.StatusCode = HttpStatusCode.NoContent;
        }

        else if (httpRequestHeader.Headers[HttpRequestHeader.Authorization] == null)
        {
            httpResponseHeader.StatusDescription = "Unauthorized";
            httpResponseHeader.StatusCode = HttpStatusCode.Unauthorized;
        }
    }
}

同样,这似乎适用于 IE 和 Chrome,但不适用于 Firefox。Preflight 现在可以用于 Firefox,但是当实际请求不包含用户凭据时,在我回复 HTTP 401 后,Firefox 似乎没有重新提交请求。事实上,我希望 Firefox 会立即将凭据与 GET 请求一起发送(看到我在 jQuery AJAX 请求中添加了“withCredentials: true”;不过,Chrome 似乎确实做到了这一点)。

我究竟做错了什么?

于 2013-10-08T11:12:54.127 回答