2013-09-06 71 views
5

Tôi muốn có một điểm cuối SSL trong dịch vụ WCF tự lưu trữ có thể chấp nhận yêu cầu bằng thông tin xác thực cơ bản HTTP hoặc chứng chỉ ứng dụng khách.Tùy chọn chấp nhận chứng chỉ ứng dụng khách trong dịch vụ WCF tự lưu trữ

Đối với dịch vụ lưu trữ IIS, IIS phân biệt giữa "Chấp nhận chứng chỉ ứng dụng khách" và "Yêu cầu chứng chỉ ứng dụng khách".

WCF's WebHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate; có vẻ giống với cài đặt "yêu cầu chứng chỉ" trong IIS.

Có cách nào để định cấu hình dịch vụ tự lưu trữ WCF để chấp nhận thông tin đăng nhập chứng chỉ ứng dụng khách nhưng không yêu cầu chúng từ mọi ứng dụng khách không? Có một WCF tương tự của IIS "Chấp nhận chứng chỉ khách hàng" cho các dịch vụ WCF tự lưu trữ?

Trả lời

0

Tôi nghĩ rằng nó không hoạt động.

Nếu bạn không thể ảnh hưởng đến máy khách sao cho chứng chỉ trống được tạo hoặc tham chiếu chưa được gán cho chứng chỉ được chấp nhận, xác thực trường hợp đặc biệt này từ phía máy chủ và đăng nhập vào tệp nhật ký thì không có cách nào. Bạn sẽ phải bắt chước hành vi IIS và bạn sẽ phải kiểm tra trước. Đó là một dự đoán. Không có chuyên môn.

Điều bạn thường làm là a) cố gắng xác thực chứng chỉ bằng cách đi qua chuỗi chứng chỉ được cung cấp b) Trong trường hợp không có chứng chỉ nào được cung cấp kiểm tra khách hàng và đăng nhập.

Tôi nghĩ '.net' không cho bạn cơ hội để kiểm soát thương lượng.

Imo mở cửa cho người đàn ông ở giữa. Đó là lý do tại sao tôi nghĩ rằng MS không cho phép điều đó và Java tương tự, afik.

Cuối cùng, tôi quyết định đặt dịch vụ sau IIS. WCF sử dụng 'IIS' (http.sys) anyway iirc. Nó không tạo ra sự khác biệt lớn nếu bạn để IIS làm nhiều hơn một chút.

SBB là một trong số ít thư viện cho phép bạn thực hiện điều đó một cách thuận tiện. Bạn có quyền truy cập vào mọi bước của thương lượng.

Khi tôi đã sử dụng Delphi và ELDOS SecureBlackbox ('trước' WCF ... net 3.0) và nó hoạt động theo cách đó. Hôm nay bạn phải tiến hành điều tra sâu rộng về phía máy chủ và mọi người di chuyển theo hướng hai mặt.

Trong Java, bạn phải tạo TrustManager đơn giản là tin tưởng mọi thứ.

Tôi nghĩ IIS là tùy chọn còn lại.

5

Tôi tìm thấy một cách để tùy chọn chấp nhận chứng chỉ ứng dụng khách SSL trong WCF, nhưng nó yêu cầu một thủ thuật bẩn. Nếu bất cứ ai có một giải pháp tốt hơn (khác hơn là "Không sử dụng WCF") Tôi rất thích nghe nó.

Sau nhiều đào xung quanh trong dịch ngược WCF Http lớp kênh, tôi đã học được một vài điều:

  1. WCF Http là nguyên khối. Có một lớp học bezillion bay xung quanh, nhưng tất cả chúng được đánh dấu "nội bộ" và do đó không thể tiếp cận. Ngăn xếp ràng buộc kênh WCF không đáng giá một đồi đậu nếu bạn đang cố gắng ngăn chặn hoặc mở rộng các hành vi HTTP cốt lõi bởi vì những thứ mà một lớp ràng buộc mới sẽ muốn ẩn chứa trong ngăn xếp HTTP đều không thể truy cập được.
  2. WCF cưỡi trên đầu trang của HttpListener/HTTPSYS, giống như IIS.HttpListener cung cấp quyền truy cập vào chứng chỉ ứng dụng khách SSL. WCF HTTP không cung cấp bất kỳ quyền truy cập vào HttpListener cơ bản, mặc dù.

