代码之家  ›  专栏  ›  技术社区  ›  Gilles Hemberg

具有WCF和windows身份验证的CORS

  •  3
  • Gilles Hemberg  · 技术社区  · 11 年前

    在强制执行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发送一个所谓的“preflight”请求。这个飞行前请求使用HTTP方法“OPTIONS”,并询问是否允许始发URL(=为我的HTML提供服务的Web服务器)将请求发送到我的服务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(未经授权)进行回复,该HTTP 401指示要使用的认证方法。然后,浏览器通常会重新提交包括用户凭据在内的请求(之后,请求将正常进行)。

    不幸的是,W3C似乎已经表示证书不应该传递到CORS飞行前消息中。因此,WCF以HTTP 401进行回复。Chrome似乎确实以某种方式发送了飞行前请求标头中的凭据(根据W3C规范,这实际上是不正确的),而Firefox则没有。 此外,W3C只识别对飞行前请求的HTTP 200响应:任何其他响应(如我收到的HTTP 401)都意味着CORS请求失败,实际请求可能无法提交。。。

    我不知道如何让这个(简单的)场景发挥作用。有人能帮忙吗?

    2 回复  |  直到 11 年前
        1
  •  0
  •   Gilles Hemberg    11 年前

    还有一段路要走。

    使用.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,以强制对除preflights之外的任何请求进行身份验证:

    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似乎做得很正确)。

    我做错了什么?

        2
  •  0
  •   Gilles Hemberg    11 年前

    尤里卡(有点)。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支持“协商”计划。。。有人知道为什么它不起作用吗?