CORS与WCF和Windows身份验证

时间:2013-10-01 19:23:37

标签: wcf windows-authentication cors

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

我的情景:

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

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

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

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

  5. W3C规定在这种情况下应使用CORS。 简单地说,这意味着浏览器将检测到我正在进行跨域请求。在将请求实际发送到我的WCF服务之前,它会向我的WCF服务URL发送所谓的“预检”请求。此预检请求使用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(我收到一个消息框说“Yay!”),但不是在Firefox中。在Firefox中,我得到一个“不!”和HTTP 401(未经授权)错误。

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

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

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

2 个答案:

答案 0 :(得分: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。预检现在可以用于Firefox了,但是当我在回复HTTP 401后,当实际请求不包含用户凭据时,Firefox似乎没有重新提交请求。事实上,我希望Firefox能够立即发送凭据和GET请求(因为我在我的jQuery AJAX请求中添加了“withCredentials:true”;但Chrome似乎确实这样做了。)

我做错了什么?

答案 1 :(得分:0)

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

<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支持“谈判”计划......任何人都知道为什么它不起作用?