Điểm chặn gần nhất tôi có thể tìm thấy là khi HttpChannelListener (lớp bên trong) mở kênh và trả về IReplyChannel. IReplyChannel có các phương thức nhận yêu cầu mới và các phương thức đó trả lại RequestContext.

Ví dụ đối tượng thực tế được xây dựng và trả về bởi các lớp nội bộ Http cho RequestContextListenerHttpContext (lớp bên trong). ListenerHttpContext giữ tham chiếu đến một số HttpListenerContext, xuất phát từ lớp System.Net.HttpListener công khai bên dưới WCF.

HttpListenerContext.Request.GetClientCertificate() là phương pháp chúng tôi cần xem liệu có chứng chỉ ứng dụng khách có sẵn trong bắt tay SSL hay không, tải nó nếu có hoặc bỏ qua nếu không có.

Thật không may, tham chiếu đến HttpListenerContext là trường riêng tư ListenerHttpContext, do đó, để thực hiện công việc này, tôi phải sử dụng một mẹo bẩn. Tôi sử dụng sự phản chiếu để đọc giá trị của trường riêng tư để tôi có thể lấy số HttpListenerContext của yêu cầu hiện tại.

Vì vậy, dưới đây là cách tôi đã làm nó:

Đầu tiên, tạo một hậu duệ của HttpsTransportBindingElement để chúng tôi có thể ghi đè BuildChannelListener<TChannel> để đánh chặn và quấn người nghe kênh trả về bởi các lớp cơ sở:

using System; 
using System.Collections.Generic; 
using System.IdentityModel.Claims; 
using System.Linq; 
using System.Security.Claims; 
using System.Security.Cryptography.X509Certificates; 
using System.ServiceModel; 
using System.ServiceModel.Channels; 
using System.Text; 
using System.Threading.Tasks; 

namespace MyNamespace.AcceptSslClientCertificate 
{ 
    public class HttpsTransportBindingElementWrapper: HttpsTransportBindingElement 
    { 
     public HttpsTransportBindingElementWrapper() 
      : base() 
     { 
     } 

     public HttpsTransportBindingElementWrapper(HttpsTransportBindingElementWrapper elementToBeCloned) 
      : base(elementToBeCloned) 
     { 
     } 

     // Important! HTTP stack calls Clone() a lot, and without this override the base 
     // class will return its own type and we lose our interceptor. 
     public override BindingElement Clone() 
     { 
      return new HttpsTransportBindingElementWrapper(this); 
     } 

     public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context) 
     { 
      var result = base.BuildChannelFactory<TChannel>(context); 
      return result; 
     } 

     // Intercept and wrap the channel listener constructed by the HTTP stack. 
     public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context) 
     { 
      var result = new ChannelListenerWrapper<TChannel>(base.BuildChannelListener<TChannel>(context)); 
      return result; 
     } 

     public override bool CanBuildChannelFactory<TChannel>(BindingContext context) 
     { 
      var result = base.CanBuildChannelFactory<TChannel>(context); 
      return result; 
     } 

     public override bool CanBuildChannelListener<TChannel>(BindingContext context) 
     { 
      var result = base.CanBuildChannelListener<TChannel>(context); 
      return result; 
     } 

     public override T GetProperty<T>(BindingContext context) 
     { 
      var result = base.GetProperty<T>(context); 
      return result; 
     } 
    } 
} 

Tiếp theo , chúng ta cần bọc ChannelListener bị chặn bởi phần tử ràng buộc vận chuyển ở trên:

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.ServiceModel.Channels; 
using System.Text; 
using System.Threading.Tasks; 

namespace MyNamespace.AcceptSslClientCertificate 
{ 
    public class ChannelListenerWrapper<TChannel> : IChannelListener<TChannel> 
     where TChannel : class, IChannel 
    { 
     private IChannelListener<TChannel> httpsListener; 

     public ChannelListenerWrapper(IChannelListener<TChannel> listener) 
     { 
      httpsListener = listener; 

      // When an event is fired on the httpsListener, 
      // fire our corresponding event with the same params. 
      httpsListener.Opening += (s, e) => 
      { 
       if (Opening != null) 
        Opening(s, e); 
      }; 
      httpsListener.Opened += (s, e) => 
      { 
       if (Opened != null) 
        Opened(s, e); 
      }; 
      httpsListener.Closing += (s, e) => 
      { 
       if (Closing != null) 
        Closing(s, e); 
      }; 
      httpsListener.Closed += (s, e) => 
      { 
       if (Closed != null) 
        Closed(s, e); 
      }; 
      httpsListener.Faulted += (s, e) => 
      { 
       if (Faulted != null) 
        Faulted(s, e); 
      }; 
     } 

     private TChannel InterceptChannel(TChannel channel) 
     { 
      if (channel != null && channel is IReplyChannel) 
      { 
       channel = new ReplyChannelWrapper((IReplyChannel)channel) as TChannel; 
      } 
      return channel; 
     } 

     public TChannel AcceptChannel(TimeSpan timeout) 
     { 
      return InterceptChannel(httpsListener.AcceptChannel(timeout)); 
     } 

     public TChannel AcceptChannel() 
     { 
      return InterceptChannel(httpsListener.AcceptChannel()); 
     } 

     public IAsyncResult BeginAcceptChannel(TimeSpan timeout, AsyncCallback callback, object state) 
     { 
      return httpsListener.BeginAcceptChannel(timeout, callback, state); 
     } 

     public IAsyncResult BeginAcceptChannel(AsyncCallback callback, object state) 
     { 
      return httpsListener.BeginAcceptChannel(callback, state); 
     } 

     public TChannel EndAcceptChannel(IAsyncResult result) 
     { 
      return InterceptChannel(httpsListener.EndAcceptChannel(result)); 
     } 

     public IAsyncResult BeginWaitForChannel(TimeSpan timeout, AsyncCallback callback, object state) 
     { 
      var result = httpsListener.BeginWaitForChannel(timeout, callback, state); 
      return result; 
     } 

     public bool EndWaitForChannel(IAsyncResult result) 
     { 
      var r = httpsListener.EndWaitForChannel(result); 
      return r; 
     } 

     public T GetProperty<T>() where T : class 
     { 
      var result = httpsListener.GetProperty<T>(); 
      return result; 
     } 

     public Uri Uri 
     { 
      get { return httpsListener.Uri; } 
     } 

     public bool WaitForChannel(TimeSpan timeout) 
     { 
      var result = httpsListener.WaitForChannel(timeout); 
      return result; 
     } 

     public void Abort() 
     { 
      httpsListener.Abort(); 
     } 

     public IAsyncResult BeginClose(TimeSpan timeout, AsyncCallback callback, object state) 
     { 
      var result = httpsListener.BeginClose(timeout, callback, state); 
      return result; 
     } 

     public IAsyncResult BeginClose(AsyncCallback callback, object state) 
     { 
      var result = httpsListener.BeginClose(callback, state); 
      return result; 
     } 

     public IAsyncResult BeginOpen(TimeSpan timeout, AsyncCallback callback, object state) 
     { 
      var result = httpsListener.BeginOpen(timeout, callback, state); 
      return result; 
     } 

     public IAsyncResult BeginOpen(AsyncCallback callback, object state) 
     { 
      var result = httpsListener.BeginOpen(callback, state); 
      return result; 
     } 

     public void Close(TimeSpan timeout) 
     { 
      httpsListener.Close(timeout); 
     } 

     public void Close() 
     { 
      httpsListener.Close(); 
     } 

     public event EventHandler Closed; 

     public event EventHandler Closing; 

     public void EndClose(IAsyncResult result) 
     { 
      httpsListener.EndClose(result); 
     } 

     public void EndOpen(IAsyncResult result) 
     { 
      httpsListener.EndOpen(result); 
     } 

     public event EventHandler Faulted; 

     public void Open(TimeSpan timeout) 
     { 
      httpsListener.Open(timeout); 
     } 

     public void Open() 
     { 
      httpsListener.Open(); 
     } 

     public event EventHandler Opened; 

     public event EventHandler Opening; 

     public System.ServiceModel.CommunicationState State 
     { 
      get { return httpsListener.State; } 
     } 
    } 

} 

