具有WCF和Windows身份验证的CORS

CORS with WCF and windows authentication

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

我的情况:

  • 我已经设置了通过webHttpBinding公开的自托管WCF服务。

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

  • HTML页面(将称为WCF服务)是从与WCF服务位于同一台机器但位于不同端口上的Web服务器提供的。这意味着我需要CORS支持才能在Firefox,Chrome,...

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

  • W3C规定在这种情况下应使用CORS。
    简而言之,这意味着浏览器将检测到我正在执行跨域请求。在将请求实际发送到我的WCF服务之前,它将向我的WCF服务URL发送一个所谓的"预检"请求。此预检请求使用HTTP方法" OPTIONS",并询问是否允许原始URL(=为我的HTML服务的Web服务器)将请求发送到我的服务URL。然后,浏览器在将实际请求发送到我的WCF服务之前需要HTTP 200响应(=" OK")。我服务的其他任何答复都将阻止实际请求的发送。

    目前,CORS尚未内置到WCF中,因此我使用WCF扩展点来添加CORS兼容性。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    <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>
             
            </baseAddresses>
          </host>
        </service>
      </services>
    </system.serviceModel>

    我已经实现了一个IDispatchMessageInspector,可以回复飞行前消息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    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调用服务:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    $.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请求失败,并且实际的请求可能未提交...

    我不知道如何使这种(简单的)情况发挥作用。谁能帮忙吗?


    尤里卡(一种)。似乎Firefox不喜欢我为服务指定的"协商"身份验证。当我将身份验证方案从"协商,匿名"更改为" Ntlm,匿名"时,它似乎起作用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    <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>
             
            </baseAddresses>
          </host>
        </service>
      </services>
    </system.serviceModel>

    我认为Firefox支持"协商"方案...任何人都知道为什么它不起作用吗?


    再往前一点。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    <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>
             
            </baseAddresses>
          </host>
        </service>
      </services>
    </system.serviceModel>

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

    然后,我想修改我的IDispatchMessageInspector以对除飞行前以外的任何请求强制执行身份验证:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    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。现在,对于Firefox来说,预检是可以的,但是当实际请求中不包含用户凭据时,在我回复了HTTP 401之后,Firefox似乎没有重新提交请求。实际上,我希望Firefox能够立即将凭据与GET请求一起发送(就像我在jQuery AJAX请求中添加" withCredentials:true"一样;尽管如此,Chrome似乎确实能够正确执行此操作)。

    我在做什么错?