简体   繁体   English

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

[英]Caching WCF ChannelFactories for multiple ASP.NET users

I have an enterprise system that is used by a handful of WinForms clients and a public-facing ASP.NET site. 我有一个企业系统,由少数WinForms客户端和面向公众的ASP.NET站点使用。 A backend WCF service provides several services to be consumed by each of these clients. 后端WCF服务提供了每个客户端要使用的几种服务。 The services require message credentials, which in the case of a WinForms app is supplied by the user when the program first starts. 这些服务需要消息凭据,在WinForms应用程序的情况下,在程序首次启动时由用户提供。

I cache ChannelFactories in the WinForm apps for performance. 我在WinForm应用程序中缓存ChannelFactories以提高性能。 I would like to do the same on the ASP.NET site. 我想在ASP.NET站点上做同样的事情。 However, since ClientCredentials are stored as part of the factory ( ChannelFactory<T>.Credentials ), will I need to cache one ChannelFactory per service per user? 但是,由于ClientCredentials存储为工厂的一部分( ChannelFactory<T>.Credentials ),我是否需要为每个用户的每个服务缓存一个ChannelFactory? It seems that even under moderate use that will add up quickly. 似乎即使在适度使用下也会很快加起来。 Additionally, I believe I will need to store them at the application-level, not the session-level, since for future scalability I can't guarantee that I will always be using InProc session state. 另外,我相信我需要将它们存储在应用程序级别,而不是会话级别,因为为了将来的可伸缩性,我无法保证我将始终使用InProc会话状态。

I don't see any way that I can create one ChannelFactory per service, and then upon creation of the channel specify credentials. 我没有看到任何方法可以为每个服务创建一个ChannelFactory,然后在创建通道时指定凭据。 Am I missing something? 我错过了什么吗?

I just ran across this unanswered question many months later, and finally I can provide an answer for it. 几个月后我刚刚遇到这个悬而未决的问题,最后我可以为它提供答案。 I went with a solution very similar to ASP.NET Web Site + Windows Forms App + WCF Service: Client Credentials . 我使用的解决方案非常类似于ASP.NET网站+ Windows Forms App + WCF服务:客户端凭证 Still I decided I would do a write-up of my approach here. 我仍然决定在这里写一下我的方法。

Regular (thick client) users of the WCF service are authenticated with a username/password, and web users are authenticated by a header provided with the request. WCF服务的常规(胖客户端)用户使用用户名/密码进行身份验证,并且Web用户通过随请求提供的标头进行身份验证。 This header can be trusted because the web server itself authenticates with a X509 certificate for which the WCF service has the public key. 此标头可以信任,因为Web服务器本身使用XCF9证书进行身份验证,WCF服务具有公钥。 So, the solution is to have a single ChannelFactory in the ASP.NET application that will insert a header into the request, telling the WCF service which user is actually making the request. 因此,解决方案是在ASP.NET应用程序中使用单个ChannelFactory,它将在请求中插入标头,告诉WCF服务哪个用户实际发出请求。

I set up two endpoints for my ServiceHost, with slightly varying URLs and different bindings. 我为我的ServiceHost设置了两个端点,URL略有不同,绑定也不同。 Both bindings are TransportWithMessageCredential, but one is message credential type Username, and the other is Certificate. 两个绑定都是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" );

I configured a ServiceCredentials object with a) the server-side certificate, b) the custom username/password validator and c) the client-side certificate. 我配置了一个ServiceCredentials对象,其中包括a)服务器端证书,b)自定义用户名/密码验证器和c)客户端证书。 This was a little confusing because WCF would by default attempt to use all of these mechanisms (A+B+C), even though one endpoint is set up for A+B and the other is configured for A+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 );

My solution was to implement an IAuthorizationPolicy, and use it as part of the ServiceHost's ServiceAuthorizationBehavior. 我的解决方案是实现IAuthorizationPolicy,并将其用作ServiceHost的ServiceAuthorizationBehavior的一部分。 This policy checks if the request had been authenticated by my implementation of UserNamePasswordValidator, and if so, I create a new IPrincipal with the identity that had been supplied. 此策略检查请求是否已通过我的UserNamePasswordValidator实现进行身份验证,如果是,则创建一个具有已提供标识的新IPrincipal。 If the request is authenticated by an X509 certificate, I look for a message header on the current request indicating who the impersonated user is, and then create a principal using that username. 如果请求通过X509证书进行身份验证,我会在当前请求中查找消息头,指示模拟用户是谁,然后使用该用户名创建主体。 My IAuthorzationPolicy.Evaluate method: 我的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;
}

Adding the policy to the ServiceHost is straightforward: 将策略添加到ServiceHost非常简单:

serviceHost.Authorization.ExternalAuthorizationPolicies = new List<IAuthorizationPolicy>() { new CustomAuthorizationPolicy() }.AsReadOnly();
serviceHost.Authorization.PrincipalPermissionMode = PrincipalPermissionMode.Custom;

