【问题标题】:Securing Service Call from a DMZ to WCF Service in Enterprise Domain保护从 DMZ 到企业域中 WCF 服务的服务调用
【发布时间】:2016-02-09 15:30:57
【问题描述】:

我们正在构建一个系统,该系统将在企业域的 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. 易于部署。过去我发现,只要 x.509 证书出现在 SSL 以外的任何情况下,它们都会给客户 IT 人员(采购、生成、管理)带来问题。这反过来又会导致我们的产品出现支持问题。

出于上述原因,我正在考虑使用用户名/密码安全性来保护 WCF 调用。该解决方案将使用自定义用户名/密码验证器。

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

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

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

【问题讨论】:

    标签: asp.net .net wcf


    【解决方案1】:

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

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

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

    简化的 ServiceClientBase 类如下所示。可以修改 if/else 块以包括对您希望支持的任何绑定的支持。关于这个类需要指出的主要一点是,如果使用了安全性并且客户端凭据类型是“用户名”,那么我们将从 .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 部分。此部分可以加密以保护凭据。请参阅Encrypting custom sections of a web.config 以获取有关加密 .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) });
        }
    }
    

    最后一块拼图是应用层的自定义配置部分,用于保存用户名/密码组合列表。本节的代码如下所示。

    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 sn-p。和以前一样,这部分可以加密。

    <my.serviceAccounts>
        <accounts>
            <add username="myusername" password="mypassword" accountType="development" />
        </accounts>
    </my.serviceAccounts>
    

    希望这可以帮助某人。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多