Я только что натолкнулся на этот вопрос без ответа много месяцев спустя, и, наконец, я могу дать ответ на него. Я выбрал решение, очень похожее на Веб-сайт 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 при каждом входе пользователя в систему. В моей ситуации сайт используется в течение длительных периодов времени, и входы не так уж часты так что это отличное решение.