我们正在构建一个系统,该系统将在IIS中托管在企业域中的许多WCF服务。在DMZ中运行的表示层服务器将调用这些服务。需要保护对WCF服务的调用(即需要认证)。该系统是一个COTS系统,将部署到许多客户站点。
WCF支持使用Windows身份验证和开箱即用的x.509证书对呼叫者进行身份验证。由于DMZ表示层服务器将位于不同的域中,因此Windows身份验证无法在此方案中保护WCF服务。
x.509证书安全性是一个选项,并已在其他SO帖子中提及,如下所示:
Accessing WCF Service using TCP from the DMZ (not on network or domain)
我对x.509证书有两个担忧:
性能。我自己还没有进行性能分析,但是从别人那里得知,验证x.509证书的开销可能会使解决方案成为非首发。我的下一个任务是在这一点上进行绩效分析。
的易于部署。我在过去发现,除了SSL之外,任何时候x.509证书都会出现在客户IT人员(采购,生成,管理)的问题上。这反过来又给我们的产品带来了支持问题。
我考虑使用用户名/密码安全性来保护WCF呼叫,原因如上所述。该解决方案将使用自定义用户名/密码验证器。
https://msdn.microsoft.com/en-us/library/aa702565(v=vs.110).aspx
凭据将存储在DMZ中表示层服务器上的web.config文件的自定义部分中。相同的凭据将存储在应用程序层服务器上的web.config文件中。包含凭据的部分将在两台服务器上加密。
还有其他建议吗?有关自定义用户名/密码验证方法的任何想法?
答案 0 :(得分:2)
我们对各种选项进行了大量测试。我们最终实现的解决方案是可配置的解决方案。它允许我们将用户名/密码安全性作为一个选项进行部署,或者回退到标准安全方法,例如x.509证书,以便那些对证书感到满意且可以管理它们的客户。
解决方案有四个主要组成部分:
简化的ServiceClientBase类如下所示。可以修改if / else块以包括对您希望支持的任何绑定的支持。关于这个类的主要注意事项是,如果使用和安全性,客户端凭据类型是“username”,那么我们将从.config文件加载用户名/密码。否则,我们会回退到使用标准WCF安全配置。
public class ServiceClientBase<TChannel> : ClientBase<TChannel>, IDisposable where TChannel : class
{
public const string AppTierServiceCredentialKey = "credentialKey";
public ServiceClientBase()
{
bool useUsernameCredentials = false;
Binding binding = this.Endpoint.Binding;
if (binding is WSHttpBinding)
{
WSHttpBinding wsHttpBinding = (WSHttpBinding)binding;
if (wsHttpBinding.Security != null && wsHttpBinding.Security.Mode == SecurityMode.TransportWithMessageCredential)
{
if (wsHttpBinding.Security.Message != null && wsHttpBinding.Security.Message.ClientCredentialType == MessageCredentialType.UserName)
{
useUsernameCredentials = true;
}
}
}
else if (binding is BasicHttpBinding)
{
BasicHttpBinding basicHttpBinding = (BasicHttpBinding)binding;
if (basicHttpBinding.Security != null && basicHttpBinding.Security.Mode == BasicHttpSecurityMode.TransportWithMessageCredential)
{
if (basicHttpBinding.Security.Message != null && basicHttpBinding.Security.Message.ClientCredentialType == BasicHttpMessageCredentialType.UserName)
{
useUsernameCredentials = true;
}
}
}
...
if (useUsernameCredentials)
{
ServiceCredentialsSection section = (ServiceCredentialsSection)ConfigurationManager.GetSection(ServiceCredentialsSection.SectionName);
CredentialsElement credentials = section.Credentials[AppTierServiceCredentialKey];
this.ClientCredentials.UserName.UserName = credentials.UserName;
this.ClientCredentials.UserName.Password = credentials.Password;
}
}
// http://blogs.msdn.com/b/jjameson/archive/2010/03/18/avoiding-problems-with-the-using-statement-and-wcf-service-proxies.aspx
void IDisposable.Dispose()
{
if (this.State == CommunicationState.Faulted)
{
this.Abort();
}
else if (this.State != CommunicationState.Closed)
{
this.Close();
}
}
}
凭据的自定义配置节类如下所示。
public class ServiceCredentialsSection : ConfigurationSection
{
public const string SectionName = "my.serviceCredentials";
public const string CredentialsTag = "credentials";
[ConfigurationProperty(CredentialsTag, IsDefaultCollection = false)]
[ConfigurationCollection(typeof(CredentialsCollection), AddItemName = "add", ClearItemsName = "clear", RemoveItemName = "remove")]
public CredentialsCollection Credentials
{
get
{
return (CredentialsCollection)this[CredentialsTag];
}
}
}
除了ServiceCredentialsSection类之外,还有一个CredentialsCollection类(扩展ConfigurationElementCollection)和一个CredentialsElement类(扩展ConfigurationElement)。我不会在这里包含CredentialsCollection类,因为它是一个很长的类,主要是股票代码。您可以在Internet上找到ConfigurationElementCollection的引用实现,例如https://msdn.microsoft.com/en-us/library/system.configuration.configurationelementcollection(v=vs.110).aspx。 CredentialsElement类如下所示。
public class CredentialsElement : ConfigurationElement
{
[ConfigurationProperty("serviceName", IsKey = true, DefaultValue = "", IsRequired = true)]
public string ServiceName
{
get { return base["serviceName"] as string; }
set { base["serviceName"] = value; }
}
[ConfigurationProperty("username", DefaultValue = "", IsRequired = true)]
public string UserName
{
get { return base["username"] as string; }
set { base["username"] = value; }
}
[ConfigurationProperty("password", DefaultValue = "", IsRequired = true)]
public string Password
{
get { return base["password"] as string; }
set { base["password"] = value; }
}
}
上面提到的类支持.config部分,如下所示。可以加密此部分以保护凭据。有关加密.config文件部分的提示,请参阅Encrypting custom sections of a web.config。
<my.serviceCredentials>
<credentials>
<add serviceName="credentialKey" username="myusername" password="mypassword" />
</credentials>
</my.serviceCredentials>
拼图的第三部分是自定义UserNamePasswordValidator。此类的代码如下所示。
public class PrivateServiceUserNamePasswordValidator : UserNamePasswordValidator
{
private IPrivateServiceAccountCache _accountsCache;
public IPrivateServiceAccountCache AccountsCache
{
get
{
if (_accountsCache == null)
{
_accountsCache = ServiceAccountsCache.Instance;
}
return _accountsCache;
}
}
public override void Validate(string username, string password)
{
if (!(AccountsCache.Validate(username, password)))
{
throw new FaultException("Unknown Username or Incorrect Password");
}
}
}
出于性能原因,我们会缓存一组凭据,验证服务调用中包含的用户名/密码对。缓存类如下所示。
public class ServiceAccountsCache : IPrivateServiceAccountCache
{
private static ServiceAccountsCache _instance = new ServiceAccountsCache();
private Dictionary<string, ServiceAccount> _accounts = new Dictionary<string, ServiceAccount>();
private ServiceAccountsCache() { }
public static ServiceAccountsCache Instance
{
get
{
return _instance;
}
}
public void Add(ServiceAccount account)
{
lock (_instance)
{
if (account == null) throw new ArgumentNullException("account");
if (String.IsNullOrWhiteSpace(account.Username)) throw new ArgumentException("Username cannot be null for a service account. Set the username attribute for the service account in the my.serviceAccounts section in the web.config file.");
if (String.IsNullOrWhiteSpace(account.Password)) throw new ArgumentException("Password cannot be null for a service account. Set the password attribute for the service account in the my.serviceAccounts section in the web.config file.");
if (_accounts.ContainsKey(account.Username.ToLower())) throw new ArgumentException(String.Format("The username '{0}' being added to the service accounts cache already exists. Verify that the username exists only once in the my.serviceAccounts section in the web.config file.", account.Username));
_accounts.Add(account.Username.ToLower(), account);
}
}
public bool Validate(string username, string password)
{
if (username == null) throw new ArgumentNullException("username");
string key = username.ToLower();
if (_accounts.ContainsKey(key) && _accounts[key].Password == password)
{
return true;
}
else
{
return false;
}
}
}
上面的缓存在应用程序启动时在Global.Application_Start方法中初始化,如下所示。
// Cache service accounts.
ServiceAccountsSection section = (ServiceAccountsSection)ConfigurationManager.GetSection(ServiceAccountsSection.SectionName);
if (section != null)
{
foreach (AccountElement account in section.Accounts)
{
ServiceAccountsCache.Instance.Add(new ServiceAccount() { Username = account.UserName, Password = account.Password, AccountType = (ServiceAccountType)Enum.Parse(typeof(ServiceAccountType), account.AccountType, true) });
}
}
最后一个难题是app层上的自定义配置部分,用于保存用户名/密码组合列表。此部分的代码如下所示。
public class ServiceAccountsSection : ConfigurationSection
{
public const string SectionName = "my.serviceAccounts";
public const string AccountsTag = "accounts";
[ConfigurationProperty(AccountsTag, IsDefaultCollection = false)]
[ConfigurationCollection(typeof(AccountsCollection), AddItemName = "add", ClearItemsName = "clear", RemoveItemName = "remove")]
public AccountsCollection Accounts
{
get
{
return (AccountsCollection)this[AccountsTag];
}
}
}
和以前一样,有一个自定义ConfigurationElementCollection类和一个自定义ConfigurationElement类。 ConfigurationElement类如下所示。
public class AccountElement : ConfigurationElement
{
[ConfigurationProperty("username", IsKey = true, DefaultValue = "", IsRequired = true)]
public string UserName
{
get { return base["username"] as string; }
set { base["username"] = value; }
}
[ConfigurationProperty("password", DefaultValue = "", IsRequired = true)]
public string Password
{
get { return base["password"] as string; }
set { base["password"] = value; }
}
[ConfigurationProperty("accountType", DefaultValue = "", IsRequired = true)]
public string AccountType
{
get { return base["accountType"] as string; }
set { base["accountType"] = value; }
}
}
这些配置类支持.config文件XML片段,如下所示。和以前一样,这部分可以加密。
<my.serviceAccounts>
<accounts>
<add username="myusername" password="mypassword" accountType="development" />
</accounts>
</my.serviceAccounts>
希望这可以帮助某人。