Tiếp theo, chúng ta cần ReplyChannelWrapper để thực hiện IReplyChannel và đánh chặn các cuộc gọi thông qua một bối cảnh yêu cầu vì vậy chúng tôi có thể snag các HttpListenerContext:

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Security.Cryptography.X509Certificates; 
using System.ServiceModel.Channels; 
using System.Text; 
using System.Threading.Tasks; 

namespace MyNamespace.AcceptSslClientCertificate 
{ 
    public class ReplyChannelWrapper: IChannel, IReplyChannel 
    { 
     IReplyChannel channel; 

     public ReplyChannelWrapper(IReplyChannel channel) 
     { 
      this.channel = channel; 

      // When an event is fired on the target channel, 
      // fire our corresponding event with the same params. 
      channel.Opening += (s, e) => 
      { 
       if (Opening != null) 
        Opening(s, e); 
      }; 
      channel.Opened += (s, e) => 
      { 
       if (Opened != null) 
        Opened(s, e); 
      }; 
      channel.Closing += (s, e) => 
      { 
       if (Closing != null) 
        Closing(s, e); 
      }; 
      channel.Closed += (s, e) => 
      { 
       if (Closed != null) 
        Closed(s, e); 
      }; 
      channel.Faulted += (s, e) => 
      { 
       if (Faulted != null) 
        Faulted(s, e); 
      }; 
     } 

     public T GetProperty<T>() where T : class 
     { 
      return channel.GetProperty<T>(); 
     } 

     public void Abort() 
     { 
      channel.Abort(); 
     } 

     public IAsyncResult BeginClose(TimeSpan timeout, AsyncCallback callback, object state) 
     { 
      return channel.BeginClose(timeout, callback, state); 
     } 

     public IAsyncResult BeginClose(AsyncCallback callback, object state) 
     { 
      return channel.BeginClose(callback, state); 
     } 

     public IAsyncResult BeginOpen(TimeSpan timeout, AsyncCallback callback, object state) 
     { 
      return channel.BeginOpen(timeout, callback, state); 
     } 

     public IAsyncResult BeginOpen(AsyncCallback callback, object state) 
     { 
      return channel.BeginOpen(callback, state); 
     } 

     public void Close(TimeSpan timeout) 
     { 
      channel.Close(timeout); 
     } 

     public void Close() 
     { 
      channel.Close(); 
     } 

     public event EventHandler Closed; 

     public event EventHandler Closing; 

     public void EndClose(IAsyncResult result) 
     { 
      channel.EndClose(result); 
     } 

     public void EndOpen(IAsyncResult result) 
     { 
      channel.EndOpen(result); 
     } 

     public event EventHandler Faulted; 

     public void Open(TimeSpan timeout) 
     { 
      channel.Open(timeout); 
     } 

     public void Open() 
     { 
      channel.Open(); 
     } 

     public event EventHandler Opened; 

     public event EventHandler Opening; 

     public System.ServiceModel.CommunicationState State 
     { 
      get { return channel.State; } 
     } 

