Authentication problems after publishing MVC 4 app

2020-02-11 01:25发布

问题:

I have a basic ASP.NET MVC 4 site which I am hosting on Azure websites. The authentication is forms authentication and has not been customised from the default template. Every time I publish, when I revisit my site, it just hangs with a very long timeout (couple of minutes perhaps) before finally showing me an error message. I can recover by deleting the cookies for the site in my browser and reloading.

Initially the problem was just trying to access pages that required authentication, but then I added this to my shared _Layout.cshtml:

@if (User.IsInRole("Admin"))
{
    <li>@Html.ActionLink("Admin", "Index", "Admin")</li>
}

which now means no pages at all are accessible after an new publish, and so I can't even click the logout link, which was another way I used to be able to fix the issue.

Have I got something configured wrong? Although I have a workaround I can use myself, this will not be a good experience for users of the site after I publish an update.

EDIT: from the ELMAH logs, it seems like forms authentication is trying to create a SQL Express database when I call IsInRole. I can't see why it would do this, as my forms authentication is all set up to use my SQL Azure database.

System.Web.HttpException (0x80004005): Unable to connect to SQL Server database. ---> System.Data.SqlClient.SqlException (0x80131904): A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: SQL Network Interfaces, error: 26 - Error Locating Server/Instance Specified)
   at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection)
   at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning()
   at System.Data.SqlClient.TdsParser.Connect(ServerInfo serverInfo, SqlInternalConnectionTds connHandler, Boolean ignoreSniOpenTimeout, Int64 timerExpire, Boolean encrypt, Boolean trustServerCert, Boolean integratedSecurity, Boolean withFailover)
   at System.Data.SqlClient.SqlInternalConnectionTds.AttemptOneLogin(ServerInfo serverInfo, String newPassword, Boolean ignoreSniOpenTimeout, TimeoutTimer timeout, SqlConnection owningObject, Boolean withFailover)
   at System.Data.SqlClient.SqlInternalConnectionTds.LoginNoFailover(ServerInfo serverInfo, String newPassword, Boolean redirectedUserInstance, SqlConnection owningObject, SqlConnectionString connectionOptions, TimeoutTimer timeout)
   at System.Data.SqlClient.SqlInternalConnectionTds.OpenLoginEnlist(SqlConnection owningObject, TimeoutTimer timeout, SqlConnectionString connectionOptions, String newPassword, Boolean redirectedUserInstance)
   at System.Data.SqlClient.SqlInternalConnectionTds..ctor(DbConnectionPoolIdentity identity, SqlConnectionString connectionOptions, Object providerInfo, String newPassword, SqlConnection owningObject, Boolean redirectedUserInstance)
   at System.Data.SqlClient.SqlConnectionFactory.CreateConnection(DbConnectionOptions options, Object poolGroupProviderInfo, DbConnectionPool pool, DbConnection owningConnection)
   at System.Data.ProviderBase.DbConnectionFactory.CreateNonPooledConnection(DbConnection owningConnection, DbConnectionPoolGroup poolGroup)
   at System.Data.ProviderBase.DbConnectionFactory.GetConnection(DbConnection owningConnection)
   at System.Data.ProviderBase.DbConnectionClosed.OpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory)
   at System.Data.SqlClient.SqlConnection.Open()
   at System.Web.Management.SqlServices.GetSqlConnection(String server, String user, String password, Boolean trusted, String connectionString)
ClientConnectionId:00000000-0000-0000-0000-000000000000
   at System.Web.Management.SqlServices.GetSqlConnection(String server, String user, String password, Boolean trusted, String connectionString)
   at System.Web.Management.SqlServices.SetupApplicationServices(String server, String user, String password, Boolean trusted, String connectionString, String database, String dbFileName, SqlFeatures features, Boolean install)
   at System.Web.Management.SqlServices.Install(String database, String dbFileName, String connectionString)
   at System.Web.DataAccess.SqlConnectionHelper.CreateMdfFile(String fullFileName, String dataDir, String connectionString)
   at System.Web.DataAccess.SqlConnectionHelper.EnsureSqlExpressDBFile(String connectionString)
   at System.Web.DataAccess.SqlConnectionHelper.GetConnection(String connectionString, Boolean revertImpersonation)
   at System.Web.Security.SqlRoleProvider.GetRolesForUser(String username)
   at WebMatrix.WebData.SimpleRoleProvider.GetRolesForUser(String username)
   at System.Web.Security.RolePrincipal.IsInRole(String role)

回答1:

After trying dozens of different suggestions from various blog posts, I have found a solution. Adding the InitialiseSimpleMembership attribute to my home controller resolves the problem.

[InitializeSimpleMembership]
public class HomeController : Controller

After making this change, I managed several successful publishes without issues. I suspect the reason is that the following line of code in the InitializeSimpleMembershipAttribute constructor needs to run before any calls to User.IsInRole are made:

WebSecurity.InitializeDatabaseConnection("DefaultConnection", "UserProfile", "UserId", "UserName", autoCreateTables: true);

I guess the best thing to do would be to run InitializeSimpleMembership in Application_Start.



回答2:

Here's a tutorial that you could follow to setup a SQL database when deploying your application on Windows Azure. You must setup the correct connection string in your web.config which by default is pointing to a local SQL Express database when you create a new ASP.NET MVC 4 application using the Internet template.

Your SQL Azure connection string will look something like this:

<connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=tcp:#server#.database.windows.net,1433;Initial Catalog=#DBName#;User ID=UserName#@#server#;Password=#password#;MultipleActiveResultSets=True" providerName="System.Data.SqlClient"/>
</connectionStrings>


回答3:

Although this question has been answered, I thought I might quickly share my experiences with the same problem. My case is slightly different to Mark's.

So, I had the InitializeSimpleMembership attribute on all my controllers (actually I had it on my base controller from which all my controllers inherit), however I was still experiencing the same problem. Now, in my base controller I was also overriding the Initialize method in order to setup some context information for our application. Part of this context setup is to check if the current user is in a particular role, hence the IsUserInRole method was being called from within this Initialize method, all before any action method is being called.

Now if one takes a look at the InitializeSimpleMembership class one notices that the initializing is actually done in the OnActionExecuting method (i.e. InitializeSimpleMembership inherits from ActionFilterAttribute):

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
      // Ensure ASP.NET Simple Membership is initialized only once per app start
      LazyInitializer.EnsureInitialized(ref _initializer, ref _isInitialized, ref _initializerLock);
}

In my case, this membership initialization was happening too late...that is, I need the Simple Membership to be initialized before I make my call to IsUserInRole in my base controller's overridden Initialize method.

The solution for me was relatively simple: I removed the InitializeSimpleMembership attribute entirely and put its logic straight into my base controller so that I could call it from my Initialize method, something like this:

public class BaseController : Controller
{
    private static SimpleMembershipInitializer _initializer;
    private static object _initializerLock = new object();
    private static bool _isInitialized;

    private class SimpleMembershipInitializer
    {
        public SimpleMembershipInitializer()
        {
            try
            {
                WebSecurity.InitializeDatabaseConnection("DefaultConnection", "UserProfile", "UserId", "UserName", autoCreateTables: true);
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException("The ASP.NET Simple Membership database could not be initialized. For more information, please see http://go.microsoft.com/fwlink/?LinkId=256588", ex);
            }
        }
    }

    ...

    protected override void Initialize(RequestContext requestContext)
    {
        base.Initialize(requestContext);
        LazyInitializer.EnsureInitialized(ref _initializer, ref _isInitialized, ref _initializerLock);
        SetupControllerContext(); // my function that calls 'IsUserInRole'
    }
}

Now I imagine that one should probably refactor this and put it in the Application_Start() method, as Mark suggested, but you get the idea :). I just wanted to explain my experience just in case anybody is doing something similar in their controller's overridden Initialize method.