4


3

複数のASP.NETユーザー向けのWCF ChannelFactoriesのキャッシュ

少数のWinFormsクライアントと公開されているASP.NETサイトで使用されるエンタープライズシステムがあります。 バックエンドWCFサービスは、これらの各クライアントが使用する複数のサービスを提供します。 サービスにはメッセージ資格情報が必要です。WinFormsアプリの場合は、プログラムが最初に起動したときにユーザーによって提供されます。

パフォーマンスのために、WinFormアプリでChannelFactoriesをキャッシュします。 ASP.NETサイトでも同じことをしたいと思います。 ただし、ClientCredentialsはファクトリー( ChannelFactory.Credentials)の一部として保存されるため、ユーザーごとのサービスごとに1つのChannelFactoryをキャッシュする必要がありますか? 適度に使用しても、すぐに追加されるようです。 さらに、将来のスケーラビリティのためにInProcセッション状態を常に使用することを保証できないため、セッションレベルではなくアプリケーションレベルで保存する必要があると思います。

サービスごとに1つのChannelFactoryを作成する方法がわかりません。その後、チャネルの作成時に資格情報を指定します。 私は何かが足りないのですか?

2 Answer


5


私は何ヶ月も後にこの未回答の質問に出くわし、ついに答えを出すことができました。 ASP.NET Webサイト+ Windowsフォームアプリ+ WCFサービス:クライアント資格情報に非常によく似たソリューションを使用しました。 それでも、私はここで自分のアプローチを書き上げることにしました。

WCFサービスの通常の(シッククライアント)ユーザーはユーザー名/パスワードで認証され、Webユーザーはリクエストで提供されるヘッダーで認証されます。 Webサーバー自体が、WCFサービスが公開キーを持っているX509証明書で認証するため、このヘッダーは信頼できます。 そのため、ASP.NETアプリケーションに単一のChannelFactoryを配置して、ヘッダーをリクエストに挿入し、どのユーザーが実際にリクエストを行っているかをWCFサービスに伝えます。

ServiceHostに2つのエンドポイントを設定し、URLを少し変えて、バインディングを変えました。 両方のバインディングはTransportWithMessageCredentialですが、1つはメッセージ資格情報タイプのユーザー名で、もう1つは証明書です。

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

a)サーバー側の証明書、b)カスタムのユーザー名/パスワード検証、c)クライアント側の証明書でSe​​rviceCredentialsオブジェクトを構成しました。 これは、一方のエンドポイントがA + B用にセットアップされ、他方のエンドポイントがA + C用に構成されている場合でも、WCFがデフォルトでこれらのメカニズム(A + B + 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の実装によって認証されているかどうかを確認し、認証されている場合は、提供されたIDで新しいIPrincipalを作成します。 要求がX509証明書によって認証される場合、現在の要求で、偽装されたユーザーが誰であるかを示すメッセージヘッダーを探し、そのユーザー名を使用してプリンシパルを作成します。 私のIAuthorzationPolicy.Evaluateメソッド:

public bool Evaluate( EvaluationContext evaluationContext, ref object state )
{
    var identity = ((List) 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( "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() { impersonatedIdentity };
        evaluationContext.Properties[ "Principal" ] = new GenericPrincipal( identity, null );
    }
    else
        throw new Exception( "Bad identity" );

    return true;
}

ポリシーをServiceHostに追加するのは簡単です。

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

これで、 `ServiceSecurityContext.Current.PrimaryIdentity`は、ユーザーがどのように認証されたかに関係なく正しいです。 これにより、WCFサービス側の面倒な作業が多少なりとも処理されます。 ASP.NETアプリケーションでは、適切なバインド(上記のcertificateBinding)をセットアップし、ChannelFactoryを作成する必要があります。 ただし、factory.Endpoint.Behaviorsに新しい動作を追加して、現在のユーザーのIDを `HttpContext.Current.User`から取得し、サービスが検索するWCF要求ヘッダーに配置します。 これは、IClientMessageInspectorを実装し、次のようなBeforeSendRequestを使用するのと同じくらい簡単です(ただし、必要に応じてnullチェックを追加します)。

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」が設定されていること、およびユーザーが実際に認証されたことを確認する方法です。 ユーザーがWebサイトにログインしようとすると、usernameBindingを使用するChannelFactoryを作成し、指定されたユーザー名/パスワードをClientCredentialsとして割り当て、WCFサービスに1つの要求を行います。 要求が成功した場合、ユーザーの資格情報が正しいことがわかります。

その後、 FormsAuthentication`クラスを使用するか、IPrincipalを HttpContext.Current.User`プロパティに直接割り当てることができます。 この時点で、usernameBindingを使用する使い捨てのChannelFactoryは不要になり、ASP.NETアプリケーション全体で1つのインスタンスが共有されるcertificateBindingを使用する単一のChannelFactoryを使用できます。 そのChannelFactoryは、 `HttpContext.Current.User`から現在のユーザーを取得し、将来のWCF要求に適切なヘッダーを挿入します。

したがって、ASP.NETアプリケーションでは、WCFサービスごとに1つのChannelFactoryが必要です。さらに、ユーザーがログインするたびに一時的なChannelFactoryを作成します。 私の状況では、サイトは長期間使用され、ログインはそれほど頻繁ではないため、これは素晴らしいソリューションです。


0


サーバーにログインしているクライアントの資格情報を使用してWCF接続を作成していますか?

私の考えは、偽装を使用するようにWCFバインディングをセットアップすることです。 事実上、チャネルファクトリはWebサイトのIDまたは最小特権IDとして接続でき、WCFコードにより、IDが正しくない場合にサービスが呼び出しを拒否する可能性があります。

これにより、サーバーへの接続が多数ある1つのチャネルファクトリを作成し、WCF呼び出しを行うときにログオンユーザーを偽装することができます(メモリからは、偽装呼び出しをusingステートメントにラップすることでこれをうまく実行できます)。

このリンクは役立つはずです:http://msdn.microsoft.com/en-us/library/ms730088.aspx[MSDN委任と偽装]

このコードサンプルはページから取得されます。

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