保护服务调用从DMZ到企业域中的WCF服务

时间:2016-02-09 15:30:57

标签: asp.net .net wcf

我们正在构建一个系统,该系统将在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证书有两个担忧:

  1. 性能。我自己还没有进行性能分析,但是从别人那里得知,验证x.509证书的开销可能会使解决方案成为非首发。我的下一个任务是在这一点上进行绩效分析。

  2. 的易于部署。我在过去发现,除了SSL之外,任何时候x.509证书都会出现在客户IT人员(采购,生成,管理)的问题上。这反过来又给我们的产品带来了支持问题。

  3. 我考虑使用用户名/密码安全性来保护WCF呼叫,原因如上所述。该解决方案将使用自定义用户名/密码验证器。

    https://msdn.microsoft.com/en-us/library/aa702565(v=vs.110).aspx

    凭据将存储在DMZ中表示层服务器上的web.config文件的自定义部分中。相同的凭据将存储在应用程序层服务器上的web.config文件中。包含凭据的部分将在两台服务器上加密。

    还有其他建议吗?有关自定义用户名/密码验证方法的任何想法?

1 个答案:

答案 0 :(得分:2)

我们对各种选项进行了大量测试。我们最终实现的解决方案是可配置的解决方案。它允许我们将用户名/密码安全性作为一个选项进行部署,或者回退到标准安全方法,例如x.509证书,以便那些对证书感到满意且可以管理它们的客户。

解决方案有四个主要组成部分:

  1. Web层用于调用应用层上的服务的ServiceClientBase类。
  2. Web层上的自定义配置部分,用于保存用户名/密码凭据,以便对应用层上的服务进行身份验证。
  3. 应用层上的自定义UserNamePasswordValidator类,用于验证凭据。
  4. 应用层上的自定义配置部分,用于保存可用于身份验证的用户名/密码组合列表。
  5. 简化的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>
    

    希望这可以帮助某人。