为多个ASP.NET用户缓存WCF ChannelFactories

时间:2010-01-15 16:41:02

标签: c# asp.net wcf caching

我有一个企业系统,由少数WinForms客户端和面向公众的ASP.NET站点使用。后端WCF服务提供了每个客户端要使用的几种服务。这些服务需要消息凭证,在WinForms应用程序的情况下,用户在程序首次启动时提供。

我在WinForm应用程序中缓存ChannelFactories以提高性能。我想在ASP.NET站点上做同样的事情。但是,由于ClientCredentials存储为工厂的一部分(ChannelFactory<T>.Credentials),我是否需要为每个用户的每个服务缓存一个ChannelFactory?似乎即使在适度使用下也会很快加起来。另外,我相信我需要将它们存储在应用程序级别,而不是会话级别,因为为了将来的可伸缩性,我不能保证我将始终使用InProc会话状态。

我没有看到任何方法可以为每个服务创建一个ChannelFactory,然后在创建通道时指定凭据。我错过了什么吗?

2 个答案:

答案 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";
    }
}