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:
- 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.
- 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 RequestContext
là ListenerHttpContext
(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.
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. –
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 –
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