我只是碰到這種懸而未決的問題跑很多個月後,最後我可以爲它提供一個答案。我採用了與ASP.NET Web Site + Windows Forms App + WCF Service: Client Credentials非常相似的解決方案。不過,我決定我會在這裏寫一下我的方法。
WCF服務的常規(胖客戶端)用戶使用用戶名/密碼進行身份驗證,並且Web用戶通過隨請求提供的標頭進行身份驗證。由於Web服務器本身使用WCF服務擁有公鑰的X509證書進行身份驗證,因此可以信任此頭文件。因此,解決方案是在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)客戶端證書對象。這有點令人困惑,因爲即使一個端點爲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實現進行了身份驗證,如果是,則創建一個具有已提供身份的新IPrincipal。如果請求已通過X509證書進行身份驗證,則在當前請求上查找消息標頭,指明模擬用戶是誰,然後使用該用戶名創建一個委託人。我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;
}
添加政策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.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
已設置,並且用戶已實際進行了身份驗證。當用戶嘗試登錄網站時,我創建了一個使用我的usernameBinding的ChannelFactory,將提供的用戶名/密碼分配爲ClientCredentials,並向我的WCF服務發出一個請求。如果請求成功,我知道用戶的憑據是正確的。
然後,我可以使用FormsAuthentication
類或直接爲HttpContext.Current.User
屬性指定IPrincipal。此時,我不再需要使用usernameBinding的一次性ChannelFactory,並且我可以使用certificateBinding使用一個ChannelFactory,其中一個實例在我的ASP.NET應用程序中共享。該ChannelFactory將從HttpContext.Current.User
中挑選當前用戶,並在未來的WCF請求中插入適當的標頭。
因此,我的ASP.NET應用程序中每個WCF服務需要一個ChannelFactory,並且每次用戶登錄時都會創建一個臨時ChannelFactory。在我的情況下,該站點使用了很長時間,登錄並非頻繁,所以這是一個很好的解決方案。
我認爲這是正確的方向,關於將網站認證爲自己(通過證書),然後基本上說「當我告訴你用戶正在做這件事情時,請相信我」。我確實發現了這個(http://stackoverflow.com/questions/345414),我認爲這讓我更接近了一點。我一直有很多其他小問題(不是全部),所以這個網站現在已經佔據了一席之地。 – 2010-02-16 18:02:28