Now, ServiceSecurityContext.Current.PrimaryIdentity is correct regardless of how the user was authenticated. 现在,无论用户如何进行身份验证, ServiceSecurityContext.Current.PrimaryIdentity都是正确的。 This more or less takes care of the heavy lifting on the side of the WCF service. 这或多或少地处理了WCF服务方面的繁重工作。 In my ASP.NET application, I have to set up an appropriate binding (certificateBinding, mentioned above), and create my ChannelFactory. 在我的ASP.NET应用程序中,我必须设置一个适当的绑定(certificateBinding,如上所述),并创建我的ChannelFactory。 However, I add a new behavior to factory.Endpoint.Behaviors to pull the current user's identity from HttpContext.Current.User and place it into the WCF request header that my service looks for. 但是,我向factory.Endpoint.Behaviors添加了一个新行为,以从HttpContext.Current.User提取当前用户的标识,并将其放入我的服务查找的WCF请求标头中。 This is as easy as implementing IClientMessageInspector and using a BeforeSendRequest such as this (although adding null-checks where appropriate): 这就像实现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;
}

Of course we still need an IEndpointBehavior to add the message inspector. 当然,我们仍然需要一个IEndpointBehavior来添加消息检查器。 You can use a fixed implementation that references your inspector; 您可以使用引用检查器的固定实现; I chose to use a generic class: 我选择使用泛型类:

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

And finally the glue to make the ChannelFactory use the proper client-side certificate and endpoint behavior: 最后,使ChannelFactory使用正确的客户端证书和端​​点行为的粘合剂:

factory.Credentials.ClientCertificate.SetCertificate( StoreLocation.LocalMachine, StoreName.My, X509FindType.FindBySubjectName, "SelfSignedWebsiteCertificate" );
factory.Endpoint.Behaviors.Add( new GenericClientInspectorBehavior( new HttpContextAuthenticationInspector() ) );

The only final piece is how to ensure that HttpContext.Current.User is set, and that the user has actually been authenticated. 唯一的最后一部分是如何确保设置HttpContext.Current.User ,以及用户实际已经过身份验证。 When the user attempts to log in to the website, I create a ChannelFactory that uses my usernameBinding, assigns the provided username/password as ClientCredentials, and makes a single request to my WCF service. 当用户尝试登录网站时,我创建一个使用我的usernameBinding的ChannelFactory,将提供的用户名/密码分配为ClientCredentials,并向我的WCF服务发出单个请求。 If the request succeeds, I know the user's credentials were correct. 如果请求成功,我知道用户的凭据是正确的。

I can then use the FormsAuthentication class or assign an IPrincipal to the HttpContext.Current.User property directly. 然后,我可以使用FormsAuthentication类或直接将IPrincipal分配给HttpContext.Current.User属性。 At this point I no longer need the single-use ChannelFactory using the usernameBinding, and I can use a single ChannelFactory using the certificateBinding where one instance is shared across my ASP.NET application. 此时我不再需要使用usernameBinding的一次性ChannelFactory,我可以使用单一的ChannelFactory使用certificateBinding,其中一个实例在我的ASP.NET应用程序中共享。 That ChannelFactory will pick up the current user from HttpContext.Current.User and insert the appropriate header in future WCF requests. ChannelFactory将从HttpContext.Current.User获取当前用户,并在将来的WCF请求中插入适当的头。

So, I need one ChannelFactory per WCF service in my ASP.NET application, plus creating a temporary ChannelFactory every time a user logs in. In my situation, the site is used for long periods and logins are not all that frequent, so this is a great solution. 因此,我的ASP.NET应用程序中每个WCF服务需要一个ChannelFactory,并且每次用户登录时都需要创建一个临时的ChannelFactory。在我的情况下,该站点使用很长时间并且登录不是那么频繁,所以这是一个很好的解决方

So you're making a WCF connection using the client credentials as they have logged into the server? 因此,您在登录服务器时使用客户端凭据进行WCF连接?

My idea would be to set up the WCF binding to use impersonation. 我的想法是设置WCF绑定以使用模拟。 Effectively the channel factory can connect as your website's identity, or as a least privilege identity, and WCF code can cause the service to reject the call if the identity isn't correct. 有效地,渠道工厂可以作为您的网站的身份或最小特权身份进行连接,如果身份不正确,WCF代码可以使服务拒绝该呼叫。

That way you can create one channel factory with many connections to the server, and you can simply impersonate the logged on user when you make the WCF call (from memory you can do this nicely by wrapping the impersonate call into a using statement.) 这样你就可以创建一个与服务器有很多连接的通道工厂,并且你可以在进行WCF调用时模拟登录用户(从内存中你可以通过将模拟调用包装到using语句中来很好地完成这项工作。)

This link should be helpful I hope: MSDN Delegation and Impersonation 这个链接应该是有用的,我希望: MSDN代表团和模仿

This code sample is taken from the page: 此代码示例取自页面:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM