多租户SQLMembershipProvider ASP.NET MVC

4
我正在尝试将一个多租户ASP.NET MVC应用程序(包括SQL Azure)迁移到Azure。每个客户都有自己的数据库,其中包含所有成员凭据,是一个独立的数据库。
我们可以在SqlMembershipProvider对象初始化时设置连接字符串,但同一会话中对不同子域的后续请求不会更改连接字符串。我找到了一个示例,该实现重写了SqlMembershipProviders ConnectionString,但这在System.Web dll的4.0版本中不可能。
我们可以实现单个成员资格数据库并针对其进行身份验证......但我们希望在此SAAS模型中保持客户凭据的隔离。
因此问题是如何为每个请求动态更改SQLMembershipProviders连接字符串?
Web.config
<membership defaultProvider="TenantMembershipProvider">
        <providers>
            <clear/>        
    <add name="TenantMembershipProvider" type="ABC.Infrastructure.MultiTenancy.TenantMembershipProvider, ABC"
         connectionStringName="ApplicationServices" enablePasswordRetrieval="true" enablePasswordReset="true" requiresQuestionAndAnswer="false"
         requiresUniqueEmail="false" passwordFormat="Clear" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6"
         minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" passwordStrengthRegularExpression="" applicationName="/"/>            

        </providers>
    </membership>

TenantMembershipProvider.cs 处理初始化。
public class TenantMembershipProvider : SqlMembershipProvider
{

    private SiteLinqSession _session;
    private MasterSession _masterSession;
    private static readonly Dictionary<string, Customer> _customers = new Dictionary<string, Customer>();
    private static string _host;


    public override void Initialize(string name, NameValueCollection config)
    {

        base.Initialize(name, config);

        string connectionString = GetConnectionString();
        FieldInfo connectionStringField = GetType().BaseType.GetField("_sqlConnectionString", BindingFlags.Instance | BindingFlags.NonPublic);
                    connectionStringField.SetValue(this, connectionString);

    }

    private string GetConnectionString()
    {
        var headers = HttpContext.Current.Request.Headers["Host"];
        string[] host = headers.Split('.');

        _host = host[0];

        if (_host == "127") _host = "demo";

        var customer = GetSite(_host);

        return BuildTenantConnectionString(customer.ConnectionSetting);

    }


    private Customer GetSite(string host)
    {
        Customer customer;

        //check dictionary if customer exists for the subdomain           
        _customers.TryGetValue(host, out customer);

        if (customer != null)
            return customer;

        //if not get the customer record and add it to the dictionary
        _masterSession = new MasterSession();
        var customers = _masterSession.All<Customer>();
        customer = customers.SingleOrDefault(x => x.SubDomain == _host);

        if (customer != null)
            _customers.Add(host, customer);

        return customer;
    }

    private string BuildTenantConnectionString(ConnectionSetting setting)
    {

        return string.Format("Data Source={0};Initial Catalog={1};User Id={2};Password={3};", setting.DataSource, setting.Catalog, setting.Username, setting.Password);

    }
}
2个回答

5
为了节省大家从Adam发布的链接中获取信息所需的时间。
在Global.asax文件中,为Application_PreRequestHandlerExecute事件。
    protected void Application_PreRequestHandlerExecute()
    {
        SetProviderConnectionString(GetConnectionString());
    }

    private void SetProviderConnectionString(string connectionString)
    {
        // Set private property of Membership, Role and Profile providers. Do not try this at home!!
        var connectionStringField = Membership.Provider.GetType().GetField("_sqlConnectionString", BindingFlags.Instance | BindingFlags.NonPublic);
        if (connectionStringField != null)
            connectionStringField.SetValue(Membership.Provider, connectionString);

        var roleField = Roles.Provider.GetType().GetField("_sqlConnectionString", BindingFlags.Instance | BindingFlags.NonPublic);
        if (roleField != null)
            roleField.SetValue(Roles.Provider, connectionString);

        var profileField = ProfileManager.Provider.GetType().GetField("_sqlConnectionString", BindingFlags.Instance | BindingFlags.NonPublic);
        if (profileField != null)
            profileField.SetValue(ProfileManager.Provider, connectionString);
    }

    private string GetConnectionString()
    {
        return string.Format("Data Source={0};", @".\SQLEXPRESS;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|demo.mdf;User Instance=true");
    }

如果您创建了自定义的MembershipProvider,则会得到BaseType。
   var connectionStringField = Membership.Provider.GetType().BaseType.GetField("_sqlConnectionString", BindingFlags.Instance | BindingFlags.NonPublic);

我不确定这是否是最合适的解决方案,但它似乎可以让membershipProvider启用动态connectionString而无需自己编写代码。尽管感觉有些hackish。


我已经在我的项目中实现了上述代码,但是connectionStringField、roleField和profileField变量总是返回null值。有什么想法吗?你可以分享一下你的web.config文件,看看我是否遗漏了什么吗?谢谢。 - Todd
你是否实现/扩展了任何提供程序?如果是,你可能需要获取基本类型,例如:connectionStringField = Membership.Provider.GetType().BaseType.GetField("_sqlConnectionString", BindingFlags.Instance | BindingFlags.NonPublic); - Steve G.

1

你需要在Application_PreRequestHandlerExecute的请求周期早期获取它。

请注意来自http://forums.asp.net/p/997608/2209437.aspx的代码。

有几种方法,其中一种是检查示例提供程序代码并使用SqlConnectionHelper(来自上面的链接)。

internal static string GetConnectionString(string specifiedConnectionString, bool lookupConnectionString, bool appLevel)
{
   //Your Conn String goes here!!
   return Factory.ConnectionString;
}

另一个涉及prerequestauthenticate。您可以在会话中存储字符串(在初始登录时设置它),并在提供程序中引用它,或者如果所有其他方法都失败,则使用基于反射的代码,尽管它似乎不太干净。 如果这些都不起作用,那么您将需要编写自己的提供程序。


谢谢,我会查看您在上面发布的链接中的ASP.NET提供程序工具包SQL示例。 - Diesel337
好的,工具包正在使用.NET框架的2.0版本,看起来可能存在访问某些Azure库的问题。Windows Azure是否与.NET 2.0框架兼容? - Diesel337

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接