gpt4 book ai didi

wcf - 选择性地接受自承载 WCF 服务中的客户端证书

转载 作者:太空宇宙 更新时间:2023-11-03 12:46:04 25 4
gpt4 key购买 nike

我希望在我的自托管 WCF 服务中有一个单一的 SSL 端点,它可以接受带有 HTTP 基本身份验证凭据或客户端证书凭据的请求。

对于 IIS 托管服务,IIS 区分“接受客户端证书”和“需要客户端证书”。

WCF 的 WebHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate; 似乎类似于 IIS 中的“需要证书”设置。

有没有一种方法可以配置 WCF 自承载服务以接受客户端证书凭据但不需要每个客户端都提供它们?是否有用于自托管 WCF 服务的 IIS“接受客户端证书”的 WCF 模拟?

最佳答案

我找到了一种在 WCF 中有选择地接受 SSL 客户端证书的方法,但它需要一个肮脏的技巧。如果有人有更好的解决方案(除了“不要使用 WCF”),我很想听听。

在深入研究反编译的 WCF Http channel 类之后,我学到了一些东西:

  1. WCF Http 是单一的。有无数的类到处乱飞,但它们都被标记为“内部”,因此无法访问。如果您试图拦截或扩展核心 HTTP 行为,WCF channel 绑定(bind)堆栈就不值一提了,因为新绑定(bind)类想要在 HTTP 堆栈中摆弄的东西都是不可访问的。
  2. WCF 运行在 HttpListener/HTTPSYS 之上,就像 IIS 一样。 HttpListener 提供对 SSL 客户端证书的访问。不过,WCF HTTP 不提供对底层 HttpListener 的任何访问。

我能找到的最近拦截点是 HttpChannelListener (内部类)打开一个 channel 并返回一个 IReplyChannel . IReplyChannel有接收新请求的方法,这些方法返回 RequestContext .

由 Http 内部类为此 RequestContext 构造和返回的实际对象实例是ListenerHttpContext (内部类)。 ListenerHttpContext持有对 HttpListenerContext 的引用,来自公众System.Net.HttpListener WCF 下的层。

HttpListenerContext.Request.GetClientCertificate()是我们需要查看SSL握手中是否有可用的客户端证书的方法,有则加载,没有则跳过。

不幸的是,对 HttpListenerContext 的引用是ListenerHttpContext的私有(private)领域,所以为了完成这项工作,我不得不求助于一个卑鄙的把戏。我使用反射来读取私有(private)字段的值,以便我可以获得 HttpListenerContext。当前请求的。

所以,这是我的做法:

首先,创建 HttpsTransportBindingElement 的后代这样我们就可以覆盖 BuildChannelListener<TChannel>拦截并包装基类返回的 channel 监听器:

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;
}
}
}

接下来,我们需要包装上面传输绑定(bind)元素拦截的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);
}
}
}

在网络服务中,我们像这样设置 channel 绑定(bind):

    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 实现中的私有(private)字段,因此此解决方案可能不适合在某些项目中部署。对于给定的 .NET 版本,它应该是稳定的,但内部结构在未来的 .NET 版本中很容易发生变化,从而导致此代码无效。

同样,如果有人有任何更好的解决方案,我欢迎提出建议。

关于wcf - 选择性地接受自承载 WCF 服务中的客户端证书,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18667220/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com