我希望在我的自托管WCF服务中拥有一个SSL端点,该端点可以接受具有HTTP基本身份验证凭据或客户端证书凭据的请求。
对于IIS托管服务,IIS区分“接受客户端证书”和“需要客户端证书”。
WCF的WebHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
似乎是IIS中“需要证书”设置的模拟。
是否有办法配置WCF自托管服务以接受客户端证书凭据但不要求每个客户端都使用它们?是否存在用于自托管WCF服务的IIS“接受客户端证书”的WCF模拟?
答案 0 :(得分:5)
我找到了一种在WCF中可选地接受SSL客户端证书的方法,但它需要一个脏技巧。如果有人有更好的解决方案(除了“不要使用WCF”),我很乐意听到它。
经过反编译的WCF Http频道课程后,我学到了一些东西:
我能找到的最接近的拦截点是HttpChannelListener
(内部类)打开频道并返回IReplyChannel
时。 IReplyChannel
具有接收新请求的方法,这些方法返回RequestContext
。
由此RequestContext
的Http内部类构造和返回的实际对象实例是ListenerHttpContext
(内部类)。 ListenerHttpContext
包含对HttpListenerContext
的引用,该System.Net.HttpListener
来自WCF下方的公共HttpListenerContext.Request.GetClientCertificate()
图层。
HttpListenerContext
是我们需要查看SSL握手中是否有可用的客户端证书的方法,如果有,则加载它,如果没有则跳过它。
不幸的是,对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;
}
}
}
来拦截并包装基类返回的通道侦听器:
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; }
}
}
}
接下来,我们需要包装上面的传输绑定元素拦截的ChannelListener:
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);
}
}
}
:
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);
在网络服务中,我们设置了这样的频道绑定:
object lazyCert = null;
if (OperationContext.Current.IncomingMessageProperties.TryGetValue(Constants.X509ClientCertificateMessagePropertyName, out lazyCert))
{
certificate = ((Lazy<X509Certificate2>)lazyCert).Value;
}
最后,在Web服务验证器中,我们使用以下代码来查看上述拦截器是否捕获了客户端证书:
HttpsTransportBindingElement.RequireClientCertificate
请注意,要使其中的任何一个工作,Constants.X509ClientCertificateMessagePropertyName
必须设置为False。如果设置为true,则WCF将仅接受带有客户端证书的SSL连接。
使用此解决方案,Web服务完全负责验证客户端证书。 WCF的自动证书验证没有参与。
{{1}}是您想要的任何字符串值。它必须是合理的唯一,以避免与标准消息属性名称冲突,但由于它仅用于在我们自己的服务的不同部分之间进行通信,因此它不需要是一个特殊的已知值。它可能是以您的公司或域名开头的URN,或者如果您真的只是一个GUID值。没人会关心。
请注意,由于此解决方案依赖于WCF HTTP实现中的内部类名称和私有字段,因此该解决方案可能不适合在某些项目中进行部署。它对于给定的.NET版本应该是稳定的,但是在未来的.NET版本中内部可能很容易改变,导致此代码无效。
同样,如果有人有更好的解决方案,我欢迎提出建议。
答案 1 :(得分:0)
我认为这不起作用。
如果您无法影响客户端以便创建空证书或接受证书的未分配引用,请从服务器端验证此特殊情况并登录到日志文件,则无法进行。您将不得不模仿IIS行为,您必须先检查。这是猜测。没有专业知识。
你通常做的是 a)尝试通过遍历链提供证书来验证证书 b)如果没有提供证书,则双重和三重检查客户端并记录事件。
我认为'.net'不会让你有机会控制谈判。
Imo打开了中间男人的大门。这就是为什么我认为MS不允许和Java类似,afik。最后我决定把服务放在IIS后面。无论如何,WCF使用'IIS'(http.sys)。如果让IIS再做一点,它就不会产生太大的影响。
SBB是少数能够以方便的方式实现这一目标的库之一。您可以访问协商的每一步。
一旦我使用Delphi和ELDOS SecureBlackbox('之前'WCF ... net 3.0)并且它就是这样工作的。今天你必须在服务器端进行广泛的调查,人们会采取双向的方法。
在Java中,您必须创建信任所有内容的TrustManager。
我认为IIS是剩下的选择。