几个月后,我遇到了这个没有答案的问题,最后我可以给出答案。我用了一个非常类似于
ASP.NET Web Site + Windows Forms App + WCF Service: Client Credentials
. 但我还是决定在这里写一篇我的方法。
wcf服务的常规(厚客户端)用户通过用户名/密码进行身份验证,web用户通过请求提供的头进行身份验证。可以信任此头,因为Web服务器本身使用WCF服务具有公钥的X509证书进行身份验证。因此,解决方案是在asp.net应用程序中有一个channelfactory,它将向请求插入一个头,告诉wcf服务哪个用户实际发出请求。
我为servicehost设置了两个端点,url和绑定略有不同。两个绑定都是transportWithMessageCredential,但一个是消息凭据类型username,另一个是证书。
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)客户端证书配置了ServiceCredentials对象。这有点让人困惑,因为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 );
我的解决方案是实现一个iaauthorizationpolicy,并将其用作servicehost的serviceauthorizationbehavior的一部分。此策略检查请求是否已由我的usernamepasswordvalidator实现进行身份验证,如果是,则使用已提供的标识创建新的iprincipal。如果请求由X509证书进行身份验证,我将在当前请求上查找一个消息头,指示模拟用户是谁,然后使用该用户名创建主体。我的授权策略。评估方法:
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.behavior s添加了一个新行为,以从中提取当前用户的标识
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
直接财产。此时,我不再需要使用usernamebinding的一次性channelfactory,我可以使用certificatebinding的一次性channelfactory,其中一个实例在我的asp.net应用程序中共享。通道工厂将从
httpcontext.current.user
并在以后的wcf请求中插入适当的头。
因此,我需要在我的asp.net应用程序中为每个wcf服务创建一个channelfactory,并在用户每次登录时创建一个临时channelfactory。在我的情况下,这个网站使用时间很长,登录次数也不多,所以这是一个很好的解决方案。