     public IAsyncResult BeginReceiveRequest(TimeSpan timeout, AsyncCallback callback, object state) 
     { 
      var r = channel.BeginReceiveRequest(timeout, callback, state); 
      return r; 
     } 

     public IAsyncResult BeginReceiveRequest(AsyncCallback callback, object state) 
     { 
      var r = channel.BeginReceiveRequest(callback, state); 
      return r; 
     } 

     public IAsyncResult BeginTryReceiveRequest(TimeSpan timeout, AsyncCallback callback, object state) 
     { 
      var r = channel.BeginTryReceiveRequest(timeout, callback, state); 
      return r; 
     } 

     public IAsyncResult BeginWaitForRequest(TimeSpan timeout, AsyncCallback callback, object state) 
     { 
      var r = channel.BeginWaitForRequest(timeout, callback, state); 
      return r; 
     } 

     private RequestContext CaptureClientCertificate(RequestContext context) 
     { 
      try 
      { 
       if (context != null 
        && context.RequestMessage != null // Will be null when service is shutting down 
        && context.GetType().FullName == "System.ServiceModel.Channels.HttpRequestContext+ListenerHttpContext") 
       { 
        // Defer retrieval of the certificate until it is actually needed. 
        // This is because some (many) requests may not need the client certificate. 
        // Why make all requests incur the connection overhead of asking for a client certificate when only some need it? 
        // We use a Lazy<X509Certificate2> here to defer the retrieval of the client certificate 
        // AND guarantee that the client cert is only fetched once regardless of how many times 
        // the message property value is retrieved. 
        context.RequestMessage.Properties.Add(Constants.X509ClientCertificateMessagePropertyName, 
         new Lazy<X509Certificate2>(() => 
         { 
          // The HttpListenerContext we need is in a private field of an internal WCF class. 
          // Use reflection to get the value of the field. This is our one and only dirty trick. 
          var fieldInfo = context.GetType().GetField("listenerContext", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); 
          var listenerContext = (System.Net.HttpListenerContext)fieldInfo.GetValue(context); 
          return listenerContext.Request.GetClientCertificate(); 
         })); 
       } 
      } 
      catch (Exception e) 
      { 
       Logging.Error("ReplyChannel.CaptureClientCertificate exception {0}: {1}", e.GetType().Name, e.Message); 
      } 
      return context; 
     } 

     public RequestContext EndReceiveRequest(IAsyncResult result) 
     { 
      return CaptureClientCertificate(channel.EndReceiveRequest(result)); 
     } 

     public bool EndTryReceiveRequest(IAsyncResult result, out RequestContext context) 
     { 
      var r = channel.EndTryReceiveRequest(result, out context); 
      CaptureClientCertificate(context); 
      return r; 
     } 

     public bool EndWaitForRequest(IAsyncResult result) 
     { 
      return channel.EndWaitForRequest(result); 
     } 

     public System.ServiceModel.EndpointAddress LocalAddress 
     { 
      get { return channel.LocalAddress; } 
     } 

     public RequestContext ReceiveRequest(TimeSpan timeout) 
     { 
      return CaptureClientCertificate(channel.ReceiveRequest(timeout)); 
     } 

     public RequestContext ReceiveRequest() 
     { 
      return CaptureClientCertificate(channel.ReceiveRequest()); 
     } 

     public bool TryReceiveRequest(TimeSpan timeout, out RequestContext context) 
     { 
      var r = TryReceiveRequest(timeout, out context); 
      CaptureClientCertificate(context); 
      return r; 
     } 

     public bool WaitForRequest(TimeSpan timeout) 
     { 
      return channel.WaitForRequest(timeout); 
     } 
    } 
} 

Trong dịch vụ web, chúng tôi thiết lập các kênh liên kết như thế này:

