WCF에서 SSL 클라이언트 인증서를 선택적으로 허용하는 방법을 찾았지만 더러운 트릭이 필요합니다. 누군가 "WCF를 사용하지 마십시오"이외의 더 나은 솔루션을 가지고 있다면 나는 그것을 듣고 싶습니다. 많은에 디 컴파일 WCF HTTP를 채널 클래스, 내가 배운 주위에 몇 가지 파고 후
:
- WCF HTTP는 모 놀리 식이다. bezillion 클래스가 주위를 날고 있지만, 모두 "내부"라고 표시되어 액세스 할 수 없습니다. 새로운 바인딩 클래스가 HTTP 스택에서 피델을 사용하기를 원할 것이기 때문에 핵심 HTTP 동작을 가로 채거나 확장하려는 경우 WCF 채널 바인딩 스택은 가치가 없습니다.
- IIS와 마찬가지로 WCF가 HttpListener/HTTPSYS 위에 놓입니다.HttpListener는 SSL 클라이언트 인증서에 대한 액세스를 제공합니다. WCF HTTP는 기본 HttpListener에 대한 액세스를 제공하지 않습니다.
가장 가까운 차단 점은 HttpChannelListener
(내부 클래스)이 채널을 열고 IReplyChannel
을 반환하는 것입니다. IReplyChannel
에는 새 요청을 수신하는 메소드가 있으며 해당 메소드는 RequestContext
을 리턴합니다.
RequestContext
에 대한 HTTP 내부 클래스에 의해 생성되고 반환 된 실제 객체 인스턴스는 ListenerHttpContext
(내부 클래스)입니다. ListenerHttpContext
에는 HttpListenerContext
에 대한 참조가 있으며, 여기에는 공개 System.Net.HttpListener
WCF 아래의 계층이 있습니다.
HttpListenerContext.Request.GetClientCertificate()
은 SSL 핸드 셰이크에서 사용할 수있는 클라이언트 인증서가 있는지 확인하고 필요하면로드하거나없는 경우 건너 뜁니다.
불행히도 HttpListenerContext
에 대한 참조는 ListenerHttpContext
의 비공개 필드이므로이 작업을 수행하려면 더러운 트릭을 사용해야합니다. 리플렉션을 사용하여 비공개 필드의 값을 읽으면 현재 요청의 HttpListenerContext
에 도착할 수 있습니다.
그래서, 여기에 어떻게 내가 해냈어 :
첫째, 그래서 우리는 기본 클래스에 의해 반환 된 채널 수신기를 차단하고 포장 BuildChannelListener<TChannel>
를 오버라이드 (override) 할 수 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; }
}
}
}
다음으로 우리는,그래서 우리는 HttpListenerContext
암초에 걸릴 수 있습니다 요청 컨텍스트를 통과 IReplyChannel
와 절편 통화 구현 :
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);
그리고 마지막으로 : 웹 서비스에서
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);
}
}
}
을, 우리는 다음과 같이 채널 바인딩을 설정 웹 서비스 인증 자에서 다음 코드를 사용하여 클라이언트 인증서가 위의 인터셉터에 의해 캡처되었는지 확인합니다.
object lazyCert = null;
if (OperationContext.Current.IncomingMessageProperties.TryGetValue(Constants.X509ClientCertificateMessagePropertyName, out lazyCert))
{
certificate = ((Lazy<X509Certificate2>)lazyCert).Value;
}
이 작업을 수행하려면 HttpsTransportBindingElement.RequireClientCertificate
을 False로 설정해야합니다. true로 설정하면 WCF는 클라이언트 인증서가있는 SSL 연결 만 수락합니다.
이 솔루션을 사용하면 웹 서비스가 클라이언트 인증서의 유효성을 완전히 확인해야합니다. 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