WCFでオプションでSSLクライアント証明書を受け入れる方法が見つかりましたが、汚いトリックが必要です。もし誰かが(WCFを使わないでください)より良い解決策を持っているなら、私はそれを聞いてみたいと思います。
ずっと逆コンパイルWCF HTTPチャンネルのクラスで周り掘り後、私はいくつかのことを学びました:
- WCF HTTPはモノリシックです。ベジリオンクラスが飛行していますが、それらのすべてが「内部」とマークされているためアクセスできません。新しいバインディングクラスがHTTPスタックで手伝ってくれるものはすべてアクセスできないため、コアHTTPビヘイビアをインターセプトまたは拡張しようとすると、WCFチャネルバインディングスタックはビッグヒルの価値がありません。
- IISと同様に、HttpListener/HTTPSYSの上にWCFが乗っています。HttpListenerは、SSLクライアント証明書へのアクセスを提供します。ただし、WCF HTTPは基になるHttpListenerへのアクセスを提供しません。
HttpChannelListener
(内部クラス)がチャネルを開き、IReplyChannel
を返したときに最も近いインターセプトポイントが見つかりました。 IReplyChannel
には新しいリクエストを受信するメソッドがあり、これらのメソッドはRequestContext
を返します。
このRequestContext
のHttp内部クラスによって構築され返された実際のオブジェクトインスタンスはListenerHttpContext
(内部クラス)です。 ListenerHttpContext
は、HttpListenerContext
への参照を保持します。これは、公開されているWCFの下にあるSystem.Net.HttpListener
のレイヤーに由来します。
HttpListenerContext.Request.GetClientCertificate()
は、SSLハンドシェイクで利用可能なクライアント証明書があるかどうかを確認し、存在する場合はロードし、存在しない場合はスキップする方法です。
残念なことに、HttpListenerContext
への参照はListenerHttpContext
のプライベートフィールドです。この作業を行うために、私は1つの汚いトリックに頼らざるを得ませんでした。私はリフレクションを使ってプライベートフィールドの値を読み取って、現在のリクエストのHttpListenerContext
にアクセスできるようにします。
だから、ここで私はそれをやった方法は次のとおりです。
まず、我々は、基本クラスによって返されたチャネルリスナーを傍受してラップするBuildChannelListener<TChannel>
をオーバーライドすることができるようにHttpsTransportBindingElement
の子孫を作成します。
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
は、私たちがHttpListenerContext
を暗礁ことができるように、要求コンテキストを渡しIReplyChannel
と切片の呼び出しを実装する:Webサービスでは
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);
そして最後に、 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リリースで容易に変更され、このコードは効果がありません。
もう一度、誰かより良い解決策があれば、私は提案を歓迎します。
ありがとうございます。あなたのような人を知ることは良いことです。それは興味深い解決策です。私はアーカイブフォルダを調べました。私は間違っていた。私はあなたが単に別の 'ソケット'を差し込むことができると思った。私はそれを混ぜた。 –
トピックをオフに - 実際にあなたを助けるかもしれません。 Portfusion。 http://sourceforge.net/p/portfusion/home/PortFusion/ http://fusion.corsis.eu/ https://github.com/corsis/PortFusion#readme –
印象的な研究、私はそれがうまくいくX509CertificateValidationMode.Customをそのまま使用して、クライアント証明書がない場合はnullを渡します。 – Sergii