var myUri = new Uri("myuri"); 
    var host = new WebServiceHost(typeof(MyService), myUri); 
    var contractDescription = ContractDescription.GetContract(typeof(MyService)); 

    if (myUri.Scheme == "https") 
    { 
     // Construct a custom binding instead of WebHttpBinding 
     // Construct an HttpsTransportBindingElementWrapper so that we can intercept HTTPS 
     // connection startup activity so that we can capture a client certificate from the 
     // SSL link if one is available. 
     // This enables us to accept a client certificate if one is offered, but not require 
     // a client certificate on every request. 
     var binding = new CustomBinding(
      new WebMessageEncodingBindingElement(), 
      new HttpsTransportBindingElementWrapper() 
      { 
       RequireClientCertificate = false, 
       ManualAddressing = true 
      }); 

     var endpoint = new WebHttpEndpoint(contractDescription, new EndpointAddress(myuri)); 
     endpoint.Binding = binding; 

     host.AddServiceEndpoint(endpoint); 

Và cuối cùng, trong trình xác thực dịch vụ web, chúng tôi sử dụng mã sau để xem liệu chứng chỉ ứng dụng khách đã được các máy đánh chặn ở trên nắm bắt chưa:

  object lazyCert = null; 
      if (OperationContext.Current.IncomingMessageProperties.TryGetValue(Constants.X509ClientCertificateMessagePropertyName, out lazyCert)) 
      { 
       certificate = ((Lazy<X509Certificate2>)lazyCert).Value; 
      } 

Lưu ý rằng đối với bất kỳ điều này để làm việc, HttpsTransportBindingElement.RequireClientCertificate phải được đặt thành Sai. Nếu nó được đặt thành true, WCF sẽ chỉ chấp nhận các kết nối SSL mang chứng chỉ ứng dụng khách.

Với giải pháp này, dịch vụ web hoàn toàn chịu trách nhiệm xác thực chứng chỉ ứng dụng khách. Xác thực chứng chỉ tự động của WCF không được đính kèm.

Constants.X509ClientCertificateMessagePropertyName là bất kỳ giá trị chuỗi nào bạn muốn. Nó cần phải hợp lý duy nhất để tránh va chạm với các tên thuộc tính thông báo tiêu chuẩn, nhưng vì nó chỉ được sử dụng để giao tiếp giữa các phần khác nhau của dịch vụ riêng của chúng ta nên nó không cần phải là một giá trị nổi tiếng đặc biệt. Nó có thể là một URN bắt đầu với công ty hoặc tên miền của bạn, hoặc nếu bạn thực sự lười biếng chỉ là một giá trị GUID. Không ai quan tâm.Lưu ý rằng vì giải pháp này phụ thuộc vào tên của lớp nội bộ và trường riêng trong triển khai WCF HTTP, giải pháp này có thể không phù hợp để triển khai trong một số dự án. Nó sẽ ổn định đối với một bản phát hành .NET cụ thể, nhưng các bên trong có thể dễ dàng thay đổi trong các bản phát hành .NET trong tương lai, làm cho mã này không hiệu quả.

Một lần nữa, nếu có bất kỳ giải pháp nào tốt hơn, tôi hoan nghênh các đề xuất.

+0

Cảm ơn bạn. Tốt để biết những người như bạn. Đó là một giải pháp thú vị. Tôi đã xem xét các thư mục lưu trữ của mình. Tôi đã sai. Tôi nghĩ bạn có thể chỉ cần cắm vào một 'ổ cắm' khác. Tôi trộn nó lên. –

+0

Tắt chủ đề - nhưng có thể giúp bạn thực hành. Portfusion. http://sourceforge.net/p/portfusion/home/PortFusion/ http://fusion.corsis.eu/ https://github.com/corsis/PortFusion#readme –

+0

Nghiên cứu ấn tượng, tôi ước nó hoạt động ra khỏi hộp với X509CertificateValidationMode.Custom, chỉ cần vượt qua null nếu không có chứng chỉ ứng dụng khách. – Sergii