我找到了一种在 WCF 中选择接受 SSL 客户端证书的方法,但它需要一个肮脏的技巧。如果有人有更好的解决方案(除了“不要使用 WCF”),我很想听听。
在对反编译的 WCF Http 通道类进行了大量研究之后,我学到了一些东西:
- WCF Http 是单片的。有无数类飞来飞去,但它们都被标记为“内部”,因此无法访问。如果您试图拦截或扩展核心 HTTP 行为,那么 WCF 通道绑定堆栈不值得一大堆 bean,因为新绑定类想要在 HTTP 堆栈中摆弄的东西都是不可访问的。
- WCF 位于 HttpListener / HTTPSYS 之上,就像 IIS 一样。HttpListener 提供对 SSL 客户端证书的访问。但是,WCF HTTP 不提供对底层 HttpListener 的任何访问。
我能找到的最接近的拦截点是HttpChannelListener
(内部类)打开一个通道并返回一个IReplyChannel
. IReplyChannel
具有接收新请求的方法,这些方法返回一个RequestContext
.
Http 内部类为此构造和返回的实际对象实例RequestContext
是ListenerHttpContext
(内部类)。ListenerHttpContext
持有对 a 的引用HttpListenerContext
,它来自System.Net.HttpListener
WCF 下的公共层。
HttpListenerContext.Request.GetClientCertificate()
是我们需要查看 SSL 握手中是否有可用的客户端证书的方法,如果有则加载它,如果没有则跳过它。
不幸的是,引用HttpListenerContext
是 的私有字段ListenerHttpContext
,所以为了完成这项工作,我不得不求助于一个肮脏的把戏。我使用反射来读取私有字段的值,以便我可以获取HttpListenerContext
当前请求的值。
所以,我是这样做的:
首先,创建一个后代,HttpsTransportBindingElement
以便我们可以重写BuildChannelListener<TChannel>
以拦截和包装基类返回的通道侦听器:
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;
}
}
}
接下来,我们需要将上面传输绑定元素截获的ChannelListener包装起来:
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; }
}
}
}
接下来,我们需要它ReplyChannelWrapper
来实现IReplyChannel
和拦截传递请求上下文的调用,以便我们可以阻止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);
}
}
}
在 Web 服务中,我们像这样设置通道绑定:
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);
最后,在 Web 服务身份验证器中,我们使用以下代码查看客户端证书是否被上述拦截器捕获:
object lazyCert = null;
if (OperationContext.Current.IncomingMessageProperties.TryGetValue(Constants.X509ClientCertificateMessagePropertyName, out lazyCert))
{
certificate = ((Lazy<X509Certificate2>)lazyCert).Value;
}
请注意,要使此功能起作用,HttpsTransportBindingElement.RequireClientCertificate
必须将其设置为 False。如果设置为 true,则 WCF 将仅接受带有客户端证书的 SSL 连接。
使用此解决方案,Web 服务完全负责验证客户端证书。未使用 WCF 的自动证书验证。
Constants.X509ClientCertificateMessagePropertyName
是您想要的任何字符串值。它需要相当独特以避免与标准消息属性名称冲突,但由于它仅用于在我们自己的服务的不同部分之间进行通信,因此它不需要是一个特殊的众所周知的值。它可能是一个以您的公司或域名开头的 URN,或者如果您真的很懒,只是一个 GUID 值。没有人会在意。
请注意,由于此解决方案依赖于 WCF HTTP 实现中的内部类和私有字段的名称,因此此解决方案可能不适合在某些项目中部署。对于给定的 .NET 版本,它应该是稳定的,但内部结构在未来的 .NET 版本中很容易发生变化,从而导致此代码无效。
同样,如果有人有更好的解决方案,我欢迎提出建议。