Кэширование WCF ChannelFactories для нескольких пользователей ASP.NET - PullRequest
4 голосов
/ 15 января 2010

У меня есть корпоративная система, которая используется несколькими клиентами WinForms и общедоступным сайтом ASP.NET. Внутренний сервис WCF предоставляет несколько сервисов для использования каждым из этих клиентов. Для служб требуются учетные данные сообщения, которые в случае приложения WinForms предоставляются пользователем при первом запуске программы.

Я кеширую ChannelFactories в приложениях WinForm для повышения производительности. Я хотел бы сделать то же самое на сайте ASP.NET. Тем не менее, поскольку ClientCredentials хранятся как часть фабрики (ChannelFactory<T>.Credentials), нужно ли мне кэшировать одну ChannelFactory для каждой службы на пользователя? Кажется, что даже при умеренном использовании это быстро сложится. Кроме того, я считаю, что мне нужно будет хранить их на уровне приложения, а не на уровне сеанса, поскольку для будущей масштабируемости я не могу гарантировать, что всегда буду использовать состояние сеанса InProc.

Я не вижу способа, чтобы я мог создать одну ChannelFactory для каждой службы, а затем при создании канала указать учетные данные. Я что-то упустил?

Ответы [ 2 ]

5 голосов
/ 31 августа 2010

Я только что натолкнулся на этот вопрос без ответа много месяцев спустя, и, наконец, я могу дать ответ на него. Я выбрал решение, очень похожее на Веб-сайт ASP.NET + приложение Windows Forms + Служба WCF: учетные данные клиента . Тем не менее я решил, что я сделаю запись моего подхода здесь.

Обычные (толстые клиенты) пользователи службы WCF проходят проверку подлинности с помощью имени пользователя / пароля, а веб-пользователи проходят проверку подлинности с помощью заголовка, предоставленного в запросе. Этот заголовок может быть доверенным, поскольку сам веб-сервер аутентифицируется с помощью сертификата X509, для которого у службы WCF есть открытый ключ. Таким образом, решение состоит в том, чтобы в приложении ASP.NET был один канал ChannelFactory, который вставит в запрос заголовок, сообщающий службе WCF, какой пользователь фактически делает запрос.

Я установил две конечные точки для своего ServiceHost, со слегка изменяющимися URL-адресами и разными привязками. Обе привязки относятся к TransportWithMessageCredential, но одна - это учетная запись типа «Имя пользователя», а другая - «Сертификат».

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's ServiceAuthorizationBehavior. Эта политика проверяет, был ли запрос аутентифицирован моей реализацией UserNamePasswordValidator, и если да, я создаю новый IPrincipal с предоставленной идентификацией. Если запрос аутентифицирован сертификатом X509, я ищу заголовок сообщения в текущем запросе, указывающий, кто является олицетворенным пользователем, и затем создаю принципала, используя это имя пользователя. Мой метод IAuthorizationPolicy. Оценить:

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 я должен установить соответствующую привязку (attributeBinding, упомянутый выше) и создать свой 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 и чтобы пользователь действительно был аутентифицирован. Когда пользователь пытается войти на сайт, я создаю ChannelFactory, которая использует мое usernameBinding, назначает предоставленное имя пользователя / пароль как ClientCredentials и делает один запрос к моей службе WCF. Если запрос выполнен успешно, я знаю, что учетные данные пользователя были правильными.

Затем я могу использовать класс FormsAuthentication или назначить IPrincipal для свойства HttpContext.Current.User напрямую. На данный момент мне больше не нужен одноразовый ChannelFactory, использующий usernameBinding, и я могу использовать один ChannelFactory, использующий CertificateBinding, где один экземпляр является общим для моего приложения ASP.NET. Этот ChannelFactory выберет текущего пользователя из HttpContext.Current.User и вставит соответствующий заголовок в будущие запросы WCF.

Итак, мне нужна одна ChannelFactory для каждой службы WCF в моем приложении ASP.NET, плюс создание временной ChannelFactory при каждом входе пользователя в систему. В моей ситуации сайт используется в течение длительных периодов времени, и входы не так уж часты так что это отличное решение.

0 голосов
/ 17 января 2010

Итак, вы устанавливаете соединение WCF, используя учетные данные клиента, когда они вошли на сервер?

Моя идея заключается в том, чтобы настроить привязку WCF для использования олицетворения. По сути, фабрика каналов может подключаться в качестве идентификатора вашего веб-сайта или идентификатора с наименьшими привилегиями, а код WCF может привести к тому, что служба отклонит вызов, если идентификатор неверный.

Таким образом, вы можете создать одну фабрику каналов с множеством подключений к серверу, и вы можете просто олицетворять вошедшего в систему пользователя, когда вы делаете вызов WCF (из памяти вы можете сделать это красиво, превратив безличный вызов в оператор using .)

Эта ссылка должна быть полезной. Надеюсь: Делегирование и олицетворение 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";
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...