我有一个企业系统,由少数WinForms客户端和面向公众的ASP.NET站点使用。后端WCF服务提供了每个客户端要使用的几种服务。这些服务需要消息凭证,在WinForms应用程序的情况下,用户在程序首次启动时提供。
我在WinForm应用程序中缓存ChannelFactories以提高性能。我想在ASP.NET站点上做同样的事情。但是,由于ClientCredentials存储为工厂的一部分(ChannelFactory<T>.Credentials
),我是否需要为每个用户的每个服务缓存一个ChannelFactory?似乎即使在适度使用下也会很快加起来。另外,我相信我需要将它们存储在应用程序级别,而不是会话级别,因为为了将来的可伸缩性,我不能保证我将始终使用InProc会话状态。
我没有看到任何方法可以为每个服务创建一个ChannelFactory,然后在创建通道时指定凭据。我错过了什么吗?
答案 0 :(得分:5)
几个月后我刚刚遇到这个悬而未决的问题,最后我可以为它提供答案。我选择了与ASP.NET Web Site + Windows Forms App + WCF Service: Client Credentials非常相似的解决方案。我仍然决定在这里写一下我的方法。
WCF服务的常规(胖客户端)用户使用用户名/密码进行身份验证,并且Web用户通过随请求提供的标头进行身份验证。此标头可以信任,因为Web服务器本身使用XCF9证书进行身份验证,WCF服务具有公钥。因此,解决方案是在ASP.NET应用程序中有一个ChannelFactory,它将向请求中插入一个标头,告诉WCF服务哪个用户实际发出了请求。
我为我的ServiceHost设置了两个端点,URL略有不同,绑定也不同。两个绑定都是TransportWithMessageCredential,但其中一个是消息凭据类型Username,另一个是Certificate。
var usernameBinding = new BasicHttpBinding( BasicHttpSecurityMode.TransportWithMessageCredential )
usernameBinding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.UserName;
var certificateBinding = new BasicHttpBinding( BasicHttpSecurityMode.TransportWithMessageCredential )
certificateBinding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.Certificate;
var serviceHost = new ServiceHost( new MyService() );
serviceHost.Description.Namespace = "http://schemas.mycompany.com/MyProject";
serviceHost.AddServiceEndpoint( typeof( T ), usernameBinding, "https://myserver/MyProject/MyService" );
serviceHost.AddServiceEndpoint( typeof( T ), certificateBinding, "https://myserver/MyProject/Web/MyService" );
我配置了一个ServiceCredentials对象,其中包括a)服务器端证书,b)自定义用户名/密码验证器和c)客户端证书。这有点令人困惑,因为WCF默认会尝试使用所有这些机制(A + B + C),即使一个端点设置为A + B而另一个端点配置为A + C.
var serviceCredentials = new ServiceCredentials();
serviceCredentials.ServiceCertificate.SetCertificate( StoreLocation.LocalMachine, StoreName.My, X509FindType.FindBySubjectName, "myserver" );
serviceCredentials.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom;
serviceCredentials.UserNameAuthentication.CustomUserNamePasswordValidator = this.UserNamePasswordValidator;
serviceCredentials.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None;
serviceCredentials.ClientCertificate.SetCertificate( StoreLocation.LocalMachine, StoreName.My, X509FindType.FindBySubjectName, "SelfSignedWebsiteCertificate" );
serviceHost.Description.Behaviors.Add( serviceCredentials );
我的解决方案是实现IAuthorizationPolicy,并将其用作ServiceHost的ServiceAuthorizationBehavior的一部分。此策略检查请求是否已通过我的UserNamePasswordValidator实现进行身份验证,如果是,则创建一个具有已提供标识的新IPrincipal。如果请求通过X509证书进行身份验证,我会在当前请求中查找消息头,指示模拟用户是谁,然后使用该用户名创建主体。我的IAuthorzationPolicy.Evaluate方法:
public bool Evaluate( EvaluationContext evaluationContext, ref object state )
{
var identity = ((List<IIdentity>) evaluationContext.Properties[ "Identities" ] ).First();
if ( identity.AuthenticationType == "MyCustomUserNamePasswordValidator" )
{
evaluationContext.Properties[ "Principal" ] = new GenericPrincipal( identity, null );
}
else if ( identity.AuthenticationType == "X509" )
{
var impersonatedUsername = OperationContext.Current.IncomingMessageHeaders.GetHeader<string>( "ImpersonatedUsername", "http://schemas.mycompany.com/MyProject" );
evaluationContext.AddClaimSet( this, new DefaultClaimSet( Claim.CreateNameClaim( impersonatedUsername ) ) );
var impersonatedIdentity = new GenericIdentity( impersonatedUsername, "ImpersonatedUsername" );
evaluationContext.Properties[ "Identities" ] = new List<IIdentity>() { impersonatedIdentity };
evaluationContext.Properties[ "Principal" ] = new GenericPrincipal( identity, null );
}
else
throw new Exception( "Bad identity" );
return true;
}
将策略添加到ServiceHost非常简单:
serviceHost.Authorization.ExternalAuthorizationPolicies = new List<IAuthorizationPolicy>() { new CustomAuthorizationPolicy() }.AsReadOnly();
serviceHost.Authorization.PrincipalPermissionMode = PrincipalPermissionMode.Custom;
现在,无论用户如何进行身份验证,ServiceSecurityContext.Current.PrimaryIdentity
都是正确的。这或多或少地处理了WCF服务方面的繁重工作。在我的ASP.NET应用程序中,我必须设置一个适当的绑定(certificateBinding,如上所述),并创建我的ChannelFactory。但是,我向factory.Endpoint.Behaviors添加了一个新行为,以从HttpContext.Current.User
中提取当前用户的标识,并将其放入我的服务查找的WCF请求标头中。这就像实现IClientMessageInspector并使用诸如此类的BeforeSendRequest一样简单(尽管在适当的地方添加空检查):
public object BeforeSendRequest( ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel )
{
request.Headers.Add( MessageHeader.CreateHeader( "ImpersonatedUsername", "http://schemas.mycompany.com/MyProject", HttpContext.Current.User.Identity.Name ) );
return null;
}
当然我们仍然需要一个IEndpointBehavior来添加消息检查器。您可以使用引用检查器的固定实现;我选择使用泛型类:
public class GenericClientInspectorBehavior : IEndpointBehavior
{
public IClientMessageInspector Inspector { get; private set; }
public GenericClientInspectorBehavior( IClientMessageInspector inspector )
{ Inspector = inspector; }
// Empty methods excluded for brevity
public void ApplyClientBehavior( ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime )
{ clientRuntime.MessageInspectors.Add( Inspector ); }
}
最后,使ChannelFactory使用正确的客户端证书和端点行为的粘合剂:
factory.Credentials.ClientCertificate.SetCertificate( StoreLocation.LocalMachine, StoreName.My, X509FindType.FindBySubjectName, "SelfSignedWebsiteCertificate" );
factory.Endpoint.Behaviors.Add( new GenericClientInspectorBehavior( new HttpContextAuthenticationInspector() ) );
唯一的最后一部分是如何确保HttpContext.Current.User
已设置,以及用户是否已经过实际身份验证。当用户尝试登录网站时,我创建一个使用我的usernameBinding的ChannelFactory,将提供的用户名/密码分配为ClientCredentials,并向我的WCF服务发出单个请求。如果请求成功,我知道用户的凭据是正确的。
然后我可以使用FormsAuthentication
类或直接为HttpContext.Current.User
属性分配IPrincipal。此时我不再需要使用usernameBinding的一次性ChannelFactory,我可以使用单一的ChannelFactory使用certificateBinding,其中一个实例在我的ASP.NET应用程序中共享。 ChannelFactory将从HttpContext.Current.User
中获取当前用户,并在将来的WCF请求中插入相应的标头。
因此,我的ASP.NET应用程序中每个WCF服务需要一个ChannelFactory,并且每次用户登录时都需要创建一个临时的ChannelFactory。在我的情况下,该站点使用很长时间并且登录不是那么频繁,所以这是一个很好的解决方案。
答案 1 :(得分:0)
因此,您在登录服务器时使用客户端凭据进行WCF连接?
我的想法是设置WCF绑定以使用模拟。有效地,渠道工厂可以作为您的网站的身份或最小特权身份进行连接,如果身份不正确,WCF代码可能导致服务拒绝该呼叫。
通过这种方式,您可以创建一个与服务器有很多连接的通道工厂,并且您可以在进行WCF调用时模拟已登录的用户(从内存中,您可以通过将模拟调用包装到using语句中来很好地完成此操作。)
此链接应该有用我希望:MSDN Delegation and Impersonation
此代码示例取自页面:
public class HelloService : IHelloService
{
[OperationBehavior]
public string Hello(string message)
{
WindowsIdentity callerWindowsIdentity =
ServiceSecurityContext.Current.WindowsIdentity;
if (callerWindowsIdentity == null)
{
throw new InvalidOperationException
("The caller cannot be mapped to a WindowsIdentity");
}
using (callerWindowsIdentity.Impersonate())
{
// Access a file as the caller.
}
return "Hello";
}
}